- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
导读
fontTools.varLib.builder是 fontTools 中负责以编程方式构造 OpenType 变体字体(Variable Fonts)核心二进制结构的工具模块。无论是 HVAR/VVHA 的度量变体存储、avar2 的 VarIdxMap/VarStore、MVAR 的变体元数据,还是 COLRv1 的变体颜色,最终都要经由本模块的buildVarRegionList、buildVarData、buildVarStore等函数组装成规范定义的表格对象。读完本文,你将掌握变体数据的三层结构(Region / VarData / VarStore)如何逐层构建、NumShorts 优化算法的工作原理、Sparse 与 Multi 变体结构的区别,以及各构建函数的参数含义与调用惯例。
模块定位:文档即源码
该模块的技术文档位于 Doc/source/varLib/builder.rst,其正文由 Sphinx 的automodule指令构成:
.. automodule:: fontTools.varLib.builder :members: :undoc-members:这意味着文档的实际内容全部来自模块源码本身——即 Lib/fontTools/varLib/builder.py 中每个公开函数的实现与 docstring。因此,理解本模块的最佳途径就是逐行阅读源码,并结合其调用方与测试确认行为。它是fontTools.varLib(OpenType Variations 支持包,参见 Doc/source/varLib/index.rst)的核心基础设施之一。
模块全景:16 个构建函数的分层结构
从源码结构看,builder.py的公开 API 按职责分为四层:
| 层级 | 函数 | 作用 |
|---|---|---|
| Region 层 | buildVarRegionAxis、buildSparseVarRegionAxis、buildVarRegion、buildSparseVarRegion、buildVarRegionList、buildSparseVarRegionList | 构造变体"区域"(Region)及其轴支持 |
| VarData 层 | buildVarData、buildMultiVarData | 构造一组区域的增量数据项 |
| VarStore 层 | buildVarStore、buildMultiVarStore | 把 RegionList 与多个 VarData 组装成变体存储 |
| 变体辅助层 | buildVarIdxMap、buildDeltaSetIndexMap、buildVarDevTable | 构造索引映射与 Device 表 |
此外,模块还向otTables注入两个运行时优化方法:VarData.calculateNumShorts(带optimize参数)与VarData.optimize,以及一个保留的弃用名称VarData_CalculateNumShorts(默认optimize=True)。这些方法用于自动决定每个增量值域的字节宽度并重排列,详见下文。
所有函数都直接操作 Lib/fontTools/ttLib/tables/otTables.py 中的otTables对象,字段布局与 Lib/fontTools/ttLib/tables/otData.py 中定义的 OpenType 表格规范一一对应。
第一层:Region —— 变体数据的空间坐标
support 概念回顾
OpenType Variations 用一系列"三角形区域"来描述某个标量(如字形宽度)如何随轴变化:每个区域在最小处贡献为 0,线性上升到峰值(peak)处贡献为 1,再线性下降回 0。fontTools 将这些区域称为support,以字典形式表示——键是轴标签(axis tag),值是三元组(min, peak, max):
{"wght": (0, 1, 1)} # 仅覆盖 wght 轴的上半段 {"wdth": (-1, -1, 0)} # 仅覆盖 wdth 轴的下半段相关术语(axis、location、normalized location、support、delta、support scalar 等)的完整定义参见 Doc/source/varLib/index.rst 的 Terminology 一节。
buildVarRegionAxis 与 buildSparseVarRegionAxis
最底层的函数把单个轴的三元组坐标转换为规范要求的VarRegionAxis记录(三个 F2Dot14 定点数:StartCoord、PeakCoord、EndCoord):
def buildVarRegionAxis(axisSupport): self = ot.VarRegionAxis() self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport] return self def buildSparseVarRegionAxis(axisIndex, axisSupport): self = ot.SparseVarRegionAxis() self.AxisIndex = axisIndex self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport] return self两者的区别在于:Sparse 版本额外记录AxisIndex,用于稀疏(sparse)变体结构中显式指明该轴坐标属于哪一根轴。
buildVarRegion:按轴顺序补齐坐标
def buildVarRegion(support, axisTags): assert all(tag in axisTags for tag in support.keys()), ( "Unknown axis tag found.", support, axisTags, ) self = ot.VarRegion() self.VarRegionAxis = [] for tag in axisTags: self.VarRegionAxis.append(buildVarRegionAxis(support.get(tag, (0, 0, 0)))) return self关键行为:
- 必须按
axisTags的顺序为每一根轴生成一个VarRegionAxis条目; - support 中未出现的轴默认补
(0, 0, 0),即该轴对此区域无贡献; - 若 support 中出现未知轴标签(不在
axisTags内),直接触发AssertionError。
buildSparseVarRegion:只保留参与轴
def buildSparseVarRegion(support, axisTags): assert all(tag in axisTags for tag in support.keys()), (...) self = ot.SparseVarRegion() self.SparseVarRegionAxis = [] for i, tag in enumerate(axisTags): if tag not in support: continue self.SparseVarRegionAxis.append(buildSparseVarRegionAxis(i, support.get(tag, (0, 0, 0)))) self.SparseRegionCount = len(self.SparseVarRegionAxis) return self与稠密版本不同,稀疏区域只保留 support 中实际出现的轴,并用AxisIndex记录其在axisTags中的位置。这对应 OpenType 稀疏变体结构中SparseVarRegion的字段(SparseRegionCount+SparseVarRegionAxis数组,见 otData.py)。
buildVarRegionList 与 buildSparseVarRegionList
def buildVarRegionList(supports, axisTags): self = ot.VarRegionList() self.RegionAxisCount = len(axisTags) self.Region = [] for support in supports: self.Region.append(buildVarRegion(support, axisTags)) self.RegionCount = len(self.Region) return selfbuildSparseVarRegionList与之结构相同,只是逐条调用buildSparseVarRegion。注意RegionAxisCount直接取axisTags的长度——按 OpenType 规范,它应当始终等于fvar表的轴数量,即使在Region为空时也不能置 0(otTables.VarRegionList.preWrite中对此有专门处理,强制对齐fvar.axisCount,并引用了 OTS 校验工具的兼容性要求)。
第二层:VarData —— 增量数据与 NumShorts 优化
buildVarData
def buildVarData(varRegionIndices, items, optimize=True): self = ot.VarData() self.VarRegionIndex = list(varRegionIndices) regionCount = self.VarRegionCount = len(self.VarRegionIndex) records = self.Item = [] if items: for item in items: assert len(item) == regionCount records.append(list(item)) self.ItemCount = len(self.Item) self.calculateNumShorts(optimize=optimize) return self参数含义:
varRegionIndices:本组数据所引用的区域在 RegionList 中的下标列表(顺序很重要,因为它决定每个数据项的列序);items:增量数据项的二维列表,每一行长度必须等于regionCount(每个区域一个增量值);optimize=True时,构造完成后立即调用calculateNumShorts(optimize=True)进行列重排与宽度选择。
核心算法:VarData_calculateNumShorts
该函数是模块中最重要的算法,在 builder.py 中实现,并挂载为ot.VarData.calculateNumShorts。它解决两个问题:
1) 逐列计算所需字节宽度
对每一列(区域),扫描所有数据项,取该列增量绝对值(考虑二进制补码)所需的最大比特位,再换算为字节数:
bl = [(i + (i < -1)).bit_length() for i in item] byte_lengths = [((b + 8) >> 3) if b else 0 for b in bit_lengths]源码注释解释了两个"魔法"细节:
+ (i < -1)用于处理二进制补码:-128的bit_length()是 8,加上该修正后得到 7(1 字节可表示的最小值);同理-65536修正后得到 15(2 字节);而用i < -1而非i < 0,是为了让-1返回 1 而不是 0;- 字节数计算加 8(而非 7)是为符号位预留,最终等价于:
0 → 0 字节;-128 ≤ i < 128 → 1 字节;-65536 ≤ i < 65536 → 2 字节;更大 → 3 字节(longWords)。
2) 列重排优化
当optimize=True时,按"宽列在前、窄列在后"的规则重排列序(同时重排VarRegionIndex与每行Item),这样NumShorts值可以取最小必要值,从而让后续数据的窄列可以用更紧凑的宽度编码:
mapping.extend(i for i, b in enumerate(byte_lengths) if b > 2) # 3 字节列 mapping.extend(i for i, b in enumerate(byte_lengths) if b == 2) # 2 字节列 mapping.extend(i for i, b in enumerate(byte_lengths) if b == 1) # 1 字节列3) NumShorts 编码
- 存在超过 2 字节的列时,
NumShorts取"最后一个宽列下标 + 1",并置位0x8000(高位标志); - 否则
NumShorts为"最后一个非窄列下标 + 1"。
以上行为在 Tests/varLib/builder_test.py 中有系统化的参数化验证:test_buildVarData_no_optimize与test_buildVarData_optimize覆盖了从 0 区域到 3 区域、从 uint8 到 long(0x8001/0x8002/0x8003)的全部场景,例如([0], [[128]], 1)断言单个 128 增量需 1 个 short,而([0], [[32768]], 0x8001)断言 32768 需 1 个 long 并置位标志。
buildMultiVarData 与稀疏多增量
def buildMultiVarData(varRegionIndices, items): self = ot.MultiVarData() self.Format = 1 self.VarRegionIndex = list(varRegionIndices) regionCount = self.VarRegionCount = len(self.VarRegionIndex) records = self.Item = [] if items: for item in items: assert (len(item) % regionCount == 0) if regionCount else not item records.append(list(item)) self.ItemCount = len(self.Item) return selfbuildMultiVarData对应 COLRv1 中用于可变复合字形(VarCompositeGlyphs)的MultiVarData结构:每个数据项的长度是区域数的整数倍(允许每个区域贡献多个增量,如位置 + 旋转角),断言条件相应地变为len(item) % regionCount == 0。test_buildMultiVarData_invalid_tuple验证了不满足该约束时会抛出AssertionError。
第三层:VarStore —— 组装完整的变体存储
buildVarStore
def buildVarStore(varRegionList, varDataList): self = ot.VarStore() self.Format = 1 self.VarRegionList = varRegionList self.VarData = list(varDataList) self.VarDataCount = len(self.VarData) return self这是最常用的高层入口:把一个VarRegionList和一组VarData打包成一个VarStore。按 otData.py 的字段定义,VarStore由Format(恒为 1)、VarRegionList偏移、VarDataCount与VarData偏移数组组成。
buildMultiVarStore
def buildMultiVarStore(varRegionList, multiVarDataList): self = ot.MultiVarStore() self.Format = 1 self.SparseVarRegionList = varRegionList self.MultiVarData = list(multiVarDataList) self.MultiVarDataCount = len(self.MultiVarData) return self对应稀疏变体结构,MultiVarStore使用SparseVarRegionList与MultiVarData列表(otData.py 中MultiVarStore、MultiVarData、SparseVarRegion、SparseVarRegionAxis的字段定义与之完全吻合)。
实战调用组合
在 Tests/varLib/instancer/instancer_test.py 中可以看到三个函数组合使用的典型模式:
vvar.VarStore = builder.buildVarStore( builder.buildVarRegionList( [{"wght": (0, 1, 1)}, {"wdth": (-1, -1, 0)}], ["wght", "wdth"], ), [builder.buildVarData([0, 1], [[0, 0], [10, 0], [100, 30], [-50, 0]])], )即:先建 RegionList(定义全部区域),再建 VarData(引用区域下标并填充增量),最后组装 VarStore。这也是 fontTools 内部各变体表构建流程的标准模式。
第四层:变体辅助构建函数
buildVarIdxMap —— 字形索引映射
def buildVarIdxMap(varIdxes, glyphOrder): self = ot.VarIdxMap() self.mapping = {g: v for g, v in zip(glyphOrder, varIdxes)} return self把"每个字形对应的变体索引"与字形顺序绑定成字典,用于 HVAR/VVAR 的AdvWidthMap、LsbMap等字段。序列化时(otTables.VarIdxMap.preWrite)会按字形顺序取出索引,并自动裁剪末尾连续重复的条目以节省空间。
buildDeltaSetIndexMap —— 紧凑索引映射
def buildDeltaSetIndexMap(varIdxes): mapping = list(varIdxes) if all(i == v for i, v in enumerate(mapping)): return None self = ot.DeltaSetIndexMap() self.mapping = mapping self.Format = 1 if len(mapping) > 0xFFFF else 0 return self用于 avar2(VarIdxMap字段)与 COLRv1(VarIndexMap字段)。两个值得注意的行为:
- 若索引恰好是
[0, 1, 2, ...]的恒等映射,直接返回None——这是"直接映射"的最优编码,不占用额外空间; Format取决于条目数是否超过 65535,超过则用 32 位条目(Format 1)。
fontTools.varLib的_add_avar与 COLRv1 合并流程都依赖该函数(见 Lib/fontTools/varLib/init.py 中buildDeltaSetIndexMap(varIdxes[t] for t in axisTags)与buildDeltaSetIndexMap(varIdxes)的调用)。
buildVarDevTable —— 变体 Device 表
def buildVarDevTable(varIdx): self = ot.Device() self.DeltaFormat = 0x8000 self.StartSize = varIdx >> 16 self.EndSize = varIdx & 0xFFFF return self将变体索引打包进一个特殊的Device表:DeltaFormat = 0x8000是 OpenType 规范中"变体索引"专用格式,高 16 位与低 16 位分别存进StartSize与EndSize。这是 Lib/fontTools/feaLib/builder.py 中构造可变锚点(anchor)设备表的标准途径:
if index is not None and index != 0xFFFFFFFF: device = buildVarDevTable(index)与周边模块的协作关系
builder.py本身是"纯构造器",不负责增量计算或区域划分,真正驱动构建流程的是 varLib 的其它模块:
- Lib/fontTools/varLib/varStore.py:
OnlineVarStoreBuilder在__init__中调用buildVarRegionList([], axisTags)与buildVarStore(regionList, [])建立空骨架,随后按需调用buildVarRegion追加区域、buildVarData新建数据块——也就是以 builder 为地基实现"在线增量式"变体存储构建; - Lib/fontTools/varLib/multiVarStore.py:
OnlineMultiVarStoreBuilder同样基于buildSparseVarRegionList、buildMultiVarStore等构建稀疏/多增量存储; - Lib/fontTools/varLib/init.py:HVAR/MVAR/avar2/COLRv1 的实际构建路径大量使用
buildVarRegionList、buildVarData、buildVarIdxMap、buildDeltaSetIndexMap等函数; - Lib/fontTools/varLib/instancer:实例化过程中利用 builder 函数重建或修改变体存储结构。
测试佐证:行为即契约
Tests/varLib/builder_test.py 是理解本模块行为的最佳契约文档:
test_buildVarData_no_optimize与test_buildVarData_optimize:完整覆盖 NumShorts 计算与列重排的 20 余种输入组合(含 uint8/short/long、乱序列重排、全零列剔除等),例如([0,1,2], [[0,128,2],...], 1)断言重排后区域顺序变为[1,0,2]、数据变为[[128,0,2],...];test_buildMultiVarData:验证多增量数据的整除约束与字段回读;test_empty_vhvar_size:验证 HVAR/VVHA 在无度量变体时仍应存在但保持空、并使用直接映射(即buildDeltaSetIndexMap返回None的场景)。
小结:何时使用 builder 模块
如果你正在为以下场景编写代码,都可以直接复用fontTools.varLib.builder:
- 自定义变体表生成:手工组装 HVAR/VVAR/MVAR 的 VarStore(
buildVarRegionList→buildVarData→buildVarStore); - avar2 / COLRv1 索引映射:使用
buildDeltaSetIndexMap获得最紧凑编码,恒等映射自动省略; - 可变锚点 / Feature Variations:用
buildVarDevTable把变体索引编码进 Device 表; - 稀疏 / 多增量变体:面向 COLRv1 复合字形使用
buildSparseVarRegionList、buildMultiVarData、buildMultiVarStore; - 嵌入 varLib 高层流程:让
OnlineVarStoreBuilder替你完成区域去重、数据块拆分与索引映射优化。
builder.py中每个函数都直接对应 OpenType 规范中的一个表格结构,行为由 builder_test.py 严格锁定,是构建任何 OpenType 变体数据的可靠基础。
- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
相关推荐
fontTools varLib featureVars 模块完全指南:为可变字体构建 OpenType FeatureVariations 条件字形替换
fontTools varLib featureVars 模块完全指南:为可变字体构建 OpenType FeatureVariations 条件字形替换 fe
开发工具fonttools 的 TupleVariation 模块:OpenType 变体字体(gvar/cvar)底层变体数据的编译与解析指南
fonttools 的 TupleVariation 模块:OpenType 变体字体(gvar/cvar)底层变体数据的编译与解析指南 导读 TupleVar
开发工具FontTools cvar 表全解析:CVT 变体数据结构的编译、解译与工程实践
FontTools cvar 表全解析:CVT 变体数据结构的编译、解译与工程实践 导读 本文聚焦 FontTools 中 OpenType cvar (Con
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考