Un diseñador elige una fuente con una a de un solo nivel para los encabezados, o un cero con barra para las tablas, o un conjunto de mayúsculas ornamentadas 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 un glifo a través de la tabla cmap, y la alternativa se ubica a unos cuantos identificadores de glifo de distancia, alcanzable sólo mediante 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 cómo leer esas reglas (del tipo de sustitución única) en Object Pascal sin necesidad de una biblioteca nativa de modelado de texto subyacente
El alcance es acotado a propósito. Los conjuntos y alternativas estilísticas son sustituciones de un glifo de entrada por un glifo de salida. Conforman la parte del diseño OpenType que puede resolverse con un recorrido pequeño y determinista de las tablas, lo que resulta ideal para un motor Pascal que busca mantenerse libre de dependencias C
Por qué Delphi puro en lugar de HarfBuzz
HarfBuzz es la respuesta obvia a "modelar este texto", y para el modelado bidireccional completo, índico o árabe es la respuesta correcta. Pero también es una biblioteca en C. Enlazarla a un producto de Delphi o C++Builder significa distribuir un objeto nativo para cada plataforma y arquitectura de destino, emparejar su convención de llamadas, rastrear su cadencia de lanzamientos y comparar los términos de su licencia con los propios. Nada de eso es difícil de forma aislada. Todo ello representa una fricción constante, que no aporta nada cuando el requisito real es simplemente "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 subtablas GSUB y un par de 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 únicamente 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, son necesarios, y una consulta de sustitución única no los reemplazará
La jerarquía GSUB, de arriba a abajo
La tabla Glyph Substitution (GSUB) está organizada como una cadena de indirecciones, y una consulta de sustitución recorre la cadena desde arriba. En la cima está la ScriptList. Una etiqueta de script como latn selecciona una entrada, y la etiqueta especial DFLT es el script predeterminado que se aplica cuando no hay un script más específico que coincida. 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 idiomas que necesitan un comportamiento distinto. El turco es el ejemplo habitual, donde la i con y sin punto exigen un manejo especial
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 (entre ellas ss01) y una lista de índices de búsqueda. Estos í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 por defecto el script DFLT y el LangSys predeterminado, que es lo que incluye la gran mayoría de diseños de texto latino, y expone una forma de anular la etiqueta de script cuando una fuente incorpora sus características bajo un script específico
Las tablas de cobertura deciden quién participa
Toda subtabla de sustitución comienza con la misma pregunta: ¿participa este glifo de entrada en esta regla y, de ser así, dónde se sitúa en la propia indexación de la regla? Esa pregunta la responde una tabla 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 (Coverage) se presenta en dos formatos. El formato 1 es una lista de identificadores de glifo ordenados en forma ascendente. Se busca 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 compuesto por un glifo de inicio, un glifo de fin y el índice de cobertura al que se asigna el glifo inicial. Un glifo dentro de un rango obtiene su índice de cobertura sumando un desplazamiento desde el inicio del rango. El formato 1 es compacto cuando los glifos participantes están dispersos, y el formato 2 cuando se agrupan de manera contigua. Ambos están ordenados, por lo que su búsqueda toma tiempo logarítmico, y ambos devuelven un índice de cobertura o bien un "no cubierto" limpio que le permite al motor no alterar el glifo
Sustitución única, los dos formatos
La sustitución única es de LookupType 1 y mapea un glifo exactamente a un reemplazo. También cuenta con dos formatos, y esta 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 igual al id del glifo de entrada más ese delta, en módulo 65536. Así es como una fuente codifica una sustitución en la que cada glifo participante se encuentra a un desplazamiento fijo con respecto a su alternativa, por ejemplo, un bloque de números alineados a una distancia constante de las correspondientes figuras de estilo antiguo. La tabla Coverage indica qué glifos califican, y un solo delta sirve para todos ellos
El formato 2 almacena un array explícito de ids de glifos sustitutos. El índice de cobertura de la tabla Coverage corresponde al índice dentro de ese array, por lo que el glifo en el índice de cobertura 0 se convierte en la primera entrada del array, el índice 1 en la segunda, y así sucesivamente. El formato 2 se usa cuando las alternativas no están a un desplazamiento uniforme, lo cual es habitual en conjuntos estilísticos creados a mano. La consulta es igual desde el lado del llamador en ambos casos: tome el glifo de entrada, páselo por la tabla Coverage y, si está cubierto, aplique el delta o lea la ranura correspondiente en el array
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 digno de mención es la transparencia (pass-through). GetSingleSubstituteGlyph devuelve el id del glifo de entrada sin cambios ante cada error o ausencia: sin fuente, sin tabla GSUB, sin característica coincidente, o si no hay coincidencia en la cobertura. Eso significa que la llamada es segura de realizar incondicionalmente. Usted solicita la alternativa y, si no la hay, obtiene exactamente lo que introdujo, por lo que el código de llamada nunca necesita hacer excepciones para una fuente que carezca de la característica
El significado de las etiquetas de características estilísticas
La etiqueta de característica es todo el vocabulario que indica qué alternativa solicita, y las etiquetas relevantes para el trabajo estilístico forman una lista corta. El par principal es salt, alternativas estilísticas, que es el acceso genérico a las formas alternativas de un glifo, y ss01 hasta ss20, los veinte conjuntos estilísticos numerados que puede definir una fuente, cada uno como un paquete de sustituciones agrupado por el diseñador bajo un nombre. Una fuente podría incluir una a de un solo nivel y una R de pata recta en ss03, por ejemplo, de modo que habilitar ese único conjunto rediseña ambas letras
En torno a estas encontramos varias etiquetas más de sustitución única. aalt (access-all-alternates) es la unión de todas las alternativas que posee un glifo, habitualmente mostrada como una característica de paleta de glifos. titl selecciona mayúsculas para titulares optimizadas para tamaños grandes. subs y sups intercambian los números a formatos verdaderos de subíndice y superíndice, en lugar de predeterminados reducidos. ordn produce formas ordinales, las letras pequeñas en superíndice comunes en inglés como 1st y 2nd. frac construye fracciones, si bien las fracciones diagonales completas también se apoyan en lógicas de ligaduras y contextuales que van más allá de una simple sustitución única. Para los casos de glifo único, el mecanismo es idéntico a ss01: pase la etiqueta a la consulta de sustitución y lea de vuelta el glifo alternativo
// 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 ejecute cualquier sustitución, un carácter tiene que convertirse en glifo, y ese es el trabajo de la tabla cmap. La consulta de sustitución arranca a partir de un id de glifo, por lo que la ruta siempre es de carácter a glifo pasando por cmap, y luego de glifo a la 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, los primeros 65536 puntos de código, y eso resulta suficiente para la mayoría del texto en alfabeto latino. No es suficiente para los puntos de código de U+10000 en adelante, los planos suplementarios, que es donde habitan los símbolos matemáticos alfanuméricos, muchos signos y algunas escrituras vivas en la actualidad
El formato 12 es la subtabla que cubre el rango completo de U+0000 a U+10FFFF. Es una lista ordenada de grupos, en la que cada grupo cuenta con un punto de código de inicio, un punto de código de fin y un id de glifo inicial, por lo que una serie contigua de puntos de código se mapea a una secuencia contigua de glifos. HotPDF resuelve los puntos de código con una estrategia híbrida que encaja con el formato de los datos. Los puntos de código del BMP se proveen desde un array directo indexado mediante el punto de código, es decir, un acceso único sin búsqueda. Los puntos de código en los planos suplementarios se proveen desde una tabla dispersa ordenada por punto de código y examinada con una búsqueda binaria. El resultado es que GetUnicodeGlyphForCodepoint acepta un tipo Cardinal completo y responde correctamente en todo el rango, retornando el id de glifo 0 (el glifo .notdef) para cualquier punto de código que la fuente no logre mapear
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 clase concreta de pregunta, y conviene ser claro sobre lo que no resuelven. LookupType 1 es uno de ocho tipos de sustitución. La consulta no gestiona la sustitución múltiple de LookupType 2 (donde un glifo se convierte en varios), ni la sustitución de ligaduras de LookupType 4 (donde varios glifos se funden en uno). Tampoco aborda los tipos contextuales y de cadena contextual (LookupTypes 5 y 6), los cuales se disparan solo si el glifo aparece en una ubicación particular; ni los tipos de extensión o cadena inversa. Una fracción en diagonal, un signo conjunto devanagari o la cascada inicial-medial-final del idioma árabe constituyen un problema de secuencia que una búsqueda de sustitución única por glifo es incapaz de expresar
Asimismo, no lleva a cabo el modelado automático (automatic shaping). Aquí no hay nada que inspeccione un fragmento de texto, decida qué características activar y las aplique en el orden dictado por 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, los cuales son locales y voluntarios, y justo la herramienta incorrecta para una escritura que precisa un reordenamiento. Trazar un límite claro permite mantener la ruta de sustitución reducida y predecible
Para los casos en los que sí se precisa trabajar a nivel de secuencia, la historia de los scripts complejos se trata en nuestro artículo sobre el modelado de texto para scripts complejos en Delphi. Si sus sustituciones forman parte de un informe mayor que también ubica imágenes y otras fuentes en la página, la guía sobre la salida de informes con fuentes e imágenes explica cómo ensamblar tales piezas. Todo ello funciona en el mismo motor, el Componente HotPDF para Delphi y C++Builder, que incorpora las consultas de sustitución GSUB junto con las API de incrustación de fuentes, subconjuntos de caracteres y texto explicadas en otros apartados de este blog