- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
导读
本文围绕 fontTools 项目中的fontTools.ttLib.reorderGlyphs模块(文档位于 Doc/source/ttLib/reorderGlyphs.rst,通过automodule指令渲染该模块的完整 API),系统讲解如何在 Python 中重排字体(TTF/OTF)的字形顺序(glyph order / glyph ID 分配)。重排字形顺序并非简单的post表改名:它必须同步刷新 GPOS、GSUB、GDEF、MATH 等 OpenType 布局表中的 Coverage、记录数组与内部排序,否则字体会在排版引擎中产生错乱甚至失效。读完本文,你将掌握reorderGlyphs()的完整调用方式、输入校验规则、基于 BFS 的 OT 表遍历原理、ReorderCoverage/ReorderList两类重排规则的作用机制,以及它在TTFont对象上的便捷入口,并可通过仓库内置测试用例验证每种场景的正确行为。
一、模块定位与文档背景
reorderGlyphs是 Lib/fontTools/ttLib/reorderGlyphs.py 定义的一个辅助模块(模块作者为 Rod Sheeter,其设计依据是一份 OpenType 排版重排内部规范文档),它的职责用模块 docstring 一句话概括:“Reorder glyphs in a font.”——按给定的新字形顺序重排字体,并让所有依赖字形索引(glyph index / GID)的表保持一致。
在 fontTools 的整体结构中,reorderGlyphs是 ttLib 的辅助模块:ttLib 负责字体的加载、解析、修改与序列化,而字形顺序的全局变更属于一种“跨表一致性”操作,因此被独立封装。用户可通过两种方式触发:
- 直接调用模块级函数
reorderGlyphs(font, new_glyph_order); - 调用 TTFont.reorderGlyphs() 便捷方法,它内部延迟导入并委托给模块级函数。
二、核心 API 与输入校验
模块唯一的顶层入口是reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]),对应实现位于 Lib/fontTools/ttLib/reorderGlyphs.py 的reorderGlyphs函数。调用前它会先做两道严格的输入校验(见同文件中的reorderGlyphs函数开头):
- 长度校验:若
len(new_glyph_order) != len(old_glyph_order),抛出ValueError,错误信息形如"New glyph order contains N glyphs, but font has M glyphs"。 - 集合一致性校验:若新旧顺序包含的字形集合不同(用
set比较),抛出ValueError,并分别列出“只在新顺序中”与“只在旧顺序中”的字形名。
这两道校验保证重排操作是纯置换(permutation),不允许增删字形。对应测试可见 Tests/ttLib/reorderGlyphs_test.py 中的test_reorder_glyphs_bad_length与test_reorder_glyphs_bad_set:前者传入["A"]触发长度错误,后者通过“追加一个AA再删掉一个元素”触发集合不匹配错误,二者均断言pytest.raises(ValueError)。
校验通过后,函数执行以下步骤:
- 调用
font.ensureDecompiled()(其实现见 Lib/fontTools/ttLib/ttFont.py#L615-L623),将即使以lazy=True打开的字体也强制反编译全部表。源码中的注释引用 fonttools issue #2060,说明原因:重排依赖字形索引的表时,所有表都必须处于已反编译状态。 - 再次检查
font.keys()中是否有仍未加载的表(not font.isLoaded(tag),isLoaded见 Lib/fontTools/ttLib/ttFont.py#L581-L584),若存在则抛出ValueError。 - 调用
font.setGlyphOrder(new_glyph_order)(见 Lib/fontTools/ttLib/ttFont.py#L1054-L1064)设置新顺序;setGlyphOrder会失效内部的反向映射缓存_reverseGlyphOrderDict,并在glyf表已加载时同步调用glyf.setGlyphOrder()。 - 遍历四大 OT 容器表并应用重排规则(见下文)。
- 若字体含
CFF或CFF2表,同步更新其charset与CharStrings字典的顺序(见下文“CFF 表处理”)。
三、为什么必须“带着表一起重排”:问题本质
字形顺序即字形 ID 的分配:getGlyphOrder()返回的列表第i项就是 GIDi(其实现见 Lib/fontTools/ttLib/ttFont.py#L1066,TrueType 字体从post表取字形名,CFF 字体从 CFF 的 charset 取)。而getGlyphID(name)(Lib/fontTools/ttLib/ttFont.py#L1207-L1217)通过反向映射字典_reverseGlyphOrderDict(构建逻辑见 Lib/fontTools/ttLib/ttFont.py#L1234-L1237)把名字映射回 ID。
OpenType 的 GPOS/GSUB/GDEF/MATH 表大量使用 Coverage 表来引用字形集合,Coverage 内部的 glyph 数组顺序与字形 ID 紧密耦合:Coverage 数组第i项对应 GIDcoverage.glyphs[i],但数组本身未必按 GID 升序排列——许多工具生成的 Coverage 按编码或构建顺序写入。同时,与 Coverage 并行的记录数组(如SinglePos的Value、PairPos的PairSet、CursivePos的EntryExitRecord、上下文表的PosRuleSet/SubRuleSet等)必须与 Coverage 的每一项一一对应。
因此,一旦改变字形顺序(即改变 GID 分配),凡是“按旧 GID 顺序隐含排序”的表结构都必须同步重排,否则字体渲染时字形引用全部错位。reorderGlyphs的核心价值就是:只改一套字形顺序,自动递归修正所有相关表。
四、两大重排规则:ReorderCoverage 与 ReorderList
模块用两个 dataclass 描述重排动作,二者均继承抽象基类ReorderRule(apply为抽象方法,见 Lib/fontTools/ttLib/reorderGlyphs.py):
4.1 ReorderCoverage:重排 Coverage 及其并行数组
ReorderCoverage的关键逻辑(apply方法):
- 通过点号路径属性(如
"MarkArray.MarkRecord"、"LigatureArray.LigatureAttach")访问嵌套属性,工具函数_get_dotted_attr按.逐级getattr解引用。 - 单 Coverage 路径:从
value上取coverage_attr(默认名为"Coverage",模块常量_COVERAGE_ATTR),若还配置了parallel_list_attr,则同时取出与该 Coverage 平行的列表,并断言二者长度相等("Nothing makes sense")。随后用_sort_by_gid(font.getGlyphID, coverage.glyphs, parallel_list)把 Coverage 的 glyph 名数组按新 GID 升序排序,并行列表按相同的键顺序同步重排(_sort_by_gid使用zip打包排序后再解包回写,保证两列表元素严格一一对应)。 - 多 Coverage 路径:少数表(如上下文表的
Coverage属性是列表)持有多个 Coverage,此时不允许同时配置并行列表(断言"Can't have multiple coverage AND a parallel list"),逐个对每个 Coverage 执行排序。
4.2 ReorderList:重排由 GID 排序的内部列表
ReorderList用于处理“列表本身按某个字形字段排序”的场景,典型例子是 PairPos Format 1 的PairSet表:PairSet 由 Coverage 按顺序引用,而每个 PairSet 内的PairValueRecord列表必须按SecondGlyph的 GID 升序排列(OpenType GPOS 规范对 pair adjustment subtable 的明确要求)。apply方法取出list_attr指向的列表,断言其确为list,然后按getattr(v, key)的字形 ID 升序原地sort。
这两类规则共同构成一个可复用的声明式框架:新增一种需要重排的表结构时,只需在规则字典里登记(表类型, 格式号)→ 规则列表,而无需改动遍历逻辑。
五、规则表:GPOS / GSUB / GDEF / MATH 的完整重排清单
模块中名为_REORDER_RULES的字典以(表类型, Format 号)为键(Format 为None表示不区分格式),集中登记了所有需要同步的重排关系(源码位于 Lib/fontTools/ttLib/reorderGlyphs.py)。这是理解本模块能力边界最直接的依据:
| 表类型 | Format | 执行的重排规则 |
|---|---|---|
GPOSSinglePos | 1 | 重排Coverage |
GPOSSinglePos | 2 | 重排Coverage并同步并行列表Value |
GPOSPairPos | 1 | 重排Coverage并同步PairSet |
GPOSPairSet | 任意 | 对每个PairValueRecord列表按SecondGlyph的 GID 升序排序 |
GPOSPairPos | 2 | 重排Coverage |
GPOSCursivePos | 1 | 重排Coverage并同步EntryExitRecord |
GPOSMarkBasePos | 1 | 分别重排MarkCoverage/MarkArray.MarkRecord与BaseCoverage/BaseArray.BaseRecord |
GPOSMarkLigPos | 1 | 分别重排MarkCoverage/MarkArray.MarkRecord与LigatureCoverage/LigatureArray.LigatureAttach |
GPOSMarkMarkPos | 1 | 分别重排Mark1Coverage/Mark1Array.MarkRecord与Mark2Coverage/Mark2Array.Mark2Record |
GPOSContextPos | 1 | 重排Coverage并同步PosRuleSet |
GPOSContextPos | 2 / 3 | 重排Coverage |
GPOSChainContextPos | 1 | 重排Coverage并同步ChainPosRuleSet |
GPOSChainContextPos | 2 / 3 | 重排Coverage(Format 3 还需分别处理BacktrackCoverage、InputCoverage、LookAheadCoverage) |
GSUBContextSubst | 1 | 重排Coverage并同步SubRuleSet |
GSUBContextSubst | 2 / 3 | 重排Coverage |
GSUBChainContextSubst | 1 | 重排Coverage并同步ChainSubRuleSet |
GSUBChainContextSubst | 2 / 3 | 重排Coverage(Format 3 同样处理三段 Coverage) |
GSUBReverseChainSingleSubst | 1 | 重排Coverage并同步Substitute,另处理BacktrackCoverage、LookAheadCoverage |
GDEFAttachList | 任意 | 重排Coverage并同步AttachPoint |
GDEFLigCaretList | 任意 | 重排Coverage并同步LigGlyph |
GDEFMarkGlyphSetsDef | 任意 | 重排Coverage |
MATHMathGlyphInfo | 任意 | 重排ExtendedShapeCoverage |
MATHMathItalicsCorrectionInfo | 任意 | 重排Coverage并同步ItalicsCorrection |
MATHMathTopAccentAttachment | 任意 | 重排TopAccentCoverage并同步TopAccentAttachment |
MATHMathKernInfo | 任意 | 重排MathKernCoverage并同步MathKernInfoRecords |
MATHMathVariants | 任意 | 分别重排VertGlyphCoverage/VertGlyphConstruction与HorizGlyphCoverage/HorizGlyphConstruction |
由表可见:规则覆盖了 GPOS 的全部 9 类常见查找表、GSUB 的上下文与反向链式单替换、GDEF 的附加点/插入字重定位/字形集,以及 MATH 的各类度量附件结构。凡表中未列出的表结构,reorderGlyphs不会做任何处理。
六、遍历机制:对 OT 数据做 BFS,应用规则
拿到规则表后,关键问题是如何找到字体中每一个需要处理的子表。模块采用广度优先遍历(BFS)实现,相关实现位于 Lib/fontTools/ttLib/reorderGlyphs.py 的_bfs_base_table/_traverse_ot_data:
- 遍历起点是每个容器表(
GDEF、GPOS、GSUB、MATH)的根BaseTable,访问器字符串形如font["GPOS"]。 - 维护一个
deque作为 frontier,路径表示为(name, value, index)元组序列,其中name是父表上取得该子表的属性名,index是子表在列表中的下标(可选)。初始路径即根表自身。 - 对每个出队的节点,调用其
iterSubTables()方法获取所有子表条目(该迭代器的实现见 Lib/fontTools/ttLib/tables/otBase.py#L1188-L1211:遍历getConverters()得到的转换器,产出子表条目,SubTableEntry是(name, value, index)的 NamedTuple,定义见 Lib/fontTools/ttLib/tables/otBase.py#L1181-L1186),将“当前路径 + 子表条目”扩展进 frontier。 - 源码注释特别指出:无需 visited 集合,因为 OT 数据只存在前向偏移(forward-offset),不可能形成环。
- 对每个访问到的节点,取
(type(value), getattr(value, "Format", None))作为键查_REORDER_RULES,命中则逐一执行规则。
由于查找表(LookupList)本身也是由BaseTable组合而成的嵌套结构,BFS 能自然穿透LookupList → Lookup → SubTable层级,无需硬编码遍历路径。
七、CFF 字体的特殊处理
对于包含CFF或CFF2表的字体,重排还涉及 PostScript 字体内部的两处结构:
CFFTopDict.charset:直接赋值为new_glyph_order;CharStrings字典:按新顺序重建{k: charstrings.get(k) for k in new_glyph_order},保证字形的 charstring 映射顺序与新的 GID 分配一致。
对应测试test_reorder_glyphs_cff(Tests/ttLib/reorderGlyphs_test.py)使用TestVGID-Regular.otf将字形顺序整体反转后断言charStrings.keys()与charset均等于新顺序。
八、TTFont 便捷方法:font.reorderGlyphs()
除模块级函数外,TTFont.reorderGlyphs() 提供了对象方法入口:
from fontTools.ttLib import TTFont font = TTFont("path/to/font.ttf") order = font.getGlyphOrder() new_order = [order[0]] + list(reversed(order[1:])) # 例如反转除 .notdef 外的字形 font.reorderGlyphs(new_order) font.save("path/to/reordered.ttf")其实现只是延迟导入模块并委托:from .reorderGlyphs import reorderGlyphs; reorderGlyphs(self, new_glyph_order)。测试test_ttfont_reorder_glyphs验证了对象方法与模块函数行为一致。
九、测试验证:重排后 Coverage 与并行数组确实同步
仓库的 Tests/ttLib/reorderGlyphs_test.py 提供了多种场景的回归验证,可作为理解模块行为的最佳样例:
- test_reorder_glyphs:对
Test-Regular.ttf将.notdef之外的顺序反转,断言 GSUB 首个子表 Coverage 数组与 GPOS 首个子表 Coverage 数组都变为原顺序的反向,证明 Coverage 确实被重排。 - test_reorder_lazy_ligatures / test_reorder_lazy_arrays:分别针对
lazy=True/False/None三种加载模式,用FontBuilder构造含 liga 特征或 kern 特征的字体,验证重排后 ligature 的Component与LigGlyph指向、PairPos 的PairValueRecord.SecondGlyph顺序以及 Value 数组的 XAdvance 对应关系全部正确。其中test_reorder_lazy_arrays还区分pairpos=False/True两种 GPOS 子表,并断言在 lazy 模式下重排前的记录确实是LazyList实例——这正印证了ensureDecompiled()的必要性。 - test_ensure_decompiled_high_level_ligatures:验证
otTables.LigatureSubst.ensureDecompiled(recurse=True)在高层(high-level)表示下可重复调用且不破坏ligatures字典,为重排前的强制反编译提供支撑。 - test_reorder_glyphs_cff:验证 CFF 字体的 charset 与 CharStrings 顺序同步。
十、适用前提与注意事项
- 纯置换操作:
reorderGlyphs只接受与字体当前字形集合完全一致的排列,增删字形需先通过其他途径(如 subset)处理。 - 必须完整加载字体:函数内部强制
ensureDecompiled(),若字体以lazy模式打开,首次调用会有反编译开销;若存在无法加载的表会直接报错。 - 规则表是有限集合:目前登记的规则覆盖 GPOS/GSUB/GDEF/MATH 的常见结构(模块源码中标注了 “TODO Port to otTraverse”,说明未来可能改用 otTraverse 遍历框架,这是从源码注释可以推断的方向),未登记的表结构不会被修改,若你的字体包含特殊扩展表,需自行确认其是否需要同步。
- CFF/CFF2 语义差异:CFF 字体的字形顺序由 charset 决定,模块通过重写 charset 与 CharStrings 映射顺序完成重排;TrueType 则由
glyf配合post表与maxp计数体现顺序变化。
结语
reorderGlyphs是 fontTools 中少有的“全局一致性”工具:它用一个声明式规则表 + BFS 遍历,把「修改字形顺序」这件看似简单、实则牵动 GPOS/GSUB/GDEF/MATH/CFF 多张表的操作封装成一行调用。无论是工具链中为了兼容第三方排版引擎而重排 GID,还是生成 subset 后需要整理字形顺序,理解它的输入约束、规则覆盖范围与遍历原理,都能帮助你安全地完成字形重排而不破坏字体内部结构。
- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
相关推荐
fontTools varLib featureVars 模块完全指南:为可变字体构建 OpenType FeatureVariations 条件字形替换
fontTools varLib featureVars 模块完全指南:为可变字体构建 OpenType FeatureVariations 条件字形替换 fe
开发工具fontTools 中 post 表的完全解析:OpenType 字体里 PostScript 元数据与字形命名机制
fontTools 中 post 表的完全解析:OpenType 字体里 PostScript 元数据与字形命名机制 本文围绕 fontTools 中 post
开发工具fontTools encodings 模块完全指南:macOS 遗留字体编码与 OpenType 字符集映射
fontTools encodings 模块完全指南:macOS 遗留字体编码与 OpenType 字符集映射 导读 :本文深入剖析 fontTools 的 f
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考