Un glifo ausente en un PDF no es un error. El productor pide un carácter que la fuente seleccionada no puede mapear, la fuente devuelve el índice de glifo cero, y el fichero que sale es estructuralmente válido, se abre en todas partes y muestra una caja vacía donde debería ir un nombre o un importe. Nadie en el pipeline generador se entera. El destinatario sí. HotPDF cierra ese círculo con TrackUnresolvedGlyphs: actívalo y la ruta de dibujo de texto registra cada punto de código cuya búsqueda de glifo resuelve al índice cero, disparando OnUnresolvedGlyph una vez por hallazgo único con el punto de código, la fuente en la que falló, la escritura a la que pertenece y una sugerencia de fuentes que lo cubrirían
La detección es la mitad de la respuesta. La otra mitad es SetFontFallbackChain, que registra una lista ordenada de fuentes por escritura, de modo que los casos comunes se resuelven solos y solo los huecos genuinos llegan a vuestro manejador. Juntos convierten una clase de defecto que antes la reportaban los clientes en una comprobación en tiempo de compilación
Por qué un glifo ausente no levanta nada?
Porque ISO 32000 no obliga a un productor a verificar la cobertura, y el índice de glifo cero es un glifo legítimo. Es .notdef, cuyo contorno elige el diseñador de la fuente: normalmente un rectángulo vacío o hueco, a veces nada en absoluto. Un visor que lo dibuja se comporta correctamente. La extracción de texto puede incluso devolver los caracteres correctos, porque el mapeo /ToUnicode se escribe desde el texto fuente y no desde los contornos, así que una comprobación automática de ida y vuelta pasará felizmente un documento cuyo texto visible tiene agujeros
La consecuencia práctica es que la cobertura hay que comprobarla en el momento del dibujo, cuando la biblioteca aún sabe qué punto de código se pidió y qué glifo ofreció realmente la fuente. Después la información ya se ha ido
El detector debe vigilar el estado de subconjunto, no el contexto de dispositivo
Aquí es donde la primera implementación se equivocó, y el motivo merece entenderse porque aplica a cualquier comprobación de cobertura acoplada a un pipeline de texto. HotPDF tiene dos rutas de texto. Una emite a través de una fuente TrueType Unicode registrada con un mapa de caracteres en memoria construido al registrarla. La otra es una ruta GDI legada que crea un contexto de dispositivo y un handle de fuente frescos por cada pasada de caracteres
Juzgar la cobertura desde la ruta GDI es desesperado. Su mapeo no es el mapeo que acaba en el content stream emitido, y ambos no están sincronizados, así que un detector que lee resultados GDI informa de todo el rango ASCII imprimible como no resuelto. La respuesta con autoridad vive en la fuente registrada: el mapa de caracteres que RegisterUnicodeTTF analiza, consultado a través de GetUnicodeGlyphForCodepoint. El detector queda por tanto condicionado al estado de subconjunto preparado, no a condición GDI alguna, y sencillamente no se ejecuta en documentos que nunca registraron una fuente Unicode, lo cual es correcto porque esos documentos están restringidos a las codificaciones estándar de todos modos
Una segunda trampa se sienta al lado. El nombre de familia GDI de una fuente y el nombre PostScript extraído del binario de la fuente al registrarla son cadenas distintas, y no de una forma que podáis normalizar: una familia llamada Arial Unicode MS lleva el nombre PostScript ArialMT. Cualquier puerta escrita como «la fuente seleccionada actualmente es la que registramos», comparando por nombre, es código muerto que nunca se dispara. Condicionad al estado, nunca a nombres de fuentes
No probéis un detector de glifos con emoji
El caso de prueba obvio es una carita sonriente, y os convencerá de que el detector está roto. Los puntos de código emoji comunes en los planos astrales resuelven por una ruta de síntesis de uso privado que los mapea directamente a un índice de glifo, así que nunca llegan a la rama general de cobertura. El detector se comporta correctamente y la prueba está midiendo la ruta equivocada
Usad en cambio un punto de código sin asignar. U+0378 está permanentemente sin asignar en Unicode, así que ninguna fuente puede mapearlo legítimamente, y ejercita exactamente la rama que queréis verificar. Esa distinción entre «la función está rota» y «la prueba eligió una entrada que esquiva la función» cuesta horas reales, y los puntos de código sin asignar son la forma más barata de evitarlo
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Se dispara una vez por punto de código único, no una por aparición
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Cableándolo en un trabajo de generación
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Hacer fallar el trabajo en lugar de enviar una página con cajas
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Las cadenas de fallback son por escritura, no por fuente
La razón de que el fallback se scope por escritura y no por fuente de origen es que los huecos de cobertura se agrupan por sistema de escritura. Una fuente de texto latina carece de devanágari, tailandés, han y emoji, todo a la vez, y el sustituto de cada uno es una fuente distinta. Declarar una cadena por escritura describe, por tanto, el despliegue real: una fuente latina para el cuerpo, una fuente CJK, una fuente emoji, una comodín
// THPDFFontScript cubre hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji y hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
El fallback y la detección son complementarios, no alternativos. Las cadenas manejan la cobertura que anticipasteis; el detector informa de la que no, que en un sistema que procesa datos de clientes arbitrarios es la mitad interesante. Nótese que sustituir una fuente cambia las métricas, así que un párrafo que cae al fallback puede refluir; si la maquetación importa, el comportamiento de cierre y subsetting de la fuente sustituida merece leerse en el artículo de cierre de subconjuntos de fuentes, y las escrituras que necesitan reordenación o unión las maneja la etapa de shaping descrita en el shaping de texto de escrituras complejas
Cómo añadir comportamiento retroactivo sin arriesgar la ruta existente
La misma versión añadió un fallback de tabla kern legada para el espaciado de pares, y la forma de acotarlo es un patrón digno de copiar. En lugar de añadir un nuevo punto de decisión a la lógica de kerning, el fallback cuelga de la rama de salida temprana que ya existía para fuentes sin tabla GPOS. Una fuente moderna con GPOS nunca llega a ella, así que su comportamiento queda sin cambios por construcción y no por pruebas. Las rutas que no registran una fuente Unicode producen dos offsets cero, así que también quedan sin cambios
Esa es la forma general de una adición retroactiva de bajo riesgo en una biblioteca de renderizado madura: encontrar la rama que actualmente no produce nada y poner ahí el nuevo comportamiento. Convierte «creemos que esto no regresó nada» en «esto no puede haber regresado nada», que es mucho mejor cosa que decir de un motor de texto por el que pasan las facturas de otros
Hacedlo una puerta, no un log
Los hallazgos de cobertura solo son útiles si algo falla por ellos. En un servicio de generación de documentos el arreglo productivo es mantener el tracking activado en el trabajo nocturno de regresión contra un corpus de nombres de clientes, direcciones y descripciones de producto reales, y hacer fallar el trabajo ante cualquier hallazgo. Como el evento se dispara una vez por punto de código único y no una por aparición, la salida se queda lo bastante pequeña para leerse incluso cuando falta una escritura entera
En producción el mismo manejador funciona mejor como telemetría: registrar el punto de código y la fuente, seguir sirviendo el documento y dejar que el agregado os diga qué escritura añadir al conjunto de fuentes del despliegue a continuación. El comportamiento de renderizado para fuentes incrustadas y sustituidas se cubre más a fondo en el renderizado de glifos de fuentes incrustadas, y la lista completa de propiedades incluido TrackUnresolvedGlyphs está documentada en la página de producto de HotPDF Delphi PDF component