Artículo técnico

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 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 identificadores (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, del tipo de sustitución única, en Object Pascal sin ninguna biblioteca de modelado (shaping) nativa subyacente

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 las convierte en un buen ajuste para un motor Pascal que desea mantenerse libre de dependencias de C

Por qué Delphi puro en lugar de HarfBuzz

HarfBuzz es la respuesta obvia a "modelar este texto" (shape this text), y para el modelado bidireccional completo, índico o árabe, es la respuesta correcta. También es una biblioteca en 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 llamadas, rastrear su cadencia de lanzamiento y leer sus términos de licencia frente a los suyos. Nada de eso es difícil de forma aislada. Todo ello es fricción que nunca desaparece, y no compra 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 subtables 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 honesto es que este enfoque maneja búsquedas de sustitución de glifos y nada más. No es resolución bidi, no es reordenamiento índico, y no es modelado contextual automático. Donde se necesitan, se necesitan, 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 (GSUB) está organizada como una cadena de indirecciones, y una consulta de sustitución recorre la cadena desde arriba. En la parte superior se encuentra la ScriptList. Una etiqueta de script como latn selecciona una entrada, y la etiqueta especial DFLT es el script predeterminado que se aplica cuando ningún script más específico coincide. La entrada del script apunta a un LangSys, el sistema de lenguaje, con un LangSys predeterminado para el caso común y otros opcionales con nombre para los lenguajes que necesitan un comportamiento diferente. El turco es el ejemplo habitual, donde la i con y sin punto exigen 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 ellos, y una lista de índices de búsqueda (lookups). Esos índices finalmente apuntan a la LookupList, donde viven las subtables de sustitución reales. Así que resolver ss01 significa: encontrar el script, encontrar su LangSys, encontrar la característica cuya etiqueta es ss01, recopilar los lookups que nombra y aplicarlos. HotPDF utiliza por defecto el script DFLT y 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 subtable 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 es respondida por una tabla de Cobertura (Coverage table), y la respuesta es un índice de cobertura, un ordinal pequeño que el resto de la subtable usa 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 de forma ascendente. Usted 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 rangos, 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 desplazándose desde el inicio del rango. El Formato 1 es compacto cuando los glifos participantes están dispersos, el Formato 2 cuando caen en series contiguas. Ambos están ordenados, por lo que ambos se buscan en tiempo logarítmico, y ambos devuelven un índice de cobertura o un claro "no cubierto" que permite al motor dejar el glifo en paz

Sustitución única, los dos formatos

La Sustitución Única es el 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 donde cada glifo participante se encuentra en un desplazamiento fijo idéntico respecto a su alternativa, por ejemplo, un bloque de números alineados ubicados a una distancia constante de los números de estilo antiguo coincidentes. La tabla de Cobertura dice qué glifos califican, y un solo delta sirve a todos ellos

El Formato 2 almacena un arreglo explícito de ids de glifos sustitutos. El índice de cobertura de la tabla de Cobertura es el índice dentro de ese arreglo, por lo 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 usa cuando las alternativas no están en un desplazamiento uniforme, lo cual es el caso común para los 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 Cobertura, y si está cubierto, aplique el delta o lea la ranura 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 el paso directo (pass-through). GetSingleSubstituteGlyph devuelve el id del glifo de entrada sin cambios en cada fallo: sin fuente, sin tabla GSUB, sin característica coincidente, sin acierto de cobertura. Eso significa que es seguro realizar la llamada incondicionalmente. Usted pregunta por la alternativa, y si no hay ninguna, obtiene exactamente lo que ingresó, por lo que el código de llamada nunca necesita hacer una excepción para 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á pidiendo, 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 ss01 a ss20, los veinte conjuntos estilísticos numerados que puede definir una fuente, cada uno un paquete nombrado de sustituciones que el diseñador agrupa. Una fuente podría poner una a de un solo piso y una R de pierna recta bajo ss03, por ejemplo, por lo que habilitar ese único conjunto reestiliza ambas

Alrededor de esas se ubican 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 cortadas para tamaños grandes. subs y sups intercambian números verdaderos de subíndice y superíndice en lugar de predeterminados reducidos a 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 la lógica de ligadura y contextual que va más allá de la simple sustitución única. 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 correspondiente

// 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;

cmap formato 12 y los planos suplementarios

Antes de que se pueda ejecutar cualquier sustitución, un carácter tiene que convertirse en un glifo, y ese es el trabajo de la tabla cmap. La consulta de sustitución comienza con un id de glifo, por lo que la ruta siempre es de carácter a glifo a través de cmap, luego de glifo a alternativa a través de GSUB. La parte interesante de cmap es su alcance. Una subtable de formato 4 cubre el Plano Multilingüe Básico, los primeros 65536 puntos de código, y eso es suficiente para la mayor parte del texto latino. No es suficiente para los puntos de código a partir de U+10000, los planos suplementarios, que es donde ahora viven los alfanuméricos matemáticos, muchos símbolos y varios guiones vivos

El Formato 12 es la subtable que cubre el rango completo de U+0000 a U+10FFFF. Es una lista ordenada de grupos, cada grupo consta de un punto de código de inicio, un punto de código de finalización y un id de glifo de inicio, por lo que una serie contigua de puntos de código se asigna a una serie contigua de glifos. HotPDF resuelve los puntos de código con una estrategia híbrida que coincide con cómo se forman los datos. Los puntos de código en el BMP se sirven de un arreglo directo indexado por el punto de código, una sola búsqueda sin buscar. Los puntos de código en los planos suplementarios se sirven desde una tabla dispersa (sparse table) ordenada por punto de código y buscada 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 una forma de pregunta, y vale la pena ser claro sobre lo que no responden. LookupType 1 es uno de los ocho tipos de sustitución. La consulta no maneja la sustitución múltiple de LookupType 2, donde un glifo se convierte en varios, ni la sustitución de ligadura de LookupType 4, donde varios glifos se convierten en uno. No maneja los tipos contextuales y de encadenamiento contextual, LookupTypes 5 y 6, que se activan solo cuando un glifo aparece en un vecindario particular, ni los tipos de extensión y de encadenamiento inverso. Una fracción diagonal, un conjunto devanagari o una cascada inicial-media-final árabe es un problema de secuencia, y una búsqueda de sustitución única por glifo no puede expresarlo

Tampoco realiza modelado automático. Nada de esto inspecciona una serie 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. Esa es exactamente la herramienta adecuada para los conjuntos estilísticos y las alternativas, que son opcionales y locales, y exactamente la herramienta equivocada para un script que necesita reordenamiento. Mantener el límite definido es lo que permite que la ruta de sustitución se mantenga pequeña y predecible

Para los casos que sí necesitan trabajo a nivel de secuencia, la historia de los scripts complejos se aborda en nuestro artículo sobre el modelado de texto para scripts complejos en Delphi. Si sus sustituciones forman parte de un trabajo de reporte más amplio que también coloca imágenes y otras fuentes en la página, la guía sobre salida de reportes con fuentes e imágenes cubre cómo encajan esas piezas. Todas ellas se ejecutan en el mismo motor, el Componente HotPDF para Delphi y C++Builder, que lleva las consultas de sustitución GSUB junto a las API de incrustación de fuentes, subconjuntos y texto cubiertas en otras partes de este blog