Matplotlib:重新审视文本/字体处理

为了开始最终报告,这里有一个 梗图 来提醒一下关于 之前的博客

关于 Matplotlib#

Matplotlib 是一个用于创建静态、动画和交互式可视化的综合库,它已成为事实上的 Python 绘图库

其字体管理器背后的许多实现灵感来自符合 W3C 标准的算法,允许用户与字体属性(如 font-sizefont-weightfont-family 等)进行交互。

然而,Matplotlib 处理字体和一般文本布局的方式并不理想,这就是 2021 年夏季的全部内容。#

我所说的“不理想”并不意味着该库存在设计缺陷,而是说设计是在 2000 年代初期完成的,现在已经过时了。

(…稍后详细介绍)

关于项目#

(PS:如果您有兴趣,这里有 我的 GSoC 项目提案链接)

总的来说,该项目分为两个主要子目标:

  1. 字体子集
  2. 字体回退

但在我们开始研究它们之前,我们应该了解一些关于字体的基本术语(它们很多,并且令人困惑

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 等也是如此)。

Previous After

之前(注意豆腐)VS 之后(CJK 字体作为回退)

为了从以字体为中心的模式迁移到以文本为中心的模式,需要执行多个步骤。

解析整个字体系列#

第一步(也是最关键的一步!)是达到一个点,即我们拥有多个字体路径(理想情况下是整个系列的单个字体文件)。这可以通过以下两种方式实现:

引用我之前博客文章中的内容

不要破坏,有很多东西在赌注!

我的第一个方法是修改现有的公共 findfont API 以包含多个文件路径。由于 Matplotlib 有一个非常庞大的用户群,它很可能会破坏一部分人的工作流程。

FamilyParsingFlowChart 第一个 PR(左),第二个 PR(右)

FT2Font 大修#

一旦我们获得了字体路径列表,我们就需要更改“字体”的内部表示。Matplotlib 有一个名为 FT2Font 的实用程序,它是用 C++ 编写的,并使用包装器作为 Python 扩展,该扩展反过来在所有后端中使用。就所有意图和目的而言,它以前意味着:FT2Font === 单个字体(如果您有兴趣,这里有一个关于 FT2Font 如何命名的 梗图!)

但现在情况不再如此,这里有一个流程图来解释现在发生了什么。

FamilyParsingFlowChart 字体回退算法

有了 PR:在 Matplotlib 中实现字体回退,每个 FT2Font 对象都有一个 std::vector<FT2Font *> fallback_list,它用于填充父缓存,如自解释流程图所示。

为简单起见,只显示了一种缓存类型(字符 -> FT2Font),而在实际实现中,有两种类型的缓存,一种如上所示,另一种用于字形(字形 ID -> FT2Font)。

注意:某些后端仅使用父 API,因此对于每个公共函数(如 load_glyphload_charget_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。

致谢#

从学习 Tom 的软件工程基础知识到学习 Jouni 关于字体表示的细枝末节;

从学习 Antony 的补丁和提示到从 Hannah 那里获得对这些博文的精彩反馈,这真是一段冒险! 💯

特别感谢:FrankSrijanAtharva 的帮助!

最后,,读者;如果您一直在关注我的 之前博客,或者您直接访问了这篇博客,无论如何,我都要感谢您。(最后一个 表情包,我保证!)

我知道我说出了每个开发人员的心声,当您选择查看他们的旅程或工作成果时,这意义重大;它可能是一个很小的网站,也可能是一个像设计完整的库一样大的项目!


我感谢 Maptlotlib(母组织:NumFOCUS),当然还有 Google Summer of Code 提供了这个不可思议的学习机会。

再见,读者! :’)

MatplotlibGSoC 考虑为 Matplotlib(一般来说是开源软件)做出贡献 ❤️

注意:这篇博文也发布在我的 个人网站 上。#