4、实战:使用 fonttools 提取并分析字体元数据
字体文件里藏着不少「身份证信息」。比如字体叫什么名字、有多少个字形、版权归谁。这些信息平时你可能不太关注,但做字体压缩时,它们就是你的第一手情报。
我个人习惯,拿到一个字体文件,第一件事就是先看看它的元数据。为什么?因为很多压缩策略,比如要不要保留某些字形、能不能改字体名,都得基于这些信息做判断。今天我们就用 Python 的 fonttools 库,把这些数据扒出来看看。
4.1 环境准备与基础读取
先装好 fonttools。如果你还没装,跑一句命令就行:
pip install fonttools
装完之后,我们就可以用 Python 来加载一个字体文件了。我拿一个常见的 .ttf 文件举例:
from fontTools.ttLib import TTFont
# 加载字体文件
font = TTFont("SourceHanSansSC-Regular.otf")
# 看看字体里有哪些表
print(font.keys())
输出会是一堆表名,比如 ['GlyphOrder', 'head', 'hhea', 'name', 'cmap', ...]。其中 name 表就是存字体名称、版权信息的地方。head 表里则存着字形数量等全局信息。
4.2 读取字体名称(Name Table)
Name Table 是字体元数据的核心。它用一组编号来区分不同字段。比如 0 是版权信息,1 是字体家族名,4 是完整字体名。我整理了一张常用对照表:
| Name ID | 字段含义 | 实际例子 |
|---|---|---|
| 0 | 版权信息 | Copyright 2024 Adobe |
| 1 | 字体家族名 | Source Han Sans SC |
| 2 | 字体子家族名 | Regular |
| 4 | 完整字体名 | Source Han Sans SC Regular |
| 5 | 版本号 | Version 2.004 |
| 6 | PostScript 名 | SourceHanSansSC-Regular |
读取这些字段的代码很简单:
name_table = font['name']
for record in name_table.names:
# 只打印 Name ID 为 0、1、4、5 的记录
if record.nameID in [0, 1, 4, 5]:
# 解码成可读的字符串
try:
text = record.toUnicode()
print(f"Name ID {record.nameID}: {text}")
except:
print(f"Name ID {record.nameID}: [无法解码]")
输出大概是这样:
Name ID 0: Copyright 2014-2021 Adobe (http://www.adobe.com/).
Name ID 1: Source Han Sans SC
Name ID 4: Source Han Sans SC Regular
Name ID 5: Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603
嗯,这里要注意一点:同一个 Name ID 可能对应多个语言版本。比如中文环境下,字体名可能是「思源黑体」,英文环境下是「Source Han Sans SC」。fonttools 会把这些记录都列出来,你需要根据 record.platformID 和 record.langID 来筛选。
record.toUnicode() 在某些编码下会抛异常。建议包一层 try-except,或者用 str(record) 做兜底。另外,有些字体厂商会在 Name ID 0 里写很长的法律声明,别被吓到,那是正常的。
4.3 读取字形数量
字形数量是衡量字体「体重」的关键指标。一个中文字体动辄两三万个字形,而英文字体可能只有几百个。这个数据存在 head 表的 numGlyphs 字段里。
head_table = font['head']
num_glyphs = head_table.numGlyphs
print(f"字形数量: {num_glyphs}")
输出:
字形数量: 43697
四万多个字形,这就是思源黑体 Regular 字重的体量。你想想看,如果每个字形都保留,压缩起来确实有挑战。但有了这个数字,你就能估算出压缩的潜力有多大。
另外,maxp 表里也存了字形数量,但 head 表更常用。我个人习惯两个都看一眼,做个交叉验证:
maxp_table = font['maxp']
print(f"maxp 中的字形数量: {maxp_table.numGlyphs}")
4.4 读取版权信息
版权信息通常藏在 Name ID 0 里。但有些字体厂商会把版权声明拆成多个字段,或者放在 OS/2 表的 achVendID 里。我们一并看看:
# 从 name 表读取版权
for record in name_table.names:
if record.nameID == 0:
print(f"版权信息: {record.toUnicode()}")
# 从 OS/2 表读取厂商 ID
os2_table = font['OS/2']
print(f"厂商 ID: {os2_table.achVendID}")
厂商 ID 是四个字符的代码,比如 Adobe 是 'ADBE',Google 是 'GOOG'。这个信息在判断字体是否可以商用、是否可以修改时很有用。
4.5 完整代码示例
把上面几段代码整合成一个函数,方便复用:
from fontTools.ttLib import TTFont
def analyze_font_metadata(font_path):
font = TTFont(font_path)
# 1. 读取字体名称
print("=== 字体名称 ===")
name_table = font['name']
target_ids = [0, 1, 2, 4, 5, 6]
for record in name_table.names:
if record.nameID in target_ids:
try:
text = record.toUnicode()
print(f" Name ID {record.nameID}: {text}")
except:
pass
# 2. 读取字形数量
print("\n=== 字形数量 ===")
head_table = font['head']
print(f" head 表: {head_table.numGlyphs}")
maxp_table = font['maxp']
print(f" maxp 表: {maxp_table.numGlyphs}")
# 3. 读取版权信息
print("\n=== 版权信息 ===")
for record in name_table.names:
if record.nameID == 0:
print(f" {record.toUnicode()}")
os2_table = font['OS/2']
print(f" 厂商 ID: {os2_table.achVendID}")
font.close()
# 使用示例
analyze_font_metadata("SourceHanSansSC-Regular.otf")
4.6 知识体系总览
下面这张 SVG 图帮你梳理 fonttools 读取元数据的整体流程:
4.7 常见问题与避坑
- 编码问题:有些老字体用非 Unicode 编码存储 Name Table。遇到乱码时,试试
record.toUnicode()的备选方案,或者直接打印原始字节。 - 缺失字段:不是所有字体都有完整的 Name ID。比如一些精简版字体可能只保留了 Name ID 1 和 4。读取前最好先判断字段是否存在。
- 字形数量不一致:head 表和 maxp 表的 numGlyphs 理论上应该一致。如果不一致,以 maxp 表为准,因为它是 OpenType 规范中定义字形数量的权威来源。
- 版权信息过长:Name ID 0 可能包含多行文本。打印时注意换行符,别让输出挤成一团。
好了,这一章的内容就到这里。你现在应该能熟练地用 fonttools 读取字体的名称、字形数量和版权信息了。这些元数据是后续所有压缩操作的基础——不了解字体,怎么敢动手压缩呢?