字体文件格式解析:TTF/OTF 结构、字距调整与 Unicode 映射

做嵌入式 GUI 这么多年,我拆过的字体文件少说也有上百个。说实话,很多人觉得字体就是个「黑盒子」——把 .ttf 扔进去,能显示字就行。但真到了 TouchGFX 里做资源压缩,你会发现不懂字体结构,连优化方向都找不到。

这一章,咱们就把 TTF/OTF 的底裤扒开看看。你不需要成为字体工程师,但得知道里面到底装了些什么。

TTF 与 OTF:核心差异在哪?

先解决一个基础问题:TTF 和 OTF 到底有什么区别?

TTF(TrueType Font)用的是二次贝塞尔曲线,数学上简单,渲染快。OTF(OpenType Font)用的是三次贝塞尔曲线,曲线更平滑,而且可以内嵌 PostScript 轮廓。我早期在 STM32F4 上做项目时,发现 OTF 的渲染计算量比 TTF 大不少——说白了,MCU 算力有限,选错格式会卡顿。

对比项 TTF (TrueType) OTF (OpenType)
轮廓曲线 二次贝塞尔 三次贝塞尔 / PostScript
渲染性能 较快 较慢(尤其小字号)
字形容量 最多 65535 个 最多 65535 个
高级排版 有限 支持连字、变体等
TouchGFX 推荐 ✅ 首选 ⚠️ 需测试
我的习惯:只要项目里没有特殊排版需求(比如阿拉伯语连字),我全部用 TTF。省心,省算力。

字体文件的「骨架」:关键表结构

一个 TTF/OTF 文件,本质上是一堆「表」的集合。每个表都有 4 字节的标签名。你不需要记住所有表,但下面这几个必须认识——它们直接决定了字体在 TouchGFX 里的表现。

TTF/OTF 核心表结构 字体文件头 cmap glyf hmtx kern 字符映射表 Unicode → 字形索引 TouchGFX 依赖此表 字形数据 轮廓 + 贝塞尔曲线 压缩主要对象 水平度量 advanceWidth + lsb 决定字符间距 字距调整表 特定字符对间距 可选,但影响美观 子集化关键 只保留用到的字符 栅格化 TouchGFX 内部转换 布局计算 决定文本换行 字距优化 可裁剪以减小体积 💡 压缩策略:保留 cmap + glyf + hmtx,kern 可选 删除不需要的 GSUB、GPOS、name 等表可大幅减小体积

cmap 表:Unicode 到字形索引的「翻译官」

cmap 表是字体里最重要的表之一。它的任务很简单:给你一个 Unicode 码点,返回对应的字形索引(glyph index)。

举个例子,字符 'A' 的 Unicode 是 U+0041,cmap 表会告诉你它在 glyf 表里的索引是 36(具体数字因字体而异)。

cmap 表有多种子格式,最常见的是 Format 4(BMP 范围,U+0000 ~ U+FFFF)和 Format 12(完整 Unicode 范围)。

关键点:TouchGFX 在加载字体时,会遍历 cmap 表来建立自己的字符映射缓存。如果你只用到 50 个字符,但字体里 cmap 表包含了 20000 个映射——这就是浪费。子集化就是只保留你需要的映射条目。

字形度量:每个字符的「尺寸说明书」

字形度量决定了字符在屏幕上怎么摆放。每个字符都有以下几个关键参数:

  • advanceWidth:字符占用的水平宽度。说白了就是「这个字占多宽」。
  • lsb (left side bearing):左侧空白距离。
  • rsb (right side bearing):右侧空白距离。
  • bbox (bounding box):字形实际轮廓的包围盒。

这些数据存在 hmtx 表和 glyf 表里。我遇到过一个问题:某个字体在 PC 上显示正常,但到嵌入式设备上文字挤在一起。查了半天,发现是 hmtx 表里的 advanceWidth 被 TouchGFX 的字体生成工具重新计算了——因为原始字体的度量单位是 2048 UPM,而 TouchGFX 内部用的是 1024 UPM,缩放时精度丢失了。

避坑指南:我曾经在 STM32H743 上调试一个中文字体,发现「一」和「二」的宽度一样。后来检查发现,原始字体里「一」的 advanceWidth 是 1024,「二」是 1024——但「一」的实际轮廓只有 800 宽。TouchGFX 默认使用 advanceWidth 做布局,导致「一」后面空了一大截。解决方案:在字体生成时启用「精确度量」模式,让 TouchGFX 使用实际轮廓宽度。

字距调整(Kerning):让文字「呼吸」

字距调整解决的是特定字符对之间的间距问题。比如 "AV" 这两个字母,如果按默认的 advanceWidth 排列,A 和 V 之间会有一个明显的空隙。字距调整表(kern 表)会告诉渲染引擎:当 A 后面跟着 V 时,V 需要向左移动 50 个单位。

在 TouchGFX 里,字距调整默认是开启的。但这里有个取舍:

场景 建议 原因
英文界面,字号 ≥ 16px 保留字距调整 视觉效果好,尤其标题
中文界面 关闭字距调整 中文字符几乎不需要 kern
小字号(≤ 12px) 关闭字距调整 像素级渲染,字距影响不明显
资源极度紧张 关闭字距调整 kern 表可能占 5%~15% 体积
我的做法:在 TouchGFX Designer 里生成字体时,我会先看项目里有没有英文标题。如果有,保留 kern;如果全是中文或纯数字,直接勾掉「Include kerning」。省下来的空间可以多放几个图标字体。

Unicode 编码与字符映射:别被「乱码」坑了

Unicode 编码是字符的唯一身份证。但字体文件里怎么存这些编码?

cmap 表里维护了一个映射表。比如:

Unicode U+0041 → Glyph Index 36
Unicode U+0042 → Glyph Index 37
Unicode U+4E2D → Glyph Index 1234  (中)
Unicode U+6587 → Glyph Index 1235  (文)

TouchGFX 在渲染文本时,会先把字符串里的每个字符转成 Unicode 码点,然后查 cmap 表拿到 glyph index,最后从 glyf 表里取出轮廓数据去渲染。

这里有个常见的坑:字体里没有你需要的字符。比如你用了 "Hello 世界",但字体只包含 ASCII 字符,没有中文字形。TouchGFX 会显示一个空白方块(tofu)。

我曾经踩过的坑:有一次客户反馈说设备上温度显示成了 "25□C"。排查后发现,字体子集化时只保留了数字和字母,但漏掉了 "°"(度符号,U+00B0)。从那以后,我每次做字体子集化都会先列一个「必须包含字符清单」,包括标点符号、单位符号、特殊字符。

实战:如何用工具查看字体结构?

光说不练假把式。我推荐两个工具:

  • FontForge:开源,功能强大。可以查看每个表的原始数据。
  • ttx(fonttools):Python 工具,能把 TTF/OTF 转成 XML,方便分析。

用 ttx 查看 cmap 表的命令:

# 安装 fonttools
pip install fonttools

# 导出 cmap 表为 XML
ttx -t cmap myfont.ttf

# 查看生成的 myfont.ttx 文件
# 你会看到类似这样的内容:
# <cmap>
#   <tableVersion version="0"/>
#   <cmap_format_4 platformID="3" platEncID="1" language="0">
#     <map code="0x41" name="A"/>   <!-- U+0041 -->
#     <map code="0x42" name="B"/>   <!-- U+0042 -->
#   </cmap_format_4>
# </cmap>

你可以数一下有多少个 <map> 条目。如果字体有 20000 个字符,但你的项目只用 100 个——嗯,这就是压缩空间所在。

小结:这一章你该记住什么?

  • TTF 和 OTF 的核心区别在于曲线类型,TouchGFX 优先用 TTF。
  • cmap 表是 Unicode 到字形索引的桥梁,子集化就是裁剪这个表。
  • 字形度量(advanceWidth、lsb、rsb)决定了文本布局,注意缩放精度。
  • 字距调整(kern)对英文有用,对中文和小字号可以关闭以节省空间。
  • 用 ttx 或 FontForge 查看字体结构,是优化前的必要步骤。