MEP14:文本处理#
状态#
讨论
分支和 Pull requests#
Issue #253 演示了一个 bug,其中使用文本的边界框而不是前进宽度会导致文本未对齐.这在整个计划中只是一个小问题,但应该作为此 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"] (default: ['sans-serif']) 直接在"内置"中按名称选择字体,但"usetex"无法做到这一点.通过使用 XeTeX,可以更容易地使用 TrueType 字体,但用户仍然希望通过 TeX 字体包使用传统的 metafont.因此,问题仍然存在,不同的文本引擎需要特定于引擎的配置,并且应该更清楚地向用户说明哪些配置适用于所有文本引擎,哪些配置是特定于引擎的.
请注意,即使排除 "usetex",仍然有不同的方法来查找字体.默认方法是使用 font_manager 中的字体列表缓存,它使用我们自己的算法来匹配字体,该算法基于 CSS font matching algorithm .它并不总是与 Linux(fontconfig),Mac 和 Windows 上的原生字体选择算法做相同的事情,并且它并不总是找到操作系统通常会拾取的所有系统字体.但是,它是跨平台的,并且总是能找到 matplotlib 附带的字体.Cairo 和 MacOSX 后端(以及可能在未来的基于 HTML5 的后端)目前绕过此机制并使用 OS 原生的机制.在不将字体嵌入到 SVG,PS 或 PDF 文件中并在第三方查看器中打开它们时,情况也是如此.缺点是(至少对于 Cairo 而言,需要与 MacOSX 确认),它们并不总是找到我们随 matplotlib 提供的字体.(但是,可以将字体添加到它们的搜索路径中,或者我们可能需要找到一种方法将我们的字体安装到操作系统期望找到它们的位置).
PS 和 PDF 中也有特殊模式,仅使用这些格式始终可用的核心字体. 在那里,字体查找机制必须仅与这些字体匹配.目前尚不清楚 OS 原生的字体查找系统是否可以处理这种情况.
matplotlib 中还提供了使用 fontconfig 进行字体选择的实验性支持,默认情况下处于关闭状态.fontconfig 是 Linux 上的原生字体选择算法,但它也是跨平台的,并且在其他平台上也能很好地工作(尽管很明显这是一个额外的依赖项).
上面提出的许多文本布局库(pango,QtTextLayout,DirectWrite 和 CoreText 等)都坚持使用来自其自身生态系统的字体选择库.
以上所有这些似乎表明我们应该放弃我们自己编写的字体选择算法,并在可能的情况下使用原生 API.这正是 Cairo 和 MacOSX 后端已经想要使用的,并且这将成为任何复杂的文本布局库的要求.在 Linux 上,我们已经有了 fontconfig 实现的骨架(也可以通过 pango 访问).在 Windows 和 Mac 上,我们可能需要编写自定义包装器.好消息是字体查找的 API 相对较小,并且本质上包括"给定一个字体属性字典,给我一个匹配的字体文件".
字体子集化
字体子集化目前使用 ttconv 处理.ttconv 是一个独立的命令行实用程序,用于将 TrueType 字体转换为子集化的 Type 3 字体(以及其他功能),它编写于 1995 年,matplotlib(好吧,是我)对其进行了分支,使其可以作为一个库工作.它仅处理 Apple 样式的 TrueType 字体,而不处理具有 Microsoft(或其他供应商)编码的字体.它根本不处理 OpenType 字体.这意味着即使 STIX 字体以 .otf 文件的形式出现,我们也必须将它们转换为 .ttf 文件才能随 matplotlib 一起发布.Linux 打包者讨厌这一点 -- 他们宁愿仅仅依赖上游的 STIX 字体.ttconv 也被证明存在一些难以修复的错误.
相反,我们应该能够使用 FreeType 来获取字体轮廓并编写我们自己的代码(可能在 Python 中)来输出子集化的字体(PS 和 PDF 上的 Type 3 以及 SVG 上的路径).Freetype 作为一个流行的且维护良好的项目,可以处理各种各样的字体.这将删除大量自定义 C 代码,并消除后端之间的一些代码重复.
请注意,以这种方式子集化字体虽然是最简单的途径,但确实会丢失字体中的 hinting 信息,因此我们将需要继续,就像我们现在所做的那样,提供一种在可能的情况下将整个字体嵌入到文件中的方法.
替代的字体子集化选项包括使用 Cairo 中内置的子集化(不清楚是否可以在没有 Cairo 其余部分的情况下使用),或者使用 fontforge_(这是一个繁重且不是非常跨平台的依赖项).
Freetype 包装器
我们的 FreeType 包装器确实可以使用重新设计.它定义了自己的图像缓冲区类(而 Numpy 数组会更容易).虽然 FreeType 可以处理各种各样的字体文件,但我们的包装器存在一些限制,使得更难支持非 Apple 供应商的 TrueType 文件,以及 OpenType 文件的某些功能.(请参阅 #2088 以获得一个可怕的结果,仅仅是为了支持 Windows 7 和 8 附带的字体).我认为对这个wrapper进行全新的重写将会大有帮助.
文本锚点和对齐方式以及旋转
基线的处理方式在 1.3.0 中进行了更改,后端现在获得的是文本基线的位置,而不是文本底部的位置.这可能是正确的行为,MEP 重构也应遵循此约定.
为了支持多行文本的对齐,文本对齐应由(建议的)文本引擎负责处理.对于给定的文本块,每个引擎都会计算该文本的边界框以及锚点在该框中的偏移量.因此,如果一个块的 va 是"top",则锚点将在框的顶部.
文本的旋转应始终围绕锚点进行.我不确定这是否与 matplotlib 中的当前行为一致,但这似乎是最明智/最不易出错的选择.[一旦我们有了可行的东西,就可以重新审视这一点].文本的旋转不应由文本引擎处理--这应该由文本引擎和渲染后端之间的一层来处理,以便可以用统一的方式来处理.[我不认为让文本引擎单独处理旋转有任何优势...]
文本对齐和锚定还存在其他问题,应作为此工作的一部分加以解决.[TODO:枚举这些].
其他要修复的小问题
mathtext 代码具有特定于后端的代码--它应该将其输出仅作为另一个文本引擎提供.但是,仍然希望将 mathtext 布局作为另一个文本引擎执行的较大布局的一部分插入,因此应该可以做到这一点.将任意文本引擎的文本布局嵌入到另一个文本引擎中是否可行是一个悬而未决的问题.
文本模式当前由全局 rcParam("text.usetex")设置,因此要么全部开启,要么全部关闭.我们应该继续使用全局 rcParam 来选择文本引擎("text.layout_engine"),但它在底层应该是 Text 对象的可以被覆盖的属性,因此如果需要,同一个图形可以组合多个文本布局引擎的结果.
实施#
将引入"文本引擎"的概念.每个文本引擎将实现多个抽象类. TextFont 接口将代表一组给定字体属性的文本.它不一定仅限于单个字体文件--如果布局引擎支持富文本,则它可以处理一个系列中的多个字体文件.给定一个 TextFont 实例,用户可以获得一个 TextLayout 实例,该实例表示给定字体中给定文本字符串的布局.从 TextLayout ,返回一个在 TextSpan s 上的迭代器,以便引擎可以使用尽可能少的跨度输出原始的可编辑文本.如果引擎更喜欢获取单个字符,则可以从 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 ,但为 Postscript 后端添加 get_ps ,这会查找此方法的存在并在可用时使用它,否则会回退到 get_spans .当图形后端和文本引擎属于同一个生态系统时,例如 Cairo 和 Pango,或者 MacOSX 和 CoreText,也可能需要这种特殊情况.
实现分为三个主要部分:
重写 freetype 包装器,并删除 ttconv.
一旦完成(1),作为概念验证,我们可以迁移到上游 STIX .otf 字体
添加对从远程 URL 加载的 Web 字体的支持.(通过使用 freetype 进行字体子集化启用).
将现有的"builtin"和"usetex"代码重构为单独的文本引擎,并遵循上面概述的 API.
实现对高级文本布局库的支持.
(1) and (2) are fairly independent, though having (1) done first will allow (2) to be simpler. (3) is dependent on (1) and (2), but even if it doesn't get done (or is postponed), completing (1) and (2) will make it easier to move forward with improving the "builtin" text engine.
向后兼容性#
文本相对于其锚点和旋转的布局将以希望是小的,但改进的方式改变. 多行文本的布局会更好,因为它会尊重水平对齐. 双向文本或其他高级 Unicode 功能的布局现在将固有地工作,如果用户当前正在使用他们自己的解决方法,这可能会破坏某些东西.
字体的选择方式会有所不同. 曾经在"builtin"和"usetex"文本渲染引擎之间起作用的技巧可能不再起作用. 操作系统找到的以前未被 matplotlib 找到的字体可能会被选择.
替代方案#
待定