3、字体资源处理:点阵字体与矢量字体、字体文件格式(BDF、FON、TTF)、字体裁剪与子集化、字体渲染引擎(FreeType、STB Truetype)的集成
字体处理,在嵌入式GUI里是个绕不开的硬骨头。我早年做第一块带屏幕的物联网面板时,就栽在字体上——显示个中文“设置”两个字,Flash直接爆了。嗯,从那以后我才真正开始认真琢磨字体资源到底该怎么管。
说白了,字体就是一堆形状的集合。但怎么存、怎么取、怎么画,这里面的门道可不少。咱们这一章就把点阵和矢量这两大流派,以及它们背后的文件格式、裁剪技巧、渲染引擎集成,一次性讲透。
核心观点:嵌入式环境里没有“万能字体方案”。点阵适合小屏低资源,矢量适合大屏高分辨率。选型的关键在于你的Flash余量、CPU算力和屏幕PPI。
3.1 点阵字体 vs 矢量字体
先看个最直观的对比。点阵字体,说白了就是把每个字符画在一个固定大小的网格里,比如16x16像素。每个像素要么黑要么白,存起来就是一个bit。矢量字体则存的是轮廓的数学描述——贝塞尔曲线、直线段这些。
| 对比维度 | 点阵字体 | 矢量字体 |
|---|---|---|
| 存储方式 | 像素位图,每个点1bit | 轮廓曲线数学描述 |
| 缩放效果 | 放大后锯齿明显 | 任意缩放不失真 |
| 内存占用 | 固定大小,随字号线性增长 | 字库文件较大,但运行时按需渲染 |
| 渲染速度 | 直接拷贝像素,极快 | 需要曲线光栅化,较慢 |
| 适用场景 | 小屏、低算力MCU | 高分辨率屏、需要多字号 |
我个人习惯是:如果屏幕分辨率低于320x240,且只需要固定字号,点阵字体是首选。反过来,如果你的设备跑着Cortex-A系列或者带FPU的M7,那矢量字体的灵活性会让你省很多事。
我的经验:曾经在一个温控器项目里,为了省Flash,我把所有中文用点阵16x16存储,英文用矢量。结果发现混合渲染时坐标对齐特别麻烦。后来干脆全用点阵,虽然大了点,但代码简单多了。有时候“笨办法”反而更可靠。
3.2 字体文件格式:BDF、FON、TTF
聊完概念,咱们看看实际文件长什么样。嵌入式里常见的三种格式,各有各的脾气。
3.2.1 BDF(Bitmap Distribution Format)
BDF是纯文本格式,点阵字体的“源代码”。你拿记事本打开就能看到每个字符的像素排布。我早期做项目时,经常手动改BDF文件来调整某些字符的间距——虽然原始,但很直接。
STARTCHAR 中
ENCODING 20013
BBX 16 16 0 -2
BITMAP
0000
7FFC
4204
7FFC
4204
7FFC
4204
4204
7FFC
4204
4204
7FFC
4204
4204
7FFC
0000
ENDCHAR
你看,每个十六进制数代表一行16个像素。BDF的好处是解析简单,坏处是文件体积大——一个16x16的GB2312字库,BDF文本格式能到好几兆。所以实际用的时候,我通常会把它转成二进制格式再烧录。
3.2.2 FON(Windows Font Resource)
FON是Windows下的点阵字体容器,本质上是个NE格式的DLL,里面嵌了多个尺寸的点阵位图。嵌入式里直接用的不多,但如果你在做Windows CE或某些兼容系统,可能会碰到。我个人建议:除非有兼容性需求,否则别碰FON,解析起来太折腾。
3.2.3 TTF(TrueType Font)
TTF是矢量字体的代表。它用二次贝塞尔曲线描述字形轮廓,外加一系列的表(tables)来管理度量信息、字距调整、提示信息(hinting)等。TTF的结构其实挺清晰的:
- head表:全局信息,版本、单位、边界框
- cmap表:字符编码到字形索引的映射
- glyf表:实际的轮廓数据
- hmtx表:水平度量,每个字符的宽度和左轴承
- kern表:字距调整
我记得第一次手动解析TTF时,被它的字节序和表偏移搞得头大。后来发现直接用FreeType库,几行代码就能搞定。但理解这些表结构,对后面做字体裁剪很有帮助。
注意:TTF的hinting信息对低分辨率显示至关重要。没有hinting的矢量字体在96dpi以下的屏幕上会糊成一团。我曾在320x240的屏上试过,结果“工”字的三横直接粘在一起了。所以选TTF时,一定要确认它包含hinting数据。
3.3 字体裁剪与子集化
这是嵌入式字体处理里最实用的技能。一个完整的TTF文件动辄几兆,但你的设备可能只需要显示几十个汉字和ASCII字符。裁剪掉不需要的字符,能省下70%-90%的空间。
字体子集化的核心思路就三步:
- 收集字符集:遍历你的UI字符串,提取所有用到的Unicode码点
- 映射字形索引:通过cmap表找到每个字符对应的glyph ID
- 重建字体文件:只保留用到的glyph,重新打包成TTF或点阵字库
工具方面,我常用的是fonttools(Python库)和pyftsubset。举个例子:
# 只保留中文和常用符号
pyftsubset NotoSansSC-Regular.ttf \
--unicodes="U+0020-007E,U+4E00-9FFF" \
--output-file=NotoSansSC-subset.ttf \
--layout-features='*' \
--flavor=woff2
这个命令把Noto Sans SC从15MB砍到了1.2MB。如果你只需要显示“温度湿度设置”这六个字,甚至可以进一步缩小到几十KB。
避坑指南:我曾经只裁剪了字符轮廓,忘了保留cmap表和hmtx表,结果渲染时所有字符都叠在一起。记住,子集化不是简单删glyph,必须同时更新所有相关的表。用现成工具比自己手写靠谱得多。
3.4 字体渲染引擎集成:FreeType vs STB Truetype
有了字体文件,怎么把它画到屏幕上?这就轮到渲染引擎登场了。嵌入式领域两大主流:FreeType和STB Truetype。
| 特性 | FreeType | STB Truetype |
|---|---|---|
| 代码体积 | 约200-300KB(可裁剪) | 约20KB(单头文件) |
| 功能完整度 | 完整支持TTF/OTF/CFF/Type1 | 仅支持TTF基础功能 |
| Hinting支持 | 完整(AutoHint + 原生Hint) | 有限(仅原生Hint) |
| 渲染质量 | 高,支持亚像素抗锯齿 | 中等,基础抗锯齿 |
| 内存占用 | 较高,需要工作缓冲区 | 极低,按需分配 |
| 集成难度 | 中等,需要配置编译选项 | 极低,直接#include即可 |
我个人习惯是:如果MCU的Flash大于512KB且需要高质量中文渲染,上FreeType。如果Flash吃紧或者只需要显示数字和英文,STB Truetype足够了。
3.4.1 FreeType集成要点
FreeType的集成其实不复杂,但有几个坑要注意。我贴一段最小集成代码:
#include <ft2build.h>
#include FT_FREETYPE_H
FT_Library library;
FT_Face face;
// 1. 初始化库
FT_Init_FreeType(&library);
// 2. 加载字体文件(从内存或文件系统)
FT_New_Memory_Face(library, font_data, font_size, 0, &face);
// 3. 设置字号(以1/64像素为单位)
FT_Set_Pixel_Sizes(face, 0, 16);
// 4. 加载并渲染字符
FT_Load_Char(face, 'A', FT_LOAD_RENDER);
// 渲染结果在 face->glyph->bitmap 中
// 5. 清理
FT_Done_Face(face);
FT_Done_FreeType(library);
这里要注意:FT_New_Memory_Face不会拷贝字体数据,所以font_data必须在整个使用周期内保持有效。我见过有人把字体数据放在局部变量里,结果一调用就崩溃。
重要:FreeType默认使用FT_CONFIG_OPTION_USE_PNG选项,如果你的工具链没有libpng,编译会报错。嵌入式环境下,我建议在ftoption.h里关掉所有不需要的模块:ZLIB、PNG、HarfBuzz等。一个最小化的FreeType可以裁到80KB左右。
3.4.2 STB Truetype集成
STB Truetype就简单多了。它只有一个头文件stb_truetype.h,直接include就能用。我经常在资源极度受限的MCU上用它:
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
stbtt_fontinfo font;
unsigned char *ttf_buffer; // 从Flash加载的字体数据
// 1. 初始化
stbtt_InitFont(&font, ttf_buffer, 0);
// 2. 获取字符度量
float scale = stbtt_ScaleForPixelHeight(&font, 16);
int ascent, descent, lineGap;
stbtt_GetFontVMetrics(&font, &ascent, &descent, &lineGap);
// 3. 获取字符位图
int width, height, xoff, yoff;
unsigned char *bitmap = stbtt_GetCodepointBitmap(
&font, scale, scale, 'A', &width, &height, &xoff, &yoff);
// 4. 绘制到framebuffer(伪代码)
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
if (bitmap[y * width + x] > 128)
draw_pixel(x + xoff, y + yoff, color);
// 5. 释放
stbtt_FreeBitmap(bitmap, NULL);
STB的API比FreeType直观得多,但功能也少。比如它不支持亚像素定位,也不支持复杂的OpenType特性。不过对于大多数嵌入式场景,够用了。
我的经验:在STM32F103上跑STB Truetype,渲染一个16x16的字符大约需要0.5ms(72MHz主频)。如果做全屏刷新,建议用DMA把渲染好的位图批量拷贝到显存,别一个像素一个像素地写。
3.5 本章知识体系
下面这张图把本章的核心脉络串起来了。从字体数据的源头(点阵/矢量),到文件格式,再到裁剪优化,最后到渲染引擎集成,是一条完整的数据流。
这张图从左到右、从上到下展示了字体数据的完整生命周期。你可以把它当作一个决策流程图:先确定用点阵还是矢量,再选文件格式,然后做裁剪,最后集成渲染引擎。每一步的选择都会影响最终的内存占用和显示效果。