MEP14 : Gestion du texte #

Statut #

  • Discussion

Branches et demandes d'extraction #

Le problème n° 253 illustre un bogue où l'utilisation de la zone de délimitation plutôt que la largeur d'avance du texte entraîne un texte mal aligné. C'est un point mineur dans le grand schéma des choses, mais il devrait être abordé dans le cadre de ce MEP.

Résumé #

En réorganisant le traitement des textes, ce MEP vise à :

  • améliorer la prise en charge des langues Unicode et non-ltr

  • améliorer la mise en page du texte (en particulier le texte multiligne)

  • permettre la prise en charge d'un plus grand nombre de polices, en particulier les polices TrueType non au format Apple et les polices OpenType.

  • rendre la configuration des polices plus facile et plus transparente

Descriptif détaillé #

Disposition du texte

À l'heure actuelle, matplotlib a deux manières différentes de rendre le texte : "intégré" (basé sur FreeType et notre propre code Python) et "usetex" (basé sur l'appel à une installation TeX). En plus du moteur de rendu "intégré", il existe également le système "mathtext" basé sur Python pour rendre les équations mathématiques à l'aide d'un sous-ensemble du langage TeX sans disposer d'une installation TeX. La prise en charge de ces deux moteurs est parsemée de nombreux fichiers source, y compris chaque backend, où l'on trouve des clauses comme

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

L'ajout d'une troisième approche de rendu de texte (plus à ce sujet plus tard) nécessiterait également la modification de tous ces endroits, et ne s'adapte donc pas.

Au lieu de cela, ce député européen propose d'ajouter un concept de "moteurs de texte", où l'utilisateur pourrait sélectionner l'une des nombreuses approches différentes pour le rendu du texte. Les implémentations de chacun d'entre eux seraient localisées dans leur propre ensemble de modules et n'auraient pas de petits morceaux autour de l'ensemble de l'arborescence des sources.

Pourquoi ajouter plus de moteurs de rendu de texte ? Le rendu de texte "intégré" présente un certain nombre de défauts.

  • Il ne gère que les langues de droite à gauche et ne gère pas de nombreuses fonctionnalités spéciales d'Unicode, telles que la combinaison de signes diacritiques.

  • Le support multiligne est imparfait et ne prend en charge que le saut de ligne manuel - il ne peut pas diviser un paragraphe en lignes d'une certaine longueur.

  • Il ne gère pas non plus les modifications de formatage en ligne afin de prendre en charge quelque chose comme Markdown, reStructuredText ou HTML. (Bien que le formatage de texte enrichi soit envisagé dans ce MEP, puisque nous voulons nous assurer que cette conception le permet, les spécificités d'une implémentation de formatage de texte enrichi sortent du cadre de ce MEP.)

Soutenir ces choses est difficile et constitue le "travail à plein temps" d'un certain nombre d'autres projets :

Parmi les options ci-dessus, il convient de noter que harfbuzz est conçu dès le départ comme une option multiplateforme avec des dépendances minimales, c'est donc un bon candidat pour une seule option à prendre en charge.

De plus, pour prendre en charge le texte enrichi, nous pourrions envisager d'utiliser WebKit , et éventuellement si cela représente une bonne option multiplateforme unique. Encore une fois, cependant, la mise en forme de texte enrichi n'entre pas dans le cadre de ce projet.

Plutôt que d'essayer de réinventer la roue et d'ajouter ces fonctionnalités au rendu de texte "intégré" de matplotlib, nous devrions fournir un moyen de tirer parti de ces projets pour obtenir une mise en page de texte plus puissante. Le moteur de rendu "intégré" devra toujours exister pour des raisons de facilité d'installation, mais son ensemble de fonctionnalités sera plus limité par rapport aux autres. [TODO : Ce MEP devrait clairement décider quelles sont ces fonctionnalités limitées et corriger les bogues pour amener l'implémentation dans un état de fonctionnement correct dans tous les cas où nous voulons qu'elle fonctionne. Je sais que @leejjoon a des idées à ce sujet.]

Sélection de la police

Passer d'une description abstraite d'une police à un fichier sur disque est la tâche de l'algorithme de sélection de polices - cela s'avère beaucoup plus compliqué qu'il n'y paraît au premier abord.

Les moteurs de rendu "intégrés" et "usetex" ont des manières très différentes de gérer la sélection des polices, compte tenu de leurs technologies différentes. TeX nécessite l'installation de packages de polices spécifiques à TeX, par exemple, et ne peut pas utiliser directement les polices TrueType. Malheureusement, malgré les différentes sémantiques pour la sélection des polices, le même ensemble de propriétés de police est utilisé pour chacune. Cela est vrai à la fois pour la FontPropertiesclasse et pour la police rcParams(qui partagent essentiellement le même code en dessous). Au lieu de cela, nous devrions définir un ensemble de paramètres de sélection de polices de base qui fonctionnera sur tous les moteurs de texte, et avoir une configuration spécifique au moteur pour permettre à l'utilisateur de faire des choses spécifiques au moteur si nécessaire. Par exemple, il est possible de sélectionner directement une police par son nom dans le « built-in » en utilisant rcParams["font.family"](par défaut :['sans-serif']), mais la même chose n'est pas possible avec "usetex". Il peut être possible de faciliter l'utilisation des polices TrueType en utilisant XeTeX, mais les utilisateurs voudront toujours utiliser les métafontes traditionnelles via les packages de polices TeX. Ainsi, le problème persiste que différents moteurs de texte nécessiteront une configuration spécifique au moteur, et il devrait être plus évident pour l'utilisateur quelle configuration fonctionnera sur tous les moteurs de texte et laquelle est spécifique au moteur.

Notez que même en excluant "usetex", il existe différentes façons de trouver des polices. La valeur par défaut est d'utiliser le cache de la liste des polices dans font_manager lequel correspond les polices en utilisant notre propre algorithme basé sur l' algorithme de correspondance des polices CSS . Il ne fait pas toujours la même chose que les algorithmes natifs de sélection de polices sous Linux ( fontconfig), Mac et Windows, et il ne trouve pas toujours toutes les polices du système que le système d'exploitation prendrait normalement. Cependant, il est multiplateforme et trouve toujours les polices fournies avec matplotlib. Les backends Cairo et MacOSX (et vraisemblablement un futur backend basé sur HTML5) contournent actuellement ce mécanisme et utilisent ceux natifs du système d'exploitation. Il en va de même lorsque vous n'incorporez pas de polices dans des fichiers SVG, PS ou PDF et que vous ne les ouvrez pas dans un visualiseur tiers. Un inconvénient est que (au moins avec Cairo, il faut confirmer avec MacOSX) ils ne trouvent pas toujours les polices que nous livrons avec matplotlib. (Il peut cependant être possible d'ajouter les polices à leur chemin de recherche, ou nous devrons peut-être trouver un moyen d'installer nos polices à un emplacement où le système d'exploitation s'attend à les trouver).

Il existe également des modes spéciaux dans PS et PDF pour n'utiliser que les polices de base qui sont toujours disponibles pour ces formats. Là, le mécanisme de recherche de polices ne doit correspondre qu'à ces polices. Il n'est pas clair si les systèmes de recherche de polices natifs du système d'exploitation peuvent gérer ce cas.

Il existe également un support expérimental pour l'utilisation de fontconfig pour la sélection de polices dans matplotlib, désactivé par défaut. fontconfig est l'algorithme natif de sélection de polices sous Linux, mais il est également multiplateforme et fonctionne bien sur les autres plates-formes (bien qu'il y ait évidemment une dépendance supplémentaire là-bas).

De nombreuses bibliothèques de mise en page de texte proposées ci-dessus (pango, QtTextLayout, DirectWrite et CoreText, etc.) insistent pour utiliser la bibliothèque de sélection de polices de leur propre écosystème.

Tout ce qui précède semble suggérer que nous devrions nous éloigner de notre algorithme de sélection de polices auto-écrit et utiliser les API natives lorsque cela est possible. C'est ce que les backends Cairo et MacOSX veulent déjà utiliser, et ce sera une exigence de toute bibliothèque de mise en page de texte complexe. Sous Linux, nous avons déjà les os d'une implémentation de fontconfig (qui pourrait également être accessible via pango). Sous Windows et Mac, nous devrons peut-être écrire des wrappers personnalisés. La bonne chose est que l'API pour la recherche de polices est relativement petite et consiste essentiellement en "étant donné un dictionnaire de propriétés de police, donnez-moi un fichier de police correspondant".

Sous-ensemble de polices

Le sous-ensemble de polices est actuellement géré à l'aide de ttconv. ttconv était un utilitaire de ligne de commande autonome pour convertir les polices TrueType en sous-ensembles de polices Type 3 (entre autres fonctionnalités) écrit en 1995, que matplotlib (enfin, j'ai) bifurqué afin de le faire fonctionner comme une bibliothèque. Il ne gère que les polices TrueType de style Apple, pas celles avec les encodages Microsoft (ou d'autres fournisseurs). Il ne gère pas du tout les polices OpenType. Cela signifie que même si les polices STIX sont fournies sous forme de fichiers .otf, nous devons les convertir en fichiers .ttf pour les expédier avec matplotlib. Les empaqueteurs Linux détestent cela - ils préfèrent simplement dépendre des polices STIX en amont. Il a également été démontré que ttconv présentait quelques bogues difficiles à corriger au fil du temps.

Au lieu de cela, nous devrions pouvoir utiliser FreeType pour obtenir les contours des polices et écrire notre propre code (probablement en Python) pour produire des sous-ensembles de polices (Type 3 sur PS et PDF et chemins sur SVG). Freetype, en tant que projet populaire et bien entretenu, gère une grande variété de polices à l'état sauvage. Cela supprimerait beaucoup de code C personnalisé et supprimerait certaines duplications de code entre les backends.

Notez que les sous-ensembles de polices de cette façon, bien que la voie la plus simple, perdent l'indication dans la police, nous devrons donc continuer, comme nous le faisons maintenant, fournir un moyen d'intégrer la police entière dans le fichier lorsque cela est possible.

Les autres options de sous-ensembles de polices incluent l'utilisation du sous-ensemble intégré à Cairo (il n'est pas clair s'il peut être utilisé sans le reste de Cairo) ou l'utilisation de fontforge (qui est une dépendance lourde et pas terriblement multiplateforme).

Emballages de type libre

Notre wrapper FreeType aurait vraiment besoin d'être retravaillé. Il définit sa propre classe de tampon d'image (quand un tableau Numpy serait plus simple). Bien que FreeType puisse gérer une grande diversité de fichiers de polices, il existe des limitations à notre wrapper qui rendent beaucoup plus difficile la prise en charge des fichiers TrueType non Apple et de certaines fonctionnalités des fichiers OpenType. (Voir # 2088 pour un résultat terrible, juste pour prendre en charge les polices fournies avec Windows 7 et 8). Je pense qu'une nouvelle réécriture de cet emballage irait loin.

Ancrage et alignement du texte et rotation

La gestion des lignes de base a été modifiée dans la version 1.3.0 de sorte que les backends reçoivent désormais l'emplacement de la ligne de base du texte, et non le bas du texte. C'est probablement le comportement correct, et la refactorisation MEP devrait également suivre cette convention.

Afin de prendre en charge l'alignement sur un texte multiligne, il devrait être de la responsabilité du moteur de texte (proposé) de gérer l'alignement du texte. Pour un morceau de texte donné, chaque moteur calcule une boîte englobante pour ce texte et le décalage du point d'ancrage dans cette boîte. Par conséquent, si le va d'un bloc était "haut", le point d'ancrage serait en haut de la boîte.

La rotation du texte doit toujours se faire autour du point d'ancrage. Je ne suis pas sûr que cela corresponde au comportement actuel de matplotlib, mais cela semble être le choix le plus sain/le moins surprenant. [Cela pourrait être revu une fois que nous aurons quelque chose qui fonctionne]. La rotation du texte ne doit pas être gérée par le moteur de texte - elle doit être gérée par une couche entre le moteur de texte et le moteur de rendu afin qu'elle puisse être gérée de manière uniforme. [Je ne vois aucun avantage à ce que la rotation soit gérée individuellement par les moteurs de texte...]

Il existe d'autres problèmes d'alignement et d'ancrage du texte qui doivent être résolus dans le cadre de ce travail. [TODO : énumérez-les].

Autres problèmes mineurs à résoudre

Le code mathtext a un code spécifique au backend -- il devrait plutôt fournir sa sortie comme un autre moteur de texte. Cependant, il est toujours souhaitable d'avoir une mise en page mathtext insérée dans le cadre d'une mise en page plus grande effectuée par un autre moteur de texte, il devrait donc être possible de le faire. C'est une question ouverte de savoir si l'intégration de la mise en page de texte d'un moteur de texte arbitraire dans un autre devrait être possible.

Le mode texte est actuellement défini par un rcParam global ("text.usetex"), il est donc soit activé, soit désactivé. Nous devrions continuer à avoir un rcParam global pour choisir le moteur de texte ("text.layout_engine"), mais il devrait sous le capot être une propriété remplaçable sur l' Textobjet, de sorte que la même figure puisse combiner les résultats de plusieurs moteurs de mise en page de texte si nécessaire .

Mise en œuvre #

Un concept de "moteur de texte" sera introduit. Chaque moteur de texte implémentera un certain nombre de classes abstraites. L' TextFontinterface représentera du texte pour un ensemble donné de propriétés de police. Il n'est pas nécessairement limité à un seul fichier de police - si le moteur de mise en page prend en charge le texte enrichi, il peut gérer plusieurs fichiers de police dans une famille. Étant donné une TextFontinstance, l'utilisateur peut obtenir une TextLayoutinstance, qui représente la disposition d'une chaîne de texte donnée dans une police donnée. À partir de a TextLayout, un itérateur sur TextSpans est renvoyé afin que le moteur puisse générer du texte brut modifiable en utilisant le moins d'étendues possible. Si le moteur préfère obtenir des caractères individuels, ils peuvent être obtenus à partir de l' TextSpaninstance :

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

Les backends graphiques qui souhaitent produire un sous-ensemble de polices construiraient probablement un dictionnaire global de fichiers où se trouvent les clés (nom de police, glyphe_id) et les valeurs sont les chemins de sorte qu'une seule copie du chemin pour chaque caractère sera stockée dans le fichier.

Casse spéciale : la fonctionnalité "usetex" est actuellement capable d'obtenir Postscript directement à partir de TeX pour l'insérer directement dans un fichier Postscript, mais pour d'autres backends, analyse un fichier DVI et génère quelque chose de plus abstrait. Pour un cas comme celui- TextLayoutci, implémenterait get_spanspour la plupart des backends, mais ajouterait get_pspour le backend Postscript, qui rechercherait la présence de cette méthode et l'utiliserait si disponible, ou reviendrait à get_spans. Ce type de casse spéciale peut également être nécessaire, par exemple, lorsque le backend graphique et le moteur de texte appartiennent au même écosystème, par exemple Cairo et Pango, ou MacOSX et CoreText.

La mise en œuvre comporte trois éléments principaux :

  1. Réécriture du wrapper freetype et suppression de ttconv.

  1. Une fois que (1) est fait, comme preuve de concept, nous pouvons passer aux polices STIX .otf en amont

  2. Ajoutez la prise en charge des polices Web chargées à partir d'une URL distante. (Activé en utilisant le type libre pour le sous-ensemble de polices).

  1. Refactoriser le code "builtin" et "usetex" existant dans des moteurs de texte séparés et suivre l'API décrite ci-dessus.

  2. Implémentation de la prise en charge des bibliothèques de mise en page de texte avancées.

(1) et (2) sont assez indépendants, bien que le fait d'avoir (1) fait en premier permettra à (2) d'être plus simple. (3) dépend de (1) et (2), mais même si cela ne se fait pas (ou est reporté), compléter (1) et (2) facilitera l'avancement de l'amélioration du "builtin" moteur de texte.

Rétrocompatibilité #

La mise en page du texte en ce qui concerne son ancre et sa rotation changera de manière, espérons-le, petite mais améliorée. La mise en page du texte multiligne sera bien meilleure, car elle respectera l'alignement horizontal. La disposition du texte bidirectionnel ou d'autres fonctionnalités Unicode avancées fonctionneront désormais de manière inhérente, ce qui peut casser certaines choses si les utilisateurs utilisent actuellement leurs propres solutions de contournement.

Les polices seront sélectionnées différemment. Les hacks qui faisaient le tri entre les moteurs de rendu de texte "builtin" et "usetex" peuvent ne plus fonctionner. Les polices trouvées par le système d'exploitation qui n'étaient pas trouvées auparavant par matplotlib peuvent être sélectionnées.

Alternatives #

À déterminer