- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
在 OpenType 字体中,vmtx(Vertical Metrics)表为每个字形提供垂直排版所需的度量数据——垂直前进高度(height)与上侧距(top sidebearing, tsb)。FontTools 通过一个仅约二十行的表实现文件 Lib/fontTools/ttLib/tables/_v_m_t_x.py 完整支持了该表的反编译、编译与 XML 转换。本文围绕这一实现,讲清vmtx表的二进制结构、它在 FontTools 内部的类继承机制,以及 subset、varLib、scaleUpem 等模块如何复用同一套 metrics 数据,帮助你在处理中日韩竖排字体时能够正确地读写、修改与调试垂直度量。
vmtx 表的作用与数据结构
vmtx是标准的 OpenType 表,与glyf、CFF、CFF2等轮廓表配合使用,为垂直文本(如竖排汉字)提供逐字形的排版参数。其官方规范定义见表类 docstring 中的引用(Microsoft OpenType 规范的 vmtx 章节,源码注释中已给出出处,参见 table__v_m_t_x)。
表的核心结构由两部分组成:
- 前
numberOfVMetrics个长度量记录:每条记录 4 字节,包含该字形的advance height(垂直前进高度,无符号 16 位)和topSidebearing(上侧距,有符号 16 位); - 剩余字形的短记录:每条仅 2 字节的 sidebearing,其 advance height 沿用前一条记录的 advance。
这种"长度量 + 短 sidebearing"的压缩方式与hmtx表完全一致,也是 FontTools 中两者共享同一套实现的基础。
核心实现:复用 hmtx 超类,仅替换四个属性
vmtx表实现的全部"代码"就是一个继承:
from fontTools import ttLib superclass = ttLib.getTableClass("hmtx") class table__v_m_t_x(superclass): """Vertical Metrics table ... """ headerTag = "vhea" advanceName = "height" sideBearingName = "tsb" numberOfMetricsName = "numberOfVMetrics"(摘自 Lib/fontTools/ttLib/tables/_v_m_t_x.py)
这里体现了 FontTools 的一个设计要点:vmtx没有自己实现任何算法,全部decompile/compile/toXML/fromXML逻辑都继承自hmtx的表类table__h_m_t_x。子类只重写了四个类属性,把hmtx的实现"参数化"到垂直场景:
| 类属性 | hmtx 中的值 | vmtx 中的值 | 含义 |
|---|---|---|---|
headerTag | hhea | vhea | 存放numberOf*Metrics的头部表 |
advanceName | width | height | advance 度量在 XML 中的属性名 |
sideBearingName | lsb | tsb | sidebearing 在 XML 中的属性名 |
numberOfMetricsName | numberOfHMetrics | numberOfVMetrics | 头部表中度量计数字段名 |
类名table__v_m_t_x的下划线命名(vmtx→_v_m_t_x)对应ttLib.getTableClass()的查表规则:getTableClass会把表 tag 转成 Python 标识符后,从对应模块中取table_<标识符>属性,见 Lib/fontTools/ttLib/ttFont.py。因此在字体对象上写font["vmtx"],返回的正是这个table__v_m_t_x实例(TTFont甚至为其提供了类型标注重载,见 ttFont.py 中的__getitem__/get重载)。
测试文件 Tests/ttLib/tables/_v_m_t_x_test.py 也印证了这种关系:VmtxTableTest直接继承hmtx的测试基类,只替换tableClass与tag两个属性,整套 hmtx 的反编译/编译/XML 测试便同时验证了 vmtx。
decompile:从二进制还原 metrics 字典
继承自table__h_m_t_x.decompile(Lib/fontTools/ttLib/tables/_h_m_t_x.py)的反编译流程对 vmtx 同样生效,要点如下:
- 从
maxp.numGlyphs取得字形总数; - 从
ttFont["vhea"].numberOfVMetrics取得长度量记录条数(头部表不存在时回退为numGlyphs;若该值超过numGlyphs会发出警告并截断); - 用
struct.unpack(">Hh" * numberOfMetrics, ...)解析前4 * numberOfMetrics字节(Hh是longMetricFormat,即 height 为无符号、tsb 为有符号); - 剩余字节按 2 字节有符号
array.array("h", ...)解析为 sidebearings,并在小端机器上做字节交换; - 全部结果写入
self.metrics字典,键为字形名,值为(height, tsb)元组;长度量之后的字形沿用最后一条记录的 height(源码中变量名为lastAdvance,对 vmtx 即lastHeight)。
数据量校验同样完整:表数据不足时抛出TTLibError(not enough 'vmtx' table data: expected N bytes, got M),多余时仅记录too much 'vmtx' table data警告。
compile:回写二进制并优化 numberOfVMetrics
compile(_h_m_t_x.py)的执行细节:
- 遍历
ttFont.getGlyphOrder()收集所有(advance, sideBearing),遇到负 advance 记录 error 日志(vmtx表无法容纳负 height,struct.pack超范围时会抛出明确错误'vmtx' table can't contain negative advance heights); - 若字体带有
vhea表,会从末尾向前收缩numberOfVMetrics:所有与最后一个 advance 相同且只剩 sidebearing 的记录会被压缩进短记录区,并把新的计数写回vhea.numberOfVMetrics(setattr(headerTable, "numberOfVMetrics", numberOfMetrics))。这是 OpenType 规范推荐的最小化存储方式,也是编译后字体体积变小的一个来源; - 数值在打包前经过
otRound取整,因此 Python API 中允许写入浮点数。
XML 格式(ttx)
toXML/fromXML决定ttx输出中 vmtx 的呈现,每个字形一行mtx标签,属性名由advanceName/sideBearingName决定:
<vmtx> <mtx name=".notdef" height="600" tsb="222"/> <mtx name="A" height="700" tsb="200"/> <mtx name="B" height="800" tsb="300"/> ... </vmtx>(以上取自仓库测试数据 Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx)
用ttx -o out.ttx in.otf导出再导入即可编辑垂直度量;属性height与tsb与水平表中的width/lsb一一对应。fromXML通过name == "mtx"分支把两个属性safeEval后存回self.metrics[glyphName]。
Python API 操作 vmtx
由于继承了hmtx的下标协议,vmtx 表的metrics字典支持直接读写与删除:
from fontTools.ttLib import TTFont font = TTFont("vertical.otf") # 读取某字形的 (height, tsb) print(font["vmtx"]["A"]) # (700, 200) # 直接下标读取 height, tsb = font["vmtx"]["A"] # 修改 font["vmtx"]["A"] = (720, 210) # 删除某个条目 del font["vmtx"]["A"] # 遍历:metrics 是普通 dict for name, (height, tsb) in font["vmtx"].metrics.items(): ... font.save("vertical-out.otf")其中__getitem__/__setitem__/__delitem__定义在 table__h_m_t_x,注意赋值时会把元组强制转成tuple。
程序化构建字体的场景下,fontBuilder提供了更高层入口 setupVerticalMetrics:传入{glyphName: (height, topSidebearing)}字典,内部经setupMetrics对每个值int(round())取整后写入mtxTable.metrics。注意其 docstring 中"for horizontal metrics"的措辞是从setupHorizontalMetrics复制时残留的笔误,实际创建的是vmtx表——以源码中self.setupMetrics("vmtx", metrics)这一行为准。
vhea 与 vmtx 的联动:recalc 垂直头部
vhea表类table__v_h_e_a(Lib/fontTools/ttLib/tables/_v_h_e_a.py)声明了dependencies = ["vmtx", "glyf", "CFF ", "CFF2"],并在保存且recalcBBoxes开启时调用recalc(ttFont),用vmtx数据联动重算头部统计量:
advanceHeightMax:取所有vmtx.metrics中最大的 height;minTopSideBearing/minBottomSideBearing/yMaxExtent:结合glyf字形包围盒(yMax - yMin)或 CFF CharString 的calcBounds逐字形计算,其中底侧距bsb = advanceHeight - tsb - boundsHeight;- 无任何轮廓字形时三个统计量归零。
从源码结构看,修改 vmtx 后如果希望 vhea 头部统计保持自洽,保存时 FontTools 会自动完成这一步(前提是 glyf/CFF 表已加载),无需手动同步字段。
相关模块中的 vmtx 应用
vmtx数据在 FontTools 各工具模块中被广泛消费,以下均为可直接核实的调用点:
- subset:
fontTools.subset通过@_add_method(ttLib.getTableClass("vmtx"))为 vmtx 类注入subset_glyphs,按保留字形集合裁剪metrics字典,被置空的字形写为(0, 0)(Lib/fontTools/subset/init.py)。因此对含竖排度量的 CJK 字体执行子集化时垂直信息不会丢失。 - varLib(可变字体):构建时
_MasterData会把 master 的getattr(m.get("vmtx"), "metrics", None)纳入插值(Lib/fontTools/varLib/init.py);当可变字体含vmtx时自动生成VVAR表(VVAR_FIELDS中metricsTag="vmtx",字段AdvHeightMap/TsbMap/BsbMap,见 varLib/init.py),且仅在"vmtx" in vf时才触发_add_VVAR。instancer 实例化 CFF2 字体时也会用 VVAR 的AdvHeightMap更新 vmtx 数值(Lib/fontTools/varLib/instancer/init.py)。 - scaleUpem:
ScalerVisitor显式注册了对hmtx与vmtx两个表类metrics属性的缩放(Lib/fontTools/ttLib/scaleUpem.py),放大/缩小 upem 时 height 与 tsb 会按同一因子缩放。 - merge:字体合并为
hmtx与vmtx设置同一mergeMap——metrics用sumDicts合并(Lib/fontTools/merge/tables.py)。 - TTGlyphSet:
getGlyphSet中的字形对象在字体含vmtx时会附带height与tsb属性,否则为None(Lib/fontTools/ttLib/ttGlyphSet.py 与 类文档),方便按 Pen 协议绘制时获取垂直信息。
小结与实践要点
vmtx在 FontTools 中是hmtx表类的纯子类,差异完全由headerTag="vhea"、advanceName="height"、sideBearingName="tsb"、numberOfMetricsName="numberOfVMetrics"四个属性表达(Lib/fontTools/ttLib/tables/_v_m_t_x.py);- 内存模型统一为
table.metrics字典:{glyphName: (height, tsb)},支持font["vmtx"][name]直接读写; - 二进制格式为
numberOfVMetrics条 4 字节(H, h)记录 + 每剩余字形 1 条 2 字节h短记录,编译时自动压缩短记录区并回写vhea.numberOfVMetrics; - ttx XML 中每字形为
<mtx name="..." height="..." tsb="..."/>,可用ttx工具导入导出; - subset、varLib、scaleUpem、merge、TTGlyphSet 均原生处理 vmtx,竖排字体的主流维护流程(子集化、可变字体化、upem 缩放、合并)无需额外适配。
调试与回归验证可参考测试用例 Tests/ttLib/tables/_v_m_t_x_test.py(完整复用 hmtx 的 decompile/compile/XML 测试矩阵)以及包含 vmtx 段落的真实 ttx 数据 Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx。
- 开发工具
【免费下载链接】fonttools
A library to manipulate font files from Python.
相关推荐
FontTools 中的 VDMX 表:垂直设备度量表的二进制格式解析与 TTX 往返
FontTools 中的 VDMX 表:垂直设备度量表的二进制格式解析与 TTX 往返 VDMX(Vertical Device Metrics) 是 Open
开发工具FontTools meta 表详解:OpenType 字体元数据表的实现原理与实战操作
FontTools meta 表详解:OpenType 字体元数据表的实现原理与实战操作 本文围绕 FontTools 中 OpenType meta 表的实现
开发工具fontTools 中的 Graphite Sill 表:S__i_l_l.py 实现深度解析
fontTools 中的 Graphite Sill 表:S__i_l_l.py 实现深度解析 Graphite 是一个面向复杂文字排版的高级字形变换引擎,其字
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考