/* mkbmfont.c : Like mkfont but the output is in BMFont format. The code is rather messy, since it's a tool meant for personal use, apologies in advance. Copyright (c) 2026 Marisa the Magician, UnSX Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Known bugs/limitations/quirks: - Unlike my old mkfont tool, proportional fonts are handled just fine, and non-bitmap font handling "just works". Unicode blocks beyond the BMP are also covered too, funny enough. - Packing algorithm for the texture atlas is very naive and may not offer the finest density, but at least it's fast and dead simple to implement in plain C. - Does not recognize visually identical glyphs that share the same code point, so there will be potential duplicates in the atlas. - Compiling with -DASCII_ONLY is required if you want to use this with Godot, as it doesn't support unicode bitmap fonts for some reason. */ #include #include #include #include #include #include #include #include FT_FREETYPE_H typedef struct { int16_t fontSize; uint8_t bitField; uint8_t charSet; uint16_t stretchH; uint8_t aa, paddingUp, paddingRight, paddingDown, paddingLeft, spacingHoriz, spacingVert, outline; } __attribute__((packed)) bmfinfo_t; typedef struct { uint16_t lineHeight, base, scaleW, scaleH, pages; uint8_t bitField, alphaChnl, redChnl, greenChnl, blueChnl; } __attribute__((packed)) bmfcommon_t; typedef struct { uint32_t id; uint16_t x, y, width, height; int16_t xoffset, yoffset, xadvance; uint8_t page, chnl; } __attribute__((packed)) bmfchar_t; typedef struct { uint32_t unicode, glyph; uint16_t x, y, w, h; int16_t xofs, yofs, xadv; uint8_t page, packed; // channels not needed (always defaults to 15) } packglyph_t; // BEGIN naive packing algo static int cmpglyph( const void *p1, const void *p2 ) { // cast for convenience packglyph_t *a = (packglyph_t*)p1, *b = (packglyph_t*)p2; // check which is taller first int hdiff = b->h-a->h; if ( hdiff ) return hdiff; // if equal height, check which is wider int wdiff = b->w-a->w; if ( wdiff ) return wdiff; // equal height and width, just sort by unicode value return a->unicode-b->unicode; } static void nextbox( int* boxw, int* boxh ) { // increase width before height if ( *boxw > *boxh ) *boxh <<= 1; else *boxw <<= 1; } // some sane defaults here #ifndef PACK_TRIES #define PACK_TRIES 100 #endif #ifndef MAX_PACK_SIDE #define MAX_PACK_SIDE 1024 #endif #ifndef MAX_PAGES #define MAX_PAGES 36 #endif int packglyphs( packglyph_t *glyphs, uint32_t nglyphs, int* boxw, int* boxh ) { // sort the glyphs first qsort(glyphs,nglyphs,sizeof(packglyph_t),cmpglyph); int pageno = 0; size_t packed = 0; for ( int i=0; i *boxw ) { y += lh; x = 0; lh = 0; } // past box height, bail out if ( y+glyphs[j].h+1 > *boxh ) break; // otherwise pack it and advance glyphs[j].x = x; glyphs[j].y = y; x += glyphs[j].w+1; // save tallest in row if ( (glyphs[j].h+1) > lh ) lh = glyphs[j].h+1; glyphs[j].page = pageno; glyphs[j].packed = 1; packed++; } if ( packed == nglyphs ) return 0; // box is already at size limit, make a new page if ( (*boxw >= MAX_PACK_SIDE) && (*boxh >= MAX_PACK_SIDE) ) { pageno++; // if we're past the page limit, fail early if ( pageno >= MAX_PAGES ) return 2; continue; } // try again from scratch with a larger box packed = 0; for ( uint32_t j=0; jx < 0) || (g->x >= (aw-g->w)) || (g->y < 0) || (g->y >= (ah-g->h)) ) { oob = 1; return; } uint8_t *gpos = idata; uint8_t *apos = adata+(g->x+g->y*aw)*4; for ( int i=0; i<(g->h); i++ ) { memcpy(apos,gpos,(g->w)*4); gpos += w*4; apos += aw*4; } } void putpixel_grayscale( uint8_t v, uint8_t a, int x, int y ) { uint32_t tpos = (x+y*w)*4; // add alpha int alph = idata[tpos+3]; alph += a; if ( alph > 255 ) alph = 255; idata[tpos+3] = alph; // blend color int col = idata[tpos]*(a-255); col += v*a; col /= 255; idata[tpos] = col; idata[tpos+1] = col; idata[tpos+2] = col; } void putpixel_color( uint8_t v, uint8_t a, int x, int y ) { uint32_t tpos = (x+y*w)*4; // add alpha int alph = idata[tpos+3]; alph += a; if ( alph > 255 ) alph = 255; idata[tpos+3] = alph; // blend color (RGB) for ( int i=0; i<3; i++ ) { int col = idata[tpos+i]*(a-255); int palent = (v*palsize)/256; col += pal[palent*3+i]*a; col /= 255; idata[tpos+i] = col; } } void putpixel( uint8_t v, uint8_t a, int x, int y ) { if ( (x < 0) || (x >= w) || (y < 0) || (y >= h) ) { oob = 1; return; } if ( palsize == 0 ) putpixel_grayscale(v,a,x,y); else putpixel_color(v,a,x,y); } uint8_t lerpg( float a ) { if ( a >= 1. ) return 255; if ( a <= 0. ) return 64; return (uint8_t)(a*191+64); } int draw_glyph( FT_Bitmap *bmp, uint8_t v, int px, int py, int oy ) { if ( !bmp->buffer ) return 0; int drawn = 0; unsigned i, j; for ( j=0; jrows; j++ ) { uint8_t rv = v; // apply gradient, if any if ( v == 255 ) { float a; int ofs = j+oy; if ( ofs < 0 ) a = 0.; else a = ofs/(float)(h-gh); if ( (gradient&3) == 1 ) rv = lerpg(1.-a); else if ( (gradient&3) == 2 ) rv = lerpg(a); else if ( (gradient&3) == 3 ) rv = lerpg((a>.5)?((1.-a)*2.):(a*2.)); } for ( i=0; iwidth; i++ ) { if ( bmp->pixel_mode == FT_PIXEL_MODE_GRAY ) { uint8_t a = bmp->buffer[i+j*bmp->pitch]; drawn |= (a > 0); putpixel(rv,a,i+px,j+py); } else if ( bmp->pixel_mode == FT_PIXEL_MODE_MONO ) { // thanks to https://stackoverflow.com/a/14905971 unsigned p = bmp->pitch; uint8_t *row = &bmp->buffer[p*j]; uint8_t a = ((row[i>>3])&(128>>(i&7)))?255:0; drawn |= (a > 0); putpixel(rv,a,i+px,j+py); } } } return drawn; } int palinv = 0; void loadpalette( const char *path ) { FILE *f = fopen(path,"rb"); if ( !f ) { fprintf(stderr,"warning: could not open palette file '%s', falling back to grayscale\n",path); return; } fseek(f,0,SEEK_END); long sz = ftell(f); fseek(f,0,SEEK_SET); if ( sz <= 0 ) { fprintf(stderr,"warning: palette is empty, falling back to grayscale\n"); goto palend; } if ( !(sz%3) ) { // RGB8 palette if ( sz > 768 ) { fprintf(stderr,"warning: palette has more than 256 entries, extra colors will be ignored\n"); palsize = 256; } else palsize = sz/3; for ( int i=0; i 1024 ) { fprintf(stderr,"warning: palette has more than 256 entries, extra colors will be ignored\n"); palsize = 256; } else palsize = sz/4; for ( int i=0; ipage != b->page ) return a->page-b->page; // sort from lowest to highest unicode value second return a->unicode-b->unicode; } // used for naming a grand total of 36 potential pages char base36( uint8_t val ) { if ( val < 10 ) return '0'+val; else return 'A'+(val-10); } int main( int argc, char **argv ) { if ( argc < 4 ) { fprintf(stderr,"usage: mkbmfont [:face index] " " [gradient type (0,11)] [color palette] [-palinv]\n"); return 1; } if ( FT_Init_FreeType(&ftlib) ) { fprintf(stderr,"error: failed to init freetype library\n"); return 2; } int pxsiz; sscanf(argv[2],"%d",&pxsiz); char bmname[256], pname[256]; snprintf(bmname,256,"%s.fnt",argv[3]); if ( argc > 4 ) { sscanf(argv[4],"%d",&gradient); if ( (gradient < 0) || (gradient >= 12) ) { fprintf(stderr,"warning: gradient type out of range, falling back to type 0.\n"); gradient = 0; } } if ( !(gradient&8) ) { gw++; gh++; if ( gradient&4 ) { gx--; gy--; gw++; gh++; } } fprintf(stderr,"info: gradient selected is '%s'.\n",grads[gradient]); if ( argc > 6 ) palinv = !strcmp(argv[6],"-palinv"); if ( argc > 5 ) loadpalette(argv[5]); char *fidx = strrchr(argv[1],':'); int fnum = 0; if ( fidx ) { *fidx = 0; sscanf(fidx+1,"%d",&fnum); } if ( FT_New_Face(ftlib,argv[1],fnum,&fnt) ) { if ( fidx ) fprintf(stderr,"error: failed to open font '%s' face %d.\n",argv[1],fnum); else fprintf(stderr,"error: failed to open font '%s'.\n",argv[1]); return 4; } fprintf(stderr,"info: loaded font \'%s %s\'.\n",fnt->family_name,fnt->style_name); int nfnt = fnt->num_faces; if ( !fidx && (nfnt > 1) ) { fprintf(stderr,"info: loaded font has %d faces, you may want to load a specific one (see below).\n",nfnt); FT_Done_Face(fnt); for ( int i=0; ifamily_name,fnt->style_name); FT_Done_Face(fnt); } FT_Done_FreeType(ftlib); return 0; } if ( FT_Set_Pixel_Sizes(fnt,0,pxsiz) ) { if ( fnt->num_fixed_sizes <= 0 ) { fprintf(stderr,"error: font pixel size of '%d' not available.\n",pxsiz); return 8; } fprintf(stderr,"warning: failed to set pixel size '%d', trying fixed sizes.\n",pxsiz); int match = -1; for ( int i=0; inum_fixed_sizes; i++ ) { if ( fnt->available_sizes[i].height == pxsiz ) continue; match = i; break; } if ( (match == -1) || FT_Select_Size(fnt,match) ) { fprintf(stderr,"error: font fixed size of '%d' not available\n",pxsiz); fprintf(stderr,"available size(s): "); for ( int i=0; inum_fixed_sizes; i++ ) fprintf(stderr,"%u%s",fnt->available_sizes[i].height,(i<(fnt->num_fixed_sizes-1))?", ":".\n"); FT_Done_Face(fnt); FT_Done_FreeType(ftlib); return 8; } } lh = (fnt->size->metrics.height>>6); FT_Select_Charmap(fnt,FT_ENCODING_UNICODE); // pre-allocate uint32_t nglyphs = MAX_GLYPHS; packglyph_t* glyphs = calloc(nglyphs,sizeof(packglyph_t)); uint32_t cur = 0; // count up and populate valid glyphs for ( uint32_t i=0; i<=MAX_UNICODE; i++ ) { FT_UInt glyph = FT_Get_Char_Index(fnt,i); if ( !glyph || FT_Load_Glyph(fnt,glyph,LOADFLAGS) ) continue; FT_Render_Glyph(fnt->glyph,RENDERMODE); glyphs[cur].unicode = i; glyphs[cur].glyph = glyph; glyphs[cur].x = 0; glyphs[cur].y = 0; if ( !fnt->glyph->bitmap.buffer ) { glyphs[cur].w = 0; glyphs[cur].h = 0; glyphs[cur].xofs = 0; glyphs[cur].yofs = 0; } else { glyphs[cur].w = fnt->glyph->bitmap.width+gw; glyphs[cur].h = fnt->glyph->bitmap.rows+gh; glyphs[cur].xofs = fnt->glyph->bitmap_left+gx; glyphs[cur].yofs = (-fnt->glyph->bitmap_top)+gy; } glyphs[cur].xadv = fnt->glyph->advance.x>>6; // calculate baseline using 'n' as reference if ( i == 'n' ) bl = pxsiz-(-fnt->glyph->bitmap_top+fnt->glyph->bitmap.rows); cur++; if ( cur > nglyphs ) { fprintf(stderr,"warning: font somehow covers more code points than expected\n"); break; } } if ( cur <= 0 ) { fprintf(stderr,"error: no valid glyphs in font.\n"); FT_Done_Face(fnt); FT_Done_FreeType(ftlib); return 16; } // reduce array size if smaller, for convenience if ( nglyphs > cur ) { nglyphs = cur; glyphs = realloc(glyphs,nglyphs*sizeof(packglyph_t)); } // update offsets to account for baseline for ( uint32_t i=0; i w ) w = glyphs[i].w; if ( glyphs[i].h > h ) h = glyphs[i].h; } fprintf(stderr,"info: max cell size is %dx%d.\n",w,h); idata = calloc(4,w*h); // third pass, pack, allocate atlas, then draw int retval = packglyphs(glyphs,nglyphs,&aw,&ah); if ( retval ) { char errormsg[][32] = {"max tries hit","max pages hit"}; fprintf(stderr,"error: failed to pack glyphs into an adequate atlas: %s.\n",errormsg[retval-1]); free(idata); FT_Done_Face(fnt); FT_Done_FreeType(ftlib); return 32; } // re-sort qsort(glyphs,nglyphs,sizeof(packglyph_t),cmpglyph_final); adata = calloc(4,aw*ah); int npages = 1; for ( uint32_t i=0; iglyph,RENDERMODE); if ( !fnt->glyph->bitmap.buffer ) continue; int oy = (h-gh) - (2+fnt->glyph->bitmap_top); if ( gradient&8 ) draw_glyph(&fnt->glyph->bitmap,255,0,0,oy); else if ( gradient&4 ) { // outline first for ( int x=-1; x<=1; x++ ) for ( int y=-1; y<=1; y++ ) { // ignore center if ( !(x|y) ) continue; draw_glyph(&fnt->glyph->bitmap,0,1+x,1+y,oy); } draw_glyph(&fnt->glyph->bitmap,255,1,1,oy); } else { // drop shadow first draw_glyph(&fnt->glyph->bitmap,0,1,1,oy); draw_glyph(&fnt->glyph->bitmap,255,0,0,oy); } // are we switching pages? if ( glyphs[i].page >= npages ) { snprintf(pname,256,"%s%c.png",argv[3],base36(npages-1)); writepng(pname,adata,aw,ah,aw*4); memset(adata,0,aw*ah*4); npages++; } // blit to canvas blitglyph(glyphs+i); // clear for next draw memset(idata,0,w*h*4); } fprintf(stderr,"info: texture atlas size is %ux%u, with %d page(s).\n",aw,ah,npages); free(idata); if ( npages == 1 ) snprintf(pname,256,"%s.png",argv[3]); else snprintf(pname,256,"%s%c.png",argv[3],base36(npages-1)); writepng(pname,adata,aw,ah,aw*4); // don't forget to write the bmfont file #ifdef USE_BINARY_BMFONT FILE *bmf = fopen(bmname,"wb"); if ( !bmf ) { fprintf(stderr,"error: failed to open '%s' for writing: %s\n",bmname,strerror(errno)); free(adata); FT_Done_Face(fnt); FT_Done_FreeType(ftlib); return 64; } uint32_t bsiz = 0; // header fputc(66,bmf); fputc(77,bmf); fputc(70,bmf); fputc(3,bmf); // info char fontName[256]; snprintf(fontName,256,"%s %s",fnt->family_name,fnt->style_name); bmfinfo_t info = { pxsiz, 2, 0, 100, 1, 0, 0, 0, 0, 1, 1, 0 }; bsiz = sizeof(bmfinfo_t)+strlen(fontName)+1; fputc(1,bmf); fwrite(&bsiz,4,1,bmf); fwrite(&info,sizeof(bmfinfo_t),1,bmf); fwrite(fontName,strlen(fontName),1,bmf); fputc(0,bmf); // common bmfcommon_t cmn = { lh, bl, aw, ah, npages, 0, 0, 4, 4, 4 }; bsiz = sizeof(bmfcommon_t); fputc(2,bmf); fwrite(&bsiz,4,1,bmf); fwrite(&cmn,sizeof(bmfcommon_t),1,bmf); // pages fputc(3,bmf); if ( npages == 1 ) { snprintf(pname,256,"%s.png",argv[3]); bsiz = strlen(pname)+1; fwrite(&bsiz,4,1,bmf); fwrite(pname,strlen(pname),1,bmf); fputc(0,bmf); } else { snprintf(pname,256,"%s0.png",argv[3]); bsiz = (strlen(pname)+1)*npages; fwrite(&bsiz,4,1,bmf); for ( int i=0; ifamily_name,fnt->style_name,pxsiz); // common fprintf(bmf,"common lineHeight=%u base=%u scaleW=%u scaleH=%u pages=%d packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4\n",lh,bl,aw,ah,npages); // pages if ( npages == 1 ) { snprintf(pname,256,"%s.png",argv[3]); fprintf(bmf,"page id=0 file=\"%s\"\n",pname); } else for ( int i=0; i