简介:这是一款支持多国语言与全Windows字符集的点阵字库生成工具,主要面向嵌入式GUI开发、MTK平台及LCD显示驱动的工程师,用于快速生成和编辑BIN/TXT/BMP/BDF格式的非等宽字库,并可直接导出自定义bin文件。工具内置unicode支持,能在同一窗口呈现多国语言,亦提供阿拉伯文、泰文等复杂文字的处理。资源包内含42个文件,主要类型包括exe程序、PDF用户手册、c/h源码及示例工程、bat批处理与各类cp*代码页文件,大小仅1.4MB,适合对字库生成流程和编码转换有需求的开发者。该资源已有2616人学习下载。压缩包中附带了FontMaker与GuiTool两份用户手册,以及readfont_demo源码,可便于二次移植;同时xls表格和codepage文档可辅助查阅字符集映射,bin/bmp文件则提供了直接可用的输出样例。对需要精简、自定制点阵字库的工程师来说,是一份小而实用的工具包。
1. 一个 Unicode 点阵字库生成工具,把 TTF 变成能烧进单片机的字模表
做小屏设备时碰到的真问题:主控 RAM 只有几十 KB,跑不了矢量字体引擎,但设备要显示中文菜单,只能把字形预先处理成点阵字库。手上只有 OTF/TTF 矢量字体,一个汉字轮廓几百个控制点,直接塞进固件不现实;手动逐个截图取模,字符间距参差不齐,换字号就得重做一遍,字符集一扩就让人想放弃。而 Unicode 点阵字库生成工具 fontmaker,就是把字体文件、字符集、点阵尺寸喂进去,自动批量渲染、二值化、按指定格式输出字库的可复现链路。它适合做嵌入式小屏菜单、硬件面板、像素化显示方案和一切需要离线渲染文字的场景。下面先从存储逻辑讲起,再给出可复现的生成步骤和调参要点,最后是五条高频避坑记录。
2. 点阵字库的存储逻辑:Unicode 码点、字模行序与 fontmaker 的转换流程
2.1 先看懂字模在内存里怎么摆的
点阵字库的核心是按码点索引的固定大小位图。以 16×16 的中文点阵为例,一个字形在内存里占 32 字节,每行两个字节、共 16 行。把每个字节的 8 个 bit 看成 8 个像素,bit 为 1 就画黑,bit 为 0 就留着空白。这种组织方式的优点是 CPU 不需要解压,直接从表头加偏移取 32 字节往显存写即可,适合没有图形加速器的单片机。
字节内部还有一个方向问题:同一行两个字节,是从左往右排,还是 bit7 对应最左边的像素?两种都有人用。BDF 标准文件里约定 bitmap 从高位到低位排列,即第一个 bit 是左边第一个点;另一些老式取模软件则习惯按 LSB first 输出。这个细节如果不统一,后面会出现整行镜像的怪现象,第 5 章会专门讲排查路径。
码点与字模的对应关系通常靠一张索引表维护。最简结构是连续区间:比如只生成 U+4E00 到 U+4EFF 这一段,那么codepoint - 0x4E00就是字模下标,无需额外索引。更通用的是非连续区间,每个条目记{codepoint, offset}两个字段。字符数量少时顺序遍历即可,几百个字符以上建议按码点排序,查询用二分。
/* 非连续码点场景下的索引项示例 */ typedef struct { uint32_t codepoint; /* Unicode 码点 */ uint16_t offset; /* 相对字库数据区起始的字节偏移 */ } font_index_t;所以,在真正使用 fontmaker 之前,想清楚这三点:一是固定字框尺寸,二是字节内位序,三是索引是否连续。这三点决定了输出格式和后续驱动读取逻辑,比命令参数本身更影响最终效果。
2.2 fontmaker 把矢量字形变成点阵位图时做了哪几件事
常见做法是依赖字体解析库加载 TTF/OTF,然后按字符集逐个处理。内部流水线大致分四步:加载字体并解析 cmap 字符映射表;把字形轮廓缩放到目标点阵尺寸并光栅化;按阈值把灰度抗锯齿像素转成纯黑白;最后按用户选择的格式排列字节输出。
cmap 是关键。TTF 里的字形索引和 Unicode 码点不是天然一一对应,必须靠 cmap 子表查。中文字体通常提供多个 cmap 子表,比如 Unicode BMP、Unicode Full、GB18030 等。fontmaker 能不能正确处理多子表,决定了它输出的是不是你要的码点。那些生成后“汉字和码点对不上”的情况,十有八九是工具选了错误的子表或自行做了码表映射。
“最新”两个字的价值也体现在这一步:Unicode 码表一直在迭代,新的 CJK 扩展区字形在陆续补充。旧工具往往把支持范围写死在 BMP 的基本区,遇到扩展字符直接输出空白;更新及时的工具则按当前码表遍历,能覆盖更多合法码点。如果你要做的产品面向繁体、生僻字或少数民族文字,这个差异非常明显。
光栅化阶段的自由度也很大。同一字号、同一点阵尺寸,字体引擎渲染出的灰度图像可能不同。点阵尺寸越小,微小的舍入差异越容易被放大。这也是为什么换一台机器、换一个环境重新生成同一份字库,笔画细节会略有差别。可复现性要靠在固定环境里固化工具版本来保证。
2.3 为什么不能靠截图或手动取模,Unicode 方案好在哪
截图取模的思路是先渲染一个很大的字符位图,再按网格切割转成 0/1。问题是每个字符渲染时的基线、字宽、边距完全依赖渲染环境,切出来的字模会有的偏左、有的偏右,有的笔画粗、有的笔画细。还有抗锯齿:截出来的灰度图要选阈值,阈值不同笔画粗细就不同,没法批量调节。
Unicode 方案的好处是把“字符身份”与“字体表现”分开。程序里跑的是码点,设备上查的是点阵字库,二者只要货真价实地按 Unicode 对应,显示就稳定。新增一个字符,不需要在源码里增加一个特殊分支,只需要往字库里补一个码点。这正好是 fontmaker 这类工具的价值:批量处理 + 可重复生成 + 统一参数。
相比区位码或 GB2312 内码方案,Unicode 还能覆盖多语言混排。一个字库里同时放拉丁字母、全角标点和 CJK 统一表意文字,用同一套索引结构就能管理。对物联网小屏这类“设备只出少数语言但未来可能多语言”的产品来说,这个扩展性很重要。
3. 从 OTF 到成品字库:fontmaker 生成 16×16 中英文字库的完整流程
3.1 先盘点字体覆盖范围和字符集
拿到一个中文字体文件后,不要急着生成,先用字体工具确认覆盖范围。常用命令来自 fontconfig 工具包,执行成本很低。
# 查看字体文件的字符覆盖范围,输出为十六进制码点区间 fc-list :file=./myfont.otf charset | sed 's/^.*: //' | head -20输出形如20-7e a0-ff 4e00-9fff,意思是该字体支持 ASCII、拉丁补充和 CJK 基本区。如果目标字符里有扩展 B 区却不在这个区间,后面生成出来的就是豆腐块。先查一遍,能省掉很多无用工。
字符集列表的常见做法是按 Unicode 区间生成,而不是手写几百个字符。项目里只有少量中文、数字和全角标点很正常,直接取区间加过滤即可。
# 按 Unicode 区间生成字符集文件,保存为 UTF-8 无 BOM ranges = [ (0x0020, 0x007E), # ASCII 可见字符 (0x3000, 0x303F), # 全角标点 (0x4E00, 0x9FFF), # CJK 基本区 ] chars = [chr(code) for lo, hi in ranges for code in range(lo, hi + 1)] with open("charset.txt", "w", encoding="utf-8", newline="\n") as fp: fp.write("".join(chars))这段代码生成的文件可能包含一些字体未覆盖的码点,没关系,生成后再检查一遍即可。注意写入时用了newline="\n",避免 Windows 环境替换出不想要的字符。字符集文件必须是 UTF-8 无 BOM,否则第 5 章那个乱码坑会找上门。
3.2 一条命令生成 16×16 中英文字库
用法因工具版本而异,我习惯的参数名如下,实际操作前先跑一次fontmaker --help确认当前版本支持项。
fontmaker \ --font=./myfont.otf \ --size=16 \ --charset=./charset.txt \ --format=c \ --name=myfont \ --fixed-width=16 \ --output=./src/myfont.h这里做四件事:指定字体文件;把点阵尺寸设为 16×16;读取字符集文件;输出 C 头文件供固件直接 include。--fixed-width=16很重要,它让每个字形占统一宽度并水平居中,否则中文是 16 像素宽、ASCII 可能只有 7 像素宽,行内排版会参差不齐。
生成结束先看退出码。工具有任何字符无法渲染时通常会打印警告,不要忽略这些警告直接去烧录。如果工具支持--warn-missing,建议默认打开,让缺失字符一目了然。
3.3 生成的 C 头文件长什么样
输出 C 头文件时,一个 16×16 字形的典型片段如下。
/* 16x16 字库,行序存储:每行 2 字节,共 16 行 */ static const uint8_t glyph_4E00[32] = { 0x00, 0x00, 0x3F, 0xF8, 0x20, 0x08, 0x20, 0x08, 0x20, 0x08, 0x20, 0x08, 0x3F, 0xF8, 0x20, 0x08, 0x20, 0x08, /* ... 后续行 */ }; static const font_index_t myfont_index[] = { {0x20, 0x0000}, /* 空格 */ {0x21, 0x0020}, /* ! */ /* ... */ {0x4E00, 0x0A80}, /* 一 */ };查表函数按码点二分查找即可,字符量在几千以内性能无忧。如果工具支持生成连续区间版本,甚至可以直接用codepoint - first_code做下标,省掉整个索引表。选择哪种取决于你后续加字符的频率:频繁增删字符用索引表,固定字符集用连续下标。
文件里通常还会带一个FONT_GLYPH_COUNT或myfont_count宏定义,用于固件侧遍历或校验。注意头文件只是字库的一种载体,它不是唯一选择;BDF、Bin 格式各有用途,下面说明。
3.4 输出格式怎么选,BDF 和 Bin 各解决什么问题
| 输出格式 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| C 头文件 | 字库随固件一起编译 | 免文件系统,直接查表 | 改动字库要重编固件 |
| BDF | 调试、预览、中转格式 | 文本可读,能肉眼核对字形 | 体积大,不适合直接烧录 |
| Bin + 索引 | 字库放外部 Flash | 体积小,可单独升级字库 | 要自己管理加载和索引 |
调试阶段我强烈建议先生成一份 BDF。BDF 是文本格式,里面直接能看到每个字符的位串,比如“A”的 bitmap 是 7C 82 82 7C 这样的数据。遇到字形错位、方向反转、缺字等问题时,BDF 是最快的定位工具。确认无误后再用同一参数生成 C 头文件或 Bin。
Bin 格式通常会把索引和字模数据放在同一个文件里,有的版本还支持 RLE 压缩。对空间敏感的产品可以考虑,但压缩会引入解压耗时,要评估主控跑不跑得动。默认非压缩方案最稳。
4. fontmaker 调参的关键参数:点阵尺寸、二值化阈值、DPI 与字符区间
4.1 点阵尺寸决定体积上限和可读性下限
点阵尺寸直接决定字库体积和显示效果。以单色字模为例,16×16 每字符 32 字节,2000 个字符约 64 KB;32×32 则膨胀四倍到 256 KB。对只有百 KB 级 Flash 的主控来说,这个差距是决定性的。
| 尺寸 | 中文观感 | 常见用途 |
|---|---|---|
| 8×8 | 笔画拥挤,勉强辨识 | 英文、数字、状态图标 |
| 12×12 | 简体可读,复杂字偏糊 | 极简状态显示 |
| 16×16 | 笔画清晰,最低可接受标准 | 中文菜单、常规界面 |
| 24×24 / 32×32 | 接近印刷效果 | 大标题、高分辨率屏 |
经验值是 16×16 起步。很多团队为了省空间选 12×12,结果复杂汉字几乎无法辨认,最后又回来改 16×16。点阵尺寸每升一档,视觉效果提升明显,但体积也线性增长,这个平衡没有银弹,只能按产品实际屏尺寸去试。先做一版 16×16 的预览图,再决定要不要省空间。
4.2 阈值与抗锯齿:笔画粗细“玄学”的根源
TTF 渲染出来的其实是灰度像素,要变成单色点阵必须做二值化。阈值取 0.5 意味着灰度超过 50% 才画黑。阈值越高,笔画越细;阈值越低,笔画越粗、边缘越糊。中文小字号点阵对阈值极其敏感,同一个“永”字,阈值 0.4 和 0.6 生成的笔画粗细差别肉眼可见。
我的习惯是先用 0.5 生成一版,看汉字是否发虚。如果笔画断裂,降阈值;如果笔画糊成一团,升阈值。一般情况下 0.4 到 0.6 之间能找到可接受点。部分工具支持 4bit 或 5bit 灰度字模,Flash 宽裕时优先用灰度,小字号抗锯齿效果比纯黑白好一个档次。
关闭抗锯齿选项有时能让笔画更利落,但也可能让斜线出现明显锯齿。这个选项没有绝对优劣,以目标屏的物理像素密度为准。Pixels 密度高的屏建议保留轻微抗锯齿,密度低的屏直接关掉。
4.3 DPI 与 hinting:为什么同一套参数生成的字体忽大忽小
字体渲染里有一个经常被忽略的换算:字号按磅定义,显示像素数由 DPI 决定。工具如果提供 DPI 参数,那么磅值 * DPI / 72才是最终像素高度。同样的--size=16,如果工具内部默认 DPI 不同,渲染结果可能偏移一两个像素。
中文字体还有 hinting 的问题。小字号下没有 hinting,横画可能只剩一个像素、竖画却占两像素,笔画粗细失衡。很多黑体开源字体自带 hinting 指令,开启后小字号渲染质量明显提升。fontmaker 通常会透传一个--hinting选项,生成完不要只看几个常用字,挑笔画多的“德”“繁”“曦”这种字看,能暴露更多问题。
遇到字体怎么调都糊的情况,我的建议是换字体而不是继续调参。有些字体本身没有 hinting,点阵尺寸又小,再怎么调阈值都救不回来。开源中文字体里可选范围很大,换一个为屏幕阅读优化的字体会立竿见影。
4.4 按 Unicode 区块精确控制字符集
Unicode 的 CJK 相关码点分布很广。基本区在 U+4E00–U+9FFF,扩展 A 在 U+3400–U+4DBF,扩展 B 在 U+20000–U+2A6DF,还有一堆标点、兼容字符散落各处。如果全量生成,体积会让你怀疑人生。更合理的做法是按语言需求取子集。
只做简体中文产品时,基本区已经覆盖绝大多数常用字,配合 GB2312 常用字表过滤,能把字符数压到 3000 以内。做繁体中文时注意基本区含繁体字,但要补一些只存在于扩展区的字。多语言混排再加对应区块,例如日文假名在 U+3040–U+30FF,韩文谚文在 U+AC00–U+D7AF。
# 生成 ASCII + CJK 基本区 + 全角标点的通配写法 fontmaker \ --font=./myfont.otf \ --size=16 \ --range=0020-007E,3000-303F,4E00-9FFF \ --format=bin \ --output=./assets/myfont.bin有一点必须提醒:U+D800–U+DFFF 是代理区,不是实际字符,任何字库都不该包含这段。部分工具按区间遍历时会把代理区也扫进去白白占空间,检查生成的索引表时顺手过滤掉。Emoji 彩色表情符号也尽量别往点阵字库里放,彩色字体和单色点阵是两个技术路线,硬塞进来只会得到一串空字形。
5. fontmaker 使用中的常见问题与避坑:从豆腐块到字模错位的排查记录
5.1 生成结果大量出现豆腐块
现象:字库生成成功,但显示时很多字符是空心方块或空白,集中在某个区段。
原因:字体文件本身不包含这些字形。工具会为缺失字符生成一个.notdef占位字形,通常就是一个空框。
解决:换字体,或在字符集生成阶段先做覆盖过滤。先用下面的命令确认字体是否覆盖目标码点。
# 检查字体是否包含某个码位,例如 U+4E00 fc-query --format='%{charset}\n' ./myfont.otf | grep -i "4e00"如果返回为空,说明字体不覆盖,换字体文件;如果返回包含 4e00,再排查字符集文件是不是写错了区间。豆腐块问题的排查看上去在生成阶段,根子往往在字体选型阶段。
5.2 中文码点错乱,显示成完全不对的汉字
现象:明明生成的是“你好”,屏幕上显示的却是毫不相关的字,而且每次错位规律一致。
原因:字符集文件编码有问题。文件被保存成了 UTF-8 with BOM 或 UTF-16,工具按普通 UTF-8 逐字节读取,BOM 占掉一个码点,后续所有多字节汉字的解析整体偏移。
解决:字符集文件一律保存为 UTF-8 无 BOM。检查时用file charset.txt看编码声明,带 BOM 会显示UTF-8 (with BOM)。这个坑我踩过不止一次,文本编辑器默认编码不靠谱,生成后先看一眼字符数是否等于预期。
5.3 字模左右镜像或字节内位序颠倒
现象:普通字符比如“A”显示出来左右翻转,或者同一字模在某块屏上正常、在另一块屏上镜像。
原因:生成工具的位序约定和显示驱动的扫描方向不一致。BDF 约定最高位对应最左边,但某些工具或老式取模器按 LSB first 输出。
解决:先用 BDF 格式生成,肉眼确认一个已知字形。以“A”的 8×8 为例,正确位串通常能看出顶部尖、中间横的轮廓。然后调整生成端的--msb-first或--lsb-first参数,匹配驱动读取逻辑。这里没有谁对谁错,只有“两边一致”。别在固件里做按位反转,能省则省。
5.4 字库体积比预期大很多,固件放不下
现象:生成完发现有十几 MB,远超 Flash 余量。
原因:直接指定了全 Unicode 区间,生成了所有 BMP 甚至扩展平面字符。一个 16×16 字符 32 字节,全量几十万字符就是几十 MB。
解决:字符集收敛到产品实际需要的范围。砍掉代理区、兼容区,只留用得到的页面;如果工具支持 RLE 压缩,评估解压耗时后决定是否开启。对于中文设备,从 3500 常用字加全角标点开始,一般不超过 200 KB。
提示:输出 Bin 格式前,先看清索引结构占用。每索引条目如果是 4 字节码点 + 2 字节偏移,2000 字符的索引就占 12 KB,这个开销要计入字库总大小。
5.5 中文和英文混排时行内对不齐,字挤成一团
现象:中文正常,但英文和数字比中文窄半格,有的靠左有的靠右,行内排版像抖动。
原因:原字体是比例字体,每个字符的 advance width 不同。生成字库时如果保留了原始宽度,而显示端没有按宽度表取字,就会出现间距混乱。
解决:生成端开启固定字框选项,常见参数是--fixed-width或--cell-size,把所有字形统一到同一宽度并水平居中。如果产品需要严格等宽排版,干脆全部按中文的 16 像素宽生成,牺牲一点英文密度换取对齐稳定性。
6. 字库生成之后:用“字形预览页”做回归验证,再烧进固件
生成完毕先别急着烧录,花五分钟生成一张预览图,把几百个字符摆在一张大图上肉眼扫一遍。这一步能发现参数调得不合理的地方,也方便在换字体、改阈值之后做回归对比。
# 把 C 头文件里的字模数组重新渲染成总览图,供人眼检查 from PIL import Image, ImageDraw def render_preview(glyphs, cols=32, cell_size=16, scale=4): rows = (len(glyphs) + cols - 1) // cols w, h = cols * cell_size * scale, rows * cell_size * scale img = Image.new("1", (w, h), 0) draw = ImageDraw.Draw(img) for idx, bits in enumerate(glyphs): x0 = (idx % cols) * cell_size * scale y0 = (idx // cols) * cell_size * scale for row in range(cell_size): for bit in range(8): # 这里按每行两字节、MSB 在左解析 if (bits[row * 2] >> (7 - bit)) & 1: draw.rectangle( [x0 + bit * scale, y0 + row * scale, x0 + bit * scale + scale - 1, y0 + row * scale + scale - 1], fill=1 ) return img img = render_preview(parse_header("./src/myfont.h")) img.save("preview.png")预览页覆盖三个重点:中文关键字的笔画是否完整;英文数字是否等高;位序是否符合预期。把生成的 PNG 和原字体的屏幕截图并排放在一起,一眼就能发现问题。调整过阈值之后再生成一次,放在旁边对比,比单看一个字符靠谱得多。
固件侧也有一点常见教训:字库的行序存储和 LCD 驱动的扫描方向必须一致。有的驱动从上往下扫,有的从下往上,如果驱动不能改方向,就按驱动方向重新生成一版,不要试图在运行时翻转每一行。这个决策在生成阶段定死,后面烧录的固件就越少。
我在这个方向上有两个习惯:第一,每次生成先出预览页检查所有字符,而不是抽查几个常见字;第二,在生成脚本里固定字体、阈值、hinting 和输出格式的完整参数,保证下次改字符集时能原样复现。字库生成这件事,参数失控造成的返工远比想象的多。这条路走通之后,后续换语言、加字符都会变得很省心,希望帮到你。
本文还有配套的精品资源,点击获取