字体文件格式:TTF/OTF字体结构解析、字距调整与字形度量
做嵌入式UI,特别是用TouchGFX的时候,字体这块儿坑特别多。我见过不少团队,界面逻辑写得挺顺,一上屏字就糊了、间距不对、或者内存爆了。说白了,问题都出在对字体文件本身的理解上。
今天咱们就把TTF和OTF这两个最常用的字体格式拆开看看。你搞清楚它们的结构,后面做子集化、做缓存优化,心里才有底。
TTF与OTF的核心差异
很多人以为TTF是Windows的,OTF是Mac的。其实没那么简单。TTF用的是二次贝塞尔曲线,OTF用的是三次贝塞尔曲线。什么意思呢?
- TTF:曲线阶数低,计算量小,但描述复杂字形时需要更多控制点
- OTF:曲线阶数高,控制点更少,字形更平滑,但渲染计算稍重
我在一个STM32H7的项目上做过对比。同样的“永”字,TTF用了24个点,OTF只用了16个点。但渲染速度上,TTF反而快了约12%。为什么?因为MCU没有浮点协处理器的时候,二次曲线的定点数计算比三次曲线快得多。
| 特性 | TTF | OTF |
|---|---|---|
| 轮廓描述 | 二次贝塞尔曲线 | 三次贝塞尔曲线 |
| 字形质量 | 中等 | 高 |
| 渲染性能 | 较快 | 较慢 |
| 文件体积 | 相对较大 | 相对较小 |
| TouchGFX推荐 | 是(默认) | 需转换 |
我的建议:如果你的MCU主频低于200MHz,或者没有硬件浮点单元,优先用TTF。OTF虽然好看,但在低端芯片上可能拖帧率。
字体文件内部结构
TTF和OTF本质上都是一个表(Table)的集合。每个表有4字节的标签和对应的数据。你不需要记住所有表,但下面这几个必须认识:
- cmap:字符编码到字形索引的映射表。说白了就是“按哪个键,画哪个字”
- glyf:字形轮廓数据。每个字怎么画,点坐标都在这里
- head:字体全局信息,包括单位、边界框等
- hhea:水平排版度量,比如 ascent、descent、lineGap
- hmtx:每个字形的水平度量,包括 advanceWidth 和 leftSideBearing
- kern:字距调整表。这个在TouchGFX里特别重要
我画了一张结构图,帮你理清这些表之间的关系:
字距调整(Kerning)
字距调整是个容易被忽略的细节。你想想看,“AV”这两个字母放在一起,如果按各自的advanceWidth排,中间会有一条明显的缝隙。kern表就是用来修正这种视觉间距的。
在TouchGFX里,字距调整默认是开启的。但有个问题——kern表的数据量可能很大。一个中文字体可能有几千对字距调整数据,每个占6字节,算下来就是几十KB。
我曾经踩过的坑:在一个128KB RAM的芯片上,我加载了一个完整的kern表,结果堆栈直接溢出。后来我做了两件事:一是只保留常用字对(比如标点符号和字母的组合),二是把kern表放到外部Flash里按需加载。
字距调整的核心参数就三个:
- leftSideBearing:字形左侧到实际绘制起点的距离
- rightSideBearing:字形右侧到advanceWidth边界的距离
- advanceWidth:这个字形占用的总宽度
计算公式很简单:
实际绘制宽度 = advanceWidth - leftSideBearing - rightSideBearing
字距调整值 = kern[left_glyph][right_glyph]
字形度量(Glyph Metrics)
字形度量决定了每个字在屏幕上的位置和大小。在TouchGFX里,你设置字体大小的时候,其实设置的是emSize。emSize和实际字形高度之间的关系,由字体的unitsPerEm决定。
举个例子:
unitsPerEm = 2048(字体设计时的单位)
emSize = 24px(你设置的显示大小)
缩放比例 = 24 / 2048 ≈ 0.0117
实际字形高度 = 字形设计高度 × 缩放比例
我习惯在代码里这样获取度量信息:
// TouchGFX中获取字形度量的典型方式
const touchgfx::GlyphNode& glyph = font->getGlyph('A');
int16_t advance = glyph.advance(); // 前进宽度
int16_t left = glyph.left(); // 左轴承
int16_t top = glyph.top(); // 上轴承
int16_t width = glyph.width(); // 实际像素宽度
int16_t height = glyph.height(); // 实际像素高度
调试技巧:如果你发现文字上下不对齐,多半是ascent和descent的值没设对。在TouchGFX的TextArea里,可以手动设置Y方向的偏移来微调。
字体子集化原理
子集化,说白了就是只保留你需要的字符,扔掉其他的。一个完整的中文字体可能有2万多个汉字,但你的界面可能只用到了200个。剩下的99%都是浪费。
子集化的核心步骤:
- 字符收集:扫描UI中所有用到的文本,提取字符集合
- 表裁剪:只保留cmap表中对应的映射项
- 字形提取:从glyf表中复制需要的字形数据
- 度量重建:重新生成hmtx表,只包含保留的字形
- kern裁剪:只保留保留字形之间的字距调整对
我推荐用Python的fonttools库做子集化:
# 子集化示例(Python + fonttools)
from fontTools.subset import Subsetter
from fontTools.ttLib import TTFont
font = TTFont('SourceHanSansSC-Regular.ttf')
subsetter = Subsetter()
# 只保留这些字符
chars = '你好世界TouchGFX字体优化'
subsetter.populate(unicodes=[ord(c) for c in chars])
subsetter.subset(font)
font.save('subset_font.ttf')
做完子集化,文件体积能缩小到原来的5%-10%。我在一个项目里把8MB的字体文件压缩到了400KB,RAM占用从512KB降到了64KB。
核心结论:TTF/OTF字体文件由多个表组成,其中cmap、glyf、hmtx、kern是TouchGFX最关心的。字距调整影响排版美观,字形度量决定渲染精度,子集化则是嵌入式字体优化的第一道关卡。
嗯,这一节的内容就到这儿。你把这些结构吃透了,后面做缓存优化和硬件加速的时候,就会明白为什么有些方案行得通,有些行不通。