MEP14: Tratamento de texto #

Estado #

  • Discussão

Filiais e solicitações pull #

O problema nº 253 demonstra um bug em que usar a caixa delimitadora em vez da largura avançada do texto resulta em texto desalinhado. Este é um ponto menor no grande esquema das coisas, mas deve ser abordado como parte deste MEP.

Resumo #

Ao reorganizar a forma como o texto é tratado, este MEP visa:

  • melhorar o suporte para idiomas Unicode e não ltr

  • melhorar o layout do texto (especialmente texto de várias linhas)

  • permitem suporte para mais fontes, especialmente fontes TrueType e fontes OpenType de formato não Apple.

  • tornar a configuração da fonte mais fácil e transparente

Descrição detalhada #

Layout de texto

No momento, o matplotlib tem duas maneiras diferentes de renderizar texto: "integrado" (baseado em FreeType e nosso próprio código Python) e "usetex" (baseado na chamada para uma instalação do TeX). Adjunto ao renderizador "incorporado", há também o sistema "mathtext" baseado em Python para renderizar equações matemáticas usando um subconjunto da linguagem TeX sem ter uma instalação do TeX disponível. O suporte para esses dois mecanismos está espalhado por muitos arquivos de origem, incluindo todos os back-end, onde se encontram cláusulas como

if rcParams['text.usetex']: # do one thing else: # do another

Adicionar uma terceira abordagem de renderização de texto (mais sobre isso posteriormente) exigiria a edição de todos esses locais também e, portanto, não é dimensionado.

Em vez disso, este MEP propõe adicionar um conceito de "mecanismos de texto", onde o usuário pode selecionar uma das várias abordagens diferentes para renderizar texto. As implementações de cada um deles seriam localizadas em seu próprio conjunto de módulos e não teriam pequenos pedaços em toda a árvore de origem.

Por que adicionar mais mecanismos de renderização de texto? A renderização de texto "incorporada" tem várias deficiências.

  • Ele lida apenas com idiomas da direita para a esquerda e não lida com muitos recursos especiais do Unicode, como combinar sinais diacríticos.

  • O suporte multilinha é imperfeito e suporta apenas a quebra de linha manual -- não pode dividir um parágrafo em linhas de um determinado comprimento.

  • Ele também não lida com alterações de formatação em linha para oferecer suporte a algo como Markdown, reStructuredText ou HTML. (Embora a formatação de rich text seja contemplada neste MEP, já que queremos garantir que esse design permita isso, as especificações de uma implementação de formatação de rich text estão fora do escopo deste MEP.)

Apoiar essas coisas é difícil e é o "trabalho de tempo integral" de vários outros projetos:

Das opções acima, deve-se notar que o harfbuzz foi projetado desde o início como uma opção de plataforma cruzada com dependências mínimas, portanto, é um bom candidato para uma única opção de suporte.

Além disso, para oferecer suporte a rich text, podemos considerar o uso do WebKit e, possivelmente, se ele representa uma boa opção de plataforma cruzada única. Mais uma vez, no entanto, a formatação de rich text está fora do escopo deste projeto.

Em vez de tentar reinventar a roda e adicionar esses recursos ao renderizador de texto "embutido" do matplotlib, devemos fornecer uma maneira de alavancar esses projetos para obter um layout de texto mais poderoso. O renderizador "embutido" ainda precisará existir por motivos de facilidade de instalação, mas seu conjunto de recursos será mais limitado em comparação com os outros. [TODO: Este MEP deve decidir claramente quais são esses recursos limitados e corrigir quaisquer bugs para colocar a implementação em um estado de funcionamento correto em todos os casos em que queremos que funcione. Eu sei que @leejjoon tem algumas ideias sobre isso.]

Seleção de fonte

Passar de uma descrição abstrata de uma fonte para um arquivo em disco é a tarefa do algoritmo de seleção de fonte -- acaba sendo muito mais complicado do que parece à primeira vista.

Os renderizadores "embutidos" e "usetex" têm maneiras muito diferentes de lidar com a seleção de fontes, devido às suas diferentes tecnologias. O TeX requer a instalação de pacotes de fontes específicos do TeX, por exemplo, e não pode usar fontes TrueType diretamente. Infelizmente, apesar da semântica diferente para seleção de fonte, o mesmo conjunto de propriedades de fonte é usado para cada um. Isso é verdade tanto para a FontPropertiesclasse quanto para a fonte relacionada rcParams(que basicamente compartilham o mesmo código abaixo). Em vez disso, devemos definir um conjunto básico de parâmetros de seleção de fonte que funcionará em todos os mecanismos de texto e ter uma configuração específica do mecanismo para permitir que o usuário faça coisas específicas do mecanismo quando necessário. Por exemplo, é possível selecionar diretamente uma fonte pelo nome no "embutido" usando rcParams["font.family"](padrão:['sans-serif']), mas o mesmo não é possível com "usetex". Pode ser possível facilitar o uso de fontes TrueType usando XeTeX, mas os usuários ainda desejarão usar as metafontes tradicionais por meio de pacotes de fontes TeX. Portanto, ainda persiste o problema de que diferentes mecanismos de texto precisarão de configuração específica do mecanismo e deve ser mais óbvio para o usuário qual configuração funcionará nos mecanismos de texto e quais são específicas do mecanismo.

Observe que, mesmo excluindo "usetex", existem diferentes maneiras de encontrar fontes. O padrão é usar o cache da lista de fontes em font_manager que as fontes correspondem usando nosso próprio algoritmo baseado no algoritmo de correspondência de fontes CSS . Nem sempre faz a mesma coisa que os algoritmos de seleção de fonte nativa no Linux ( fontconfig), Mac e Windows, e nem sempre encontra todas as fontes no sistema que o sistema operacional normalmente selecionaria. No entanto, é multiplataforma e sempre encontra as fontes fornecidas com o matplotlib. Os back-ends Cairo e MacOSX (e presumivelmente um futuro back-end baseado em HTML5) atualmente ignoram esse mecanismo e usam os nativos do sistema operacional. O mesmo se aplica ao não incorporar fontes em arquivos SVG, PS ou PDF e abri-los em um visualizador de terceiros. Uma desvantagem é que (pelo menos com Cairo, precisa confirmar com MacOSX) eles nem sempre encontram as fontes que enviamos com matplotlib. (Pode ser possível adicionar as fontes ao seu caminho de pesquisa, ou talvez seja necessário encontrar uma maneira de instalar nossas fontes em um local que o sistema operacional espera encontrá-las).

Também existem modos especiais no PS e PDF para usar apenas as fontes principais que estão sempre disponíveis para esses formatos. Lá, o mecanismo de pesquisa de fonte deve corresponder apenas a essas fontes. Não está claro se os sistemas de pesquisa de fontes nativas do sistema operacional podem lidar com esse caso.

Também há suporte experimental para usar fontconfig para seleção de fonte no matplotlib, desativado por padrão. fontconfig é o algoritmo de seleção de fonte nativa no Linux, mas também é multiplataforma e funciona bem em outras plataformas (embora obviamente haja uma dependência adicional lá).

Muitas das bibliotecas de layout de texto propostas acima (pango, QtTextLayout, DirectWrite e CoreText etc.) insistem em usar a biblioteca de seleção de fontes de seu próprio ecossistema.

Tudo o que foi dito acima parece sugerir que devemos nos afastar de nosso algoritmo de seleção de fonte auto-escrito e usar as APIs nativas sempre que possível. Isso é o que os back-ends do Cairo e do MacOSX já desejam usar e será um requisito de qualquer biblioteca de layout de texto complexo. No Linux, já temos os ossos de uma implementação fontconfig (que também pode ser acessada através do pango). No Windows e no Mac, podemos precisar escrever wrappers personalizados. O bom é que a API para pesquisa de fonte é relativamente pequena e consiste essencialmente em "dado um dicionário de propriedades de fonte, forneça um arquivo de fonte correspondente".

Subconjunto de fontes

A subconfiguração de fonte é atualmente tratada usando ttconv. ttconv era um utilitário de linha de comando autônomo para converter fontes TrueType em fontes Type 3 subconjuntos (entre outros recursos) escrito em 1995, que matplotlib (bem, eu) bifurquei para fazê-lo funcionar como uma biblioteca. Ele lida apenas com fontes TrueType no estilo Apple, não com as codificações da Microsoft (ou de outro fornecedor). Ele não lida com fontes OpenType. Isso significa que, embora as fontes STIX venham como arquivos .otf, temos que convertê-los em arquivos .ttf para enviá-los com matplotlib. Os empacotadores do Linux odeiam isso - eles preferem depender apenas das fontes STIX upstream. O ttconv também demonstrou ter alguns bugs que foram difíceis de corrigir ao longo do tempo.

Em vez disso, devemos ser capazes de usar o FreeType para obter os contornos da fonte e escrever nosso próprio código (provavelmente em Python) para gerar subconjuntos de fontes (Tipo 3 em PS e PDF e caminhos em SVG). O Freetype, como um projeto popular e bem mantido, lida com uma ampla variedade de fontes em estado selvagem. Isso removeria muito código C personalizado e removeria alguma duplicação de código entre os back-ends.

Observe que a criação de subconjuntos de fontes dessa maneira, embora seja a rota mais fácil, perde a dica na fonte; portanto, precisaremos continuar, como fazemos agora, fornecendo uma maneira de incorporar a fonte inteira no arquivo sempre que possível.

As opções alternativas de subconjuntos de fontes incluem o uso do subconjunto embutido no Cairo (não está claro se pode ser usado sem o resto do Cairo) ou o uso do fontforge (que é uma dependência pesada e não terrivelmente multiplataforma).

Invólucros de tipo livre

Nosso wrapper FreeType realmente precisa de uma reformulação. Ele define sua própria classe de buffer de imagem (quando uma matriz Numpy seria mais fácil). Embora o FreeType possa lidar com uma grande diversidade de arquivos de fonte, há limitações em nosso wrapper que tornam muito mais difícil oferecer suporte a arquivos TrueType que não sejam de fornecedores da Apple e a certos recursos de arquivos OpenType. (Consulte # 2088 para obter um resultado terrível disso, apenas para oferecer suporte às fontes fornecidas com o Windows 7 e 8). Acho que uma nova reescrita deste invólucro ajudaria bastante.

Ancoragem de texto e alinhamento e rotação

A manipulação das linhas de base foi alterada na versão 1.3.0, de modo que os backends agora recebem a localização da linha de base do texto, não a parte inferior do texto. Este é provavelmente o comportamento correto, e a refatoração MEP também deve seguir esta convenção.

Para oferecer suporte ao alinhamento em texto de várias linhas, deve ser responsabilidade do mecanismo de texto (proposto) lidar com o alinhamento do texto. Para um determinado pedaço de texto, cada mecanismo calcula uma caixa delimitadora para esse texto e o deslocamento do ponto de ancoragem dentro dessa caixa. Portanto, se o va de um bloco fosse "topo", o ponto de ancoragem estaria no topo da caixa.

A rotação do texto deve ser sempre em torno do ponto de ancoragem. Não tenho certeza se isso está alinhado com o comportamento atual no matplotlib, mas parece ser a escolha mais sã/menos surpreendente. [Isso pode ser revisitado assim que tivermos algo funcionando]. A rotação do texto não deve ser tratada pelo mecanismo de texto -- isso deve ser feito por uma camada entre o mecanismo de texto e o back-end de renderização para que possa ser tratado de maneira uniforme. [Não vejo nenhuma vantagem em a rotação ser tratada pelos mecanismos de texto individualmente...]

Existem outros problemas com alinhamento e ancoragem de texto que devem ser resolvidos como parte deste trabalho. [TODO: enumere estes].

Outros pequenos problemas para corrigir

O código mathtext possui um código específico de back-end -- ele deve fornecer sua saída como apenas outro mecanismo de texto. No entanto, ainda é desejável ter o layout de texto matemático inserido como parte de um layout maior executado por outro mecanismo de texto, portanto, deve ser possível fazer isso. É uma questão em aberto se a incorporação do layout de texto de um mecanismo de texto arbitrário em outro deve ser possível.

O modo de texto é atualmente definido por um rcParam global ("text.usetex"), portanto, está totalmente ativado ou desativado. Devemos continuar a ter um rcParam global para escolher o mecanismo de texto ("text.layout_engine"), mas sob o capô deve ser uma propriedade substituível no Textobjeto, para que a mesma figura possa combinar os resultados de vários mecanismos de layout de texto, se necessário .

Implementação #

Um conceito de "motor de texto" será introduzido. Cada mecanismo de texto implementará várias classes abstratas. A TextFontinterface representará texto para um determinado conjunto de propriedades de fonte. Ele não está necessariamente limitado a um único arquivo de fonte - se o mecanismo de layout oferecer suporte a rich text, ele poderá lidar com vários arquivos de fonte em uma família. Dada uma TextFontinstância, o usuário pode obter uma TextLayoutinstância, que representa o layout de uma determinada string de texto em uma determinada fonte. De um TextLayout, um iterador sobre TextSpans é retornado para que o mecanismo possa gerar texto editável bruto usando o menor número de extensões possível. Se o mecanismo preferir obter caracteres individuais, eles podem ser obtidos na TextSpaninstância:

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

Infra-estruturas gráficas que desejam produzir subconjuntos de fontes provavelmente criariam um dicionário global de arquivos de caracteres onde as chaves são (fontname, glyph_id) e os valores são os caminhos para que apenas uma cópia do caminho para cada caractere seja armazenada em o arquivo.

Casing especial: A funcionalidade "usetex" atualmente é capaz de obter Postscript diretamente do TeX para inserir diretamente em um arquivo Postscript, mas para outros backends, analisa um arquivo DVI e gera algo mais abstrato. Para um caso como este, TextLayoutimplementaria get_spanspara a maioria dos back-ends, mas adicionaria get_pspara o back-end Postscript, que procuraria a presença desse método e o usaria, se disponível, ou voltaria para get_spans. Esse tipo de caixa especial também pode ser necessário, por exemplo, quando o back-end gráfico e o mecanismo de texto pertencem ao mesmo ecossistema, por exemplo, Cairo e Pango, ou MacOSX e CoreText.

Existem três partes principais para a implementação:

  1. Reescrevendo o wrapper freetype e removendo ttconv.

  1. Depois que (1) estiver concluído, como prova de conceito, podemos passar para as fontes STIX .otf upstream

  2. Adicione suporte para fontes da web carregadas de um URL remoto. (Ativado usando freetype para subconjunto de fonte).

  1. Refatorar o código "embutido" e "usetex" existente em mecanismos de texto separados e seguir a API descrita acima.

  2. Implementação de suporte para bibliotecas avançadas de layout de texto.

(1) e (2) são bastante independentes, embora ter (1) feito primeiro permitirá que (2) seja mais simples. (3) depende de (1) e (2), mas mesmo que não seja feito (ou seja adiado), concluir (1) e (2) facilitará o avanço na melhoria do "embutido" motor de texto.

Compatibilidade com versões anteriores #

O layout do texto em relação à sua âncora e rotação mudará de maneiras pequenas, mas aprimoradas. O layout do texto multilinha ficará muito melhor, pois respeitará o alinhamento horizontal. O layout do texto bidirecional ou outros recursos Unicode avançados agora funcionarão inerentemente, o que pode interromper algumas coisas se os usuários estiverem usando suas próprias soluções alternativas.

As fontes serão selecionadas de forma diferente. Os hacks que costumavam funcionar entre os mecanismos de renderização de texto "integrado" e "usetex" podem não funcionar mais. As fontes encontradas pelo sistema operacional que não foram encontradas anteriormente pelo matplotlib podem ser selecionadas.

Alternativas #

TBD