4. 字体压缩原理:无损压缩 vs 有损压缩、霍夫曼编码在字体压缩中的应用、字典压缩算法
说到字体压缩,我得先跟你交个底——嵌入式系统里,字库往往是吃 Flash 的大户。我早年做过一个工业仪表项目,UI 里需要显示 8 种语言,结果字库直接占了 6MB Flash。产品经理看着剩余空间直摇头,说「再加个功能就得换芯片了」。那会儿我才真正意识到,字体压缩不是锦上添花,而是刚需。
这一章,咱们就把压缩的底牌翻出来看看。你不需要成为算法专家,但得明白:什么时候该用无损,什么时候能接受有损,以及 TouchGFX 背后到底用了哪些招数。
4.1 无损压缩 vs 有损压缩:选哪个?
先问个问题:字体数据能不能丢?
答案是——字形轮廓不能丢,但颜色信息可以商量。这就引出了两种压缩路数:
| 对比维度 | 无损压缩 | 有损压缩 |
|---|---|---|
| 还原度 | 100% 还原原始数据 | 允许一定偏差 |
| 压缩比 | 通常 2:1 ~ 5:1 | 可达 10:1 以上 |
| 适用场景 | 小字号、关键文本、抗锯齿边缘 | 大字号、装饰性字体、背景文字 |
| 典型算法 | 霍夫曼编码、LZ77、LZW | 量化、子像素采样 |
| TouchGFX 支持 | 内置(默认) | 需手动配置 |
我的经验法则:正文用无损,标题用有损。正文一旦出现锯齿,用户一眼就能看出来;标题字号大,稍微损失一点边缘细节,肉眼根本分不清。
我记得有个同事,为了省空间把所有字体都用了有损压缩。结果在 16 号字上,字母「e」的封闭区域出现了断点,看起来像「c」。QA 提了个 bug,他排查了两天才发现是压缩参数太激进。嗯,从那以后我定了个规矩:小于 20px 的字号,强制走无损。
4.2 霍夫曼编码:给高频字符「瘦身」
霍夫曼编码,说白了就是给出现频率高的字符分配短编码,给低频字符分配长编码。你想想看,英文字母里「e」出现的频率最高,如果每个字母都用 8 位存储,那「e」和「z」占的空间一样大,这显然不合理。
在字体压缩里,霍夫曼编码通常作用于像素数据流。比如一个字符的位图数据是:
原始像素(8x8 字符,1bit/pixel):
00011000
00111100
01100110
11000011
11000011
01100110
00111100
00011000
统计频率:0 出现 40 次,1 出现 24 次
霍夫曼编码:0 → 1(1bit),1 → 0(1bit)
压缩后:40*1 + 24*1 = 64bits → 64bits(没省?别急)
你可能会说:「这也没省啊?」——没错,当只有两种符号时,霍夫曼编码确实没优势。但实际字体数据远不止两种值:抗锯齿字体有 4 级灰度、16 级灰度甚至 256 级灰度。灰度级越多,霍夫曼的效果越明显。
实战技巧:在 TouchGFX 的字体生成工具里,你可以选择「Optimize for Size」。它背后做的就是霍夫曼编码 + 游程编码的组合。我建议你生成字体时,先跑一次默认设置,看看压缩比。如果不够理想,再手动调整灰度级数。
我曾经优化过一个中文点阵字库,16x16 点阵、256 级灰度。原始大小是 16*16*8 = 256 字节/字。用霍夫曼编码后,平均降到 120 字节/字,压缩比超过 2:1。而且——完全无损,每个像素的灰度值都和原来一模一样。
4.3 字典压缩算法:找重复,然后「指回去」
字典压缩的核心思想很简单:如果一段数据之前出现过,就别重复存了,记个「页码+偏移」就行。最经典的实现是 LZ77 和它的变种 LZW。
在字体场景里,字典压缩特别适合处理:
- 相同字符的不同字号:比如 12px 和 14px 的「A」,轮廓相似度很高
- 字符的局部重复:比如「m」的两个拱形、「品」的三个「口」
- 字体的公共笔画:宋体的横、楷体的捺,在不同字里反复出现
TouchGFX 的字体缓存机制,其实就暗含了字典思想。它会把最近使用的字符位图缓存到 RAM 里,下次再渲染同一个字符时,直接从缓存取,不用重新解码。这叫时间局部性字典,虽然不压缩存储,但压缩了解码时间。
注意:字典压缩在字体上的效果,跟字符集大小强相关。如果你只用了 26 个英文字母,字典里就那么几个模式,压缩比不会太高。但如果你要显示 2000 个常用汉字,字典里能匹配到的重复模式就多了,压缩比能到 3:1 甚至 4:1。
4.4 三种压缩策略的对比与选择
我把这三种方法放在一起,给你一个直观的对比:
| 算法 | 压缩比(典型) | 解码速度 | 内存占用 | 最佳场景 |
|---|---|---|---|---|
| 霍夫曼编码 | 1.5:1 ~ 3:1 | 快(查表) | 低(仅需码表) | 灰度位图、小字号 |
| 字典压缩(LZ77) | 2:1 ~ 4:1 | 中等(需滑动窗口) | 中(需窗口缓冲区) | 大字符集、中文 |
| 有损量化 | 4:1 ~ 10:1 | 极快(直接截位) | 极低 | 大字号、装饰字 |
你可能会问:「能不能组合使用?」——当然可以。我做过一个方案:先用有损量化把 256 级灰度降到 16 级,再用霍夫曼编码压缩。压缩比从 2:1 直接跳到 6:1,而且肉眼几乎看不出区别。但要注意,组合压缩的解码逻辑会变复杂,MCU 性能不够的话,反而拖慢渲染速度。
4.5 本章知识体系总览
下面这张图,把三种压缩策略的关系和适用边界画清楚了。你可以把它当作选型时的「导航图」:
我的建议:刚开始做字体压缩时,别一上来就搞组合拳。先用 TouchGFX 自带的无损压缩跑一遍,看看 Flash 占用。如果还超标,再针对大字号单独开有损。一步一步来,出了问题也好定位。