MEP14: 文本处理#

状态#

  • 讨论

分支和拉取请求#

问题 #253 展示了一个错误,即使用边界框而不是文本的前进宽度会导致文本对齐错误。这在整体计划中是一个小问题,但应作为此 MEP 的一部分来解决。

摘要#

通过重新组织文本的处理方式,此 MEP 旨在:

  • 改进对 Unicode 和非从左到右语言的支持

  • 改进文本布局(尤其是多行文本)

  • 允许支持更多字体,特别是非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 字体包使用传统的元字体。因此,问题依然存在,即不同的文本引擎将需要引擎特定的配置,并且应该更明显地向用户展示哪些配置可以在所有文本引擎中工作,哪些是引擎特定的。

需要注意的是,即使不包括“usetex”,也有不同的方式来查找字体。默认情况下,使用 font_manager 中的字体列表缓存,该缓存通过基于 CSS 字体匹配算法 的自有算法来匹配字体。这并不总是与 Linux (fontconfig)、Mac 和 Windows 上的原生字体选择算法做同样的事情,并且它并不总是能找到操作系统通常会识别的所有系统字体。然而,它是跨平台的,并且总是能找到随 matplotlib 一起提供的字体。Cairo 和 MacOSX 后端(以及可能是未来的基于 HTML5 的后端)目前绕过了这个机制,使用了操作系统原生的字体选择机制。当不在 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 字体转换为子集化的 Type 3 字体(以及其他功能),于1995年编写,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 包装器

我们的 FreeType 封装确实需要重新设计。它定义了自己的图像缓冲类(使用 Numpy 数组会更简单)。虽然 FreeType 可以处理多种多样的字体文件,但我们的封装存在一些限制,使得支持非苹果供应商的 TrueType 文件和 OpenType 文件的某些功能变得非常困难。(参见 #2088,这是一个糟糕的结果,只是为了支持 Windows 7 和 8 附带的字体)。我认为对这个封装进行全新的重写会有很大帮助。

文本锚定、对齐和旋转

在1.3.0版本中,基线的处理方式发生了变化,现在后端接收的是文本基线的位置,而不是文本的底部。这可能是正确的处理方式,MEP重构也应该遵循这一约定。

为了支持多行文本的对齐,处理文本对齐应该是(提议的)文本引擎的责任。对于给定的文本块,每个引擎计算该文本的边界框以及该框内锚点的偏移量。因此,如果一个块的 va 是“top”,锚点将位于框的顶部。

文本的旋转应始终围绕锚点进行。我不确定这与matplotlib中的当前行为是否一致,但这似乎是最合理/最不令人惊讶的选择。[一旦我们有了一些可行的东西,这可以重新审视]。文本的旋转不应由文本引擎处理——这应由文本引擎和渲染后端之间的层处理,以便以统一的方式处理。[我看不出旋转由各个文本引擎单独处理有什么优势...]

文本对齐和锚定还存在其他问题,这些问题应作为此工作的一部分来解决。 [TODO: 列举这些问题]。

其他需要修复的小问题

mathtext 代码有特定于后端的代码——它应该将其输出作为另一个文本引擎提供。然而,仍然希望将 mathtext 布局插入到由另一个文本引擎执行的更大布局中,因此应该可以这样做。是否可以将任意文本引擎的文本布局嵌入到另一个文本引擎中,这是一个开放的问题。

文本模式目前由一个全局的 rcParam("text.usetex")设置,因此它要么全部开启,要么全部关闭。我们应该继续有一个全局的 rcParam 来选择文本引擎("text.layout_engine"),但它应该在底层是 Text 对象上可覆盖的属性,因此相同的图形可以在必要时结合多个文本布局引擎的结果。

实现#

将引入一个“文本引擎”的概念。每个文本引擎将实现一些抽象类。TextFont 接口将表示给定一组字体属性的文本。它不一定局限于单个字体文件——如果布局引擎支持富文本,它可能会处理一个系列中的多个字体文件。给定一个 TextFont 实例,用户可以获取一个 TextLayout 实例,该实例表示在给定字体中给定字符串的布局。从 TextLayout 中,返回一个 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

希望输出字体子集的图形后端可能会构建一个文件全局字符字典,其中键是 (字体名称, 字形ID),值是路径,以便每个字符在文件中只存储一个路径副本。

特殊情况处理:当前的“usetex”功能能够直接从TeX获取Postscript并插入到Postscript文件中,但对于其他后端,它会解析DVI文件并生成更抽象的内容。对于这种情况,TextLayout 将为大多数后端实现 get_spans,但为Postscript后端添加 get_ps,该后端会检查此方法的存在并在可用时使用它,否则回退到 get_spans。这种特殊情况处理也可能是必要的,例如,当图形后端和文本引擎属于同一生态系统时,例如Cairo和Pango,或MacOSX和CoreText。

实现中有三个主要部分:

  1. 重写 freetype 包装器,并移除 ttconv。

    1. 一旦(1)完成,作为概念验证,我们可以转移到上游的 STIX .otf 字体

    2. 添加对从远程URL加载的网络字体的支持。(通过使用freetype进行字体子集化来启用)。

  2. 将现有的“builtin”和“usetex”代码重构为独立的文本引擎,并遵循上述API。

  3. 实现对高级文本布局库的支持。

(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特性的布局现在将自然工作,如果用户当前使用自己的解决方法,这可能会破坏一些东西。

字体将会有不同的选择。过去在“内置”和“usetex”文本渲染引擎之间勉强能用的黑客技巧可能不再有效。操作系统找到的但之前matplotlib未找到的字体可能会被选中。

替代方案#

待定