// Sprite packing method borrowed from glorp engine and heavily modified. // For license safety, just run this as a build tool, don't build it into your game/program. // https://github.com/zorbathut/glorp // data we need to provide: // sx, sy // dx, dy // ox, oy // wx // line height // dist-per-pixel #include #include #include FT_FREETYPE_H #include #include #include #include #include #include "image/png_load.h" #include "image/zim_save.h" #define CHECK(x) if (!(x)) { printf("%i: CHECK failed on this line\n", __LINE__); exit(1); } using namespace std; static int global_id; static bool highcolor = false; enum Effect { FX_COPY = 0, FX_RED_TO_ALPHA_SOLID_WHITE = 1, // for alpha fonts FX_RED_TO_INTENSITY_ALPHA_255 = 2, FX_PREMULTIPLY_ALPHA = 3, FX_PINK_TO_ALPHA = 4, // for alpha fonts FX_INVALID = 5, }; const char *effect_str[5] = { "copy", "r2a", "r2i", "pre", "p2a", }; Effect GetEffect(const char *text) { for (int i = 0; i < 5; i++) { if (!strcmp(text, effect_str[i])) { return (Effect)i; } } return FX_INVALID; } template struct Image { vector > dat; void resize(int x, int y) { dat.resize(y); for(int i = 0; i < y; i++) dat[i].resize(x); } int width() const { return (int)dat[0].size(); } int height() const { return (int)dat.size(); } void copyfrom(const Image &img, int ox, int oy, int effect) { CHECK(img.dat[0].size() + ox <= dat[0].size()); CHECK(img.dat.size() + oy <= dat.size()); for (int y = 0; y < (int)img.dat.size(); y++) { for (int x = 0; x < (int)img.dat[y].size(); x++) { switch (effect) { case FX_COPY: dat[y + oy][ox + x] = img.dat[y][x]; break; case FX_RED_TO_ALPHA_SOLID_WHITE: dat[y + oy][ox + x] = 0x00FFFFFF | (img.dat[y][x] << 24); break; case FX_RED_TO_INTENSITY_ALPHA_255: dat[y + oy][ox + x] = 0xFF000000 | img.dat[y][x] | (img.dat[y][x] << 8) | (img.dat[y][x] << 16); break; case FX_PREMULTIPLY_ALPHA: { unsigned int color = img.dat[y][x]; unsigned int a = color >> 24; unsigned int r = (color & 0xFF) * a >> 8, g = (color & 0xFF00) * a>> 8, b = (color & 0xFF0000) * a >> 8; color = (color & 0xFF000000) | (r & 0xFF) | (g & 0xFF00) | (b & 0xFF0000); // Simulate 4444 color = color & 0xF0F0F0F0; color |= color >> 4; dat[y + oy][ox + x] = color; break; } case FX_PINK_TO_ALPHA: dat[y + oy][ox + x] = ((img.dat[y][x]&0xFFFFFF) == 0xFF00FF) ? 0x00FFFFFF : (img.dat[y][x] | 0xFF000000); break; default: dat[y + oy][ox + x] = 0xFFFF00FF; break; } } } } void set(int sx, int sy, int ex, int ey, unsigned char fil) { for(int y = sy; y < ey; y++) fill(dat[y].begin() + sx, dat[y].begin() + ex, fil); } bool LoadPNG(const char *png_name) { unsigned char *img_data; int w, h; if (1 != pngLoad(png_name, &w, &h, &img_data, false)) { printf("Failed to load %s\n", png_name); exit(1); return false; } dat.resize(h); for (int y = 0; y < h; y++) { dat[y].resize(w); memcpy(&dat[y][0], img_data + 4 * y * w, 4 * w); } free(img_data); return true; } void SavePNG(const char *png_name) { // Save PNG FILE *fil = fopen(png_name, "wb"); png_structp png_ptr; png_infop info_ptr; png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); CHECK(png_ptr); info_ptr = png_create_info_struct(png_ptr); CHECK(info_ptr); png_init_io(png_ptr, fil); //png_set_compression_level(png_ptr, Z_BEST_COMPRESSION); png_set_IHDR(png_ptr, info_ptr, dat[0].size(), dat.size(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_write_info(png_ptr, info_ptr); for(int y = 0; y < (int)dat.size(); y++) { png_write_row(png_ptr, (png_byte*)&dat[y][0]); } png_write_end(png_ptr, NULL); png_destroy_write_struct(&png_ptr, &info_ptr); } void SaveZIM(const char *zim_name, int zim_format) { uint8 *image_data = new uint8[width() * height() * 4]; for (int y = 0; y < height(); y++) { memcpy(image_data + y * width() * 4, &dat[y][0], width() * 4); } ::SaveZIM(zim_name, width(), height(), width() * 4, zim_format | ZIM_DITHER, image_data); } }; template bool operator<(const Image &lhs, const Image &rhs) { return lhs.dat.size() * lhs.dat[0].size() > rhs.dat.size() * rhs.dat[0].size(); } struct Data { // item ID int id; // dimensions of its spot in the world int sx, sy, ex, ey; // offset from the origin float ox, oy; // distance to move the origin forward float wx; int effect; }; bool operator<(const Data &lhs, const Data &rhs) { return lhs.id < rhs.id; // should be unique } string out_prefix; int NextPowerOf2(int x) { int powof2 = 1; // Double powof2 until >= val while (powof2 < x) powof2 <<= 1; return powof2; } struct Bucket { vector, Data> > items; void AddItem(const Image &img, const Data &dat) { items.push_back(make_pair(img, dat)); } vector Resolve(int image_width, Image &dest) { // Place all the little images - whatever they are. // Uses greedy fill algorithm. Slow but works surprisingly well, CPUs are fast. Image masq; masq.resize(image_width, 1); dest.resize(image_width, 1); sort(items.begin(), items.end()); for(int i = 0; i < (int)items.size(); i++) { int idx = items[i].first.dat[0].size(); int idy = items[i].first.dat.size(); CHECK(idx <= image_width); bool found = false; for(int ty = 0; ty < 2047 && !found; ty++) { if(ty + idy + 1 > (int)dest.dat.size()) { masq.resize(image_width, ty + idy + 1); dest.resize(image_width, ty + idy + 1); } // Brute force packing. for(int tx = 0; tx < image_width - (int)items[i].first.dat[0].size() && !found; tx++) { bool valid = !(masq.dat[ty][tx] || masq.dat[ty + idy - 1][tx] || masq.dat[ty][tx + idx - 1] || masq.dat[ty + idy - 1][tx + idx - 1]); if (valid) { for(int ity = 0; ity < idy && valid; ity++) for(int itx = 0; itx < idx && valid; itx++) if(masq.dat[ty + ity][tx + itx]) { valid = false; } } if (valid) { dest.copyfrom(items[i].first, tx, ty, items[i].second.effect); masq.set(tx, ty, tx + idx + 1, ty + idy + 1, 255); items[i].second.sx = tx; items[i].second.sy = ty; items[i].second.ex = tx + idx; items[i].second.ey = ty + idy; found = true; // printf("Placed %d at %dx%d-%dx%d\n", items[i].second.id, tx, ty, tx + idx, ty + idy); } } } } if ((int)dest.dat.size() > image_width) { printf("PACKING FAIL : height=%i", (int)dest.dat.size()); exit(1); } dest.resize(image_width, NextPowerOf2(dest.dat.size())); // Output the glyph data. vector dats; for(int i = 0; i < (int)items.size(); i++) dats.push_back(items[i].second); return dats; } }; const int supersample = 16; const int distmult = 64 * 3; // this is "one pixel in the final version equals 64 difference". reduce this number to increase the "blur" radius, increase it to make things "sharper" const int maxsearch = (128 * supersample + distmult - 1) / distmult; struct Closest { FT_Bitmap bmp; Closest(FT_Bitmap bmp) : bmp(bmp) { } float find_closest(int x, int y, char search) { int best = 1 << 30; for(int i = 1; i <= maxsearch; i++) { if(i * i >= best) break; for(int f = -i; f < i; f++) { int dist = i * i + f * f; if(dist >= best) continue; if(safe_access(x + i, y + f) == search || safe_access(x - f, y + i) == search || safe_access(x - i, y - f) == search || safe_access(x + f, y - i) == search) best = dist; } } return sqrt((float)best); } char safe_access(int x, int y) { if(x < 0 || y < 0 || x >= bmp.width || y >= bmp.rows) return 0; return bmp.buffer[x + y * bmp.width]; } }; void RasterizeFont(const char *fontfile, int fontsize, float *metrics_height, Bucket *bucket) { FT_Library freetype; CHECK(FT_Init_FreeType(&freetype) == 0); FT_Face font; CHECK(FT_New_Face(freetype, fontfile, 0, &font) == 0); printf("%d glyphs, %08x flags, %d units, %d strikes\n", (int)font->num_glyphs, (int)font->face_flags, (int)font->units_per_EM, (int)font->num_fixed_sizes); CHECK(FT_Set_Pixel_Sizes(font, 0, fontsize * supersample) == 0); // Character range. TODO: Make definable. We might want unicode // Convert all characters to bitmaps. for(int kar = 32; kar < 128; kar++) { Image img; if (0 != FT_Load_Char(font, kar, FT_LOAD_RENDER|FT_LOAD_MONOCHROME)) { img.resize(1,1); Data dat; dat.id = global_id++; dat.sx = 0; dat.sy = 0; dat.ex = 0; dat.ey = 0; dat.ox = 0; dat.oy = 0; dat.wx = 0; dat.effect = FX_RED_TO_ALPHA_SOLID_WHITE; bucket->AddItem(img, dat); continue; } // printf("%dx%d %p\n", font->glyph->bitmap.width, font->glyph->bitmap.rows, font->glyph->bitmap.buffer); const int bord = (128 + distmult - 1) / distmult + 1; if(font->glyph->bitmap.buffer) { FT_Bitmap tempbitmap; FT_Bitmap_New(&tempbitmap); FT_Bitmap_Convert(freetype, &font->glyph->bitmap, &tempbitmap, 1); Closest closest(tempbitmap); // No resampling, just sets the size of the image. img.resize((tempbitmap.width + supersample - 1) / supersample + bord * 2, (tempbitmap.rows + supersample - 1) / supersample + bord * 2); int lmx = img.dat[0].size(); int lmy = img.dat.size(); // AA by finding distance to character. Probably a fairly decent approximation but why not do it right? for(int y = 0; y < lmy; y++) { int cty = (y - bord) * supersample + supersample / 2; for(int x = 0; x < lmx; x++) { int ctx = (x - bord) * supersample + supersample / 2; float dist; if(closest.safe_access(ctx, cty)) { dist = closest.find_closest(ctx, cty, 0); } else { dist = -closest.find_closest(ctx, cty, 1); } dist = dist / supersample * distmult + 127.5; dist = floor(dist + 0.5); if(dist < 0) dist = 0; if(dist > 255) dist = 255; // Only set the red channel. We process when adding the image. img.dat[y][x] = (unsigned char)dist; } } FT_Bitmap_Done(freetype, &tempbitmap); } else { img.resize(1, 1); } Data dat; dat.id = global_id++; dat.sx = 0; dat.sy = 0; dat.ex = img.dat[0].size(); dat.ey = img.dat.size(); dat.ox = (float)font->glyph->metrics.horiBearingX / 64 / supersample - bord; dat.oy = -(float)font->glyph->metrics.horiBearingY / 64 / supersample - bord; dat.wx = (float)font->glyph->metrics.horiAdvance / 64 / supersample; dat.effect = FX_RED_TO_ALPHA_SOLID_WHITE; bucket->AddItem(img, dat); } *metrics_height = font->size->metrics.height; FT_Done_FreeType(freetype); } bool LoadImage(const char *imagefile, Effect effect, Bucket *bucket) { Image img; bool success = false; if (!strcmp(imagefile, "white.png")) { img.dat.resize(16); for (int i = 0; i < 16; i++) { img.dat[i].resize(16); for (int j = 0; j < 16; j++) { img.dat[i][j] = 0xFFFFFFFF; } } success = true; } else { success = img.LoadPNG(imagefile); // printf("loaded image: %ix%i\n", (int)img.dat[0].size(), (int)img.dat.size()); } if (!success) { return false; } Data dat; memset(&dat, 0, sizeof(dat)); dat.id = global_id++; dat.sx = 0; dat.sy = 0; dat.ex = img.dat[0].size(); dat.ey = img.dat.size(); dat.effect = effect; bucket->AddItem(img, dat); return true; } // Use the result array, and recorded data, to generate C++ tables for everything. struct FontDesc { string name; int first_char_id; int last_char_id; float ascend; float descend; float height; float metrics_height; void ComputeHeight(const vector &results, float distmult) { ascend = 0; descend = 0; for(int i = first_char_id; i < last_char_id; i++) { ascend = max(ascend, -results[i].oy); descend = max(descend, results[i].ey - results[i].sy + results[i].oy); } height = metrics_height / 64.0 / supersample; } void OutputSelf(FILE *fil, float tw, float th, const vector &results) { fprintf(fil, "const AtlasFont font_%s = {\n", name.c_str()); fprintf(fil, " %ff, // padding\n", height - ascend - descend); fprintf(fil, " %ff, // height\n", ascend + descend); fprintf(fil, " %ff, // ascend\n", ascend); fprintf(fil, " %ff, // distslope\n", distmult / 256.0); fprintf(fil, " {\n"); CHECK(last_char_id - first_char_id == 96); for(int i = first_char_id; i < last_char_id; i++) { fprintf(fil, " {%ff, %ff, %ff, %ff, %1.4ff, %1.4ff, %1.4ff, %i, %i}, // %i\n", /*results[i].id, */ results[i].sx / tw, results[i].sy / th, results[i].ex / tw, results[i].ey / th, results[i].ox, results[i].oy, results[i].wx, results[i].ex - results[i].sx, results[i].ey - results[i].sy, (i - first_char_id + 32)); } fprintf(fil, " },\n"); fprintf(fil, " \"%s\", // name\n", name.c_str()); fprintf(fil, "};\n"); } void OutputIndex(FILE *fil) { fprintf(fil, " &font_%s,\n", name.c_str()); } void OutputHeader(FILE *fil, int index) { fprintf(fil, "#define %s %i\n", name.c_str(), index); } }; struct ImageDesc { string name; Effect effect; int result_index; void OutputSelf(FILE *fil, float tw, float th, const vector &results) { int i = result_index; float toffx = 0.5f / tw; float toffy = 0.5f / th; fprintf(fil, " {%ff, %ff, %ff, %ff, %d, %d, \"%s\"},\n", results[i].sx / tw + toffx, results[i].sy / th + toffy, results[i].ex / tw - toffx, results[i].ey / th - toffy, results[i].ex - results[i].sx, results[i].ey - results[i].sy, name.c_str()); } void OutputHeader(FILE *fil, int index) { fprintf(fil, "#define %s %i\n", name.c_str(), index); } }; int main(int argc, char **argv) { // initProgram(&argc, const_cast(&argv)); // /usr/share/fonts/truetype/msttcorefonts/Arial_Black.ttf // /usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-R.ttf CHECK(argc >= 3); if (argc > 3) { highcolor = true; printf("RGBA8888 enabled!\n"); } printf("Reading script %s\n", argv[1]); const char *atlas_name = argv[2]; string image_name = string(atlas_name) + "_atlas.zim"; out_prefix = argv[2]; vector fonts; vector images; Bucket bucket; char line[512]; FILE *script = fopen(argv[1], "r"); if (!fgets(line, 512, script)) { printf("Error fgets-ing\n"); } int image_width; sscanf(line, "%i", &image_width); printf("Texture width: %i\n", image_width); while (!feof(script)) { if (!fgets(line, 511, script)) break; if (!strlen(line)) break; char *rest = strchr(line, ' '); if (rest) { *rest = 0; rest++; } char *word = line; if (!strcmp(word, "font")) { // Font! char fontname[256]; char fontfile[256]; int pixheight; sscanf(rest, "%s %s %i", fontname, fontfile, &pixheight); printf("Font: %s (%s) in size %i\n", fontname, fontfile, pixheight); FontDesc fnt; fnt.first_char_id = (int)bucket.items.size(); float metrics_height; RasterizeFont(fontfile, pixheight, &metrics_height, &bucket); fnt.name = fontname; fnt.last_char_id = (int)bucket.items.size(); CHECK(fnt.last_char_id - fnt.first_char_id == 96); fnt.metrics_height = metrics_height; fonts.push_back(fnt); } else if (!strcmp(word, "image")) { char imagename[256]; char imagefile[256]; char effectname[256]; sscanf(rest, "%s %s %s", imagename, imagefile, effectname); Effect effect = GetEffect(effectname); printf("Image %s with effect %s (%i)\n", imagefile, effectname, (int)effect); ImageDesc desc; desc.name = imagename; desc.effect = effect; desc.result_index = (int)bucket.items.size(); images.push_back(desc); if (!LoadImage(imagefile, effect, &bucket)) { fprintf(stderr, "Failed to load image %s\n", imagefile); } } else { fprintf(stderr, "Warning: Failed to parse line starting with %s\n", line); } } fclose(script); // Script read, all subimages have been generated. // Place the subimages onto the main texture. Also writes to png. Image dest; // Place things on the bitmap. printf("Resolving...\n"); vector results = bucket.Resolve(image_width, dest); if (highcolor) { printf("Writing .ZIM %ix%i RGBA8888...\n", dest.width(), dest.height()); dest.SaveZIM(image_name.c_str(), ZIM_RGBA8888); } else { printf("Writing .ZIM %ix%i RGBA4444...\n", dest.width(), dest.height()); dest.SaveZIM(image_name.c_str(), ZIM_RGBA4444); } // Also save PNG for debugging. printf("Writing .PNG %s\n", (image_name + ".png").c_str()); dest.SavePNG((image_name + ".png").c_str()); printf("Done. Outputting source files %s_atlas.cpp/h.\n", out_prefix.c_str()); // Sort items by ID. sort(results.begin(), results.end()); FILE *cpp_file = fopen((out_prefix + "_atlas.cpp").c_str(), "wb"); fprintf(cpp_file, "// C++ generated by atlastool from %s (hrydgard@gmail.com)\n\n", argv[1]); fprintf(cpp_file, "#include \"%s\"\n\n", (out_prefix + "_atlas.h").c_str()); for (int i = 0; i < (int)fonts.size(); i++) { FontDesc &xfont = fonts[i]; xfont.ComputeHeight(results, distmult); xfont.OutputSelf(cpp_file, dest.width(), dest.height(), results); } if (fonts.size()) { fprintf(cpp_file, "const AtlasFont *%s_fonts[%i] = {\n", atlas_name, (int)fonts.size()); for (int i = 0; i < (int)fonts.size(); i++) { fonts[i].OutputIndex(cpp_file); } fprintf(cpp_file, "};\n"); } if (images.size()) { fprintf(cpp_file, "const AtlasImage %s_images[%i] = {\n", atlas_name, (int)images.size()); for (int i = 0; i < (int)images.size(); i++) { images[i].OutputSelf(cpp_file, dest.width(), dest.height(), results); } fprintf(cpp_file, "};\n"); } fprintf(cpp_file, "const Atlas %s_atlas = {\n", atlas_name); fprintf(cpp_file, " \"%s\",\n", image_name.c_str()); if (fonts.size()) { fprintf(cpp_file, " %s_fonts, %i,\n", atlas_name, (int)fonts.size()); } else { fprintf(cpp_file, " 0, 0,\n"); } if (images.size()) { fprintf(cpp_file, " %s_images, %i,\n", atlas_name, (int)images.size()); } else { fprintf(cpp_file, " 0, 0,\n"); } fprintf(cpp_file, "};\n"); // Should output a list pointing to all the fonts as well. fclose(cpp_file); FILE *h_file = fopen((out_prefix + "_atlas.h").c_str(), "wb"); fprintf(h_file, "// Header generated by atlastool from %s (hrydgard@gmail.com)\n\n", argv[1]); fprintf(h_file, "#pragma once\n"); fprintf(h_file, "#include \"gfx/texture_atlas.h\"\n\n"); if (fonts.size()) { fprintf(h_file, "// FONTS_%s\n", atlas_name); for (int i = 0; i < (int)fonts.size(); i++) { fonts[i].OutputHeader(h_file, i); } fprintf(h_file, "\n\n"); } if (images.size()) { fprintf(h_file, "// IMAGES_%s\n", atlas_name); for (int i = 0; i < (int)images.size(); i++) { images[i].OutputHeader(h_file, i); } fprintf(h_file, "\n\n"); } fprintf(h_file, "extern const Atlas %s_atlas;\n", atlas_name); fprintf(h_file, "extern const AtlasImage %s_images[%i];\n", atlas_name, (int)images.size()); fclose(h_file); // TODO: Turn into C++ arrays. }