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 inclinado para las tablas, o un conjunto de mayúsculas swash para una portada. Esos glifos ya están en la fuente. Simplemente no son los predeterminados. La a predeterminada se asigna desde el carácter a través de la tabla cmap a un glifo, y la alternativa se encuentra a unos pocos IDs de glifos de distancia, accesible solo a través de una regla de sustitución. Producir esa alternativa en un PDF significa leer la regla y emitir el glifo sustituto en el flujo de contenido. Este artículo trata sobre la lectura de esas reglas, las de tipo sustitución única, en Object Pascal sin ninguna librería de modelado nativa subyaceente.

El alcance es estrecho a propósito. Los conjuntos estilísticos y las alternativas son sustituciones de un glifo de entrada por un glifo de salida. Son la parte del diseño de OpenType que se puede resolver con un recorrido de tabla pequeño y determinista, lo que los convierte en una buena opción para un motor en Pascal que desea mantenerse libre de dependencias de C.

Por qué Delphi puro en lugar de HarfBuzz

HarfBuzz is la respuesta obvia para "dar forma a este texto", y para el modelado completo bidireccional, índico o árabe es la respuesta correcta. También es una librería de C. Vincularla a un producto de Delphi o C++Builder significa enviar un objeto nativo para cada plataforma y arquitectura de destino, coincidir con su convención de llamada, realizar un seguimiento de su cadencia de lanzamientos y leer los términos de su licencia frente a la suya. Nada de eso es difícil de forma aislada. Todo ello es una fricción que nunca desaparece, y no aporta nada cuando el requisito real es "dame la forma ss01 de esta letra".

La sustitución única no necesita un motor de modelado. Necesita un analizador para un puñado de formatos de subtabla GSUB y una búsqueda binaria o dos. Escribir eso en Pascal mantiene toda la cadena de herramientas dentro de un solo compilador. El límite honesto es que este enfoque maneja 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 estos son necesarios, son necesarios, y una consulta de sustitución única no los reemplazará.

La jerarquía GSUB, de arriba a abajo

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

El LangSys nombra un conjunto de índices de características. Cada índice apunta a la FeatureList, donde un registro de característica lleva una etiqueta de cuatro bytes, ss01 entre ellas, y una lista de índices de búsqueda. Esos índices finalmente apuntan a la LookupList, donde residen las subtablas de sustitución reales. Así 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 and el LangSys predeterminado, que es lo que incluye 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 en su lugar.

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 sitúa en la propia indexación de la regla? Esa pregunta es respondida por 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 viene en dos formatos. El formato 1 es una lista de IDs de glifos ordenados en orden ascendente. Encuentra un glifo con una búsqueda binaria, y su posición en la lista es su índice de cobertura. El formato 2 es una lista de registros de rango, cada uno con un glifo de inicio, un glifo de fin y el índice de cobertura al que se asigna el glifo de inicio. Un glifo dentro de un rango obtiene su índice de cobertura compensándolo desde el inicio del rango. El formato 1 es compacto cuando los glifos participantes están dispersos; el formato 2, cuando se encuentran en ejecuciones contiguas. Ambos están ordenados, por lo que ambos se buscan en tiempo logarítmico, y ambos devuelven un índice de cobertura o un "no cubierto" limpio que permite al motor dejar el glifo en paz.

Sustitución única, los dos formatos

La sustitución única es LookupType 1, y asigna un glifo a exactamente un reemplazo. También tiene dos formatos, y la división es 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 de su alternativa; por ejemplo, un bloque de cifras de alineación colocadas a una distancia constante de las cifras de estilo antiguo correspondientes. La tabla de cobertura indica qué glifos califican, y el delta único sirve para todos ellos.

El formato 2 almacena una matriz explícita de IDs de glifos sustitutos. El índice de cobertura de la tabla de cobertura es el índice de esa matriz, por lo que el glifo en el índice de cobertura 0 se convierte en la primera entrada de la matriz, el índice de cobertura 1 en la segunda, y así sucesivamente. El formato 2 se utiliza cuando las alternativas no están en un desplazamiento uniforme, que es el caso común para conjuntos estilísticos creados a mano. La consulta es la misma desde el lado del llamador de cualquier manera. Tome el glifo de entrada, páselo por la cobertura y, si está cubierto, aplique el delta o lea la ranura de la matriz.

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 el paso directo. GetSingleSubstituteGlyph devuelve el ID de glifo de entrada sin cambios en cada error: sin fuente, sin tabla GSUB, sin característica coincidente, sin coincidencia de cobertura. Eso significa que la llamada es segura de hacer incondicionalmente. Solicita la alternativa y, si no hay ninguna, obtiene de vuelta exactamente lo que introdujo, por lo que el código de llamada nunca necesita tratar de manera especial una fuente que carece de la característica.

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

La etiqueta de característica es todo el vocabulario 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ísticos numerados que puede definir una fuente, cada uno de ellos un paquete con nombre 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, por lo que habilitar ese único conjunto cambia el estilo de ambas.

Alrededor de esas se encuentran varias etiquetas de sustitución única más. aalt es acceso a todas las alternativas, la unión de cada alternativa que tiene un glifo, usualmente presentada como una característica de paleta de glifos. titl selecciona mayúsculas de titulación diseñadas para tamaños grandes. subs y sups intercambian figuras de subíndice y superíndice reales en lugar de los valores predeterminados reducidos. ordn produce formas ordinales, las letras elevadas en 1st y 2nd. frac construye fracciones, aunque las fracciones diagonales completas también se apoyan en la lógica de ligaduras y contextual que va más allá de la sustitución única simple. Para los casos de un solo glifo, el mecanismo es idéntico a ss01: pase la etiqueta a la consulta de sustitución y lea 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 complementarios

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 el camino es siempre de carácter a glifo a través de cmap, y luego de glifo a alternativa a través de GSUB. La parte 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 latino. No es suficiente para los puntos de código desde U+10000 hacia arriba, los planos complementarios, que es donde viven ahora los alfanuméricos matemáticos, muchos símbolos y varias escrituras vivas.

El formato 12 es la subtabla que cubre el rango completo de U+0000 a U+10FFFF. Es una lista ordenada de grupos, cada grupo con un punto de código de inicio, un punto de código de fin y un ID de glifo de inicio, de modo que una ejecución contigua de puntos de código se asigna a una ejecución contigua de glifos. HotPDF resuelve los puntos de código con una estrategia híbrida que coincide con la forma de los datos. Los puntos de código en el BMP se sirven desde una matriz directa indexada por el punto de código, una sola búsqueda sin exploración. Los puntos de código en los planos complementarios se sirven desde una tabla dispersa ordenada por punto de código y explorada con una búsqueda binaria. El resultado es que GetUnicodeGlyphForCodepoint toma 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 asigne.

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 se detienen estas consultas

Las API de sustitución única responden a un tipo de pregunta, y vale la pena tener claro qué es 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. No maneja los tipos contextuales y de encadenamiento contextual, LookupTypes 5 y 6, que se disparan 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 árabe inicial-media-final es 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 bloque de texto, decide qué características activar y las aplica en el orden que requiere el script. El llamador elige la etiqueta de la característica y la aplica glifo por glifo. Esa es exactamente la herramienta adecuada para conjuntos y alternativas estilísticas, que son locales y de suscripción voluntaria, y exactamente la herramienta incorrecta para un script que necesita reordenamiento. Mantener el límite definido es lo que permite que la ruta de sustitución siga siendo pequeña y predecible.

Para los casos que sí necesitan trabajo a nivel de secuencia, la historia del script complejo se aborda en nuestro artículo sobre el modelado de texto de script complejo en Delphi. Si sus sustituciones son parte de un trabajo de informe más grande que también coloca imágenes y otras fuentes en la página, la guía sobre la salida de informes con fuentes e imágenes cubre cómo encajan esas piezas. All of these run on the same engine, the HotPDF Component for Delphi and C++Builder, which carries the GSUB substitution queries alongside the font embedding, subsetting, and text APIs covered elsewhere on this blog.