Artículo técnico

Renderizar fuentes incrustadas de PDF en Delphi con HotPDF

HotPDF renderiza fuentes incrustadas de PDF en Delphi sin instalar nada en el equipo: la canalización de renderizado de glifos incrustados en HPDFGlyphRender.pas analiza los programas de fuentes almacenados dentro del propio PDF (contornos glyf de TrueType de FontFile2, cadenas de caracteres CFF Tipo 2 de FontFile3 y flujos de contenido de glifos Tipo 3) y los reproduce como rutas vectoriales de GDI rellenas. Este artículo representa el análisis profundo de fidelidad de fuentes detrás de la renderización de páginas PDF a un TBitmap con HotPDF: ese texto cubre el renderizador en su totalidad, mientras que este analiza cómo el texto en esas páginas obtiene sus formas exactas

¿Por qué un PDF se renderiza con cuadros en lugar de texto?

Los cuadros, espacios en blanco o caracteres ligeramente incorrectos en el resultado renderizado de un PDF casi siempre significan que el renderizador le solicitó una fuente al sistema operativo en lugar de utilizar la incrustada en el archivo. La queja llega siempre de la misma manera: el documento se ve perfecto en el equipo que lo produjo, pero luego un cliente lo abre en un servidor limpio o en un escritorio restringido y la factura en japonés muestra cuadrados vacíos (tofu), o una tipografía sustituta similar desplaza cada salto de línea. Las fuentes nunca estuvieron en ese equipo, sino únicamente dentro del PDF, y un renderizador que se limita a la sustitución por fuentes del sistema no puede verlas. Los subconjuntos de fuentes lo empeoran: un subconjunto puede contener cuarenta glifos bajo códigos de caracteres asignados de manera privada a ese único archivo, una asignación que ninguna fuente instalada comparte

La norma ISO 32000-1 §9.9 define tres contenedores para un programa de fuentes incrustado en el descriptor de fuentes: FontFile contiene un programa Tipo 1 original, FontFile2 un programa TrueType y FontFile3 un CFF puro (Type1C o CIDFontType0C) o un contenedor OpenType. Una cuarta variante, la fuente Tipo 3 de la norma ISO 32000-1 §9.6.5, no incrusta nada binario; cada glifo es un pequeño flujo de contenido de PDF ejecutado en el lugar. Los tres contenedores difieren en la matemática de sus contornos (B-splines cuadráticas frente a cadenas de caracteres cúbicas frente a operadores de página arbitrarios), por lo que un renderizador fiel necesita un intérprete independiente para cada uno, además de una capa de codificación que convierta los códigos de caracteres en el índice de glifo correcto antes de procesar cualquier contorno

¿Cómo convierte HotPDF contornos glyf de TrueType en rutas GDI?

THPDFEmbeddedTTF en HPDFGlyphRender.pas lee la tabla loca para localizar cada registro de glifo, recorre los contornos glyf punto por punto y genera una ruta GDI. Dos convenciones de TrueType requieren un manejo explícito. En primer lugar, los puntos consecutivos fuera de la curva implican un punto sobre la curva en su punto medio, y un contorno cuyos puntos están todos fuera de la curva comienza en el punto medio de sus puntos último y primero; si se omite alguna de estas reglas, los glifos redondeados presentarán facetas planas o se colapsarán. En segundo lugar, las curvas TrueType son curvas de Bézier cuadráticas, mientras que el método PolyBezierTo de GDI acepta cúbicas, por lo que cada segmento cuadrático se eleva de grado de manera exacta en lugar de aplanarse en segmentos de línea

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

La elevación de grado es sin pérdidas: la curva cúbica traza la misma curva geométrica, por lo que el contorno renderizado coincide con lo que dibuja un visor compatible a partir de la misma tabla, en cualquier nivel de zoom. El trabajo restante es la colocación. Cada glifo se crea en unidades de fuente (normalmente una cuadrícula de 1000 o 2048 unidades por em), y the renderizador compone la matriz de escala, la matriz de texto y la matriz de transformación actual en una única transformación de glifo a dispositivo antes de rellenar la ruta. El orden es más importante de lo que parece: si compone las mismas tres matrices a la inversa, cada glifo se colapsará hacia el origen, lo que resultará en una página de aspecto incorrecto cuyo fallo real es una sola línea de álgebra matricial

Cómo maneja las fuentes CFF el intérprete de cadenas de caracteres Tipo 2

THPDFEmbeddedCFF les proporciona a los programas FontFile3 un intérprete real de cadenas de caracteres (charstrings) Tipo 2: analiza las estructuras CFF INDEX, el Top DICT y el Private DICT, luego ejecuta cada cadena de caracteres y emite segmentos de ruta directamente a GDI. Primero se retira el contenedor OpenType (el contenedor OTTO) para acceder a la tabla CFF pura; los flujos CIDFontType0C y Type1C directos se consumen directamente. Las cadenas de caracteres son un lenguaje de pila compacto, y tres de sus convenciones deciden si el intérprete se mantiene sincronizado con el flujo de bytes. El prefijo de ancho opcional significa que el primer operador que vacía la pila puede llevar un operando inicial adicional. El operador hintmask implica un vstemhm cuando los operandos aún están en la pila, y la cantidad de bytes de máscara a omitir depende de la cantidad acumulada de trazos (stems); si se equivoca en la cuenta una sola vez, cada código de operación subsiguiente se leerá mal. Y las llamadas a subrutinas añaden una desviación (bias) a su índice (107, 1131 o 32768 según la cantidad de subrutinas) antes de la búsqueda, de modo que una llamada sin desviación apunta a una subrutina completamente incorrecta

El formato CFF indexado por CID añade una indirección que complica las implementaciones sencillas: el código de carácter selecciona un CID, pero el índice de la cadena de caracteres es un GID, y el juego de caracteres de la fuente mapea GID a CID, por lo que el renderizador construye el mapeo inverso de CID a GID antes de dibujar, y selecciona el Private DICT por glifo a través de FDSelect para las fuentes que contienen varios. Los programas Type1C indexados por nombre, el contenedor habitual para fuentes Tipo 1 simples, resuelven los códigos de un byte a través de la codificación integrada del programa CFF o a través del mecanismo de codificación a nivel de PDF descrito a continuación. Una advertencia sincera: el intérprete lee los operadores de sugerencia (hint) para mantener sincronizado el flujo de bytes pero no ejecuta las sugerencias, un límite que se analiza al final

¿Qué es una fuente Tipo 3 y cómo se dibuja?

Un glifo Tipo 3 no es un contorno en absoluto; la norma ISO 32000-1 §9.6.5 lo define como un flujo de contenido, por lo que HotPDF lo renderiza guardando el estado gráfico, componiendo la matriz de fuente, el tamaño de fuente y la matriz de texto en la CTM, y ejecutando el procedimiento del glifo a través del mismo intérprete de operadores que dibuja las páginas, con los propios /Resources de la fuente al alcance. Dos detalles de la especificación son importantes para la corrección. Las anchuras /Widths de Tipo 3 se expresan en el espacio del glifo en lugar del espacio de texto de 1/1000 que utiliza cualquier otro tipo de fuente, por lo que los avances deben pasar por /FontMatrix; de lo contrario, las fuentes de código de barras con una matriz de 0.01 avanzan incorrectamente por un orden de magnitud. Y un procedimiento de glifo que se inicia con el operador d1 realiza dos promesas que el renderizador aplica: el dibujo se recorta a la caja delimitadora declarada y, según la norma ISO 32000-1 §9.6.5.2, el glifo ignora sus propios operadores de color y se dibuja con el color de relleno actual del llamador, por lo que rg, g, k y sus equivalentes de trazo dentro del procedimiento se suprimen durante la ejecución de ese glifo. Si se omite la regla de color, una fuente de código de barras d1 estampada en azul por la página se dibujará en negro; si se omite el recorte, un glifo mal formado dibujará fuera de su celda

Cómo se convierten los códigos de caracteres en IDs de glifos

Los intérpretes de contornos son solo la mitad del trabajo, debido a que los bytes en una cadena de texto de PDF son códigos de caracteres y no índices de glifos, y la norma ISO 32000-1 dedica dos apartados a este mapeo. Para fuentes simples, el apartado §9.6.6 prescribe una prioridad estricta: una matriz /Differences invalida la codificación base (WinAnsiEncoding, MacRomanEncoding o StandardEncoding), que a su vez invalida el mapa del propio programa de fuentes. HotPDF resuelve esa cadena en una tabla de código a GID de 256 entradas, traduciendo los nombres de glifos a índices de glifos por tres vías: coincidencia exacta de juego de caracteres dentro de un programa CFF, nombres numéricos del tipo gNN/glyphNN tomados como índices literales y traducción de nombre a Unicode de la lista de glifos de Adobe (Adobe Glyph List), seguida de una búsqueda en la tabla cmap para programas TrueType. Para fuentes compuestas, el apartado §9.7 deja a cargo a CIDToGIDMap: el caso común es /Identity, pero la entrada puede ser un flujo de pares big-endian indexados por CID; el propio resultado Unicode de HotPDF utiliza exactamente esa forma de flujo para subconjuntos compactos, por lo que la ruta del flujo no es un caso extraño

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Cuando se requiere una búsqueda en la tabla cmap de TrueType, HotPDF recorre una cadena de contingencia en lugar de confiar en una única subtabla: primero se aplican las subtablas Unicode de Windows (formato 4, luego formato 12 para planos complementarios), después la subtabla de símbolos (3,0) con su convención de área de uso privado F000 reflejada en el byte bajo (la razón por la que una fuente de símbolos como Wingdings responde a códigos ASCII simples) y luego los formatos heredados 6 y 0. Las subtablas de formato 2 no se interpretan de forma deliberada: mapean páginas de códigos multibyte heredadas como Shift-JIS y Big5, no Unicode, y las fuentes CJK modernas invariablemente contienen una subtabla de formato 4 o formato 12 de todos modos. Cualquier código que no funcione en ninguna de estas rutas recurre al dibujo de GDI para ese glifo individual, de modo que un carácter no mapeable degrada un solo glifo y no todo el bloque de texto

Lo que no realiza la ruta incrustada

Vale la pena definir los límites con claridad. No se ejecutan las sugerencias (hinting); los contornos se rellenan tal como fueron creados, lo cual es indistinguible de un resultado con sugerencias a 150 DPI o superior, pero puede diferir en un píxel respecto a un rasterizador con sugerencias a tamaños muy pequeños. Los programas Tipo 1 originales en FontFile (cadenas de caracteres cifradas con eexec) no se interpretan y no se aplican los ejes de fuentes variables OpenType; en ambos casos, al igual que con un programa de fuentes dañado o una tabla glyf sin ninguna cmap utilizable, se recurre al dibujo con fuentes del sistema en lugar de fallar al mostrar la página. Este mismo enfoque de prioridad de fidelidad se extiende a otras partes del renderizador (los patrones de sombreado axial y radial reciben el mismo trato que merecen los degradados), y el lado de generación tiene su propia particularidad en cómo EndDoc ordena los subconjuntos de fuentes

El uso de esta canalización no requiere ningún código específico de fuentes: cada uno de los mecanismos anteriores se activa automáticamente dentro de la llamada de renderizado de la página

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

La consecuencia práctica es la que le interesa a su bandeja de entrada de soporte: un PDF que lleva sus fuentes se renderiza con esas fuentes en un servidor de compilación, en un contenedor de Windows o en el escritorio de un cliente que nunca ha visto ese tipo de letra. La canalización de renderizado de glifos incrustados se distribuye como parte de HotPDF Component para Delphi and C++Builder: una biblioteca VCL nativa que cubre la creación, edición, extracción de texto y renderizado de páginas de PDF sin dependencias de DLL externas