MEP44:文本处理

状态

  • Discussion

分支和请求

问题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"] (default: ['sans-serif']) 但“usetex”也不可能做到这一点。使用Xetex可能会使使用TrueType字体更容易,但用户仍然希望通过Tex字体包使用传统的元字体。因此,问题仍然存在,不同的文本引擎将需要特定于引擎的配置,用户应该更清楚哪些配置可以跨文本引擎工作,哪些配置是特定于引擎的。

请注意,即使排除“usetex”,也有不同的字体查找方法。默认情况下使用字体列表缓存 font_manager 使用我们自己的基于 CSS font matching algorithm . 它并不总是和Linux上的本机字体选择算法做同样的事情。 (fontconfig) 它并不总是能在系统上找到操作系统通常会选择的所有字体。但是,它是跨平台的,并且总是可以找到matplotlib附带的字体。Cairo和MacOSX后端(可能是未来基于HTML5的后端)目前绕过了这种机制,使用OS本机后端。当不在SVG、PS或PDF文件中嵌入字体并在第三方查看器中打开它们时,情况也是如此。缺点是(至少对于Cairo,需要用MacOSX确认)他们并不总是能找到matplotlib附带的字体。(不过,可以将字体添加到它们的搜索路径,或者我们可能需要找到一种方法,将字体安装到操作系统希望找到它们的位置)。

PS和PDF中还有一些特殊的模式,只能使用那些格式始终可用的核心字体。在这里,字体查找机制必须只与那些字体匹配。目前尚不清楚操作系统本机字体查找系统能否处理这种情况。

也有实验支持使用 fontconfig 对于matplotlib中的字体选择,默认情况下关闭。fontconfig是Linux上的本机字体选择算法,但也是跨平台的,在其他平台上也能很好地工作(尽管显然还有一个附加的依赖项)。

上面提出的许多文本布局库(pango、qttextlayout、directwrite和coretext等)都坚持使用自己生态系统中的字体选择库。

以上所有这些似乎都表明我们应该远离自己编写的字体选择算法,在可能的情况下使用本地API。这正是cairo和macosx后端已经想要使用的,并且它将是任何复杂文本布局库的一个需求。在Linux上,我们已经有了 fontconfig 实施(也可以通过Pango访问)。在Windows和Mac上,我们可能需要编写自定义包装。好的是,字体查找的API相对较小,基本上由“给定的字体属性字典,给我一个匹配的字体文件”组成。

字体细分

当前使用ttconv处理字体子设置。ttconv是一个独立的命令行实用程序,用于将TrueType字体转换为1995年编写的子集类型3字体(以及其他功能),Matplotlib(嗯,i)将其复刻以使其作为库工作。它只处理苹果风格的TrueType字体,而不是微软(或其他供应商)编码的字体。它根本不处理OpenType字体。这意味着,即使Stix字体是.otf文件,我们也必须将它们转换为.ttf文件,以便将它们与matplotlib一起装运。Linux包装商讨厌这样——他们宁愿依赖上游的Stix字体。Ttconv还显示了一些随着时间推移难以修复的错误。

相反,我们应该能够使用FreeType来获取字体轮廓,并编写我们自己的代码(可能是Python)来输出子集字体(PS和PDF上的type3和SVG上的paths)。Freetype是一个流行且维护良好的项目,可以处理各种各样的字体。这将删除大量自定义C代码,并删除后端之间的一些代码重复。

请注意,这种方式设置子字体虽然是最简单的方式,但会丢失字体中的提示,因此我们需要继续,正如我们现在所做的,提供一种在可能的情况下将整个字体嵌入到文件中的方法。

可选字体子集设置选项包括使用内置于cairo的子集(不清楚是否可以在不使用cairo的情况下使用),或使用 fontforge (这是一种沉重的跨平台依赖关系)。

自由式包装机

我们的freetype包装器真的可以使用返工。它定义自己的图像缓冲区类(当numpy数组更容易时)。虽然freetype可以处理大量的字体文件,但是我们的包装器有一些限制,使得支持非Apple供应商TrueType文件和OpenType文件的某些功能变得更加困难。(请参见2088了解这一可怕的结果,只是为了支持Windows7和8附带的字体)。我认为重新编写这个包装纸会有很长的路要走。

文本定位、对齐和旋转

基线的处理在1.3.0中进行了更改,这样后端就可以得到文本基线的位置,而不是文本的底部。这可能是正确的行为,并且MEP重构也应该遵循此约定。

为了支持多行文本的对齐,文本引擎应该负责处理文本对齐。对于给定的文本块,每个引擎都为该文本计算一个边界框以及该框中定位点的偏移量。因此,如果块的va为“top”,则锚定点将位于盒子的顶部。

文本的旋转应始终围绕定位点。我不确定这是否符合Matplotlib当前的行为,但它似乎是最明智/最不令人惊讶的选择。 [一旦我们有了新的进展,我们就可以重新审视这个问题了。] . 文本引擎不应该处理文本的旋转——它应该由文本引擎和渲染后端之间的层处理,以便以统一的方式处理。 [我看不到文本引擎单独处理旋转的任何优势…]

文本对齐和锚定还有其他问题,应作为本工作的一部分加以解决。 [托多:列举这些] .

其他需要解决的小问题

MathText代码具有后端特定的代码——它应该只作为另一个文本引擎提供输出。但是,仍然需要将MathText布局作为另一个文本引擎执行的较大布局的一部分插入,因此应该可以这样做。是否可以将任意文本引擎的文本布局嵌入到另一个引擎中,这是一个悬而未决的问题。

文本模式当前由全局rcparam(“text.usetex”)设置,因此它要么全部打开,要么全部关闭。我们应该继续有一个全局RCPARAM来选择文本引擎(“text.layout_engine”),但是它应该在引擎盖下是 Text 对象,因此如果需要,同一个图可以组合多个文本布局引擎的结果。

实施

将介绍“文本引擎”的概念。每个文本引擎将实现许多抽象类。这个 TextFont 接口将表示给定字体属性集的文本。它不一定局限于单个字体文件——如果布局引擎支持富文本,它可以处理一个系列中的许多字体文件。给定一个 TextFont 例如,用户可以 TextLayout 实例,它表示给定字体中给定文本字符串的布局。从A TextLayout ,迭代器 TextSpans 返回,以便引擎可以使用尽可能少的跨距输出原始可编辑文本。如果引擎希望获得单个字符,则可以从 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)时,也可能需要这种特殊的大小写。

实施主要有三个部分:

  1. 重写freetype包装器,并删除ttconv。
  1. 一旦(1)完成,作为概念证明,我们可以移动到上游stix.otf字体
  2. 添加对从远程URL加载的Web字体的支持。(通过对字体子集使用freetype启用)。
  1. 将现有的“builtin”和“usetex”代码重构为单独的文本引擎,并遵循上面概述的API。
  2. 实现对高级文本布局库的支持。

(1)和(2)是相当独立的,尽管(1)先完成将使(2)更简单。(3)依赖于(1)和(2),但即使它没有完成(或被推迟),完成(1)和(2)将使改进“内置”文本引擎更容易前进。

向后兼容性

文本相对于其锚定点和旋转的布局将以希望较小但改进的方式更改。多行文本的布局将更好,因为它将考虑水平对齐。双向文本或其他高级Unicode功能的布局现在将内在地工作,如果用户当前正在使用他们自己的解决方法,这可能会破坏一些东西。

字体的选择将不同。用于“builtin”和“usetex”文本呈现引擎之间排序工作的黑客可能不再有效。可以选择操作系统找到的Matplotlib以前没有找到的字体。

选择

TBD