4. 字体子集化技术:Unicode范围裁剪、只保留用到的字符、中文字体子集化的实战技巧

字体太大,是TouchGFX项目里最常见的性能杀手之一。尤其是中文字体,一套GB2312全字库动不动就几MB,放在MCU上根本吃不消。怎么办?子集化。

说白了,就是只保留你真正用到的字符。别把整个字库都塞进去。我刚开始做嵌入式UI时,也犯过这种错误——直接把一个完整的黑体字库扔进工程,结果Flash直接爆了。后来才明白,一个显示“温度:25℃”的界面,根本不需要“饕餮”这种生僻字。

4.1 什么是字体子集化

字体子集化,就是从完整的字体文件中,提取出你需要的字符子集,生成一个新的、更小的字体文件。这个新文件只包含你指定的字符,其他字符全部丢弃。

举个例子:
完整的中文字体文件(GB2312)约 3MB,包含 6763 个汉字。
你的界面只用了 200 个汉字,子集化后字体文件可能只有 100KB 左右。
节省了 97% 的存储空间。

核心原则:只保留用到的字符,一个不多,一个不少。

4.2 Unicode范围裁剪:最基础的子集化方式

Unicode范围裁剪,就是指定一个或多个Unicode编码范围,只保留这个范围内的字符。这是最直接、最简单的子集化方法。

常见的Unicode范围:

范围名称 Unicode范围 包含字符数 典型用途
基本拉丁字母 U+0020 - U+007E 95 英文、数字、标点
拉丁文补充-1 U+00A0 - U+00FF 96 西欧语言特殊字符
中日韩统一表意文字 U+4E00 - U+9FFF 20992 中文汉字(常用)
CJK统一表意文字扩展A U+3400 - U+4DBF 6582 生僻汉字
全角符号 U+FF00 - U+FFEF 240 全角标点、全角字母

在TouchGFX Designer中,你可以直接在字体配置界面设置Unicode范围。比如,你只需要显示英文和数字,那就只保留 U+0020 - U+007E 这个范围。这样生成的字体文件会非常小。

我的经验:如果你不确定界面用了哪些字符,可以先在代码里把所有用到的字符串收集起来,然后提取出所有字符的Unicode编码,再根据这些编码去裁剪范围。这样最精准。

4.3 只保留用到的字符:精确到每个字符

Unicode范围裁剪虽然简单,但不够精细。比如,你只用了“你好世界”四个字,但U+4E00 - U+9FFF范围包含了20992个汉字,你保留了全部。这显然浪费了。

更精确的做法是:只保留你真正用到的字符。一个字符都不多留。

具体步骤:

  1. 收集所有用到的字符串:从UI设计文件、代码、资源文件中提取所有文本内容。
  2. 去重:把重复的字符去掉,只保留唯一的字符列表。
  3. 生成字符列表文件:把去重后的字符保存到一个文本文件中,每行一个字符。
  4. 使用工具生成子集字体:用FontForge、pyftsubset等工具,根据字符列表生成子集字体。

举个例子,假设你的界面只有以下文本:

温度:25℃
湿度:60%
状态:正常

去重后的字符列表:

温
度
:
2
5
℃
湿
6
0
%
状
态
正
常

一共13个字符。生成的子集字体文件,大小可能只有几KB。

注意:一定要包含空格、标点符号、数字、字母等基础字符。很多新手只保留了汉字,结果界面上的数字和标点显示成了方框。我曾经就踩过这个坑,排查了半天才发现是字体子集里没包含空格。

4.4 中文字体子集化的实战技巧

中文字体子集化,比英文字体要复杂一些。因为中文字体文件本身就很大,而且很多工具对中文支持不够好。下面分享几个实战技巧。

4.4.1 使用pyftsubset工具

pyftsubset是fonttools库中的一个工具,专门用于字体子集化。它支持OpenType、TrueType等格式,对中文支持很好。

安装方法:

pip install fonttools

使用方法:

pyftsubset 原字体.ttf --text-file=字符列表.txt --output-file=子集字体.ttf

参数说明:

  • 原字体.ttf:原始字体文件
  • --text-file:字符列表文件,每行一个字符
  • --output-file:输出的子集字体文件

我习惯在项目根目录下放一个 chars.txt 文件,里面记录所有用到的字符。每次更新UI后,重新生成一下这个文件,然后跑一遍pyftsubset命令,就能得到最新的子集字体。

4.4.2 处理特殊字符

中文字体里,有些特殊字符容易被忽略:

  • 全角空格(U+3000):中文排版中常用
  • 全角逗号、句号(U+FF0C、U+3002):中文标点
  • 破折号(U+2014):中文文本中常见
  • 省略号(U+2026):中文文本中常见
  • 摄氏度符号(U+2103):温度显示
  • 百分号(U+0025):百分比显示

这些字符如果不包含在子集里,界面上就会显示成方框或乱码。

我的建议:在生成字符列表时,把常用的标点符号、数字、字母都加进去。哪怕界面里暂时没用,也建议保留。因为后期需求变更时,你不需要重新生成字体。我一般会保留一个“基础字符集”,包含英文大小写字母、数字、常用标点、全角标点,大概200个字符左右。这个基础字符集可以复用到所有项目。

4.4.3 处理字体格式

TouchGFX支持TrueType(.ttf)和OpenType(.otf)格式。但子集化后的字体,建议使用TrueType格式,因为兼容性更好。

如果你从pyftsubset得到的是OpenType格式,可以用FontForge转换成TrueType格式:

fontforge -lang=ff -c 'Open("子集字体.otf"); Generate("子集字体.ttf");'

4.5 子集化流程总结

下面这张图,展示了我个人常用的字体子集化流程:

字体子集化实战流程 收集所有字符串 去重,生成字符列表 添加基础字符集 运行pyftsubset 导入TouchGFX Designer 关键说明 1. 从UI文件、代码、资源 文件中提取所有文本 2. 去重后得到唯一字符列表 3. 添加200个基础字符 (字母、数字、标点) 4. pyftsubset命令生成 子集字体文件 5. 在TouchGFX Designer中 导入子集字体 6. 编译运行,验证显示 效果

4.6 避坑指南

字体子集化虽然好用,但有几个坑需要注意。我一个个说。

坑1:忘记包含空格

空格(U+0020)是最容易被忽略的字符。没有空格,所有单词都会连在一起。我曾经在某个项目里,子集化后界面上的英文文本全部挤成一团,排查了半天才发现是空格没包含。

坑2:忘记包含换行符

换行符(U+000A)也是容易被忽略的。没有换行符,多行文本会显示成一行。如果你界面里有换行需求,一定要把换行符加进去。

坑3:子集化后字体渲染异常

有些字体在子集化后,某些字符的渲染会变得很奇怪。比如笔画缺失、位置偏移等。这通常是字体本身的bug,或者子集化工具对某些特殊字符处理不当。遇到这种情况,可以换一个字体试试,或者用FontForge手动修复。

我的经验:子集化完成后,一定要在目标设备上实际测试一下。不要在PC上看效果,因为PC的字体渲染引擎和MCU上的不一样。我习惯在开发板上跑一个“字体测试界面”,把所有用到的字符都显示出来,逐个检查。

4.7 自动化子集化脚本

手动跑pyftsubset命令太麻烦。我写了一个简单的Python脚本,可以自动完成子集化流程:

import os
import subprocess

def generate_subset_font(input_font, chars_file, output_font):
    """
    生成子集字体
    :param input_font: 原始字体文件路径
    :param chars_file: 字符列表文件路径
    :param output_font: 输出字体文件路径
    """
    cmd = [
        "pyftsubset",
        input_font,
        f"--text-file={chars_file}",
        f"--output-file={output_font}"
    ]
    subprocess.run(cmd, check=True)
    print(f"子集字体已生成:{output_font}")

if __name__ == "__main__":
    generate_subset_font(
        input_font="SourceHanSansSC-Regular.ttf",
        chars_file="chars.txt",
        output_font="subset_SourceHanSansSC-Regular.ttf"
    )

把这个脚本放在项目根目录,每次更新UI后,更新一下 chars.txt 文件,然后运行脚本,就能自动生成子集字体。

4.8 小结

字体子集化,是TouchGFX性能优化里性价比最高的手段之一。你只需要花几分钟时间,就能把字体文件从几MB压缩到几十KB,而且对显示效果没有任何影响。

记住三个关键点:

  • Unicode范围裁剪:适合快速裁剪,但不够精细
  • 只保留用到的字符:最精确,但需要维护字符列表
  • 中文字体子集化:注意特殊字符和基础字符集