Technical Article

Alternativas estilísticas GSUB de OpenType en Delphi puro

Un diseñador elige una fuente con una a de un solo piso para los encabezados, o un cero con barra diagonal para las tablas, o un conjunto de mayúsculas ornamentales (swash) para una portada. Esos glifos ya están en la fuente; simplemente no son los predeterminados. La a predeterminada se mapea desde el carácter a través de la tabla cmap a un glifo, y la alternativa se encuentra a unos pocos identificadores de glifo de distancia, accesible únicamente a través de una regla de sustitución. Producir esa alternativa en un PDF implica leer la regla y emitir el glifo sustituto en el flujo de contenido. Este artículo trata sobre la lectura de esas reglas, específicamente las de sustitución única, en Object Pascal sin ninguna biblioteca de modelado nativa subyacente.

El alcance es estrecho a propósito. Los conjuntos y alternativas estilísticas son sustituciones de tipo un-glifo-de-entrada por un-glifo-de-salida. Son la sección del diseño de OpenType que se puede resolver con un recorrido de tabla pequeño y determinista, lo que los hace ideales para un motor de Pascal que desea mantenerse libre de dependencias de C.

Por qué Delphi puro en lugar de HarfBuzz

HarfBuzz es la respuesta obvia para \"dar forma a este texto\", y para el modelado bidireccional completo, índico o árabe es la solución correcta. Sin embargo, también es una biblioteca de C. Vincularla a un producto de Delphi o C++Builder implica distribuir un objeto nativo para cada plataforma y arquitectura de destino, coincidir con su convención de llamada, rastrear su ritmo de lanzamiento y revisar sus términos de licencia frente a los suyos. Nada de eso es difícil por separado, pero representa una fricción constante y no aporta nada cuando el requisito real es simplemente \"obtener la forma ss01 de esta letra\".

La sustitución única no necesita un motor de modelado. Requiere un analizador para unos pocos formatos de subtabla GSUB y una o dos búsquedas binarias. Escribir eso en Pascal mantiene toda la cadena de herramientas dentro de un solo compilador. El límite real es que este enfoque maneja las búsquedas de sustitución de glifos y nada más. No es resolución bidireccional, no es reordenamiento índico y no es modelado contextual automático. Donde estas características son necesarias, se deben implementar las herramientas adecuadas, y una consulta de sustitución única no las reemplazará.

La jerarquía GSUB, de arriba a abajo

La tabla de sustitución de glifos (Glyph Substitution) está organizada como una cadena de indirecciones, y una consulta de sustitución recorre la cadena desde la cima. En la parte superior está ScriptList. Una etiqueta de script como latn selecciona una entrada, y la etiqueta especial DFLT representa el script predeterminado que se aplica cuando no coincide ningún otro script más específico. La entrada del script apunta a un LangSys (sistema de idioma), con un LangSys predeterminado para el caso común y otros nombrados opcionales para idiomas que requieren un comportamiento diferente. El turco es el ejemplo habitual, donde la i con punto y sin punto exigen su propio manejo.

El LangSys nombra un conjunto de índices de características. Cada índice apunta a FeatureList, donde un registro de característica lleva una etiqueta de cuatro bytes, entre ellas ss01, y una lista de índices de búsqueda. Esos índices finalmente apuntan a LookupList, donde residen las subtablas de sustitución reales. De modo que resolver ss01 significa: encontrar el script, encontrar su LangSys, encontrar la característica cuya etiqueta es ss01, recopilar las búsquedas que nombra y aplicarlas. HotPDF utiliza de forma predeterminada el script DFLT y el LangSys predeterminado, que es lo que distribuye la gran mayoría de los diseños de texto latino, y expone una forma de anular la etiqueta del script cuando una fuente conecta sus características bajo un script específico.

Las tablas de cobertura deciden quién participa

Cada subtabla de sustitución comienza con la misma pregunta: ¿este glifo de entrada participa en esta regla y, de ser así, dónde se ubica en el propio índice de la regla? Esa pregunta se responde mediante una tabla de cobertura (Coverage), y la respuesta es un índice de cobertura, un pequeño ordinal que el resto de la subtabla utiliza para buscar en qué se convierte el glifo.

La cobertura se presenta en dos formatos. El Formato 1 es una lista de identificadores de glifos ordenados ascendentemente. Se localiza un glifo mediante una búsqueda binaria, y su posición en la lista representa su índice de cobertura. El Formato 2 es una lista de registros de rango, donde cada uno contiene un glifo inicial, un glifo final y el índice de cobertura al que se mapea el glifo inicial. Un glifo dentro de un rango obtiene su índice de cobertura calculando el desplazamiento desde el inicio del rango. El Formato 1 es compacto cuando los glifos participantes están dispersos; el Formato 2 es útil cuando forman bloques contiguos. Ambos están ordenados, por lo que se buscan en tiempo logarítmico, y ambos devuelven un índice de cobertura o un valor limpio de \"no cubierto\" que permite al motor no modificar el glifo.

Sustitución única, los dos formatos

La sustitución única es LookupType 1, y mapea un glifo a exactamente un reemplazo. También tiene dos formatos, y la división responde a una optimización de espacio. El Formato 1 almacena un único delta con signo. El ID del glifo de salida es el ID del glifo de entrada más ese delta, módulo 65536. Así es como una fuente codifica una sustitución en la que cada glifo participante se encuentra a un desplazamiento fijo constante respecto de su alternativa; por ejemplo, un grupo de números alineados colocados a una distancia constante de los números correspondientes de estilo antiguo (oldstyle). La tabla de cobertura indica qué glifos califican, y el delta único sirve para todos ellos.

El Formato 2 almacena un arreglo explícito de identificadores de glifos sustitutos. El índice de cobertura de la tabla de cobertura es el índice dentro de ese arreglo, de modo que el glifo en el índice de cobertura 0 se convierte en la primera entrada del arreglo, el índice de cobertura 1 en la segunda, y así sucesivamente. El Formato 2 se utiliza cuando las alternativas no están a un desplazamiento uniforme, que es el caso común para los conjuntos estilísticos construidos a mano. La consulta es la misma desde la perspectiva del llamador en ambos casos: tomar el glifo de entrada, pasarlo a través de Coverage y, si está cubierto, aplicar el delta o leer la posición del arreglo.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\\Fonts\\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

El contrato que vale la pena notar es la transferencia directa. GetSingleSubstituteGlyph devuelve el ID de glifo de entrada sin cambios ante cualquier fallo: si no hay fuente, no hay tabla GSUB, no coincide la característica o no hay coincidencia en la cobertura. Esto significa que la llamada es segura de realizar incondicionalmente. Se solicita la alternativa y, si no existe, se obtiene exactamente lo que se introdujo, por lo que el código de llamada nunca necesita tratar de forma especial a una fuente que carezca de la característica.

Qué significan las etiquetas de características estilísticas

La etiqueta de característica es el vocabulario completo de la alternativa que está solicitando, y las etiquetas relevantes para el trabajo estilístico son una lista corta. El par principal es salt (alternativas estilísticas), el acceso general a las formas alternativas de un glifo, y de ss01 a ss20, los veinte conjuntos estilísticas numerados que una fuente puede definir, cada uno como un grupo nombrado de sustituciones que el diseñador agrupa. Una fuente podría colocar una a de un solo piso y una R de pata recta bajo ss03, por ejemplo, de modo que habilitar ese único conjunto rediseña ambas letras.

A su alrededor se ubican varias etiquetas más de sustitución única. aalt es el acceso a todas las alternativas, la unión de cada alternativa que posee un glifo, usualmente presentada como una característica de paleta de glifos. titl selecciona mayúsculas de título diseñadas para tamaños grandes. subs y sups intercambian números en subíndice y superíndice reales en lugar de los valores predeterminados reducidos de escala. ordn produce formas ordinales, las letras elevadas en 1.º y 2.º. frac construye fracciones, aunque las fracciones diagonales completas también se apoyan en lógica contextual y de ligaduras que supera la sustitución única simple. Para los casos de un solo glifo, el mecanismo es idéntico a ss01: pasar la etiqueta a la consulta de sustitución y leer el glifo alternativo devuelto.

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

Formato cmap 12 y los planos suplementarios

Antes de que pueda ejecutarse cualquier sustitución, un carácter debe convertirse en un glifo, y ese es el trabajo de la tabla cmap. La consulta de sustitución comienza a partir de un ID de glifo, por lo que la ruta siempre es de carácter a glifo a través de cmap, y luego de glifo a alternativa a través de GSUB. La sección interesante de cmap es su alcance. Una subtabla de formato 4 cubre el Plano Multilingüe Básico (BMP), los primeros 65,536 puntos de código, y eso es suficiente para la mayor parte del texto en latín. No es suficiente para los puntos de código de U+10000 en adelante, los planos suplementarios, que es donde residen actualmente los caracteres alfanuméricos matemáticos, muchos símbolos y varias escrituras vivas.

El Formato 12 es la subtabla que cubre todo el rango de U+0000 a U+10FFFF. Es una lista ordenada de grupos, donde cada grupo contiene un punto de código de inicio, un punto de código final y un ID de glifo de inicio, de modo que un bloque contiguo de puntos de código se mapea a un bloque contiguo de glifos. HotPDF resuelve los puntos de código mediante una estrategia híbrida que se adapta a la forma de los datos. Los puntos de código en el BMP se sirven desde un arreglo directo indexado por el punto de código, una consulta única sin búsqueda. Los puntos de código en los planos suplementarios se sirven desde una tabla dispersa ordenada por punto de código y analizada mediante búsqueda binaria. El resultado es que GetUnicodeGlyphForCodepoint recibe un Cardinal completo y responde correctamente en todo el rango, devolviendo el ID de glifo 0, el glifo .notdef, para cualquier punto de código que la fuente no mapee.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

Dónde terminan estas consultas

Las API de sustitución única responden a un tipo de pregunta específico, y vale la pena tener claro lo que no responden. LookupType 1 es uno de los ocho tipos de sustitución. La consulta no maneja la sustitución múltiple LookupType 2, donde un glifo se convierte en varios, ni la sustitución de ligaduras LookupType 4, donde varios glifos se convierten en uno. Tampoco maneja los tipos contextuales y contextuales encadenados, LookupTypes 5 y 6, que se activan solo cuando un glifo aparece en un entorno particular, ni los tipos de extensión y encadenamiento inverso. Una fracción diagonal, una conjunción Devanagari o una cascada inicial-medial-final árabe representan un problema de secuencia, y una búsqueda de sustitución única por glifo no puede expresarlo.

Tampoco realiza modelado automático. Nada aquí inspecciona un fragmento de texto, decide qué características activar y las aplica en el orden que requiere el script. El llamador elige la etiqueta de característica y la aplica glifo por glifo. Es exactamente la herramienta adecuada para conjuntos y alternativas estilísticas, que son locales y de inclusión voluntaria, y la herramienta incorrecta para un script que requiere reordenamiento. Mantener la frontera clara es lo que permite que la ruta de sustitución se mantenga pequeña y predecible.

Para los casos que sí requieren trabajo a nivel de secuencia, la sección sobre scripts complejos se trata en nuestro artículo sobre modelado de texto de scripts complejos en Delphi. Si sus sustituciones son parte de una tarea de generación de informes más grande que también coloca imágenes y otras fuentes en la página, la guía para la salida de informes con fuentes e imágenes cubre cómo encajan esas piezas. Todas ellas se ejecutan en el mismo motor, el HotPDF Component para Delphi y C++Builder, que implementa las consultas de sustitución GSUB junto con las API de incrustación de fuentes, subconjuntos y texto tratadas en otras secciones de este blog.