restructured project to match a more standard cmake setup

This commit is contained in:
Noel Berry
2020-12-31 13:43:23 -08:00
parent c841bd82a1
commit 241d863ac4
97 changed files with 233 additions and 264 deletions

471
src/images/aseprite.cpp Normal file
View File

@ -0,0 +1,471 @@
#include <blah/images/aseprite.h>
#include <blah/streams/filestream.h>
#include <blah/core/filesystem.h>
#include <blah/core/log.h>
#define STBI_NO_STDIO
#define STBI_ONLY_ZLIB
#include "../third_party/stb_image.h"
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MUL_UN8(a, b, t) \
((t) = (a) * (uint16_t)(b) + 0x80, ((((t) >> 8) + (t) ) >> 8))
using namespace Blah;
Aseprite::Aseprite()
{
}
Aseprite::Aseprite(const char* path)
{
FileStream fs(path, FileMode::Read);
parse(fs);
}
Aseprite::Aseprite(Stream& stream)
{
parse(stream);
}
Aseprite::Aseprite(const Aseprite& src)
{
mode = src.mode;
width = src.width;
height = src.height;
layers = src.layers;
frames = src.frames;
tags = src.tags;
slices = src.slices;
palette = src.palette;
}
Aseprite::Aseprite(Aseprite&& src) noexcept
{
mode = src.mode;
width = src.width;
height = src.height;
layers = std::move(src.layers);
frames = std::move(src.frames);
tags = std::move(src.tags);
slices = std::move(src.slices);
palette = std::move(src.palette);
}
Aseprite& Aseprite::operator=(const Aseprite& src)
{
mode = src.mode;
width = src.width;
height = src.height;
layers = src.layers;
frames = src.frames;
tags = src.tags;
slices = src.slices;
palette = src.palette;
return *this;
}
Aseprite& Aseprite::operator=(Aseprite&& src) noexcept
{
mode = src.mode;
width = src.width;
height = src.height;
layers = std::move(src.layers);
frames = std::move(src.frames);
tags = std::move(src.tags);
slices = std::move(src.slices);
palette = std::move(src.palette);
return *this;
}
Aseprite::~Aseprite()
{
}
void Aseprite::parse(Stream& stream)
{
if (!stream.is_readable())
{
BLAH_ERROR("Stream is not readable");
return;
}
int frame_count = 0;
// header
{
// filesize
stream.read<uint32_t>(Endian::Little);
// magic number
auto magic = stream.read<uint16_t>(Endian::Little);
if (magic != 0xA5E0)
{
BLAH_ERROR("File is not a valid Aseprite file");
return;
}
// main info
frame_count = stream.read<uint16_t>(Endian::Little);
width = stream.read<uint16_t>(Endian::Little);
height = stream.read<uint16_t>(Endian::Little);
mode = static_cast<Aseprite::Modes>(stream.read<uint16_t>(Endian::Little) / 8);
// don't care about other info
stream.read<uint32_t>(Endian::Little); // Flags
stream.read<uint16_t>(Endian::Little); // Speed (deprecated)
stream.read<uint32_t>(Endian::Little); // Should be 0
stream.read<uint32_t>(Endian::Little); // Should be 0
stream.read<uint8_t>(Endian::Little); // Palette entry
stream.seek(stream.position() + 3); // Ignore these bytes
stream.read<uint16_t>(Endian::Little); // Number of colors (0 means 256 for old sprites)
stream.read<int8_t>(Endian::Little); // Pixel width
stream.read<int8_t>(Endian::Little); // Pixel height
stream.seek(stream.position() + 92); // For Future
}
frames.resize(frame_count);
// frames
for (int i = 0; i < frame_count; i++)
{
auto frameStart = stream.position();
auto frameEnd = frameStart + stream.read<uint32_t>(Endian::Little);
unsigned int chunks = 0;
// frame header
{
auto magic = stream.read<uint16_t>(Endian::Little); // magic number
if (magic != 0xF1FA)
{
BLAH_ERROR("File is not a valid Aseprite file");
return;
}
auto old_chunk_count = stream.read<uint16_t>(Endian::Little);
frames[i].duration = stream.read<uint16_t>(Endian::Little);
stream.seek(stream.position() + 2); // for future
auto new_chunk_count = stream.read<uint32_t>(Endian::Little);
if (old_chunk_count == 0xFFFF)
chunks = new_chunk_count;
else
chunks = old_chunk_count;
}
// make frame image
frames[i].image = Image(width, height);
// frame chunks
for (unsigned int j = 0; j < chunks; j++)
{
auto chunkStart = stream.position();
auto chunkEnd = chunkStart + stream.read<uint32_t>(Endian::Little);
auto chunkType = static_cast<Chunks>(stream.read<uint16_t>(Endian::Little));
switch (chunkType)
{
case Chunks::Layer: parse_layer(stream, i); break;
case Chunks::Cel: parse_cel(stream, i, chunkEnd); break;
case Chunks::Palette: parse_palette(stream, i); break;
case Chunks::UserData: parse_user_data(stream, i); break;
case Chunks::FrameTags: parse_tag(stream, i); break;
case Chunks::Slice: parse_slice(stream, i); break;
default: break;
}
stream.seek(chunkEnd);
}
stream.seek(frameEnd);
}
}
void Aseprite::parse_layer(Stream& stream, int frame)
{
layers.emplace_back();
auto& layer = layers.back();
layer.flag = static_cast<LayerFlags>(stream.read<uint16_t>(Endian::Little));
layer.visible = ((int)layer.flag & (int)LayerFlags::Visible) == (int)LayerFlags::Visible;
layer.type = static_cast<LayerTypes>(stream.read<uint16_t>(Endian::Little));
layer.child_level = stream.read<uint16_t>(Endian::Little);
stream.read<uint16_t>(Endian::Little); // width
stream.read<uint16_t>(Endian::Little); // height
layer.blendmode = stream.read<uint16_t>(Endian::Little);
layer.alpha = stream.read<uint8_t>(Endian::Little);
stream.seek(stream.position() + 3); // for future
layer.name.set_length(stream.read<uint16_t>(Endian::Little));
stream.read(layer.name.cstr(), layer.name.length());
layer.userdata.color = 0xffffff;
layer.userdata.text = "";
m_last_userdata = &(layer.userdata);
}
void Aseprite::parse_cel(Stream& stream, int frameIndex, size_t maxPosition)
{
Frame& frame = frames[frameIndex];
frame.cels.emplace_back();
auto& cel = frame.cels.back();
cel.layer_index = stream.read<uint16_t>(Endian::Little);
cel.x = stream.read<uint16_t>(Endian::Little);
cel.y = stream.read<uint16_t>(Endian::Little);
cel.alpha = stream.read<uint8_t>(Endian::Little);
cel.linked_frame_index = -1;
auto celType = stream.read<uint16_t>(Endian::Little);
stream.seek(stream.position() + 7);
// RAW or DEFLATE
if (celType == 0 || celType == 2)
{
auto width = stream.read<uint16_t>(Endian::Little);
auto height = stream.read<uint16_t>(Endian::Little);
auto count = width * height * (int)mode;
cel.image = Image(width, height);
// RAW
if (celType == 0)
{
stream.read(cel.image.pixels, count);
}
// DEFLATE (zlib)
else
{
// this could be optimized to use a buffer on the stack if we only read set chunks at a time
// stbi's zlib doesn't have that functionality though
auto size = maxPosition - stream.position();
if (size > INT32_MAX)
size = INT32_MAX;
char* buffer = new char[size];
stream.read(buffer, size);
int olen = width * height * sizeof(Color);
int res = stbi_zlib_decode_buffer((char*)cel.image.pixels, olen, buffer, (int)size);
delete[] buffer;
if (res < 0)
{
BLAH_ERROR("Unable to parse Aseprite file");
return;
}
}
// convert to pixels
// note: we work in-place to save having to store stuff in a buffer
if (mode == Modes::Grayscale)
{
auto src = (unsigned char*)cel.image.pixels;
auto dst = cel.image.pixels;
for (int d = width * height - 1, s = (width * height - 1) * 2; d >= 0; d--, s -= 2)
dst[d] = Color(src[s], src[s], src[s], src[s + 1]);
}
else if (mode == Modes::Indexed)
{
auto src = (unsigned char*)cel.image.pixels;
auto dst = cel.image.pixels;
for (int i = width * height - 1; i >= 0; i--)
dst[i] = palette[src[i]];
}
}
// REFERENCE
// this cel directly references a previous cel
else if (celType == 1)
{
cel.linked_frame_index = stream.read<uint16_t>(Endian::Little);
}
// draw to frame if visible
if ((int)layers[cel.layer_index].flag & (int)LayerFlags::Visible)
{
render_cel(&cel, &frame);
}
cel.userdata.color = 0xffffff;
cel.userdata.text = "";
m_last_userdata = &(cel.userdata);
}
void Aseprite::parse_palette(Stream& stream, int frame)
{
/* size */ stream.read<uint32_t>(Endian::Little);
auto start = stream.read<uint32_t>(Endian::Little);
auto end = stream.read<uint32_t>(Endian::Little);
stream.seek(stream.position() + 8);
palette.resize(palette.size() + (end - start + 1));
for (int p = 0, len = static_cast<int>(end - start) + 1; p < len; p++)
{
auto hasName = stream.read<uint16_t>(Endian::Little);
palette[start + p] = stream.read<Color>(Endian::Little);
if (hasName & 0xF000)
{
int len = stream.read<uint16_t>(Endian::Little);
stream.seek(stream.position() + len);
}
}
}
void Aseprite::parse_user_data(Stream& stream, int frame)
{
if (m_last_userdata != nullptr)
{
auto flags = stream.read<uint32_t>(Endian::Little);
// has text
if (flags & (1 << 0))
{
m_last_userdata->text.set_length(stream.read<uint16_t>(Endian::Little));
stream.read(m_last_userdata->text.cstr(), m_last_userdata->text.length());
}
// has color
if (flags & (1 << 1))
m_last_userdata->color = stream.read<Color>(Endian::Little);
}
}
void Aseprite::parse_tag(Stream& stream, int frame)
{
auto count = stream.read<uint16_t>(Endian::Little);
stream.seek(stream.position() + 8);
for (int t = 0; t < count; t++)
{
Tag tag;
tag.from = stream.read<uint16_t>(Endian::Little);
tag.to = stream.read<uint16_t>(Endian::Little);
tag.loops = static_cast<LoopDirections>(stream.read<int8_t>(Endian::Little));
stream.seek(stream.position() + 8);
tag.color = Color(stream.read<int8_t>(), stream.read<int8_t>(), stream.read<int8_t>(Endian::Little), 255);
stream.seek(stream.position() + 1);
tag.name.set_length(stream.read<uint16_t>(Endian::Little));
stream.read(tag.name.cstr(), tag.name.length());
tags.push_back(tag);
}
}
void Aseprite::parse_slice(Stream& stream, int frame)
{
int count = stream.read<uint32_t>(Endian::Little);
int flags = stream.read<uint32_t>(Endian::Little);
stream.read<uint32_t>(Endian::Little); // reserved
String name;
name.set_length(stream.read<uint16_t>(Endian::Little));
stream.read(name.cstr(), name.length());
for (int s = 0; s < count; s++)
{
slices.emplace_back();
auto& slice = slices.back();
slice.name = name;
slice.frame = stream.read<uint32_t>(Endian::Little);
slice.origin.x = stream.read<int32_t>(Endian::Little);
slice.origin.y = stream.read<int32_t>(Endian::Little);
slice.width = stream.read<uint32_t>(Endian::Little);
slice.height = stream.read<uint32_t>(Endian::Little);
// 9 slice (ignored atm)
if (flags & (1 << 0))
{
stream.read<int32_t>(Endian::Little);
stream.read<int32_t>(Endian::Little);
stream.read<uint32_t>(Endian::Little);
stream.read<uint32_t>(Endian::Little);
}
// pivot point
slice.has_pivot = false;
if (flags & (1 << 1))
{
slice.has_pivot = true;
slice.pivot.x = stream.read<uint32_t>(Endian::Little);
slice.pivot.y = stream.read<uint32_t>(Endian::Little);
}
slice.userdata.color = 0xffffff;
slice.userdata.text = "";
m_last_userdata = &(slice.userdata);
}
}
void Aseprite::render_cel(Cel* cel, Frame* frame)
{
Layer& layer = layers[cel->layer_index];
while (cel->linked_frame_index >= 0)
{
auto& frame = frames[cel->linked_frame_index];
for (auto& it : frame.cels)
if (it.layer_index == cel->layer_index)
{
cel = &it;
break;
}
}
int t;
unsigned char opacity = MUL_UN8(cel->alpha, layer.alpha, t);
if (opacity <= 0)
return;
auto src = cel->image.pixels;
auto srcX = cel->x;
auto srcY = cel->y;
auto srcW = cel->image.width;
auto srcH = cel->image.height;
auto dst = frame->image.pixels;
auto dstW = frame->image.width;
auto dstH = frame->image.height;
// blit pixels
int left = MAX(0, srcX);
int right = MIN(dstW, srcX + srcW);
int top = MAX(0, srcY);
int bottom = MIN(dstH, srcY + srcH);
if (layer.blendmode == 0)
{
for (int dx = left, sx = -MIN(srcX, 0); dx < right; dx++, sx++)
{
for (int dy = top, sy = -MIN(srcY, 0); dy < bottom; dy++, sy++)
{
Color* srcColor = (src + sx + sy * srcW);
Color* dstColor = (dst + dx + dy * dstW);
if (srcColor->a != 0)
{
auto sa = MUL_UN8(srcColor->a, opacity, t);
auto ra = dstColor->a + sa - MUL_UN8(dstColor->a, sa, t);
dstColor->r = (unsigned char)(dstColor->r + (srcColor->r - dstColor->r) * sa / ra);
dstColor->g = (unsigned char)(dstColor->g + (srcColor->g - dstColor->g) * sa / ra);
dstColor->b = (unsigned char)(dstColor->b + (srcColor->b - dstColor->b) * sa / ra);
dstColor->a = (unsigned char)ra;
}
}
}
}
else
{
BLAH_ERROR("Aseprite blendmodes aren't implemented");
}
}

245
src/images/font.cpp Normal file
View File

@ -0,0 +1,245 @@
#include <blah/images/font.h>
#include <blah/streams/filestream.h>
#include <blah/math/calc.h>
#include <blah/core/log.h>
using namespace Blah;
#define STBTT_STATIC
#define STB_TRUETYPE_IMPLEMENTATION
#include "../third_party/stb_truetype.h"
String GetName(stbtt_fontinfo* font, int nameId)
{
int length = 0;
// get the name
const uint16_t* ptr = (const uint16_t*)stbtt_GetFontNameStr(font, &length,
STBTT_PLATFORM_ID_MICROSOFT,
STBTT_MS_EID_UNICODE_BMP,
STBTT_MS_LANG_ENGLISH,
nameId);
// we want the size in wide chars
length /= 2;
String str;
if (length > 0)
str.append_utf16(ptr, ptr + length, Calc::is_little_endian());
return str;
}
Font::Font()
{
m_font = nullptr;
m_data = nullptr;
m_ascent = 0;
m_descent = 0;
m_line_gap = 0;
m_valid = false;
}
Font::Font(Stream& stream) : Font()
{
load(stream);
}
Font::Font(const char* path) : Font()
{
FileStream fs(path, FileMode::Read);
if (fs.is_readable())
load(fs);
}
Font::Font(Font&& src) noexcept
{
m_font = src.m_font;
m_data = src.m_data;
m_family_name = src.m_family_name;
m_style_name = src.m_style_name;
m_ascent = src.m_ascent;
m_descent = src.m_descent;
m_line_gap = src.m_line_gap;
m_valid = src.m_valid;
src.m_family_name.clear();
src.m_style_name.clear();
src.m_valid = false;
src.m_font = nullptr;
src.m_data = nullptr;
}
Font& Font::operator=(Font&& src) noexcept
{
m_font = src.m_font;
m_data = src.m_data;
m_family_name = src.m_family_name;
m_style_name = src.m_style_name;
m_ascent = src.m_ascent;
m_descent = src.m_descent;
m_line_gap = src.m_line_gap;
m_valid = src.m_valid;
src.m_family_name.clear();
src.m_style_name.clear();
src.m_valid = false;
src.m_font = nullptr;
src.m_data = nullptr;
return *this;
}
Font::~Font()
{
dispose();
}
void Font::load(Stream& stream)
{
dispose();
if (!stream.is_readable())
{
BLAH_ERROR("Unable to load a font as the Stream was not readable");
return;
}
// create data buffer
auto size = stream.length();
m_data = new unsigned char[size];
stream.read(m_data, size);
// init font
m_font = new stbtt_fontinfo();
auto fn = (stbtt_fontinfo*)m_font;
stbtt_InitFont(fn, m_data, 0);
m_family_name = GetName(fn, 1);
m_style_name = GetName(fn, 2);
// properties
stbtt_GetFontVMetrics(fn, &m_ascent, &m_descent, &m_line_gap);
m_valid = true;
}
void Font::dispose()
{
delete (stbtt_fontinfo*)m_font;
delete[] m_data;
m_font = nullptr;
m_data = nullptr;
m_family_name.dispose();
m_style_name.dispose();
}
const char* Font::family_name() const
{
return m_family_name.cstr();
}
const char* Font::style_name() const
{
return m_style_name.cstr();
}
int Font::ascent() const
{
return m_ascent;
}
int Font::descent() const
{
return m_descent;
}
int Font::line_gap() const
{
return m_line_gap;
}
int Font::height() const
{
return m_ascent - m_descent;
}
int Font::line_height() const
{
return m_ascent - m_descent + m_line_gap;
}
int Font::get_glyph(Codepoint codepoint) const
{
if (!m_font)
return 0;
return stbtt_FindGlyphIndex((stbtt_fontinfo*)m_font, codepoint);
}
float Font::get_scale(float size) const
{
if (!m_font)
return 0;
return stbtt_ScaleForMappingEmToPixels((stbtt_fontinfo*)m_font, size);
}
float Font::get_kerning(int glyph1, int glyph2, float scale) const
{
if (!m_font)
return 0;
return stbtt_GetGlyphKernAdvance((stbtt_fontinfo*)m_font, glyph1, glyph2) * scale;
}
Font::Char Font::get_character(int glyph, float scale) const
{
Char ch;
if (!m_font)
return ch;
int advance, offsetX, x0, y0, x1, y1;
stbtt_GetGlyphHMetrics((stbtt_fontinfo*)m_font, glyph, &advance, &offsetX);
stbtt_GetGlyphBitmapBox((stbtt_fontinfo*)m_font, glyph, scale, scale, &x0, &y0, &x1, &y1);
int w = (x1 - x0);
int h = (y1 - y0);
// define character
ch.glyph = glyph;
ch.width = w;
ch.height = h;
ch.advance = advance * scale;
ch.offset_x = offsetX * scale;
ch.offset_y = (float)y0;
ch.scale = scale;
ch.has_glyph = (w > 0 && h > 0 && stbtt_IsGlyphEmpty((stbtt_fontinfo*)m_font, glyph) == 0);
return ch;
}
bool Font::get_image(const Font::Char& ch, Color* pixels) const
{
if (ch.has_glyph)
{
// we actually use the image buffer as our temporary buffer, and fill the pixels out backwards after
// kinda weird but it works & saves creating more memory
unsigned char* src = (unsigned char*)pixels;
stbtt_MakeGlyphBitmap((stbtt_fontinfo*)m_font, src, ch.width, ch.height, ch.width, ch.scale, ch.scale, ch.glyph);
int len = ch.width * ch.height;
for (int a = (len - 1) * 4, b = (len - 1); b >= 0; a -= 4, b -= 1)
{
src[a + 0] = src[b];
src[a + 1] = src[b];
src[a + 2] = src[b];
src[a + 3] = src[b];
}
return true;
}
return false;
}
bool Font::is_valid() const
{
return m_valid;
}

301
src/images/image.cpp Normal file
View File

@ -0,0 +1,301 @@
#include <blah/images/image.h>
#include <blah/streams/stream.h>
#include <blah/streams/filestream.h>
#include <blah/core/log.h>
using namespace Blah;
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#define STBI_ONLY_BMP
#include "../third_party/stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../third_party/stb_image_write.h"
namespace
{
int Blah_STBI_Read(void* user, char* data, int size)
{
int64_t read = ((Stream*)user)->read(data, size);
return (int)read;
}
void Blah_STBI_Skip(void* user, int n)
{
((Stream*)user)->seek(((Stream*)user)->position() + n);
}
int Blah_STBI_Eof(void* user)
{
int64_t position = ((Stream*)user)->position();
int64_t length = ((Stream*)user)->length();
if (position >= length)
return 1;
return 0;
}
void Blah_STBI_Write(void* context, void* data, int size)
{
((Stream*)context)->write((char*)data, size);
}
}
Image::Image()
{
width = height = 0;
pixels = nullptr;
m_stbi_ownership = false;
}
Image::Image(Stream& stream)
{
width = height = 0;
pixels = nullptr;
m_stbi_ownership = false;
from_stream(stream);
}
Image::Image(const char* file)
{
width = height = 0;
pixels = nullptr;
m_stbi_ownership = false;
FileStream fs(file, FileMode::Read);
if (fs.is_readable())
from_stream(fs);
}
Image::Image(int w, int h)
{
BLAH_ASSERT(w >= 0 && h >= 0, "Image width and height must be larger than 0");
width = w;
height = h;
pixels = new Color[width * height];
m_stbi_ownership = false;
memset(pixels, 0, (size_t)width * (size_t)height * sizeof(Color));
}
Image::Image(const Image& src)
{
width = src.width;
height = src.height;
m_stbi_ownership = src.m_stbi_ownership;
pixels = nullptr;
if (src.pixels != nullptr && width > 0 && height > 0)
{
pixels = new Color[width * height];
memcpy(pixels, src.pixels, sizeof(Color) * width * height);
}
}
Image& Image::operator=(const Image& src)
{
width = src.width;
height = src.height;
m_stbi_ownership = src.m_stbi_ownership;
pixels = nullptr;
if (src.pixels != nullptr && width > 0 && height > 0)
{
pixels = new Color[width * height];
memcpy(pixels, src.pixels, sizeof(Color) * width * height);
}
return *this;
}
Image::Image(Image&& src) noexcept
{
width = src.width;
height = src.height;
pixels = src.pixels;
m_stbi_ownership = src.m_stbi_ownership;
src.width = src.height = 0;
src.pixels = nullptr;
src.m_stbi_ownership = false;
}
Image& Image::operator=(Image&& src) noexcept
{
width = src.width;
height = src.height;
pixels = src.pixels;
m_stbi_ownership = src.m_stbi_ownership;
src.width = src.height = 0;
src.pixels = nullptr;
src.m_stbi_ownership = false;
return *this;
}
Image::~Image()
{
dispose();
}
void Image::from_stream(Stream& stream)
{
dispose();
if (!stream.is_readable())
{
BLAH_ERROR("Unable to load image as the Stream was not readable");
return;
}
stbi_io_callbacks callbacks;
callbacks.eof = Blah_STBI_Eof;
callbacks.read = Blah_STBI_Read;
callbacks.skip = Blah_STBI_Skip;
int x, y, comps;
uint8_t* data = stbi_load_from_callbacks(&callbacks, &stream, &x, &y, &comps, 4);
if (data == nullptr)
{
BLAH_ERROR("Unable to load image as the Stream's data was not a valid image");
return;
}
m_stbi_ownership = true;
pixels = (Color*)data;
width = x;
height = y;
}
void Image::dispose()
{
if (m_stbi_ownership)
stbi_image_free(pixels);
else
delete[] pixels;
pixels = nullptr;
width = height = 0;
m_stbi_ownership = false;
}
void Image::premultiply()
{
if (pixels != nullptr)
{
for (int n = 0; n < width * height; n ++)
{
pixels[n].r = (uint8_t)(pixels[n].r * pixels[n].a / 255);
pixels[n].g = (uint8_t)(pixels[n].g * pixels[n].a / 255);
pixels[n].b = (uint8_t)(pixels[n].b * pixels[n].a / 255);
}
}
}
void Image::set_pixels(const RectI& rect, Color* data)
{
for (int y = 0; y < rect.h; y++)
{
int to = rect.x + ((rect.y + y) * width);
int from = (y * rect.w);
memcpy(pixels + to, data + from, sizeof(Color) * rect.w);
}
}
bool Image::save_png(const char* file) const
{
FileStream fs(file, FileMode::Write);
return save_png(fs);
}
bool Image::save_png(Stream& stream) const
{
BLAH_ASSERT(pixels != nullptr, "Image Pixel data cannot be null");
BLAH_ASSERT(width > 0 && height > 0, "Image Width and Height must be larger than 0");
if (stream.is_writable())
{
stbi_write_force_png_filter = 0;
stbi_write_png_compression_level = 0;
if (stbi_write_png_to_func(Blah_STBI_Write, &stream, width, height, 4, pixels, width * 4) != 0)
return true;
else
Log::error("stbi_write_png_to_func failed");
}
else
{
Log::error("Cannot save Image, the Stream is not writable");
}
return false;
}
bool Image::save_jpg(const char* file, int quality) const
{
FileStream fs(file, FileMode::Write);
return save_jpg(fs, quality);
}
bool Image::save_jpg(Stream& stream, int quality) const
{
BLAH_ASSERT(pixels != nullptr, "Image Pixel data cannot be null");
BLAH_ASSERT(width > 0 && height > 0, "Image Width and Height must be larger than 0");
if (quality < 1)
{
Log::warn("jpg quality value should be between 1 and 100; input was %i", quality);
quality = 1;
}
else if (quality > 100)
{
Log::warn("jpg quality value should be between 1 and 100; input was %i", quality);
quality = 100;
}
if (stream.is_writable())
{
if (stbi_write_jpg_to_func(Blah_STBI_Write, &stream, width, height, 4, pixels, quality) != 0)
return true;
else
Log::error("stbi_write_jpg_to_func failed");
}
else
{
Log::error("Cannot save Image, the Stream is not writable");
}
return false;
}
void Image::get_pixels(Color* dest, const Point& destPos, const Point& destSize, RectI sourceRect)
{
// can't be outside of the source image
if (sourceRect.x < 0) sourceRect.x = 0;
if (sourceRect.y < 0) sourceRect.y = 0;
if (sourceRect.x + sourceRect.w > width) sourceRect.w = width - sourceRect.x;
if (sourceRect.y + sourceRect.h > height) sourceRect.h = height - sourceRect.y;
// can't be larger than our destination
if (sourceRect.w > destSize.x - destPos.x)
sourceRect.w = destSize.x - destPos.x;
if (sourceRect.h > destSize.y - destPos.y)
sourceRect.h = destSize.y - destPos.y;
for (int y = 0; y < sourceRect.h; y++)
{
int to = destPos.x + (destPos.y + y) * destSize.x;
int from = sourceRect.x + (sourceRect.y + y) * width;
memcpy(dest + to, pixels + from, sizeof(Color) * (int)sourceRect.w);
}
}
Image Image::get_sub_image(const RectI& sourceRect)
{
Image img(sourceRect.w, sourceRect.h);
get_pixels(img.pixels, Point::zero, Point(img.width, img.height), sourceRect);
return img;
}

334
src/images/packer.cpp Normal file
View File

@ -0,0 +1,334 @@
#include <blah/images/packer.h>
#include <blah/core/log.h>
#include <algorithm>
#include <cstring>
using namespace Blah;
Packer::Packer()
: max_size(8192), power_of_two(true), spacing(1), padding(1), m_dirty(false) { }
Packer::Packer(int max_size, int spacing, bool power_of_two)
: max_size(max_size), power_of_two(power_of_two), spacing(spacing), padding(1), m_dirty(false) { }
Packer::Packer(Packer&& src) noexcept
{
max_size = src.max_size;
power_of_two = src.power_of_two;
spacing = src.spacing;
padding = src.padding;
m_dirty = src.m_dirty;
pages = std::move(src.pages);
entries = std::move(src.entries);
m_buffer = std::move(src.m_buffer);
}
Packer& Packer::operator=(Packer&& src) noexcept
{
max_size = src.max_size;
power_of_two = src.power_of_two;
spacing = src.spacing;
padding = src.padding;
m_dirty = src.m_dirty;
pages = std::move(src.pages);
entries = std::move(src.entries);
m_buffer = std::move(src.m_buffer);
return *this;
}
Packer::~Packer()
{
dispose();
}
void Packer::add(uint64_t id, int width, int height, const Color* pixels)
{
add_entry(id, width, height, pixels);
}
void Packer::add(uint64_t id, const Image& image)
{
add_entry(id, image.width, image.height, image.pixels);
}
void Packer::add(uint64_t id, const String& path)
{
add(id, Image(path.cstr()));
}
void Packer::add_entry(uint64_t id, int w, int h, const Color* pixels)
{
m_dirty = true;
Entry entry(id, RectI(0, 0, w, h));
// trim
int top = 0, left = 0, right = w, bottom = h;
// TOP:
for (int y = 0; y < h; y++)
for (int x = 0, s = y * w; x < w; x++, s++)
if (pixels[s].a > 0)
{
top = y;
goto JUMP_LEFT;
}
JUMP_LEFT:
for (int x = 0; x < w; x++)
for (int y = top, s = x + y * w; y < h; y++, s += w)
if (pixels[s].a > 0)
{
left = x;
goto JUMP_RIGHT;
}
JUMP_RIGHT:
for (int x = w - 1; x >= left; x--)
for (int y = top, s = x + y * w; y < h; y++, s += w)
if (pixels[s].a > 0)
{
right = x + 1;
goto JUMP_BOTTOM;
}
JUMP_BOTTOM:
for (int y = h - 1; y >= top; y--)
for (int x = left, s = x + y * w; x < right; x++, s++)
if (pixels[s].a > 0)
{
bottom = y + 1;
goto JUMP_END;
}
JUMP_END:;
// pixels actually exist in this source
if (right >= left && bottom >= top)
{
entry.empty = false;
// store size
entry.frame.x = -left;
entry.frame.y = -top;
entry.packed.w = (right - left);
entry.packed.h = (bottom - top);
// create pixel data
entry.memory_index = m_buffer.position();
// copy pixels over
if (entry.packed.w == w && entry.packed.h == h)
{
m_buffer.write((char*)pixels, sizeof(Color) * w * h);
}
else
{
for (int i = 0; i < entry.packed.h; i++)
m_buffer.write((char*)(pixels + left + (top + i) * entry.frame.w), sizeof(Color) * entry.packed.w);
}
}
entries.push_back(entry);
}
void Packer::pack()
{
if (!m_dirty)
return;
m_dirty = false;
pages.clear();
// only if we have stuff to pack
auto count = entries.size();
if (count > 0)
{
// get all the sources sorted largest -> smallest
Vector<Entry*> sources;
{
sources.resize(count);
int index = 0;
for (int i = 0; i < entries.size(); i++)
sources[index++] = &entries[i];
std::sort(sources.begin(), sources.end(), [](Packer::Entry* a, Packer::Entry* b)
{
return a->packed.w * a->packed.h > b->packed.w * b->packed.h;
});
}
// make sure the largest isn't too large
if (sources[0]->packed.w + padding * 2 > max_size || sources[0]->packed.h + padding * 2 > max_size)
{
BLAH_ERROR("Source image is larger than max atlas size");
return;
}
// we should never need more nodes than source images * 3
// if this causes problems we could change it to use push_back I suppose
Vector<Node> nodes;
nodes.resize(count * 4);
int packed = 0, page = 0;
while (packed < count)
{
if (sources[packed]->empty)
{
packed++;
continue;
}
int from = packed;
int index = 0;
Node* root = nodes[index++].Reset(RectI(0, 0, sources[from]->packed.w + padding * 2 + spacing, sources[from]->packed.h + padding * 2 + spacing));
while (packed < count)
{
if (sources[packed]->empty)
{
packed++;
continue;
}
int w = sources[packed]->packed.w + padding * 2 + spacing;
int h = sources[packed]->packed.h + padding * 2 + spacing;
Node* node = root->Find(w, h);
// try to expand
if (node == nullptr)
{
bool canGrowDown = (w <= root->rect.w) && (root->rect.h + h < max_size);
bool canGrowRight = (h <= root->rect.h) && (root->rect.w + w < max_size);
bool shouldGrowRight = canGrowRight && (root->rect.h >= (root->rect.w + w));
bool shouldGrowDown = canGrowDown && (root->rect.w >= (root->rect.h + h));
if (canGrowDown || canGrowRight)
{
// grow right
if (shouldGrowRight || (!shouldGrowDown && canGrowRight))
{
Node* next = nodes[index++].Reset(RectI(0, 0, root->rect.w + w, root->rect.h));
next->used = true;
next->down = root;
next->right = node = nodes[index++].Reset(RectI(root->rect.w, 0, w, root->rect.h));
root = next;
}
// grow down
else
{
Node* next = nodes[index++].Reset(RectI(0, 0, root->rect.w, root->rect.h + h));
next->used = true;
next->down = node = nodes[index++].Reset(RectI(0, root->rect.h, root->rect.w, h));
next->right = root;
root = next;
}
}
}
// doesn't fit
if (node == nullptr)
break;
// add
node->used = true;
node->down = nodes[index++].Reset(RectI(node->rect.x, node->rect.y + h, node->rect.w, node->rect.h - h));
node->right = nodes[index++].Reset(RectI(node->rect.x + w, node->rect.y, node->rect.w - w, h));
sources[packed]->packed.x = node->rect.x + padding;
sources[packed]->packed.y = node->rect.y + padding;
packed++;
}
// get page size
int pageWidth, pageHeight;
if (power_of_two)
{
pageWidth = 2;
pageHeight = 2;
while (pageWidth < root->rect.w)
pageWidth *= 2;
while (pageHeight < root->rect.h)
pageHeight *= 2;
}
else
{
pageWidth = root->rect.w;
pageHeight = root->rect.h;
}
// create each page
{
pages.emplace_back(pageWidth, pageHeight);
// copy image data to image
for (int i = from; i < packed; i++)
{
sources[i]->page = page;
if (!sources[i]->empty)
{
RectI dst = sources[i]->packed;
Color* src = (Color*)(m_buffer.data() + sources[i]->memory_index);
// TODO:
// Optimize this?
if (padding > 0)
{
pages[page].set_pixels(RectI(dst.x - padding, dst.y, dst.w, dst.h), src);
pages[page].set_pixels(RectI(dst.x + padding, dst.y, dst.w, dst.h), src);
pages[page].set_pixels(RectI(dst.x, dst.y - padding, dst.w, dst.h), src);
pages[page].set_pixels(RectI(dst.x, dst.y + padding, dst.w, dst.h), src);
}
pages[page].set_pixels(dst, src);
}
}
}
page++;
}
}
}
void Packer::clear()
{
pages.clear();
entries.clear();
m_dirty = false;
}
void Packer::dispose()
{
pages.clear();
entries.clear();
m_buffer.close();
max_size = 0;
power_of_two = 0;
spacing = 0;
m_dirty = false;
}
Packer::Node::Node()
: used(false), rect(0, 0, 0, 0), right(nullptr), down(nullptr) { }
Packer::Node* Packer::Node::Find(int w, int h)
{
if (used)
{
Packer::Node* r = right->Find(w, h);
if (r != nullptr)
return r;
return down->Find(w, h);
}
else if (w <= rect.w && h <= rect.h)
return this;
return nullptr;
}
Packer::Node* Packer::Node::Reset(const RectI& rect)
{
used = false;
this->rect = rect;
right = nullptr;
down = nullptr;
return this;
}