diff --git a/mkbmfont.c b/mkbmfont.c new file mode 100644 index 0000000..828ef28 --- /dev/null +++ b/mkbmfont.c @@ -0,0 +1,802 @@ +/* + 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