Un glifo faltante 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 archivo que sale es estructuralmente válido, se abre en todas partes y muestra una caja vacía donde debería estar un nombre o un importe. Nadie en el pipeline de generación se entera. El destinatario sí. HotPDF cierra ese círculo con TrackUnresolvedGlyphs: actívenlo y la vía de dibujo de texto registra cada punto de código cuya búsqueda de glifo se 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 su manejador. Juntas convierten una clase de defecto que antes reportaban los clientes en una comprobación en tiempo de compilación
Por qué un glifo faltante no levanta nada
Porque ISO 32000 no impone ninguna obligación al productor de 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. 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 de origen y no desde los contornos, así que una comprobación automática de ida y vuelta pasará felizmente un documento cuyo texto visible tiene huecos
La consecuencia práctica es que la cobertura tiene que comprobarse en el momento del dibujo, cuando la biblioteca todavía sabe qué punto de código se pidió y qué glifo ofreció realmente la fuente. Después la información ya no existe
El detector debe vigilar el estado del subset, no el contexto de dispositivo
Aquí fue donde la primera implementación se equivocó, y la razón vale entenderla porque aplica a cualquier comprobación de cobertura acoplada a un pipeline de texto. HotPDF tiene dos vías 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 vía GDI antigua que crea un contexto de dispositivo y un handle de fuente nuevos por cada corrida de caracteres
Juzgar la cobertura desde la vía GDI no tiene esperanza. Su mapeo no es el mapeo que acaba en el flujo de contenido emitido, y ambos no están sincronizados, así que un detector que lee resultados GDI reporta todo el rango ASCII imprimible como no resuelto. La respuesta autoritativa vive en la fuente registrada: el mapa de caracteres que RegisterUnicodeTTF analiza, consultado a través de GetUnicodeGlyphForCodepoint. El detector queda entonces condicionado al estado de subset listo, no a ninguna condición GDI, y simplemente no corre sobre documentos que nunca registraron una fuente Unicode, lo que es correcto porque esos documentos están limitados de todos modos a las codificaciones estándar
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 puedan normalizar: una familia llamada Arial Unicode MS lleva el nombre PostScript ArialMT. Toda guarda escrita como "la fuente seleccionada actualmente es la que registramos", comparada por nombre, es código muerto que nunca se dispara. Condicionen al estado, nunca a los nombres de fuente
No prueben un detector de glifos con emoji
El caso de prueba obvio es una carita sonriente, y los convencerá de que el detector está roto. Los puntos de código emoji comunes de los planos astrales se resuelven por una vía de síntesis de uso privado que los mapea a un índice de glifo directamente, así que nunca llegan a la rama general de cobertura. El detector se comporta correctamente y la prueba está midiendo la vía equivocada
Usen 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 quieren 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 manera 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 vez 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;
// Conectándolo a 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
// Fallar el trabajo en lugar de entregar una página con cajas
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Las cadenas de respaldo son por escritura, no por fuente
La razón por la que el respaldo se acota por escritura y no por fuente de origen es que los huecos de cobertura se agrupan por sistema de escritura. A una fuente de texto latina le faltan devanagari, 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 respaldo y la detección son complementarios y no alternativos. Las cadenas atienden la cobertura que anticiparon; el detector reporta la cobertura que no, que en un sistema que procesa datos arbitrarios de clientes es la mitad interesante. Nótese que sustituir una fuente cambia las métricas, así que un párrafo con respaldo puede refluir; si la maquetación importa, el comportamiento de cierre y de subsetting de la fuente sustituta vale la pena estudiarlo en el artículo de cierre de subconjuntos de fuentes, y las escrituras que necesitan reordenamiento o unión las maneja la etapa de shaping descrita en el shaping de texto de escrituras complejas
Cómo añadir comportamiento retroactivamente sin arriesgar la vía existente
La misma versión añadió un respaldo de tabla kern antigua para el espaciado de pares, y la forma en que se acotó es un patrón que vale copiar. En lugar de añadir un nuevo punto de decisión a la lógica de kerning, el respaldo cuelga de la rama de salida temprana que ya existía para fuentes sin tabla GPOS. Una fuente moderna con GPOS nunca la alcanza, así que su comportamiento queda sin cambios por construcción y no por pruebas. Las vías que no registran una fuente Unicode producen dos desplazamientos cero, así que también quedan sin cambios
Esa es la forma general de una mejora retroactiva de bajo riesgo en una biblioteca de renderizado madura: encontrar la rama que hoy no produce nada y poner el nuevo comportamiento ahí. Convierte "creemos que esto no hizo regresiones" en "esto no puede haber hecho regresiones", que es algo mucho mejor que decir de un motor de texto por el que pasan las facturas de otros
Háganlo una compuerta, no un registro
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 rastreo encendido en el trabajo nocturno de regresión contra un corpus de nombres, direcciones y descripciones de producto de clientes reales, y fallar el trabajo ante cualquier hallazgo. Como el evento se dispara una vez por punto de código único y no una vez por aparición, la salida se mantiene 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 les diga qué escritura añadir al conjunto de fuentes del despliegue. 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 incluida TrackUnresolvedGlyphs está documentada en la página del producto HotPDF Delphi PDF component