- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
导读
本文聚焦 FontTools 中 OpenTypecvar(Control Value Table Variations,CVT 变体)表的完整实现,讲解其二进制布局、TupleVariation数据结构、tuple/delta 编码规则,以及它如何与cvt、fvar表协同支撑 TrueType 可变字体的 Hinting 变体。读完本文,你将掌握cvar表从编译、解译到 XML 序列化的全链路原理,并能在 varLib 构建、instancer 实例化等真实场景中熟练操作该表。
一、cvar表是什么:为 TrueType 指令的 CVT 值提供变体
在 OpenType 可变字体(Variable Fonts)体系中,gvar负责字形轮廓的变体,而cvar则是与之平行的另一张变体表——它的服务对象不是轮廓点,而是Control Value Table(cvt)中的控制值。
_c_v_a_r.rst明确定义:cvar是一个 OpenType 表,其类文档(fontTools.ttLib.tables._c_v_a_r模块)说明它包含的是cvt表中数值的变体(variations for the values in acvttable)。在 TrueType 字体中,cvt表存放一列可供字体指令(如 fpgm/prep 中的 hinting 指令)引用的控制值,而cvar则让这些控制值能够随可变轴的变化而变化——例如在字重(wght)轴上让某个 CVT 值按特定支持区间动态调整。
从源码结构看,_c_v_a_r.py中的table__c_v_a_r类继承自DefaultTable.DefaultTable,并声明了它的依赖关系:
dependencies = ["cvt ", "fvar"]这意味着cvar表在编译时依赖cvt表(提供 CVT 值的数量与顺序)和fvar表(提供可变轴的 axisTag 列表)。FontTools 在保存字体时会先按依赖顺序写入这些表(见 ttFont.py 中_writeTable对dependencies的递归处理)。
二、二进制格式:头部四元组与 TupleVariationStore
cvar表的二进制布局由_c_v_a_r.py中定义的CVAR_HEADER_FORMAT描述,采用大端序:
majorVersion: H # 主版本号 minorVersion: H # 次版本号 tupleVariationCount: H # tuple 变体个数 offsetToData: H # 变体数据(shared points/deltas)相对表起始的偏移表头共 8 字节(CVAR_HEADER_SIZE)。在__init__中,默认majorVersion, minorVersion = 1, 0,且variations初始为空列表。
compile方法(_c_v_a_r.py)的编译流程清晰地展示了表结构:
- 从
self.variations中过滤掉hasImpact()为 False 的变体(即没有任何有效 delta 的 tuple,省略后对字体无可见影响); - 以
len(ttFont["cvt "].values)作为 pointCount,以fvar各轴的axisTag作为 axisTags,调用compileTupleVariationStore生成(tupleVariationCount, tuples, data); - 将表头打包后拼接
tuples(tuple 记录区)与data(共享点与 delta 数据区)输出。
其中offsetToData = CVAR_HEADER_SIZE + len(tuples),即数据区紧跟表头与 tuple 记录之后。
decompile方法(_c_v_a_r.py)与之对称:先解出表头,断言majorVersion == 1,再以tupleVariationCount、pointCount=len(cvt.values)、dataPos=offsetToData调用decompileTupleVariationStore恢复self.variations列表。
三、核心数据结构:TupleVariation与轴区域三元组
cvar的变体集合由fontTools.ttLib.tables.TupleVariation模块中的TupleVariation类承载,这也是_c_v_a_r.rst用独立小节专门引用该模块的原因。
每个TupleVariation包含两个核心字段(TupleVariation.py):
axes:一个 dict,键为 axisTag,值为(minValue, peakValue, maxValue)三元组,刻画该变体在对应轴上的支持区间(support region);coordinates:一个与 CVT 值一一对应的 delta 列表。对cvar而言每个元素是单个数值(表示该 CVT 索引的调整量),对gvar而言则是(x, y)二元组。
getCoordWidth()(TupleVariation.py)正是通过判断首个非 None delta 的类型来区分两种表:int/float返回 1(cvar),(x, y)二元组返回 2(gvar)。
关于None的语义,源码注释给出了重要说明(见__iadd__方法):在gvar中未显式给出的点需要推断(inferred deltas),而在cvar中未给出的 CVT 索引意味着不做调整,等价于 0。
getUsedPoints()(TupleVariation.py)返回被使用的索引集合:若所有 delta 均非 None,返回空集合表示"全部使用";否则返回非 None 索引的 frozenset;一个都不用时返回None。hasImpact()则用于判定该变体是否有实际影响(任一坐标非 None 即为 True),无影响的变体在编译时会被剔除。
四、tuple 记录编码:flags、峰值与区域推断
每个 tuple 变体在二进制中由 TupleVariationHeader 描述,其核心是一个 16 位tupleIndex/flags 字段。相关标志常量定义于TupleVariation.py:
| 标志 | 值 | 含义 |
|---|---|---|
EMBEDDED_PEAK_TUPLE | 0x8000 | 峰值坐标内嵌在当前 tuple 记录中(否则引用共享 tuple 索引) |
INTERMEDIATE_REGION | 0x4000 | 使用显式的中间区域(start/end tuple),否则按峰值推断 |
PRIVATE_POINT_NUMBERS | 0x2000 | 使用私有点编号集(否则使用共享点集) |
TUPLES_SHARE_POINT_NUMBERS | 0x8000 | 记录在 tupleVariationCount 高位,表示存在共享点集 |
峰值坐标每个轴以 2 字节有符号定点数(14 位小数,fl2fi(x, 14))编码。compileCoord为每个轴打包 peak 值,未涉及的轴写入\0\0。
compileIntermediateCoord(TupleVariation.py)会比较显式min/max与默认推断值是否一致:对任一轴而言,默认min = min(peak, 0)、默认max = max(peak, 0),只有存在差异时才需要编码中间区域(start 与 end 各 axisCount×2 字节)。
若未设置INTERMEDIATE_REGION,解译时通过inferRegion_(TupleVariation.py)按同一规则从峰值推断出区域,从而压缩存储。
五、共享机制与 delta 编码优化
共享点集(shared points)
compileTupleVariationStore(TupleVariation.py)会统计所有变体使用的点集,找出"被多个 tuple 复用、节省字节最多"的点集作为共享点集,将其编码后放在数据区开头,并把tupleVariationCount的高位置位(TUPLES_SHARE_POINT_NUMBERS)。复用该点集的 tuple 在数据区不再重复写入点编号(以b""表示"使用共享点")。
点编号本身采用delta 编码 + 分段 run的紧凑形式(compilePoints,TupleVariation.py):先写总点数(小于 0x80 用 1 字节,否则用 2 字节并在首字节置POINTS_ARE_WORDS标志),随后每段 run 以 1 字节头部记录(run 长度减一,POINTS_ARE_WORDS位指示 8/16 位编码),数据为该段内相邻点号的增量。
delta 值的多级编码
compileDeltaValues_(TupleVariation.py)将 delta 序列按 run 编码,每个 run 的头部字节用 6 个低有效位(DELTA_RUN_COUNT_MASK = 0x3F)记录长度减一(范围 1..64),高两位(DELTAS_SIZE_MASK = 0xC0)指示数值宽度:
| 头部高位组合 | 名称 | delta 存储宽度 |
|---|---|---|
DELTAS_ARE_ZERO(0x80) | 全零 run | 无数据,仅头部 |
0x40未置位 | bytes run | 有符号 8 位整数 |
DELTAS_ARE_WORDS(0x40) | words run | 有符号 16 位整数 |
DELTAS_ARE_LONGS(0xC0) | longs run | 有符号 32 位整数 |
在optimizeSize=True(默认)时,编译器会贪心地选择每段 run 的最优编码:连续零值用 zero-run 压缩、字节范围用 bytes、16 位范围用 words、超出 16 位的用 longs;同时避免在可继续用更小宽度时过早截断 run(源码注释中以[15, 15, 0, 15, 15]等序列举例说明何时应开启新 run 以获得更小体积)。解译侧decompileDeltas_(TupleVariation.py)依据同样的头部规则恢复出完整 delta 序列。
六、XML 表示:<tuple>、<coord>与<delta>
cvar表的 XML 序列化(toXML/fromXML,见_c_v_a_r.py)以<version>开头,然后逐个输出<tuple>。每个 tuple 内包含:
<coord>:描述轴区域。仅有峰值时写作axis+value(14 位定点数精度,fl2str(value, 14));存在显式中间区域时附加min与max属性;<delta>:对cvar写作cvt+value(CVT 索引与该值调整量);对gvar则写作pt+x/y。坐标值通过safeEval从字符串安全求值。
解析时(TupleVariation.fromXML,TupleVariation.py),coord的value经str2fl(attrs["value"], 14)转为浮点,未给出的min/max依据峰值自动推断默认区域,最终存入axes[axis] = (min, peak, max)三元组。
下面是一段来自_c_v_a_r_test.py的真实 XML 样例(两个 tuple,wght/wdth双轴),可直观对照上一节的区域规则:
<version major="1" minor="0"/> <tuple> <coord axis="wght" value="1.0"/> <delta cvt="2" value="3"/> <delta cvt="3" value="1"/> <delta cvt="4" value="4"/> </tuple> <tuple> <coord axis="wght" value="-1.0"/> <coord axis="wdth" value="0.8"/> <delta cvt="2" value="9"/> <delta cvt="3" value="7"/> <delta cvt="4" value="8"/> </tuple>注意第二个 tuple 的wght峰值是 -1.0,推断出的区域为 (-1.0, -1.0, 0.0),即该变体只作用于 wght 轴的负区间;wdth峰值 0.8 则对应区域 (0.0, 0.8, 0.8)。
七、cvar与cvt、fvar的协同
在 FontTools 中,三张表的职责边界非常清晰:
cvt(_c_v_t.py):存放一列有符号 16 位整数值(array.array("h")),是 TrueType 指令可引用的控制值基准;XML 中以<cv value="..." index="..."/>表示;fvar(_f_v_a_r.py):声明可变轴(axisTag、min/default/max)与命名实例,cvar通过fvar.axes的 axisTag 列表与轴序对应起来;cvar:为cvt的每个值按轴区域提供 delta,形成"随轴可变的 CVT"。
正因cvar的 pointCount 与轴序分别取自cvt与fvar,其dependencies = ["cvt ", "fvar"]是强制的:解译时ttFont["cvt "]、ttFont["fvar"]必须已就绪,否则无法确定 delta 的索引含义与轴标签。
八、生产与消费:varLib 构建、instancer 与 mutator 中的 cvar
可变字体构建时生成 cvar
在varLib的可变字体构建流程中,_merge_TTHinting(varLib/__init__.py)负责把各 master 的 TrueType hinting 合并:先比对各 master 的fpgm/prep与字形 hinting 是否兼容,不兼容则整体丢弃 hinting(_remove_TTHinting会连带删除cvar、cvt、fpgm、prep表);随后对各 master 的cvt值列表建立变体模型(getDeltasAndSupports),将非零 delta 包装为TupleVariation并写入新建的cvar表(cvar.version = 1)。这一逻辑印证了cvar在仓库中的典型产出路径。
实例化(instancer)时应用 cvar
varLib.instancer的instantiateCvar(varLib/instancer/__init__.py)在实例化可变字体时:先通过instantiateTupleVariationStore计算指定轴限制下的默认 delta,再用setCvarDeltas把 delta 加到cvt表对应值上;若变体被完全消解(cvar.variations为空),则删除cvar表。这正是"实例化 = 把变体折叠进默认值"思想的体现。
mutator 中的 cvar
类似的,varLib.mutator(varLib/mutator.py)在生成指定坐标的静态字体时,也显式处理cvar:对每个变体按轴坐标缩放 delta,并累加到cvt值上。
九、测试与验证:从字节到对象的闭环
_c_v_a_r_test.py提供了针对本表最直接的验证样例:构造含 5 个 CVT 值([0, 0, 0, 1000, -2000])与wght/wdth双轴的字体,然后以两组TupleVariation(其一为内嵌峰值 + 私有点,其二为共享点)做编译/解译/XML 双向往返:
test_compile:编译结果与手写字节流CVAR_PRIVATE_POINT_DATA完全一致(逐字节比对hexStr);test_compile_shared_points:启用useSharedPoints=True后,与共享点版本的字节流CVAR_DATA一致,其中tupleVariationCount = 0x8002演示了共享点标志如何置位;test_decompile/test_decompile_shared_points:反向解译并逐轴、逐 delta 断言与原始对象近似相等;test_fromXML/test_toXML:验证 XML 双向序列化与CVAR_XML一致。
Tests/ttLib/tables/TupleVariation_test.py则覆盖了TupleVariation的相等性判断、hasImpact、坐标编译、共享峰值/中间区域/私有点/共享点等编码组合,可作为深入研读本表二进制编码的补充材料。若想快速观察真实字体的cvar,可在当前仓库中直接用 FontTools 自带的ttx工具将含 cvar 的字体转存为 XML 后查看<tuple>/<coord>/<delta>结构。
结语
cvar表是 TrueType 可变字体 hinting 体系的关键一环:它以 tuple 为单位描述 CVT 控制值随轴的变化,借助共享点、中间区域推断、多级 delta 编码等手段在紧凑性与表达力之间取得平衡。理解_c_v_a_r.py与TupleVariation.py的实现,不仅有助于调试 hinting 变体问题,也能让你在基于 FontTools 构建、实例化或定制可变字体时,准确掌控这一容易被忽视的底层数据通路。
- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
相关推荐
fonttools 的 TupleVariation 模块:OpenType 变体字体(gvar/cvar)底层变体数据的编译与解析指南
fonttools 的 TupleVariation 模块:OpenType 变体字体(gvar/cvar)底层变体数据的编译与解析指南 导读 TupleVar
开发工具FontTools 中的 BASE 表:OpenType 基线表的编译、反编译与构建全流程
FontTools 中的 BASE 表:OpenType 基线表的编译、反编译与构建全流程 本文围绕 FontTools 仓库中 BASE (Baseline)
开发工具fontTools MVAR 表完整指南:可变字体全局度量变形的编译、解析与应用
fontTools MVAR 表完整指南:可变字体全局度量变形的编译、解析与应用 MVAR(Metrics Variations)是 OpenType 可变字体
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考