
Matplotlib:重新审视文本/字体处理
为了开始最终报告,这里有一个 梗图 来提醒一下关于 之前的博客 。
关于 Matplotlib#
Matplotlib 是一个用于创建静态、动画和交互式可视化的综合库,它已成为事实上的 Python 绘图库。
其字体管理器背后的许多实现灵感来自符合 W3C 标准的算法,允许用户与字体属性(如 font-size
、font-weight
、font-family
等)进行交互。
然而,Matplotlib 处理字体和一般文本布局的方式并不理想,这就是 2021 年夏季的全部内容。#
我所说的“不理想”并不意味着该库存在设计缺陷,而是说设计是在 2000 年代初期完成的,现在已经过时了。
(…稍后详细介绍)
关于项目#
(PS:如果您有兴趣,这里有 我的 GSoC 项目提案链接)
总的来说,该项目分为两个主要子目标:
- 字体子集
- 字体回退
但在我们开始研究它们之前,我们应该了解一些关于字体的基本术语(它们很多,并且令人困惑)
PR:澄清/改进关于 family-names 与 generic-families 的文档 对这些术语中的一部分进行了澄清。下一部分包含一个链接的 PR,它也解释了字体类型以及它们与 Matplotlib 的相关性。
字体子集#
使用 PR:[Doc] 字体类型和字体子集 创建了一个关于字体和 Matplotlib 的易于阅读的指南,该指南目前已发布在 Matplotlib 的 DevDocs 上。
摘录自我之前博客文章(以及 文档)
字体可以被视为这些字形的集合,因此子集的最终目标是找出对于特定字符数组需要哪些字形,并将仅这些字形嵌入到输出中。
PDF、PS/EPS 和 SVG 输出文档格式很特殊,因为它们中的文本可以编辑,即,如果文本可编辑,用户可以从文档(例如,从 PDF 文件)中复制/搜索文本。
Matplotlib 和子集#
PDF、PS/EPS 和 SVG 后端曾经支持字体子集,但仅限于少数类型。这意味着,在 2021 年夏季之前,Matplotlib 可以为 PDF、PS/EPS 后端生成类型 3 子集,但它无法生成类型 42/TrueType 子集。
随着 PR:PS/PDF 中的 Type42 子集 合并,用户可以期望他们的 PDF/PS/EPS 文档包含来自原始字体的子集字形。
这对希望使用商业(或 CJK)字体的人特别有用。许多字体的许可证要求子集,因此不能从 Matplotlib 生成的输出文件中轻松复制它们。
字体回退#
Matplotlib 被设计为在运行时使用单个字体。用户可以指定 font.family
,该属性应该对应于 CSS 属性,但这仅用于查找用户系统上存在的单个字体。
一旦找到该字体(几乎总是能找到,因为 Matplotlib 附带了一组默认字体),所有用户文本都将仅通过该字体进行渲染。(如果找不到字符,它以前会显示“豆腐”)
现在我们有了字体回退等概念,这似乎是一种过时的文本渲染方法,但这些概念在 2000 年代初期并没有得到很好的讨论。即使让单个字体工作也被认为是一个困难的工程问题。
这主要是由于缺乏对字体表示的任何标准化(Adobe 有自己的字体表示,Apple 和 Microsoft 等也是如此)。
![]() |
![]() |
---|
之前(注意豆腐)VS 之后(CJK 字体作为回退)
为了从以字体为中心的模式迁移到以文本为中心的模式,需要执行多个步骤。
解析整个字体系列#
第一步(也是最关键的一步!)是达到一个点,即我们拥有多个字体路径(理想情况下是整个系列的单个字体文件)。这可以通过以下两种方式实现:
引用我之前博客文章中的内容
不要破坏,有很多东西在赌注!
我的第一个方法是修改现有的公共 findfont
API 以包含多个文件路径。由于 Matplotlib 有一个非常庞大的用户群,它很可能会破坏一部分人的工作流程。
第一个 PR(左),第二个 PR(右)
FT2Font 大修#
一旦我们获得了字体路径列表,我们就需要更改“字体”的内部表示。Matplotlib 有一个名为 FT2Font 的实用程序,它是用 C++ 编写的,并使用包装器作为 Python 扩展,该扩展反过来在所有后端中使用。就所有意图和目的而言,它以前意味着:FT2Font === 单个字体
(如果您有兴趣,这里有一个关于 FT2Font 如何命名的 梗图!)
但现在情况不再如此,这里有一个流程图来解释现在发生了什么。
字体回退算法
有了 PR:在 Matplotlib 中实现字体回退,每个 FT2Font 对象都有一个 std::vector<FT2Font *> fallback_list
,它用于填充父缓存,如自解释流程图所示。
为简单起见,只显示了一种缓存类型(字符 -> FT2Font),而在实际实现中,有两种类型的缓存,一种如上所示,另一种用于字形(字形 ID -> FT2Font)。
注意:某些后端仅使用父 API,因此对于每个公共函数(如
load_glyph
、load_char
、get_kerning
等),我们从父 FT2Font 缓存中找到具有该字形的 FT2Font 对象!
在 PDF/PS/EPS 中嵌入多字体#
现在我们有多个字体可以渲染字符串,我们还需要为那些特殊的后端(即 PDF/PS 等)嵌入它们。这是通过对特定后端进行一些修补完成的。
通过这样做,用户可以创建包含嵌入(和子集)的多个字体的 PDF 或 PS/EPS 文档。
结论#
从小的贡献到最终在如此庞大的库的核心模块上工作,这条路并非我所想象的那样,在设计这些问题的解决方案时,我学到了很多东西。
我所做的工作最终将影响到每一个 Matplotlib 用户。#
…因为所有绘图最终都会经过新的代码路径!
我认为,仅此一项就值回整个 GSoC 项目。
拉取请求统计数据#
为了统计数据(并使 GSoC 听起来不那么吓人),这里列出了我在 2021 年夏季之前对 Matplotlib 做出的贡献,其中大部分的差异只是一两行代码。
创建时间 | PR 标题 | 差异 | 状态 |
---|---|---|---|
2020 年 11 月 2 日 | 将 ScalarMappable.set_array 扩展为接受类似数组的输入 | (+28 −4) | 已合并 |
2020 年 11 月 8 日 | 为 mathtext 添加 overset 和 underset 支持 | (+71 −0) | 已合并 |
2020 年 11 月 14 日 | 用测试覆盖范围为 streamplot 网格进行严格递增检查 | (+54 −2) | 已合并 |
2021 年 1 月 11 日 | WIP:添加通过文本框编辑子图配置的支持 | (+51 −11) | 草稿 |
2021 年 1 月 18 日 | 修复 over/under mathtext 符号 | (+7,459 −4,169) | 已合并 |
2021 年 2 月 11 日 | 添加 overset/underset 新特性条目 | (+28 −17) | 已合并 |
2021 年 5 月 15 日 | 在使用 mathtext 字体作为刻度时警告用户 | (+28 −0) | 已合并 |
这里列出了我在 2021 年夏季期间打开的 PR。
- [状态:✅] 澄清/改进关于 family-names 与 generic-families 的文档
- [状态: ✅] 在 Text 中添加 parse_math 并将其对 TextBox 设置为默认的 False
- [状态: ✅] PS/PDF 中的 Type42 子集
- [状态: ✅] [文档] 字体类型和字体子集
- [状态: 🚧] [包含 findfont 的 diff] 在 font_manager 中解析所有字体系列
- [状态: 🚧] [不包含 findfont 的 diff] 在 font_manager 中解析所有字体系列
- [状态: 🚧] 在 Matplotlib 中实现字体回退
- [状态: 🚧] 为 PDF 后端实现多字体嵌入
- [状态: 🚧] 为 PS 后端实现多字体嵌入
致谢#
从学习 Tom 的软件工程基础知识到学习 Jouni 关于字体表示的细枝末节;
从学习 Antony 的补丁和提示到从 Hannah 那里获得对这些博文的精彩反馈,这真是一段冒险! 💯
特别感谢:Frank、Srijan 和 Atharva 的帮助!
最后,您,读者;如果您一直在关注我的 之前博客,或者您直接访问了这篇博客,无论如何,我都要感谢您。(最后一个 表情包,我保证!)
我知道我说出了每个开发人员的心声,当您选择查看他们的旅程或工作成果时,这意义重大;它可能是一个很小的网站,也可能是一个像设计完整的库一样大的项目!
我感谢 Maptlotlib(母组织:NumFOCUS),当然还有 Google Summer of Code 提供了这个不可思议的学习机会。
再见,读者! :’)
考虑为 Matplotlib(一般来说是开源软件)做出贡献 ❤️