MEP14:文本处理#
状态#
讨论
分支和拉取请求#
问题 #253 演示了使用边界框而不是文本的提前宽度导致文本未对齐的错误。这是大计划中的一个小问题,但它应该作为本 MEP 的一部分加以解决。
摘要#
通过重新组织文本的处理方式,本 MEP 旨在:
改进对 Unicode 和非 ltr 语言的支持
改进文本布局(尤其是多行文本)
允许支持更多字体,尤其是非 Apple 格式的 TrueType 字体和 OpenType 字体。
使字体配置更简单、更透明
详细说明#
文本布局
目前,matplotlib 有两种不同的方式来呈现文本:“内置”(基于 FreeType 和我们自己的 Python 代码)和“usetex”(基于调用 TeX 安装)。除了“内置”渲染器之外,还有基于 Python 的“mathtext”系统,用于使用 TeX 语言的子集渲染数学方程式,而无需安装 TeX。对这两个引擎的支持散布在许多源文件中,包括每个后端,其中一个找到像这样的子句
if rcParams['text.usetex']: # do one thing else: # do another
添加第三种文本渲染方法(稍后会详细介绍)也需要编辑所有这些位置,因此无法缩放。
相反,该 MEP 建议添加“文本引擎”的概念,用户可以在其中选择许多不同方法中的一种来呈现文本。这些中的每一个的实现都将本地化到它们自己的模块集,并且在整个源代码树周围没有小块。
为什么要添加更多的文本渲染引擎?“内置”文本渲染有许多缺点。
它只处理从右到左的语言,不处理 Unicode 的许多特殊功能,例如组合变音符号。
多行支持是不完善的,只支持手动换行——它不能把一个段落分成一定长度的行。
它也不处理内联格式更改以支持 Markdown、reStructuredText 或 HTML 等内容。(虽然在本 MEP 中考虑了富文本格式,但由于我们要确保这种设计允许它,富文本格式实现的细节超出了本 MEP 的范围。)
支持这些东西很困难,并且是许多其他项目的“全职工作”:
在上述选项中,应该注意的是,harfbuzz从一开始就被设计为具有最小依赖性的跨平台选项,因此是支持单个选项的良好候选者。
此外,为了支持富文本,我们可以考虑使用 WebKit,并可能代表一个好的单一跨平台选项。然而,富文本格式再次超出了本项目的范围。
与其尝试重新发明轮子并将这些功能添加到 matplotlib 的“内置”文本渲染器中,不如提供一种方法来利用这些项目来获得更强大的文本布局。出于易于安装的原因,“内置”渲染器仍然需要存在,但与其他渲染器相比,它的功能集将更加有限。[TODO:这个 MEP 应该清楚地决定那些有限的功能是什么,并修复任何错误以使实现在我们希望它工作的所有情况下都能正常工作。我知道@leejjoon 对此有一些想法。]
字体选择
从字体的抽象描述到磁盘上的文件是字体选择算法的任务——它比最初看起来要复杂得多。
鉴于其不同的技术,“内置”和“usetex”渲染器在处理字体选择方面有着非常不同的方式。例如,TeX 需要安装 TeX 特定的字体包,并且不能直接使用 TrueType 字体。不幸的是,尽管字体选择有不同的语义,但它们都使用了相同的字体属性集。FontProperties
类和字体相关rcParams
(基本上共享相同的代码)都是如此
。相反,我们应该定义一组核心字体选择参数,这些参数将适用于所有文本引擎,并具有特定于引擎的配置,以允许用户在需要时执行特定于引擎的事情。例如,可以在“内置”中使用
rcParams["font.family"]
(默认:['sans-serif']
),但使用“usetex”是不可能的。通过使用 XeTeX 可以更容易地使用 TrueType 字体,但用户仍然希望通过 TeX 字体包使用传统的元字体。所以问题仍然存在,不同的文本引擎将需要特定于引擎的配置,并且用户应该更清楚哪些配置将跨文本引擎工作,哪些是特定于引擎的。
请注意,即使不包括“usetex”,也有不同的方法可以找到字体。默认使用字体列表缓存,font_manager
其中使用我们自己的基于CSS 字体匹配算法的算法匹配字体。它并不总是与 Linux 上的本机字体选择算法(fontconfig)、Mac 和 Windows,并且它并不总能在系统上找到操作系统通常会选择的所有字体。但是,它是跨平台的,并且总能找到 matplotlib 附带的字体。Cairo 和 MacOSX 后端(可能是未来基于 HTML5 的后端)目前绕过此机制并使用 OS-native 的。当不在 SVG、PS 或 PDF 文件中嵌入字体并在第三方查看器中打开它们时,情况也是如此。一个缺点是(至少对于 Cairo,需要用 MacOSX 确认)他们并不总能找到我们随 matplotlib 提供的字体。(不过,可以将字体添加到它们的搜索路径中,或者我们可能需要找到一种方法将我们的字体安装到操作系统期望找到它们的位置)。
PS 和 PDF 中还有一些特殊模式,仅使用这些格式始终可用的核心字体。在那里,字体查找机制必须只匹配那些字体。目前尚不清楚操作系统原生字体查找系统是否可以处理这种情况。
在 matplotlib 中使用fontconfig进行字体选择也有实验性支持,默认情况下是关闭的。fontconfig 是 Linux 上的本机字体选择算法,但也是跨平台的,并且在其他平台上运行良好(尽管显然在那里是一个额外的依赖项)。
上面提出的许多文本布局库(pango、QtTextLayout、DirectWrite 和 CoreText 等)都坚持使用自己生态系统中的字体选择库。
以上所有似乎都表明我们应该摆脱我们自己编写的字体选择算法,并尽可能使用本机 API。这就是 Cairo 和 MacOSX 后端已经想要使用的东西,它将是任何复杂文本布局库的要求。在 Linux 上,我们已经有了fontconfig实现的骨架(也可以通过 pango 访问)。在 Windows 和 Mac 上,我们可能需要编写自定义包装器。好消息是用于字体查找的 API 相对较小,基本上由“给定字体属性字典,给我一个匹配的字体文件”组成。
字体子集
当前使用 ttconv 处理字体子集。ttconv 是一个独立的命令行实用程序,用于将 TrueType 字体转换为 1995 年编写的子集 Type 3 字体(以及其他功能),matplotlib(好吧,我)为了使其作为库工作而对其进行了分叉。它只处理 Apple 风格的 TrueType 字体,而不是使用 Microsoft(或其他供应商)编码的字体。它根本不处理 OpenType 字体。这意味着即使 STIX 字体以 .otf 文件的形式出现,我们也必须将它们转换为 .ttf 文件以将它们与 matplotlib 一起提供。Linux 打包者讨厌这一点——他们宁愿只依赖上游的 STIX 字体。ttconv 也被证明有一些随着时间的推移难以修复的错误。
相反,我们应该能够使用 FreeType 来获取字体轮廓并编写我们自己的代码(可能在 Python 中)来输出子集字体(PS 和 PDF 上的 Type 3 和 SVG 上的路径)。Freetype,作为一个流行且维护良好的项目,可以处理各种各样的字体。这将删除大量自定义 C 代码,并删除后端之间的一些代码重复。
请注意,以这种方式对字体进行子集化虽然是最简单的方法,但确实会丢失字体中的提示,因此我们需要像现在一样继续提供一种在可能的情况下将整个字体嵌入文件中的方法。
替代字体子集选项包括使用 Cairo 内置的子集(不清楚它是否可以在没有 Cairo 的其余部分的情况下使用),或使用fontforge(这是一个沉重的跨平台依赖项)。
自由类型包装器
我们的 FreeType 包装器真的可以使用返工。它定义了自己的图像缓冲区类(当 Numpy 数组更容易时)。虽然 FreeType 可以处理种类繁多的字体文件,但我们的包装器存在一些限制,这使得支持非 Apple 供应商的 TrueType 文件和 OpenType 文件的某些功能变得更加困难。(请参阅#2088 以了解这个可怕的结果,只是为了支持 Windows 7 和 8 附带的字体)。我认为重新重写这个包装器会有很长的路要走。
文本锚定、对齐和旋转
基线的处理在 1.3.0 中发生了变化,因此后端现在给出了文本基线的位置,而不是文本的底部。这可能是正确的行为,MEP 重构也应该遵循这个约定。
为了支持多行文本的对齐,(提议的)文本引擎应该负责处理文本对齐。对于给定的文本块,每个引擎都会计算该文本的边界框以及该框内锚点的偏移量。因此,如果块的 va 为“顶部”,则锚点将位于框的顶部。
文本的旋转应始终围绕锚点。我不确定这是否与 matplotlib 中的当前行为一致,但这似乎是最明智/最不令人惊讶的选择。[一旦我们有一些工作可以重新访问]。文本的旋转不应该由文本引擎处理——它应该由文本引擎和渲染后端之间的层处理,以便可以以统一的方式处理。[我认为文本引擎单独处理旋转没有任何优势......]
文本对齐和锚定还有其他问题应该作为这项工作的一部分来解决。[TODO:列举这些]。
其他需要修复的小问题
mathtext 代码具有特定于后端的代码——它应该将其输出作为另一个文本引擎提供。但是,仍然希望将 mathtext 布局作为另一个文本引擎执行的更大布局的一部分插入,因此应该可以这样做。是否可以将任意文本引擎的文本布局嵌入到另一个引擎中,这是一个悬而未决的问题。
文本模式当前由全局 rcParam(“text.usetex”)设置,因此它要么全部打开,要么全部关闭。我们应该继续有一个全局的 rcParam 来选择文本引擎(“text.layout_engine”),但它应该是Text
对象上的一个可覆盖属性,所以如果需要,同一个图形可以组合多个文本布局引擎的结果.
实施#
将介绍“文本引擎”的概念。每个文本引擎都将实现许多抽象类。该TextFont
界面将代表一组给定字体属性的文本。它不一定限于单个字体文件——如果布局引擎支持富文本,它可以处理一个系列中的多个字体文件。给定一个TextFont
实例,用户可以获得一个TextLayout
实例,它表示给定字体的给定文本字符串的布局。从 aTextLayout
中,返回一个对 s 的迭代器,TextSpan
因此引擎可以使用尽可能少的跨度输出原始可编辑文本。如果引擎宁愿获取单个字符,则可以从TextSpan
实例中获取它们:
class TextFont(TextFontBase):
def __init__(self, font_properties):
"""
Create a new object for rendering text using the given font properties.
"""
pass
def get_layout(self, s, ha, va):
"""
Get the TextLayout for the given string in the given font and
the horizontal (left, center, right) and verticalalignment (top,
center, baseline, bottom)
"""
pass
class TextLayout(TextLayoutBase):
def get_metrics(self):
"""
Return the bounding box of the layout, anchored at (0, 0).
"""
pass
def get_spans(self):
"""
Returns an iterator over the spans of different in the layout.
This is useful for backends that want to editable raw text as
individual lines. For rich text where the font may change,
each span of different font type will have its own span.
"""
pass
def get_image(self):
"""
Returns a rasterized image of the text. Useful for raster backends,
like Agg.
In all likelihood, this will be overridden in the backend, as it can
be created from get_layout(), but certain backends may want to
override it if their library provides it (as freetype does).
"""
pass
def get_rectangles(self):
"""
Returns an iterator over the filled black rectangles in the layout.
Used by TeX and mathtext for drawing, for example, fraction lines.
"""
pass
def get_path(self):
"""
Returns a single Path object of the entire laid out text.
[Not strictly necessary, but might be useful for textpath
functionality]
"""
pass
class TextSpan(TextSpanBase):
x, y # Position of the span -- relative to the text layout as a whole
# where (0, 0) is the anchor. y is the baseline of the span.
fontfile # The font file to use for the span
text # The text content of the span
def get_path(self):
pass # See TextLayout.get_path
def get_chars(self):
"""
Returns an iterator over the characters in the span.
"""
pass
class TextChar(TextCharBase):
x, y # Position of the character -- relative to the text layout as
# a whole, where (0, 0) is the anchor. y is in the baseline
# of the character.
codepoint # The unicode code point of the character -- only for informational
# purposes, since the mapping of codepoint to glyph_id may have been
# handled in a complex way by the layout engine. This is an int
# to avoid problems on narrow Unicode builds.
glyph_id # The index of the glyph within the font
fontfile # The font file to use for the char
def get_path(self):
"""
Get the path for the character.
"""
pass
想要输出字体子集的图形后端可能会建立一个文件全局字符字典,其中键是 (fontname, glyph_id),值是路径,因此每个字符的路径只有一个副本将存储在文件。
特殊套管:“usetex”功能目前能够直接从 TeX 获取 Postscript 以直接插入 Postscript 文件,但对于其他后端,解析 DVI 文件并生成更抽象的内容。对于这样的情况,TextLayout
将为
get_spans
大多数后端实现,但get_ps
为 Postscript 后端添加,它将查找此方法的存在并在可用时使用它,或者回退到get_spans
. 这种特殊的外壳也可能是必要的,例如,当图形后端和文本引擎属于同一个生态系统时,例如 Cairo 和 Pango,或者 MacOSX 和 CoreText。
实现有三个主要部分:
重写 freetype 包装器,并删除 ttconv。
一旦 (1) 完成,作为概念证明,我们可以移动到上游 STIX .otf 字体
添加对从远程 URL 加载的 Web 字体的支持。(通过使用 freetype 进行字体子集化启用)。
将现有的“内置”和“usetex”代码重构为单独的文本引擎,并遵循上述 API。
实现对高级文本布局库的支持。
(1) 和 (2) 是相当独立的,尽管首先完成 (1) 将使 (2) 更简单。(3) 依赖于 (1) 和 (2),但即使它没有完成(或被推迟),完成 (1) 和 (2) 将更容易推进改进“内置”文本引擎。
向后兼容性#
文本相对于其锚点和旋转的布局将以希望很小但改进的方式发生变化。多行文本的布局会更好,因为它会尊重水平对齐。双向文本或其他高级 Unicode 功能的布局现在将固有地工作,如果用户当前使用他们自己的解决方法,这可能会破坏一些事情。
字体的选择会有所不同。过去在“内置”和“usetex”文本渲染引擎之间工作的黑客可能不再有效。可以选择由操作系统找到的以前没有被 matplotlib 找到的字体。
替代方案#
待定