MEP14: Gestione del testo #

Stato n.

  • Discussione

Filiali e Pull request #

Il numero 253 mostra un bug in cui l'utilizzo del riquadro di delimitazione anziché della larghezza di avanzamento del testo produce un testo non allineato. Questo è un punto secondario nel grande schema delle cose, ma dovrebbe essere affrontato come parte di questo deputato.

Estratto n.

Riorganizzando il modo in cui viene gestito il testo, questo eurodeputato mira a:

  • migliorare il supporto per Unicode e lingue non ltr

  • migliorare il layout del testo (in particolare il testo su più righe)

  • consentire il supporto per più caratteri, in particolare caratteri TrueType non in formato Apple e caratteri OpenType.

  • rendere la configurazione dei caratteri più semplice e trasparente

Descrizione dettagliata #

Disposizione del testo

Al momento, matplotlib ha due modi diversi per rendere il testo: "integrato" (basato su FreeType e il nostro codice Python) e "usetex" (basato sulla chiamata a un'installazione TeX). In aggiunta al renderer "incorporato" c'è anche il sistema "mathtext" basato su Python per il rendering di equazioni matematiche usando un sottoinsieme del linguaggio TeX senza avere un'installazione TeX disponibile. Il supporto per questi due motori è disseminato in molti file sorgente, incluso ogni backend, dove si trovano clausole come

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

L'aggiunta di un terzo approccio di rendering del testo (ne parleremo più avanti) richiederebbe anche la modifica di tutti questi punti e quindi non si ridimensiona.

Invece, questo eurodeputato propone di aggiungere un concetto di "motori di testo", in cui l'utente può selezionare uno dei molti approcci diversi per rendere il testo. Le implementazioni di ciascuno di questi sarebbero localizzate nel proprio set di moduli e non avrebbero piccoli pezzi attorno all'intero albero dei sorgenti.

Perché aggiungere più motori di rendering del testo? Il rendering del testo "incorporato" presenta una serie di carenze.

  • Gestisce solo le lingue da destra a sinistra e non gestisce molte caratteristiche speciali di Unicode, come la combinazione di segni diacritici.

  • Il supporto multilinea è imperfetto e supporta solo l'interruzione di riga manuale: non può spezzare un paragrafo in righe di una certa lunghezza.

  • Inoltre, non gestisce le modifiche di formattazione in linea per supportare qualcosa come Markdown, reStructuredText o HTML. (Sebbene la formattazione RTF sia contemplata in questo MEP, poiché vogliamo assicurarci che questo design lo consenta, le specifiche di un'implementazione della formattazione RTF sono al di fuori dell'ambito di questo MEP.)

Sostenere queste cose è difficile ed è il "lavoro a tempo pieno" di una serie di altri progetti:

Delle opzioni di cui sopra, va notato che harfbuzz è progettato fin dall'inizio come un'opzione multipiattaforma con dipendenze minime, quindi è un buon candidato per il supporto di una singola opzione.

Inoltre, per supportare il rich text, potremmo prendere in considerazione l'utilizzo di WebKit e possibilmente se rappresenta una buona singola opzione multipiattaforma. Ancora una volta, tuttavia, la formattazione rich text esula dall'ambito di questo progetto.

Piuttosto che provare a reinventare la ruota e aggiungere queste funzionalità al renderer di testo "incorporato" di matplotlib, dovremmo fornire un modo per sfruttare questi progetti per ottenere un layout di testo più potente. Il renderer "incorporato" dovrà ancora esistere per motivi di facilità di installazione, ma il suo set di funzionalità sarà più limitato rispetto agli altri. [TODO: Questo MEP dovrebbe decidere chiaramente quali sono queste funzionalità limitate e correggere eventuali bug per portare l'implementazione in uno stato di funzionamento corretto in tutti i casi in cui vogliamo che funzioni. So che @leejjoon ha qualche idea su questo.]

Selezione dei caratteri

Passare da una descrizione astratta di un carattere a un file su disco è il compito dell'algoritmo di selezione del carattere: risulta essere molto più complicato di quanto sembri a prima vista.

I renderer "integrati" e "usetex" hanno modi molto diversi di gestire la selezione dei caratteri, date le loro diverse tecnologie. TeX richiede l'installazione di pacchetti di font specifici per TeX, ad esempio, e non può utilizzare direttamente i font TrueType. Sfortunatamente, nonostante la diversa semantica per la selezione dei caratteri, per ciascuno viene utilizzato lo stesso insieme di proprietà dei caratteri. Questo è vero sia per la FontPropertiesclasse che per i font correlati rcParams(che fondamentalmente condividono lo stesso codice sottostante). Invece, dovremmo definire un set base di parametri di selezione dei caratteri che funzioneranno su tutti i motori di testo e avere una configurazione specifica del motore per consentire all'utente di eseguire operazioni specifiche del motore quando richiesto. Ad esempio, è possibile selezionare direttamente un carattere per nome nel "integrato" utilizzando rcParams["font.family"](predefinito:['sans-serif']), ma lo stesso non è possibile con "usetex". Potrebbe essere possibile semplificare l'uso dei font TrueType utilizzando XeTeX, ma gli utenti vorranno comunque utilizzare i metafont tradizionali tramite i pacchetti di font TeX. Quindi il problema rimane che i diversi motori di testo avranno bisogno di una configurazione specifica del motore e dovrebbe essere più ovvio per l'utente quale configurazione funzionerà sui motori di testo e quali sono specifici del motore.

Nota che anche escludendo "usetex", ci sono diversi modi per trovare i caratteri. L'impostazione predefinita è utilizzare la cache dell'elenco dei caratteri in font_manager cui corrisponde i caratteri utilizzando il nostro algoritmo basato sull'algoritmo di corrispondenza dei caratteri CSS . Non sempre fa la stessa cosa degli algoritmi di selezione dei caratteri nativi su Linux ( fontconfig), Mac e Windows, e non sempre trova tutti i caratteri sul sistema che normalmente il sistema operativo raccoglierebbe. Tuttavia, è multipiattaforma e trova sempre i caratteri forniti con matplotlib. I backend Cairo e MacOSX (e presumibilmente un futuro backend basato su HTML5) attualmente ignorano questo meccanismo e utilizzano quelli nativi del sistema operativo. Lo stesso vale quando non si incorporano i caratteri nei file SVG, PS o PDF e li si apre in un visualizzatore di terze parti. Uno svantaggio è che (almeno con Cairo, è necessario confermare con MacOSX) non sempre trovano i caratteri che spediamo con matplotlib. (Potrebbe essere possibile aggiungere i caratteri al loro percorso di ricerca, tuttavia, oppure potrebbe essere necessario trovare un modo per installare i nostri caratteri in una posizione in cui il sistema operativo si aspetta di trovarli).

Esistono anche modalità speciali in PS e PDF per utilizzare solo i caratteri principali che sono sempre disponibili per quei formati. Lì, il meccanismo di ricerca dei caratteri deve corrispondere solo a quei caratteri. Non è chiaro se i sistemi di ricerca dei font nativi del sistema operativo possano gestire questo caso.

Esiste anche un supporto sperimentale per l'utilizzo di fontconfig per la selezione dei caratteri in matplotlib, disattivato per impostazione predefinita. fontconfig è l'algoritmo di selezione del carattere nativo su Linux, ma è anche multipiattaforma e funziona bene su altre piattaforme (anche se ovviamente c'è una dipendenza aggiuntiva lì).

Molte delle librerie di layout di testo proposte sopra (pango, QtTextLayout, DirectWrite e CoreText ecc.) insistono nell'usare la libreria di selezione dei caratteri dal proprio ecosistema.

Tutto quanto sopra sembra suggerire che dovremmo allontanarci dal nostro algoritmo di selezione dei caratteri scritto da noi stessi e utilizzare le API native ove possibile. Questo è ciò che i backend Cairo e MacOSX vogliono già utilizzare e sarà un requisito per qualsiasi libreria di layout di testo complessa. Su Linux, abbiamo già le ossa di un'implementazione di fontconfig (a cui si potrebbe accedere anche tramite pango). Su Windows e Mac potrebbe essere necessario scrivere wrapper personalizzati. La cosa bella è che l'API per la ricerca dei caratteri è relativamente piccola e consiste essenzialmente in "dato un dizionario di proprietà dei caratteri, dammi un file di caratteri corrispondente".

Sottoimpostazione dei caratteri

La sottoimpostazione dei caratteri è attualmente gestita tramite ttconv. ttconv era un'utilità a riga di comando autonoma per convertire i font TrueType in font Type 3 subsettati (tra le altre funzionalità) scritti nel 1995, che matplotlib (beh, io) ha biforcato per farlo funzionare come una libreria. Gestisce solo i caratteri TrueType in stile Apple, non quelli con le codifiche Microsoft (o di altri fornitori). Non gestisce affatto i caratteri OpenType. Ciò significa che anche se i caratteri STIX vengono forniti come file .otf, dobbiamo convertirli in file .ttf per fornirli con matplotlib. I pacchettizzatori di Linux lo odiano: preferiscono dipendere solo dai caratteri STIX a monte. È stato anche dimostrato che ttconv ha alcuni bug che sono stati difficili da correggere nel tempo.

Invece, dovremmo essere in grado di utilizzare FreeType per ottenere i contorni dei caratteri e scrivere il nostro codice (probabilmente in Python) per generare caratteri sottoinsiemi (tipo 3 su PS e PDF e percorsi su SVG). Freetype, in quanto progetto popolare e ben mantenuto, gestisce un'ampia varietà di caratteri in natura. Ciò rimuoverebbe molto codice C personalizzato e rimuoverebbe alcuni duplicati di codice tra i backend.

Si noti che la sottoimpostazione dei caratteri in questo modo, sebbene sia il percorso più semplice, perde il suggerimento nel carattere, quindi dovremo continuare, come facciamo ora, fornire un modo per incorporare l'intero carattere nel file ove possibile.

Le opzioni di sottoimpostazione dei caratteri alternative includono l'utilizzo della sottoimpostazione incorporata in Cairo (non è chiaro se può essere utilizzata senza il resto di Cairo) o l'utilizzo di fontforge (che è una dipendenza pesante e non terribilmente multipiattaforma).

Wrapper di tipo libero

Il nostro wrapper FreeType potrebbe davvero utilizzare una rielaborazione. Definisce la propria classe buffer immagine (quando un array Numpy sarebbe più semplice). Sebbene FreeType sia in grado di gestire un'enorme varietà di file di font, esistono limitazioni al nostro wrapper che rendono molto più difficile il supporto di file TrueType di fornitori non Apple e alcune funzionalità dei file OpenType. (Vedi # 2088 per un risultato terribile di questo, solo per supportare i caratteri forniti con Windows 7 e 8). Penso che una nuova riscrittura di questo involucro farebbe molto.

Ancoraggio del testo, allineamento e rotazione

La gestione delle linee di base è stata modificata nella versione 1.3.0 in modo tale che ai backend venga ora assegnata la posizione della linea di base del testo, non la parte inferiore del testo. Questo è probabilmente il comportamento corretto e anche il refactoring MEP dovrebbe seguire questa convenzione.

Per supportare l'allineamento su testo multilinea, dovrebbe essere responsabilità del motore di testo (proposto) gestire l'allineamento del testo. Per una determinata porzione di testo, ogni motore calcola un riquadro di delimitazione per quel testo e l'offset del punto di ancoraggio all'interno di tale riquadro. Pertanto, se la va di un blocco fosse "in alto", il punto di ancoraggio sarebbe nella parte superiore della scatola.

La rotazione del testo dovrebbe sempre avvenire attorno al punto di ancoraggio. Non sono sicuro che sia in linea con il comportamento attuale in matplotlib, ma sembra la scelta più sana/meno sorprendente. [Questo potrebbe essere rivisitato una volta che avremo qualcosa che funziona]. La rotazione del testo non dovrebbe essere gestita dal motore di testo, che dovrebbe essere gestito da un livello tra il motore di testo e il backend di rendering in modo che possa essere gestito in modo uniforme. [Non vedo alcun vantaggio nel fatto che la rotazione venga gestita individualmente dai motori di testo...]

Ci sono altri problemi con l'allineamento e l'ancoraggio del testo che dovrebbero essere risolti come parte di questo lavoro. [TODO: elencali].

Altri problemi minori da risolvere

Il codice mathtext ha un codice specifico per il backend: dovrebbe invece fornire il suo output solo come un altro motore di testo. Tuttavia, è ancora desiderabile inserire il layout mathtext come parte di un layout più ampio eseguito da un altro motore di testo, quindi dovrebbe essere possibile farlo. È una questione aperta se sia possibile incorporare il layout del testo di un motore di testo arbitrario in un altro.

La modalità testo è attualmente impostata da un rcParam globale ("text.usetex") quindi è tutto attivo o tutto spento. Dovremmo continuare ad avere un rcParam globale per scegliere il motore di testo ("text.layout_engine"), ma sotto il cofano dovrebbe esserci una proprietà sovrascrivibile Textsull'oggetto, quindi la stessa figura può combinare i risultati di più motori di layout del testo se necessario .

Implementazione n.

Verrà introdotto il concetto di "motore di testo". Ogni motore di testo implementerà un numero di classi astratte. L' TextFontinterfaccia rappresenterà il testo per un determinato insieme di proprietà del carattere. Non è necessariamente limitato a un singolo file di font: se il motore di layout supporta il testo ricco, può gestire un numero di file di font in una famiglia. Data TextFontun'istanza, l'utente può ottenere TextLayoutun'istanza, che rappresenta il layout per una data stringa di testo in un dato font. Da a TextLayout, viene restituito un iteratore su TextSpans in modo che il motore possa produrre testo modificabile non elaborato utilizzando il minor numero possibile di intervalli. Se il motore preferisce ottenere singoli caratteri, possono essere ottenuti TextSpandall'istanza:

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

I backend grafici che vogliono generare un sottoinsieme di caratteri creerebbero probabilmente un dizionario globale di caratteri di file in cui le chiavi sono (fontname, glyph_id) e i valori sono i percorsi in modo che venga memorizzata solo una copia del percorso per ogni carattere il file.

Case speciale: la funzionalità "usetex" attualmente è in grado di ottenere Postscript direttamente da TeX per inserirlo direttamente in un file Postscript, ma per altri backend, analizza un file DVI e genera qualcosa di più astratto. Per un caso come questo, TextLayoutimplementerebbe get_spansper la maggior parte dei backend, ma aggiungerebbe get_psper il backend Postscript, che cercherebbe la presenza di questo metodo e lo userebbe se disponibile, o ripiegherebbe su get_spans. Questo tipo di involucro speciale può anche essere necessario, ad esempio, quando il backend grafico e il motore testuale appartengono allo stesso ecosistema, ad esempio Cairo e Pango, oppure MacOSX e CoreText.

Ci sono tre parti principali per l'implementazione:

  1. Riscrivere il wrapper freetype e rimuovere ttconv.

  1. Una volta che (1) è fatto, come prova del concetto, possiamo passare ai font STIX .otf a monte

  2. Aggiungi il supporto per i caratteri Web caricati da un URL remoto. (Abilitato utilizzando freetype per la sottoimpostazione dei caratteri).

  1. Refactoring del codice "integrato" e "usetex" esistente in motori di testo separati e per seguire l'API descritta sopra.

  2. Implementazione del supporto per librerie di layout di testo avanzate.

(1) e (2) sono abbastanza indipendenti, anche se avere (1) fatto per primo permetterà a (2) di essere più semplice. (3) dipende da (1) e (2), ma anche se non viene fatto (o è posticipato), il completamento di (1) e (2) renderà più facile andare avanti con il miglioramento del "integrato" motore di testo.

Compatibilità con le versioni precedenti #

Il layout del testo rispetto al suo ancoraggio e rotazione cambierà in modi, si spera piccoli, ma migliorati. Il layout del testo multilinea sarà molto migliore, in quanto rispetterà l'allineamento orizzontale. Il layout del testo bidirezionale o altre funzionalità Unicode avanzate ora funzioneranno in modo intrinseco, il che potrebbe interrompere alcune cose se gli utenti stanno attualmente utilizzando le proprie soluzioni alternative.

I caratteri verranno selezionati in modo diverso. Gli hack che prima funzionavano tra i motori di rendering del testo "builtin" e "usetex" potrebbero non funzionare più. I caratteri trovati dal sistema operativo che non sono stati precedentemente trovati da matplotlib possono essere selezionati.

Alternative #

da definire