4、实战:使用 fonttools 提取并分析字体元数据

字体文件里藏着不少「身份证信息」。比如字体叫什么名字、有多少个字形、版权归谁。这些信息平时你可能不太关注,但做字体压缩时,它们就是你的第一手情报。

我个人习惯,拿到一个字体文件,第一件事就是先看看它的元数据。为什么?因为很多压缩策略,比如要不要保留某些字形、能不能改字体名,都得基于这些信息做判断。今天我们就用 Python 的 fonttools 库,把这些数据扒出来看看。

核心目标:学会用 fonttools 读取字体的 Name Table、字形数量、版权信息,并理解这些字段的实际用途。

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 表里则存着字形数量等全局信息。

小提示:fonttools 支持 .ttf、.otf、.woff、.woff2 等常见格式。加载时它会自动识别,你不需要手动改后缀。

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'。这个信息在判断字体是否可以商用、是否可以修改时很有用。

实战经验:我在做字体分包时,经常需要检查版权信息。有些免费字体虽然可以商用,但要求保留版权声明。如果你在压缩时把 Name ID 0 删了,可能会违反授权协议。所以,压缩前先读版权,压缩后记得保留。

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 读取元数据的整体流程:

fonttools 字体元数据读取流程 TTFont(font_path) font['name'] font['head'] font['OS/2'] Name Table Name ID 0~6 字体名/版权/版本 head 表 numGlyphs 字形数量 OS/2 表 achVendID 厂商 ID 输出元数据报告 说明:三个分支并行读取,最终汇总输出。字形数量建议交叉验证 head 和 maxp 两个表。

4.7 常见问题与避坑

  • 编码问题:有些老字体用非 Unicode 编码存储 Name Table。遇到乱码时,试试 record.toUnicode() 的备选方案,或者直接打印原始字节。
  • 缺失字段:不是所有字体都有完整的 Name ID。比如一些精简版字体可能只保留了 Name ID 1 和 4。读取前最好先判断字段是否存在。
  • 字形数量不一致:head 表和 maxp 表的 numGlyphs 理论上应该一致。如果不一致,以 maxp 表为准,因为它是 OpenType 规范中定义字形数量的权威来源。
  • 版权信息过长:Name ID 0 可能包含多行文本。打印时注意换行符,别让输出挤成一团。
我的习惯:每次拿到新字体,我都会跑一遍这个分析脚本,把结果存成 JSON 文件。后续做压缩、分包、重命名时,直接查 JSON 就行,不用反复加载字体文件。省时省力。

好了,这一章的内容就到这里。你现在应该能熟练地用 fonttools 读取字体的名称、字形数量和版权信息了。这些元数据是后续所有压缩操作的基础——不了解字体,怎么敢动手压缩呢?