news 2026/10/13 2:12:52

fontTools tfmLib 详解:用 Python 解析 TeX Font Metrics (TFM) 字体度量文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fontTools tfmLib 详解:用 Python 解析 TeX Font Metrics (TFM) 字体度量文件
  • 开发工具

【免费下载链接】fonttools

A library to manipulate font files from Python.

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

tfmLib是 fontTools 中专用于只读解析 TFM(TeX Font Metrics)文件的模块。TFM 是 TeX 排版系统用来存储字体度量信息(字宽、高度、深度、斜切修正、连字、字偶距、字体参数等)的二进制格式。读完本文,你将能够:用TFM类从路径或文件对象加载任意 TFM 文件;通过属性访问字体元数据、字符度量、kerning 与 ligature 表;理解 TFM 二进制文件布局、12.20 定点数编码以及模块内部的结构校验逻辑,并可用测试数据文件自行验证所有示例。

一、TFM 文件在磁盘上长什么样

TFM 格式的定义源自 TeX 配套工具 TFtoPL 的 WEB 源码(模块 docstring 中给出了格式出处说明)。从 Lib/fontTools/tfmLib.py 的源码结构看,一个 TFM 文件由若干**32 位大端字(big-endian word)**顺序拼接而成,文件开头是 6 个字的“尺寸表”(SIZES_FORMAT,Lib/fontTools/tfmLib.py#L42-L56):

lf: h # 整个文件的长度(字数) lh: h # 头部数据的长度(字数) bc: h # 字体中最小的字符代码 ec: h # 字体中最大的字符代码 nw: h # 字宽表字数 nh: h # 高度表字数 nd: h # 深度表字数 ni: h # italic correction 表字数 nl: h # ligature/kern 表字数 nk: h # kern 表字数 ne: h # 可伸缩字符表字数 np: h # 字体参数字数

尺寸表之后依次是:头部数据(lh个字)、逐字符信息(ec - bc + 1个字,每字符 4 字节)、各度量表、ligature/kern 指令表、kern 值表、可伸缩字符 recipe 表、字体参数表。解析器会先校验这些子文件大小之和必须等于声明的总长lf,否则抛出异常(见后文第五节)。

TFM 中所有浮点度量值(字宽、高度、designsize 等)都以12.20 定点数存储:12 位整数位、20 位小数位,源码中定义为FIXED_FORMAT = "12.20F"(Lib/fontTools/tfmLib.py#L60),读取时自动转换为 Python float。所有度量值都是相对于designsize的 em 单位,例如designsize为 10 的字体,QUAD值约为 1.0,表示一个 em 宽。

二、API 入口:TFM 类

模块只暴露两个符号:TFM类与TFMException异常(Lib/fontTools/tfmLib.py#L153-L160)。TFM.__init__接受一个参数file,既可以是文件路径(字符串),也可以是任何带read()方法的文件对象(如io.BytesIO),构造时立即完成全部解析:

from fontTools.tfmLib import TFM tfm = TFM("Tests/tfmLib/data/cmr10.tfm") print(tfm) # <TFM for CMR in TeX text at 10pt>

模块 docstring(也就是 Doc/source/tfmLib.rst 通过automodule指令自动渲染的内容)给出了标准用法示例,仓库中的 doctest 数据文件 Tests/tfmLib/data/cmr10.tfm 可直接复现:

>>> from fontTools.tfmLib import TFM >>> tfm = TFM("Tests/tfmLib/data/cmr10.tfm") >>> >>> # Accessing an attribute gets you metadata. >>> tfm.checksum 1274110073 >>> tfm.designsize 10.0 >>> tfm.codingscheme 'TeX text' >>> tfm.family 'CMR' >>> tfm.seven_bit_safe_flag False >>> tfm.face 234

__repr__会输出<TFM for {family} in {codingscheme} at {designsize}pt>,方便调试时一眼确认字体身份。

三、读取文件头部元数据

TFM 头部支持多个“版本”,解析器按lh(头部字数)从大到小匹配四种头部格式(Lib/fontTools/tfmLib.py#L62-L89):

格式最小 lh(字)包含字段
HEADER_FORMAT12checksum、designsize
HEADER_FORMAT212上述 +codingscheme(40 字节字符串)
HEADER_FORMAT317上述 +family(20 字节字符串)
HEADER_FORMAT418上述 +seven_bit_safe_flag、两个保留字节、face

无论实际头部多大,解析器会先把所有头部属性初始化为空值,再按实际长度填充;lh超过 18 的额外头部字会按HEADER19、HEADER20…… 的键名存入tfm.extraheader字典(cmr10 的extraheader为空字典{})。

face字段是一个单字节值。从源码结构看,当face < 18时会被解码为一个 3 字符样式码:"MBL"[b % 3] + "RI"[s] + "RCE"[b // 3](s = face % 2,b = face // 2),分别表示字体族类型、罗马/斜体、文本/大写/数学扩展;face >= 18时(如 cmr10 的 234)则原样保留为整数。

模块解析后还会根据codingscheme推导tfm.fonttype:以TEX MATH SY开头为MATHSY(1),以TEX MATH EX开头为MATHEX(2),其余为VANILLA(0)。这个值直接决定fontdimens中哪些参数会被命名(见第四节)。

四、字符度量与字体参数

4.1tfm.chars:逐字符度量字典

chars是以字符代码(整数)为键的字典,值是该字符的度量字典。源码中每个字符由 4 个字节描述:第 0 字节是字宽表索引(为 0 表示该字符不存在),第 1 字节高 4 位/低 4 位分别是高度表、深度表索引,第 2 字节高 6 位是 italic 表索引、低 2 位是 tag,第 3 字节是 remainder(Lib/fontTools/tfmLib.py#L359-L392)。由此得到如下规则:

  • width始终存在;height、depth、italic仅在对应索引非 0(即“适用”)时出现;
  • tag 为LIST_TAG时提供nextlarger(下一个更大同类字符的代码);
  • tag 为EXT_TAG时提供varchar字典,含top/mid/bot/rep四个键,值为可伸缩字符 recipe 的各部分字符代码(某部分索引为 0 时回退为字符自身代码c);
  • tag 为LIG_TAG时该字符进入 ligature/kern 表解析流程。

cmr10 中的g字符实测输出:

>>> tfm.chars[ord("g")] {'width': 0.5000019073486328, 'height': 0.4305553436279297, 'depth': 0.1944446563720703, 'italic': 0.013888359069824219}

4.2tfm.fontdimens:命名化的字体参数

np个字体的参数字按顺序解析,前 7 个固定为“基础参数”(BASE_PARAMS,Lib/fontTools/tfmLib.py#L99-L107):SLANT、SPACE、STRETCH、SHRINK、XHEIGHT、QUAD、EXTRASPACE。超出部分按字体类型命名:

  • VANILLA(普通字体):通常只有上述 7 个参数;
  • MATHSY(TeX math sy,如 cmsy10):额外最多 15 个参数(NUM1…AXISHEIGHT,共 22 个字);
  • MATHEX(TeX math ex,如 cmex10):额外最多 6 个参数(DEFAULTRULETHICKNESS、BIGOPSPACING1..5,共 13 个字)。

仓库测试 Tests/tfmLib/tfmLib_test.py 用三个样本文件固化了三类字体的精确期望值,可直接作为参考:

# test_read_fontdimens_vanilla: cmr10.tfm assert tfm.fontdimens == { "SLANT": 0.0, "SPACE": 0.33333396911621094, "STRETCH": 0.16666698455810547, "SHRINK": 0.11111164093017578, "XHEIGHT": 0.4305553436279297, "QUAD": 1.0000028610229492, "EXTRASPACE": 0.11111164093017578, }

MATHSY(cmsy10)在 7 个基础参数之外包含NUM1、DENOM1、SUP1、SUB1、DELIM1、DELIM2、AXISHEIGHT等 15 个数学排版参数;MATHEX(cmex10)则包含DEFAULTRULETHICKNESS与BIGOPSPACING1..5,完整数值见 Tests/tfmLib/tfmLib_test.py#L44-L90。

五、kerning 与 ligatures 表解析

TFM 用一个混合的 ligature/kern 指令表存储连字与字偶距。每条指令 4 字节(LIG_KERN_COMMAND,Lib/fontTools/tfmLib.py#L91-L97):skip_byte、next_char、op_byte、remainder。解析循环(Lib/fontTools/tfmLib.py#L396-L432)的语义如下:

  • skip_byte == 128(STOP_FLAG):该字符的列表结束;
  • skip_byte > 128:先按256 * op_byte + remainder跳转到新地址,随后结束;
  • op_byte >= 128(KERN_FLAG):该字为kern,值为kern(256 * (op_byte - 128) + remainder),写入tfm.kerning[c][next_char];
  • 其余为ligature,op_byte是连字编码,标准编码被解码为形如LIG、LIG/、/LIG、LIG/>的名字,非标准编码原样输出,结果写入tfm.ligatures[c][next_char] = (lig, remainder)。

cmr10 的实测输出:

>>> tfm.kerning[ord("c")] {104: -0.02777862548828125, 107: -0.02777862548828125} # c→h / c→k >>> tfm.ligatures[ord("f")] {105: ('LIG', 12), 102: ('LIG', 11), 108: ('LIG', 13)} # fi / ff / fl

另外,lig/kern 表的首条与末条指令若skip_byte == 255,则分别解析为tfm.right_boundary_char/tfm.left_boundary_char(左边界字符代码固定记为 256)。没有边界字符时两者为None,cmr10 即属此情况。

5.1 结构校验与 TFMException

与 TeX/TFtoPL 不同(它们会做功能性检查甚至“纠正”坏文件),fontTools 的策略是尽可能按原样输出文件,只要它能被解析;对于结构性错误则抛出TFMException(Lib/fontTools/tfmLib.py#L179-L227),具体检查包括:

  1. 文件短于 6 个字(尺寸表)——“Too short input file”;
  2. lf为负或零——“claims to have negative or zero length”;
  3. 实际字节数少于lf * 4——“fewer bytes than it claims”;
  4. 任一子文件长度为负;
  5. 头部长度lh < 2;
  6. 字符代码区间bc..ec非法(bc > ec + 1或ec > 255);
  7. 字宽/高度/深度/italic 任一表为空——“Incomplete subfiles for character dimensions”;
  8. 可伸缩 recipe 数量ne > 256;
  9. 各子文件大小之和 ≠lf——“Subfile sizes don't add up to the stated total”。

六、命令行快速检查

Lib/fontTools/tfmLib.py#L435-L459 内置了一个__main__入口,直接把 TFM 文件的全部属性打印出来,适合在没有完整环境时快速查看某个 TFM 文件内容:

python3 Lib/fontTools/tfmLib.py Tests/tfmLib/data/cmr10.tfm

输出逐行包含tfm.checksum、tfm.designsize、tfm.codingscheme、tfm.fonttype、tfm.family、tfm.seven_bit_safe_flag、tfm.face、tfm.extraheader、tfm.fontdimens、tfm.right_boundary_char、tfm.left_boundary_char、tfm.kerning、tfm.ligatures、tfm.chars,最后一行是repr(tfm)。

七、实现细节:基于 sstruct 的二进制读取

tfmLib不直接使用标准库struct,而是建立在 fontTools 的 SuperStruct 层 Lib/fontTools/misc/sstruct.py 之上,导入calcsize、unpack、unpack2(Lib/fontTools/tfmLib.py#L40)。sstruct 支持“每行一个名称:格式符”的声明式格式串(对应 TFM 各固定布局的定义),并原生支持n.mF定点数格式——这正是12.20F能被自动转成 float 的原因。定点数转换本身由 Lib/fontTools/misc/fixedTools.py 的fixedToFloat/floatToFixed完成。逐字符度量则绕开了解包开销,直接以偏移函数(char_info、width_index、height_index等闭包)按字节寻址,源码注释坦言这种“非惯用 Python”的写法是阅读 TFtoPL literate WEB 文档后的产物。

八、测试与样本数据

测试套件 Tests/tfmLib/tfmLib_test.py 基于 Tests/tfmLib/data/ 下的 4 个真实 TFM 文件:

  • cmr10.tfm、cmsy10.tfm、cmex10.tfm:Computer Modern 的正文/数学符号/数学扩展三类字体,分别覆盖 VANILLA、MATHSY、MATHEX 三种fontdimens命名路径;
  • dummy-space.tfm:FontForge 生成的最小字体(152 字节),专测边界字符解析。

关键断言包括:glob参数化地读取所有cm*.tfm并断言designsize == 10.0、extraheader == {}、len(chars) == 128;边界字符用例断言right_boundary_char == 1、left_boundary_char == 256;三类fontdimens用例给出全字典精确比对(见第四节)。

tfmLib是通过 NEWS.rst 中 “Added new library for parsing TeX Font Metric (TFM) files (#2354)” 一条记录引入的新库;它是纯读取模块——仓库中没有对应的 TFM 写出功能,因此它适合用于“从 TeX 字体度量提取数据”的场景(如研究字体度量、生成对照数据),而不是 TFM 的生产/修改流程。

  • 开发工具

【免费下载链接】fonttools

A library to manipulate font files from Python.

项目地址:https://gitcode.com/gh_mirrors/fo/fonttools
点击查看免费下载
上一篇:Ornith-1.0-35B与vLLM集成:高性能推理服务器配置指南
下一篇:5分钟学会Vue-PHP全栈框架:前端与后端的高效结合

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

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

单链表删除倒数第N个节点:双指针与虚拟头结点的核心细节

1. 题目拆解&#xff1a;先搞清楚“倒数第N个”到底怎么定位1.1 题目结构与基本要求LeetBook 乐扣题库里的第 19 题&#xff0c;题目原文非常短&#xff1a;给你一个单链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;返回链表的头结点。结点的定义只有 val 和 next 两个…

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

CC Switch:构建模型即契约的本地AI编码工作流

1. 这不是“换模型”&#xff0c;而是重构本地开发流的起点“别再手改配置了&#xff1a;用 CC Switch 把自定义模型接进 Claude Code”——光看标题&#xff0c;你可能以为这是个简单的工具替换教程。但实际操作过的人会立刻意识到&#xff1a;它解决的从来不是“能不能连上”…

作者头像 李华
网站建设 2026/10/13 2:10:09

高德地图IconLayer图标点图层实战:配置、性能优化与避坑指南

1. 从散点图到图标点&#xff1a;为什么需要IconLayer很多人第一次接触地图数据可视化时&#xff0c;都是从最基础的散点图入手的。在地图上打一堆圆点&#xff0c;根据经纬度定位&#xff0c;看起来已经挺像那么回事了。但真正做过实际项目的人都知道&#xff0c;纯色圆点在表…

作者头像 李华
网站建设 2026/10/13 2:10:06

相关系数与决定系数:数据分析中R²和r的误用与正确解读

1. 从一个被问烂了的问题说起&#xff1a;R到底能不能当饭吃做数据分析这行&#xff0c;有个问题几乎每隔几周就会在群里、在评审会上、在代码review里冒出来一次&#xff1a;“我这个模型R都0.95了&#xff0c;为什么业务方还是说预测不准&#xff1f;”每次看到这种提问&…

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

FineDataLink对接美团开放平台接口:从订单同步到数据落库的完整实践

直接做这种事通常不会太顺利&#xff0c;因为在真正动手之前&#xff0c;你看到的往往只是一句“帮我拉一下美团订单数据”。我在接到这个需求的时候&#xff0c;第一反应不是去FineDataLink里建任务&#xff0c;而是先坐下来把业务到底要什么搞清楚。今天这篇就围绕“帆软Fine…

作者头像 李华