ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

fontTools reorderGlyphs 模块详解:安全重排字体字形顺序并同步所有 OpenType 布局表

fontTools reorderGlyphs 模块详解:安全重排字体字形顺序并同步所有 OpenType 布局表
  • 开发工具

【免费下载链接】fonttools

A library to manipulate font files from Python.

项目地址:https://gitcode.com/gh_mirrors/fo/fonttools
点击查看免费下载

导读

本文围绕 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函数开头):

  1. 长度校验:若len(new_glyph_order) != len(old_glyph_order),抛出ValueError,错误信息形如"New glyph order contains N glyphs, but font has M glyphs"。
  2. 集合一致性校验:若新旧顺序包含的字形集合不同(用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执行的重排规则
GPOSSinglePos1重排Coverage
GPOSSinglePos2重排Coverage并同步并行列表Value
GPOSPairPos1重排Coverage并同步PairSet
GPOSPairSet任意对每个PairValueRecord列表按SecondGlyph的 GID 升序排序
GPOSPairPos2重排Coverage
GPOSCursivePos1重排Coverage并同步EntryExitRecord
GPOSMarkBasePos1分别重排MarkCoverage/MarkArray.MarkRecord与BaseCoverage/BaseArray.BaseRecord
GPOSMarkLigPos1分别重排MarkCoverage/MarkArray.MarkRecord与LigatureCoverage/LigatureArray.LigatureAttach
GPOSMarkMarkPos1分别重排Mark1Coverage/Mark1Array.MarkRecord与Mark2Coverage/Mark2Array.Mark2Record
GPOSContextPos1重排Coverage并同步PosRuleSet
GPOSContextPos2 / 3重排Coverage
GPOSChainContextPos1重排Coverage并同步ChainPosRuleSet
GPOSChainContextPos2 / 3重排Coverage(Format 3 还需分别处理BacktrackCoverage、InputCoverage、LookAheadCoverage)
GSUBContextSubst1重排Coverage并同步SubRuleSet
GSUBContextSubst2 / 3重排Coverage
GSUBChainContextSubst1重排Coverage并同步ChainSubRuleSet
GSUBChainContextSubst2 / 3重排Coverage(Format 3 同样处理三段 Coverage)
GSUBReverseChainSingleSubst1重排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.

项目地址:https://gitcode.com/gh_mirrors/fo/fonttools
点击查看免费下载
上一篇:微信小程序终极性能优化:recycle-view组件完整指南
下一篇:Thor核心功能解析:为什么它是Mac用户必备的效率工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进