3、字体压缩技术:点阵字体与矢量字体、字体子集化、字体渲染优化、自定义字体格式
嵌入式GUI里,字体这块儿,说实话,是很多项目后期才想起来要优化的。我见过不少团队,功能都调通了,结果发现字库占了一半的Flash空间,最后不得不砍动画效果来腾地方。今天咱们就把字体压缩这件事儿彻底聊透。
3.1 点阵字体 vs 矢量字体:选哪个?
先搞清楚这两个东西的本质区别。点阵字体,说白了就是一张张像素图。每个字符对应一个固定大小的位图,比如16x16、24x24。矢量字体存的则是数学曲线,用贝塞尔曲线描述字形轮廓,渲染时再实时计算像素。
| 对比维度 | 点阵字体 | 矢量字体 |
|---|---|---|
| 存储方式 | 像素矩阵 | 贝塞尔曲线+控制点 |
| 缩放效果 | 放大锯齿明显 | 任意缩放不失真 |
| 渲染速度 | 极快(直接拷贝像素) | 较慢(需要光栅化) |
| 内存占用 | 固定大小,多尺寸需多套 | 一套数据,多尺寸复用 |
| 适用场景 | 低分辨率屏、固定字号 | 高分辨率屏、动态字号 |
我在一个智能手表项目里就踩过坑。屏幕只有1.3寸,分辨率160x128,团队选了矢量字体,结果每次刷新时间显示都要卡顿一下。后来换成点阵字体,渲染速度直接提升了5倍。所以我的建议是:屏幕分辨率低于200PPI,且字号固定不变,优先用点阵。反之,如果屏幕是高清屏,或者用户能调字体大小,那就老老实实用矢量。
3.2 字体子集化:只留你需要的字符
你想想看,一个完整的宋体字库,GB2312就有6763个汉字,加上ASCII符号,轻松超过1MB。但你的产品可能只需要显示时间、温度、几个菜单项,总共不到200个字符。剩下的99%都是浪费。
字体子集化,就是只从原字库中提取你实际用到的字符,生成一个精简版。我常用的工具有两个:fonttools(Python库)和pyftsubset(命令行工具)。
# 用 pyftsubset 提取子集
pyftsubset NotoSansSC-Regular.ttf \
--text="0123456789:.-℃%ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" \
--output-file=NotoSansSC-Subset.ttf \
--flavor=woff \
--layout-features='*'
这个命令只保留了数字、冒号、点、百分号、温度符号和大小写字母。原来2.3MB的字体,子集化后只有28KB。嗯,这里要注意:--text参数里一定要包含所有可能出现的字符,包括空格和标点。我曾经漏了空格,结果界面上所有文字都挤在一起,排查了半天。
如果你的项目支持多语言,比如中文+英文+俄文,可以分别对每种语言做子集化,然后合并。但要注意不同字体的字形风格要统一,否则界面会显得很乱。
3.3 字体渲染优化:让文字更清晰
矢量字体渲染慢,主要慢在光栅化这一步。把数学曲线变成像素,需要做抗锯齿、hinting(微调)等操作。在嵌入式平台上,CPU资源有限,得想办法优化。
我常用的优化手段有这几个:
- 预渲染缓存:把常用字符提前渲染成位图,存到内存里。显示时直接拷贝,不用再算。我习惯在初始化时把ASCII字符和数字全部预渲染好,中文按需渲染并缓存。
- 降低抗锯齿等级:全屏抗锯齿(4x4采样)效果最好,但计算量也最大。在320x240以下的屏幕上,2x2采样人眼基本看不出区别,速度能快一倍。
- 关闭不必要的hinting:Hinting是为了让小字号更清晰,但会大幅增加渲染时间。如果你的屏幕分辨率够高(比如300PPI以上),可以关掉hinting,让FreeType用原生渲染。
// FreeType 渲染配置示例
FT_Library library;
FT_Face face;
FT_Init_FreeType(&library);
FT_New_Face(library, "font.ttf", 0, &face);
FT_Set_Pixel_Sizes(face, 0, 16);
// 关闭 hinting,使用原生渲染
FT_Load_Char(face, 'A', FT_LOAD_NO_HINTING | FT_LOAD_TARGET_NORMAL);
// 或者使用轻量级抗锯齿
FT_Load_Char(face, 'B', FT_LOAD_TARGET_LIGHT);
3.4 自定义字体格式:为嵌入式量身定制
标准TrueType/OpenType格式,里面包含了很多嵌入式平台用不到的信息,比如字形名称、版权信息、复杂的布局表。这些冗余数据完全可以去掉。
我参与过一个项目,自己定义了一种极简字体格式,叫它EFont吧。结构非常简单:
// EFont 极简字体格式
typedef struct {
uint32_t magic; // 魔数,用于校验
uint16_t num_glyphs; // 字形数量
uint16_t height; // 字体高度
uint32_t index_offset; // 索引表偏移
uint32_t data_offset; // 字形数据偏移
} EFontHeader;
// 每个字形的索引项
typedef struct {
uint32_t unicode; // Unicode编码
uint32_t offset; // 数据偏移
uint32_t size; // 数据大小
uint16_t width; // 字形宽度
uint16_t height; // 字形高度
} EFontGlyphIndex;
这个格式去掉了所有元数据,只保留字形数据和索引。同样的字符集,比标准TTF小了40%。而且解析代码只有200行,不依赖FreeType,非常适合裸机环境。
当然,自定义格式也有代价:你得自己写一个PC端的转换工具,把TTF转成EFont。而且不支持复杂的排版特性,比如连字、竖排。如果你的产品只需要显示简单的文本,这个方案性价比很高。
3.5 知识体系总览
下面这张图,把字体压缩的整个决策流程串起来了。你可以把它当成一个检查清单,做项目时对着走一遍,基本不会漏掉关键点。
从图上能看出来,整个流程是分叉再汇聚的。先根据屏幕和字号需求选字体类型,然后分别做子集化或渲染优化,最后统一做压缩和部署。每一步都有取舍,没有银弹。
好了,字体压缩这块儿就聊到这儿。核心就三句话:点阵省CPU费存储,矢量省存储费CPU,子集化永远是最划算的优化。下个项目里,记得先算算你的字库占了多少空间,再决定怎么搞。