news 2026/10/13 23:53:17

fontTools 中 AAT `cidg` 表的解析与序列化:从 TTX 文档页到 otBase 实现机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fontTools 中 AAT `cidg` 表的解析与序列化:从 TTX 文档页到 otBase 实现机制
  • 开发工具

【免费下载链接】fonttools

A library to manipulate font files from Python.

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

本文基于 fontTools 仓库中 Doc/source/ttLib/tables/_c_i_d_g.rst 这一官方文档页,系统讲解 AAT 字体中cidg(CID to Glyph ID)映射表的用途与二进制结构,并沿着文档页背后的automodule指令一路深入到 table__c_i_d_g 的源码实现:读完后你可以理解 fontTools 是如何用BaseTTXConverter基类 +otData数据驱动的方式完成该表的解包(decompile)、打包(compile)与 TTX XML 互转,并能借助仓库中的真实测试数据验证完整往返流程。

一、cidg表是什么:Apple AAT 的 CID 映射

cidg是 Apple Advanced Typography(AAT)体系中的一张表,用于记录CID(Character/Composite ID)到字体内 GlyphID 的映射。文档页给出的核心描述如下(引自 表类 docstring):

  • cidg表的结构与gcid表几乎相同,只是映射方向相反:cidg是 CID → GlyphID,gcid是 GlyphID → CID;
  • 它主要用于PDF 渲染场景:PDF 可以只声明“请使用符合 Adobe-Japan-1 收集(collection)的字体”,而不内嵌具体字形子集。此时渲染端需要一个 CID 到字体字形的查找表,cidg就承担这一角色——尤其当实际提供字形的是一枚 TrueType 字体时,这份映射必不可少;
  • 与之对应的gidc表在这个场景下是有损的(lossy),已被cidg取代(obsoleted)。

文档页还给出了一个真实世界的例子:macOS 10.12.6 预装的/System/Library/Fonts/PingFang.ttc字体集合中的第一枚字体就携带cidg表。仓库内的测试数据正是以它为蓝本构造的(见后文第四节)。

在 OpenType 官方规范中并不存在cidg/gcid,它们属于 Apple AAT 扩展。fontTools 对这类 AAT 表的解析同样走 OpenType 表模型(otTables/otData),这一点从 Lib/fontTools/ttLib/tables/otData.py 中的表定义即可确认。

二、文档页本体:automodule驱动的极简 RST

Doc/source/ttLib/tables/_c_i_d_g.rst 全文只有两行实质内容:

``cidg``: CID to Glyph ID table ------------------------------- The ``cidg`` table is an Apple Advanced Typography (AAT) table. .. automodule:: fontTools.ttLib.tables._c_i_d_g :members: :undoc-members:

这里的automodule是 Sphinx 指令:它会从 Python 模块fontTools.ttLib.tables._c_i_d_g中提取所有成员(包括无 docstring 的成员)的文档字符串,自动生成 API 文档。也就是说,该文档页真正承载技术内容的是 Lib/fontTools/ttLib/tables/_c_i_d_g.py 中类的 docstring,RST 文件本身只是入口。这一模式在 fontTools 所有 ttLib 表文档中是一致的——想读某一表的“官方文档”,先找到对应 RST,再顺藤摸到同名.py模块的 docstring 即可。

三、table__c_i_d_g类:一个“空”的适配器

整个 Lib/fontTools/ttLib/tables/_c_i_d_g.py 文件(含注释)只有 24 行,类体只有一个pass:

# coding: utf-8 from .otBase import BaseTTXConverter class table__c_i_d_g(BaseTTXConverter): """CID to Glyph ID table ...(docstring,见上)... """ pass

类名table__c_i_d_g是 fontTools 对表 tag 的命名约定:把 tagcidg中的每个字母之间加上下划线,并在最前面再加一个下划线(前导下划线是 Python 私有模块内类的惯例)。它没有定义任何自定义方法,全部能力来自基类。

3.1 基类BaseTTXConverter提供什么

BaseTTXConverter定义在 Lib/fontTools/ttLib/tables/otBase.py,其 docstring 把它称为“TTX(实际是 ttLib)表模型与 OpenType 表模型之间的适配器”(an adapter between the TTX table model and the OpenType table model)。与cidg直接相关的核心方法有:

  • decompile(self, data, font)(L85-L92):在表被访问时自动触发。它创建OTTableReader,按表 tag 从otTables模块取出对应表类,再调用table.decompile(reader, font)完成二进制 → Python 对象树的转换。也就是说,cidg的具体字段解析逻辑并不在_c_i_d_g.py里,而是由otTables.cidg这个数据驱动类完成,而该类的字段规格来自otData.py(见下节)。
  • compile(self, font):保存时自动触发,把对象树序列化为二进制。基类实现中包含面向GSUB/GPOS的 HarfBuzz repacker 与偏移溢出修复状态机,但cidg这类简单结构表不会触发这些复杂路径。
  • toXML(self, writer, font)/fromXML(self, name, attrs, content, font)(L238-L248):委托给self.table.toXML2/self.table.fromXML,完成对象树与 TTX XML 的互转;fromXML之后还会调用populateDefaults()补全缺省字段。
  • ensureDecompiled(self, recurse):配合TTFont的惰性加载(lazy load),保证在真正访问字段前表已解包。

因此对cidg而言,table__c_i_d_g的全部价值就是:声明自己是cidg这个 tag 的转换器,让ttLib在加载字体时知道该 tag 的二进制数据应交给otTables模型处理。

四、二进制结构:CIDGlyphMapping字段定义

cidg表的字段规格在 Lib/fontTools/ttLib/tables/otData.py 中以数据形式声明:

( "cidg", [ FieldSpec( "struct", "CIDGlyphMapping", description="CID-to-glyph mapping table." ), ], ), ( "CIDGlyphMappingFormat0", [ FieldSpec("uint16", "Format", description="Format of the CID-to-glyph mapping table, = 0."), FieldSpec("uint16", "DataFormat", description="Currenty unused, set to zero."), FieldSpec("uint32", "StructLength", description="Size of the table in bytes."), FieldSpec("uint16", "Registry", description="The registry ID."), FieldSpec("char64", "RegistryName", description="The registry name in ASCII; unused bytes should be set to 0."), FieldSpec("uint16", "Order", description="The order ID."), FieldSpec("char64", "OrderName", description="The order name in ASCII; unused bytes should be set to 0."), FieldSpec("uint16", "SupplementVersion", description="The supplement version."), FieldSpec("CIDGlyphMap", "Mapping", description="A mapping from CIDs to the glyphs in the font, " "starting with CID 0. If a CID from the identified " "collection has no glyph in the font, 0xFFFF is used"), ], ),

从该定义可以直接读出cidg表的完整布局(format 0):

偏移(字节)字段类型含义
0Formatuint16格式号,当前只支持 0
2DataFormatuint16保留,置 0
4StructLengthuint32整个表结构占用的字节数
8Registryuint16收集(registry)ID,如 Adobe 体系
10RegistryNamechar6464 字节 ASCII 注册名,空字节补 0
74Orderuint16顺序(order)ID
76OrderNamechar6464 字节 ASCII 顺序名,空字节补 0
140SupplementVersionuint16补充版本
142MappingCIDGlyphMapCID → GlyphID 映射(见下)

Mapping部分采用“CID 数组”编码:先写一个 uint16 的元素个数,再写等量的 uint16 字形索引,数组下标即 CID;0xFFFF表示该 CID 在本字体中没有对应字形。反向的gcid表在 otData.py L4227 起有对称定义(GlyphCIDMapping),字段方向互换。

4.1 两个关键转换器

CIDGlyphMap(Lib/fontTools/ttLib/tables/otConverters.py L2242-L2273)负责Mapping字段的三种互转,值得注意的是它把二进制数组在内存中表示成字典({cid: glyphName})而不是数组——稀疏映射可以丢掉0xFFFF空项,XML 也随之紧凑:

class CIDGlyphMap(BaseConverter): def read(self, reader, font, tableDict): numCIDs = reader.readUShort() result = {} for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): if glyphID != 0xFFFF: result[cid] = font.getGlyphName(glyphID) return result def write(self, writer, font, tableDict, value, repeatIndex=None): items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()} count = max(items) + 1 if items else 0 writer.writeUShort(count) for cid in range(count): writer.writeUShort(items.get(cid, 0xFFFF))

read时遇到0xFFFF直接跳过;write时按max(items) + 1重算数组长度,缺失的 CID 回填0xFFFF,保证往返一致性。xmlRead/xmlWrite则按<CID cid="0" glyph=".notdef"/>这样的简单标签逐条输出(见第五节示例)。

StructWithLength(otConverters.py L684-L708)处理CIDGlyphMapping这个整体子结构。在 buildConverters 中有专门分支:名为CIDGlyphMapping或GlyphCIDMapping的字段一律映射到StructWithLength。它解决的是StructLength自指问题——写入时先把StructLength位置填上占位值0xDEADBEEF(按字段静态长度截断为 4/2/1 字节),编译完整个子结构后测量实际长度,再把占位字节替换为真实长度:

class StructWithLength(Struct): def read(self, reader, font, tableDict): pos = reader.pos table = self.tableClass() table.decompile(reader, font) reader.seek(pos + table.StructLength) return table def write(self, writer, font, tableDict, value, repeatIndex=None): ... value.StructLength = deadbeef value.compile(writer, font) length = writer.getDataLength() - before ... writer.items[lengthIndex] = lengthWriter.getAllData()

读取侧同样依赖StructLength来定位子结构边界(reader.seek(pos + table.StructLength))。这也解释了为什么otData.py里StructLength字段名会被 buildConverters 识别为“计算型”数值(ComputedULong)——它的值在编译期自动得出,不需要用户手工维护。

五、真实数据走读:仓库测试中的解包与打包

仓库为cidg提供了专门的往返测试 Tests/ttLib/tables/_c_i_d_g_test.py,测试数据是一段 152 字节的十六进制串,文件头注释明确说明:它与 macOS 10.12.6 上 PingFang.ttc 首枚字体中真实cidg表结构相同、只是规模更小。带注释的布局如下:

CIDG_DATA = deHexStr( "0000 0000 " # 0: Format=0, Flags=0 "0000 0098 " # 4: StructLength=152 "0000 " # 8: Registry=0 "41 64 6F 62 65 " # 10: RegistryName="Adobe" + ("00" * 59) + "0002 " # 15: <padding> # 74: Order=2 "43 4E 53 31 " # 76: Order="CNS1" + ("00" * 60) + "0000 " # 80: <padding> # 140: SupplementVersion=0 "0004 " # 142: Count "0000 " # 144: GlyphID[0]=.notdef "FFFF " # 146: CIDs[1]=<None> "0003 " # 148: CIDs[2]=C "0001 " # 150: CIDs[3]=A )

注意其中 CID 1 是0xFFFF(空项),与第四节的0xFFFF约定完全对应;CID 0/2/3 分别映射到.notdef、C、A。解包后再序列化为 TTX XML,测试断言的输出是:

<CIDGlyphMapping Format="0"> <DataFormat value="0"/> <!-- StructLength=152 --> <Registry value="0"/> <RegistryName value="Adobe"/> <Order value="2"/> <OrderName value="CNS1"/> <SupplementVersion value="0"/> <Mapping> <CID cid="0" glyph=".notdef"/> <CID cid="2" glyph="C"/> <CID cid="3" glyph="A"/> </Mapping> </CIDGlyphMapping>

可以观察到三点实现行为:StructLength在 XML 中被写成注释(它是编译期计算值);char64字段在 XML 中被还原为无填充的短字符串("Adobe"而非 64 字节数组);0xFFFF空项(CID 1)不出现在<Mapping>中,这正是CIDGlyphMap字典化表示的体现。

测试的两个用例分别覆盖往返的两个方向:

def testDecompileToXML(self): table = newTable("cidg") table.decompile(CIDG_DATA, self.font) self.assertEqual(getXML(table.toXML, self.font), CIDG_XML) def testCompileFromXML(self): table = newTable("cidg") for name, attrs, content in parseXML(CIDG_XML): table.fromXML(name, attrs, content, font=self.font) self.assertEqual(hexStr(table.compile(self.font)), hexStr(CIDG_DATA))

第二个用例特别有说服力:从 XML 重新构建的表编译后,二进制字节与原始CIDG_DATA完全一致(newTable来自 Lib/fontTools/ttLib/ttLib,FakeFont/getXML/parseXML等测试辅助工具来自 Lib/fontTools/misc/testTools.py)。

六、实际使用方式

6.1 通过 API 访问

对携带cidg表的字体,标准用法与其他 ttLib 表一致:

from fontTools.ttLib import TTFont font = TTFont("SomeAATFont.ttf") if "cidg" in font: table = font["cidg"] # 类型为 table__c_i_d_g(惰性触发 decompile) mapping = table.table.Mapping # {cid: glyphName},空 CID 不出现 print(mapping) font.save("Out.ttf") # 触发 compile,StructLength 等计算值自动回填

类型层面,Lib/fontTools/ttLib/ttFont.py 为TTFont.__getitem__提供了针对该 tag 的精确重载:

def __getitem__(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g: ... def get(self, tag: Literal["cidg"]) -> _c_i_d_g.table__c_i_d_g | None: ...

(同文件中对gcid也有对称重载,见 L757 附近)。模块导入路径在 Lib/fontTools/ttLib/tables/init.py 中登记:from . import _c_i_d_g。

6.2 通过ttx命令行查看

fontTools 自带的 ttx 工具 支持按 tag 导出/导入任意已支持表:

ttx -t cidg SomeAATFont.ttf # 导出 cidg 表为 XML ttx SomeAATFont.ttx # 从 XML 重新打包(若只保留 cidg 则重建该表)

导出的 XML 结构与上文测试断言中的CIDG_XML形态一致,可直接用编辑器修改<CID>条目后再写回字体。

七、小结与延伸阅读

  • cidg是 Apple AAT 中 CID → GlyphID 的无损映射表,主要服务于 PDF 按“已知收集”引用字形的场景,取代了有损的gidc;
  • fontTools 中该表没有任何手工序列化代码:table__c_i_d_g 是 BaseTTXConverter 的空子类,字段布局由 otData.py 数据驱动声明,CIDGlyphMap、StructWithLength等通用转换器在 otConverters.py 中实现;
  • 官方文档页 Doc/source/ttLib/tables/_c_i_d_g.rst 通过 Sphinxautomodule直接渲染上述源码 docstring,阅读路径为:RST 文档页 → 表模块 docstring → otData 字段规格 → 转换器实现 → Tests/ttLib/tables/_c_i_d_g_test.py 往返验证;
  • 反向映射可参考同结构的gcid表:Lib/fontTools/ttLib/tables/_g_c_i_d.py,以及 otData.py 中 GlyphCIDMapping 定义。
  • 开发工具

【免费下载链接】fonttools

A library to manipulate font files from Python.

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 23:52:55

《C陷阱与缺陷》:C语言系统性排雷与工程化避坑指南

1. 这不是一本普通C语言书——它是一份“系统性排雷指南”你手头那本《C陷阱与缺陷》封面可能已经泛黄&#xff0c;页脚卷了边&#xff0c;书页间还夹着几根咖啡渍干涸后留下的褐色印痕。我第一次读它是在某高校嵌入式实验室的旧书架上&#xff0c;当时正为一段看似天衣无缝的指…

作者头像 李华
网站建设 2026/10/13 23:52:40

SpringBoot读取resources目录文件:原理、方法与避坑指南

刚接手后端时我有段时间很困惑&#xff1a;各项配置明明存在&#xff0c;打包后就是读不到。后来发现多数这类问题都出在resources目录文件的读取姿势上。这篇文章就把SpringBoot下获取resources目录文件的常见方法、原理、坑一次性和大家说透&#xff0c;适合正在调资源文件的…

作者头像 李华
网站建设 2026/10/13 23:52:13

UE/Unity与国产低代码数字孪生平台:选型对比与实战解码

这个问题我最近被问了不下十次&#xff0c;每次都是在一些项目交流会上&#xff0c;有甲方也有同行。很多人看到“国产低代码数字孪生平台”铺天盖地的案例&#xff0c;就开始焦虑&#xff1a;是不是投入大量人力学UE、Unity白学了&#xff1f;引擎这条路是不是走错了&#xff…

作者头像 李华
网站建设 2026/10/13 23:50:56

Python pprint模块:结构化数据打印与调试技巧

1. 为什么需要结构化输出工具在Python开发中&#xff0c;调试和日志记录时经常需要打印复杂的数据结构。当面对嵌套字典、多层列表或自定义对象时&#xff0c;传统的print()函数输出就像一团乱麻。我曾经在调试一个包含30层嵌套的JSON数据时&#xff0c;print()的输出让我盯着屏…

作者头像 李华
网站建设 2026/10/13 23:50:12

SAP固定资产操作全链路:从AS01建卡到AFAB折旧与ABST2对账

简介&#xff1a;SAP固定资产操作手册.docx是一份面向SAP财务顾问、关键用户及资产会计的实操型SAP-FI培训资料。手册围绕固定资产全生命周期展开&#xff0c;涵盖资产主数据创建、更改与显示&#xff0c;外购处理&#xff0c;在建工程资本化&#xff0c;公司内部与公司间资产转…

作者头像 李华
网站建设 2026/10/13 23:48:07

CVPR 2026自动驾驶技术风向:BEV进化与端到端落地

每年CVPR放榜前后&#xff0c;都是自动驾驶圈子里最热闹的时间段。作为长期做感知和规划落地的人&#xff0c;我最关心的不是某个模型又在排行榜刷了多少分&#xff0c;而是这届会议能不能回答几个真问题&#xff1a;BEV感知还有没有价值、端到端能不能真正闭环、安全和数据问题…

作者头像 李华