MEP14: Manejo de texto #
Estado #
Discusión
Sucursales y solicitudes de extracción #
El problema n.º 253 demuestra un error en el que el uso del cuadro delimitador en lugar del ancho de avance del texto da como resultado un texto desalineado. Este es un punto menor en el gran esquema de las cosas, pero debe abordarse como parte de este MEP.
Resumen #
Al reorganizar cómo se maneja el texto, este eurodiputado tiene como objetivo:
mejorar el soporte para Unicode y lenguajes que no son ltr
mejorar el diseño del texto (especialmente el texto de varias líneas)
permitir la compatibilidad con más fuentes, especialmente fuentes TrueType que no sean de formato Apple y fuentes OpenType.
hacer que la configuración de la fuente sea más fácil y transparente
Descripción detallada #
Diseño de texto
En la actualidad, matplotlib tiene dos formas diferentes de representar texto: "incorporado" (basado en FreeType y nuestro propio código Python) y "usetex" (basado en llamar a una instalación de TeX). Junto al renderizador "incorporado", también está el sistema de "texto matemático" basado en Python para representar ecuaciones matemáticas utilizando un subconjunto del lenguaje TeX sin tener una instalación de TeX disponible. El soporte para estos dos motores está esparcido por muchos archivos fuente, incluyendo cada backend, donde uno encuentra cláusulas como
if rcParams['text.usetex']: # do one thing else: # do another
Agregar un tercer enfoque de representación de texto (más sobre esto más adelante) también requeriría editar todos estos lugares y, por lo tanto, no se escala.
En cambio, este eurodiputado propone agregar un concepto de "motores de texto", donde el usuario podría seleccionar uno de los muchos enfoques diferentes para representar el texto. Las implementaciones de cada uno de estos estarían localizadas en su propio conjunto de módulos, y no tendrían pequeñas piezas alrededor de todo el árbol fuente.
¿Por qué agregar más motores de representación de texto? La representación de texto "incorporada" tiene una serie de deficiencias.
Solo maneja idiomas de derecha a izquierda y no maneja muchas características especiales de Unicode, como la combinación de signos diacríticos.
El soporte de varias líneas es imperfecto y solo admite el salto de línea manual; no puede dividir un párrafo en líneas de cierta longitud.
Tampoco maneja cambios de formato en línea para admitir algo como Markdown, reStructuredText o HTML. (Aunque el formato de texto enriquecido se contempla en este MEP, dado que queremos asegurarnos de que este diseño lo permita, los detalles de una implementación de formato de texto enriquecido están fuera del alcance de este MEP).
Apoyar estas cosas es difícil y es el "trabajo de tiempo completo" de una serie de otros proyectos:
De las opciones anteriores, debe tenerse en cuenta que harfbuzz está diseñado desde el principio como una opción multiplataforma con dependencias mínimas, por lo que es un buen candidato para una sola opción de soporte.
Además, para admitir texto enriquecido, podríamos considerar el uso de WebKit y, posiblemente, si representa una buena opción única multiplataforma. De nuevo, sin embargo, el formato de texto enriquecido está fuera del alcance de este proyecto.
En lugar de tratar de reinventar la rueda y agregar estas funciones al procesador de texto "incorporado" de matplotlib, debemos proporcionar una forma de aprovechar estos proyectos para obtener un diseño de texto más potente. El renderizador "incorporado" seguirá existiendo por razones de facilidad de instalación, pero su conjunto de funciones será más limitado en comparación con los demás. [POR HACER: Este eurodiputado debe decidir claramente cuáles son esas características limitadas y corregir cualquier error para que la implementación funcione correctamente en todos los casos en que queremos que funcione. Sé que @leejjoon tiene algunas ideas sobre esto.]
Selección de fuente
Pasar de una descripción abstracta de una fuente a un archivo en el disco es la tarea del algoritmo de selección de fuentes; resulta mucho más complicado de lo que parece al principio.
Los renderizadores "incorporados" y "usetex" tienen formas muy diferentes de manejar la selección de fuentes, dadas sus diferentes tecnologías. TeX requiere la instalación de paquetes de fuentes específicos de TeX, por ejemplo, y no puede usar fuentes TrueType directamente. Desafortunadamente, a pesar de las diferentes semánticas para la selección de fuentes, se usa el mismo conjunto de propiedades de fuentes para cada una. Esto es cierto tanto para la
FontProperties
clase como para la fuente rcParams
(que básicamente comparten el mismo código debajo). En su lugar, debemos definir un conjunto básico de parámetros de selección de fuentes que funcionarán en todos los motores de texto y tener una configuración específica del motor para permitir que el usuario haga cosas específicas del motor cuando sea necesario. Por ejemplo, es posible seleccionar directamente una fuente por nombre en el uso "incorporado"
rcParams["font.family"]
(predeterminado:['sans-serif']
), pero no es posible lo mismo con "usetex". Puede ser posible facilitar el uso de fuentes TrueType usando XeTeX, pero los usuarios aún querrán usar las metafuentes tradicionales a través de los paquetes de fuentes TeX. Entonces, el problema sigue siendo que los diferentes motores de texto necesitarán una configuración específica del motor, y debería ser más obvio para el usuario qué configuración funcionará en todos los motores de texto y cuáles son específicos del motor.
Tenga en cuenta que incluso excluyendo "usetex", hay diferentes formas de encontrar fuentes. El valor predeterminado es usar la caché de lista de fuentes en font_manager
la que se comparan las fuentes usando nuestro propio algoritmo basado en el algoritmo de coincidencia de fuentes CSS . No siempre hace lo mismo que los algoritmos nativos de selección de fuentes en Linux ( fontconfig), Mac y Windows, y no siempre encuentra todas las fuentes en el sistema que el sistema operativo normalmente detectaría. Sin embargo, es multiplataforma y siempre encuentra las fuentes que se envían con matplotlib. Los backends de Cairo y MacOSX (y presumiblemente un futuro backend basado en HTML5) actualmente eluden este mecanismo y usan los nativos del sistema operativo. Lo mismo ocurre cuando no se incrustan fuentes en archivos SVG, PS o PDF y se abren en un visor de terceros. Una desventaja es que (al menos con Cairo, necesita confirmar con MacOSX) no siempre encuentran las fuentes que enviamos con matplotlib. (Sin embargo, es posible agregar las fuentes a su ruta de búsqueda, o es posible que necesitemos encontrar una manera de instalar nuestras fuentes en una ubicación que el sistema operativo espera encontrar).
También hay modos especiales en PS y PDF para usar solo las fuentes principales que siempre están disponibles para esos formatos. Allí, el mecanismo de búsqueda de fuentes solo debe coincidir con esas fuentes. No está claro si los sistemas de búsqueda de fuentes nativos del sistema operativo pueden manejar este caso.
También hay soporte experimental para usar fontconfig para la selección de fuentes en matplotlib, desactivado de forma predeterminada. fontconfig es el algoritmo nativo de selección de fuentes en Linux, pero también es multiplataforma y funciona bien en otras plataformas (aunque obviamente es una dependencia adicional allí).
Muchas de las bibliotecas de diseño de texto propuestas anteriormente (pango, QtTextLayout, DirectWrite y CoreText, etc.) insisten en usar la biblioteca de selección de fuentes de su propio ecosistema.
Todo lo anterior parece sugerir que deberíamos alejarnos de nuestro algoritmo de selección de fuentes escrito por nosotros mismos y usar las API nativas cuando sea posible. Eso es lo que los backends de Cairo y MacOSX ya quieren usar, y será un requisito de cualquier biblioteca de diseño de texto complejo. En Linux, ya tenemos los huesos de una implementación de fontconfig (a la que también se puede acceder a través de pango). En Windows y Mac, es posible que necesitemos escribir envoltorios personalizados. Lo bueno es que la API para la búsqueda de fuentes es relativamente pequeña y consiste esencialmente en "dado un diccionario de propiedades de fuentes, dame un archivo de fuente coincidente".
Subconjunto de fuentes
El subconjunto de fuentes se maneja actualmente mediante ttconv. ttconv era una utilidad de línea de comandos independiente para convertir fuentes TrueType en fuentes Type 3 subdivididas (entre otras características) escrita en 1995, que matplotlib (bueno, yo) bifurqué para que funcionara como una biblioteca. Solo maneja fuentes TrueType estilo Apple, no las que tienen codificaciones de Microsoft (u otros proveedores). No maneja fuentes OpenType en absoluto. Esto significa que aunque las fuentes STIX vienen como archivos .otf, tenemos que convertirlas a archivos .ttf para enviarlas con matplotlib. Los empaquetadores de Linux odian esto, prefieren simplemente depender de las fuentes STIX ascendentes. También se ha demostrado que ttconv tiene algunos errores que han sido difíciles de corregir con el tiempo.
En su lugar, deberíamos poder usar FreeType para obtener los contornos de las fuentes y escribir nuestro propio código (probablemente en Python) para generar subconjuntos de fuentes (Tipo 3 en PS y PDF y rutas en SVG). Freetype, como proyecto popular y bien mantenido, maneja una amplia variedad de fuentes en la naturaleza. Esto eliminaría una gran cantidad de código C personalizado y eliminaría algunas duplicaciones de código entre backends.
Tenga en cuenta que subdividir las fuentes de esta manera, aunque es la ruta más fácil, pierde las sugerencias en la fuente, por lo que deberemos continuar, como lo hacemos ahora, proporcionando una forma de incrustar la fuente completa en el archivo donde sea posible.
Las opciones alternativas de subconjuntos de fuentes incluyen el uso del subconjunto integrado en Cairo (no está claro si se puede usar sin el resto de Cairo), o el uso de fontforge (que es una dependencia pesada y no terriblemente multiplataforma).
Envolturas de tipo libre
Nuestro contenedor FreeType realmente podría necesitar una reelaboración. Define su propia clase de búfer de imagen (cuando una matriz Numpy sería más fácil). Si bien FreeType puede manejar una gran diversidad de archivos de fuentes, existen limitaciones en nuestro contenedor que hacen que sea mucho más difícil admitir archivos TrueType que no sean de Apple y ciertas características de los archivos OpenType. (Consulte el n.º 2088 para obtener un resultado terrible de esto, solo para admitir las fuentes que se envían con Windows 7 y 8). Creo que una nueva reescritura de este envoltorio sería de gran ayuda.
Anclaje de texto y alineación y rotación
El manejo de las líneas de base se cambió en 1.3.0 de modo que los backends ahora reciben la ubicación de la línea de base del texto, no la parte inferior del texto. Este es probablemente el comportamiento correcto, y la refactorización MEP también debería seguir esta convención.
Para admitir la alineación en texto de varias líneas, debe ser responsabilidad del motor de texto (propuesto) manejar la alineación del texto. Para un fragmento de texto determinado, cada motor calcula un cuadro delimitador para ese texto y el desplazamiento del punto de anclaje dentro de ese cuadro. Por lo tanto, si el va de un bloque fuera "superior", el punto de anclaje estaría en la parte superior de la caja.
La rotación del texto siempre debe estar alrededor del punto de anclaje. No estoy seguro de que se alinee con el comportamiento actual en matplotlib, pero parece la opción más sensata/menos sorprendente. [Esto podría revisarse una vez que tengamos algo funcionando]. La rotación de texto no debe ser manejada por el motor de texto; eso debe ser manejado por una capa entre el motor de texto y el backend de representación para que pueda manejarse de manera uniforme. [No veo ninguna ventaja en que los motores de texto manejen la rotación individualmente...]
Hay otros problemas con la alineación y el anclaje del texto que deben resolverse como parte de este trabajo. [TODO: enumerar estos].
Otros problemas menores para solucionar
El código de texto matemático tiene un código específico de back-end; en su lugar, debe proporcionar su salida como otro motor de texto. Sin embargo, aún es deseable tener el diseño de texto matemático insertado como parte de un diseño más grande realizado por otro motor de texto, por lo que debería ser posible hacer esto. Es una pregunta abierta si debería ser posible incrustar el diseño de texto de un motor de texto arbitrario en otro.
El modo de texto está configurado actualmente por un rcParam global ("text.usetex"), por lo que está todo activado o desactivado. Deberíamos seguir teniendo un rcParam global para elegir el motor de texto ("text.layout_engine"), pero bajo el capó debería ser una propiedad reemplazable en el Text
objeto, por lo que la misma figura puede combinar los resultados de múltiples motores de diseño de texto si es necesario. .
Implementación #
Se introducirá un concepto de "motor de texto". Cada motor de texto implementará una serie de clases abstractas. La TextFont
interfaz representará texto para un conjunto dado de propiedades de fuente. No se limita necesariamente a un solo archivo de fuente: si el motor de diseño admite texto enriquecido, puede manejar varios archivos de fuente en una familia. Dada una TextFont
instancia, el usuario puede obtener una TextLayout
instancia, que representa el diseño de una cadena de texto determinada en una fuente determinada. Desde a TextLayout
, se devuelve un iterador sobre TextSpan
s para que el motor pueda generar texto editable sin procesar utilizando la menor cantidad de intervalos posible. Si el motor prefiere obtener caracteres individuales, se pueden obtener de la TextSpan
instancia:
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
Los backends gráficos que desean generar un subconjunto de fuentes probablemente crearían un diccionario global de archivos de caracteres donde las claves son (fontname, glyph_id) y los valores son las rutas para que solo se almacene una copia de la ruta para cada carácter en el archivo.
Carcasa especial: la funcionalidad "usetex" actualmente puede obtener Postscript directamente de TeX para insertarlo directamente en un archivo Postscript, pero para otros backends, analiza un archivo DVI y genera algo más abstracto. Para un caso como este, TextLayout
se implementaría
get_spans
para la mayoría de los backends, pero se agregaría get_ps
para el backend Postscript, que buscaría la presencia de este método y lo usaría si estuviera disponible, o recurriría a get_spans
. Este tipo de carcasa especial también puede ser necesaria, por ejemplo, cuando el backend de gráficos y el motor de texto pertenecen al mismo ecosistema, por ejemplo, Cairo y Pango, o MacOSX y CoreText.
Hay tres piezas principales para la implementación:
Reescribiendo el contenedor de tipo libre y eliminando ttconv.
Una vez que (1) está hecho, como prueba de concepto, podemos pasar a las fuentes STIX .otf aguas arriba
Agregue soporte para fuentes web cargadas desde una URL remota. (Habilitado mediante el uso de tipo libre para el subconjunto de fuentes).
Refactorización del código "incorporado" y "usetex" existente en motores de texto separados y para seguir la API descrita anteriormente.
Implementación de soporte para bibliotecas de diseño de texto avanzadas.
(1) y (2) son bastante independientes, aunque tener (1) hecho primero permitirá que (2) sea más simple. (3) depende de (1) y (2), pero incluso si no se hace (o se pospone), completar (1) y (2) hará que sea más fácil avanzar con la mejora del "incorporado" motor de texto
Compatibilidad con versiones anteriores #
El diseño del texto con respecto a su ancla y rotación cambiará en formas pequeñas pero mejoradas. El diseño del texto de varias líneas será mucho mejor, ya que respetará la alineación horizontal. El diseño de texto bidireccional u otras características avanzadas de Unicode ahora funcionarán de forma inherente, lo que puede romper algunas cosas si los usuarios están utilizando actualmente sus propias soluciones alternativas.
Las fuentes se seleccionarán de manera diferente. Es posible que los hacks que solían funcionar entre los motores de representación de texto "incorporados" y "usetex" ya no funcionen. Se pueden seleccionar fuentes encontradas por el sistema operativo que matplotlib no haya encontrado previamente.
Alternativas #
Por determinar