Artículo técnico

Renderizado de fuentes PDF incrustadas en Delphi con HotPDF

HotPDF renderiza fuentes PDF incrustadas en Delphi sin instalar nada en la máquina: la línea de procesamiento de renderizado de glifos incrustados en HPDFGlyphRender.pas analiza los programas de fuentes almacenados dentro del propio PDF (contornos TrueType glyf 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 es el análisis profundo de fidelidad de fuentes detrás de renderizar páginas PDF en un TBitmap con HotPDF: aquel artículo cubre el renderizador en su conjunto, este aborda cómo el texto de esas páginas adquiere 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 la salida PDF renderizada casi siempre significan que el renderizador solicitó una fuente al sistema operativo en lugar de utilizar la incrustada en el archivo. La queja llega siempre del mismo modo: el documento se ve perfecto en la máquina que lo generó, 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 fuente de aspecto similar sustituida desplaza cada salto de línea. Las fuentes nunca estuvieron en esa máquina (solo dentro del PDF) y un renderizador que se limita a la sustitución de fuentes del sistema no puede verlas. Los subconjuntos de fuentes (subset fonts) lo empeoran: un subconjunto puede contener cuarenta glifos bajo códigos de caracteres asignados de forma 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 fuente: FontFile contiene un programa original Tipo 1, FontFile2 un programa TrueType y FontFile3 un CFF básico (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 PDF ejecutado en su 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 tocar cualquier contorno

¿Cómo convertir contornos TrueType glyf 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 curva (off-curve) implican un punto sobre curva (on-curve) en su punto medio, y un contorno cuyos puntos están todos fuera de curva comienza en el punto medio de su último y primer punto; si se omite cualquiera de las reglas, los glifos redondeados adquieren caras planas o se colapsan. 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 forma exacta en lugar de aplanarse en segmentos de línea

// Elevación exacta del grado: cuadrática (P0, Q, P2) -> cúbica (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;
// luego PolyBezierTo con C1, C2, P2 — curva geométricamente idéntica

La elevación del grado no tiene pérdidas: la cúbica traza la misma curva idéntica, por lo que el contorno renderizado coincide con lo que dibuja un visor compatible a partir de la misma tabla, con cualquier nivel de zoom. El trabajo restante es la colocación. Cada glifo se define en unidades de fuente (normalmente una cuadrícula de 1000 o 2048 unidades por em), y el 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; una página con aspecto incorrecto cuyo verdadero error es una única línea de álgebra matricial

Cómo el intérprete de charstrings de Tipo 2 gestiona las fuentes CFF

THPDFEmbeddedCFF proporciona a los programas FontFile3 un intérprete de charstrings de Tipo 2 auténtico: analiza las estructuras INDEX de CFF, el Top DICT y el Private DICT, y luego ejecuta cada charstring y emite segmentos de ruta directamente a GDI. Primero se elimina el contenedor OpenType (el contenedor OTTO) para llegar a la tabla CFF básica; los flujos directos CIDFontType0C y Type1C se consumen sin intermediarios. Las charstrings 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 el número de bytes de máscara a omitir depende del número acumulado de trazos (stems); si se calcula mal el número una sola vez, cada código de operación subsiguiente se interpretará de forma errónea. Y las llamadas a subrutinas añaden una desviación (bias) a su índice (107, 1131 o 32768 según el número de subrutinas) antes de la búsqueda, por lo que una llamada sin desviación terminará en una subrutina completamente incorrecta

CFF indexado por CID (CID-keyed) añade una indirección que dificulta las implementaciones ingenuas: el código de carácter selecciona un CID, pero el índice de charstring es un GID, y el juego de caracteres (charset) de la fuente asigna GID a CID; por lo tanto, el renderizador construye la asignación inversa CID a GID antes de dibujar, y selecciona el Private DICT por glifo a través de FDSelect para fuentes que contienen varios. Los programas Type1C indexados por nombre, el contenedor habitual para fuentes simples de Tipo 1, resuelven los códigos de un byte a través de la codificación integrada del programa CFF o a través del sistema de codificación a nivel de PDF que se describe a continuación. Una advertencia honesta: el intérprete lee los operadores de sugerencias (hints) para mantener sincronizado el flujo, pero no ejecuta el "hinting", límite que se aborda al final

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

Un glifo Tipo 3 no es en absoluto un contorno: 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 en la pila, componiendo la matriz de la fuente, el tamaño de la fuente y la matriz de texto sobre la CTM, y ejecutando el procedimiento del glifo a través del mismo intérprete de operadores que dibuja las páginas, con el propio /Resources de la fuente en su alcance. Dos detalles de la especificación son cruciales para la corrección. Los anchos /Widths de Tipo 3 se expresan en el espacio del glifo en lugar del espacio de texto de 1/1000 que utilizan todos los demás tipos de fuentes, por lo que los avances deben pasar a través de /FontMatrix; las fuentes de códigos de barras con una matriz de 0.01 avanzarían erróneamente en un orden de magnitud si no se hiciera así. Y un procedimiento de glifo que se inicia con el operador d1 hace dos promesas que el renderizador exige: la pintura se recorta a la caja delimitadora (bounding box) 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 duración de ese glifo. Si se omite la regla de color, una fuente de código de barras d1 sellada en azul por la página saldrá de color negro; si se omite el recorte, un glifo malformado se pintará fuera de su celda

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

Los intérpretes de contornos son solo la mitad del trabajo, porque los bytes en una cadena de texto PDF son códigos de caracteres, no índices de glifo, y la norma ISO 32000-1 dedica dos subapartados a esta asignación. 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 nombres de glifo a índices de glifo mediante tres vías: coincidencia exacta de juego de caracteres (charset) dentro de un programa CFF, nombres numéricos gNN/glyphNN tomados como índices literales, y traducción de nombre a Unicode mediante la Lista de Glifos de Adobe (Adobe Glyph List) seguida de una consulta cmap para programas TrueType. Para fuentes compuestas, el apartado §9.7 coloca a CIDToGIDMap al cargo: el caso habitual es /Identity, pero la entrada puede ser un flujo de pares en formato big-endian indexados por CID; y la propia salida Unicode de HotPDF utiliza precisamente ese formato de flujo para subconjuntos compactos, por lo que la ruta de flujo no es un caso inusual

// /CIDToGIDMap como un flujo: pares Word en formato big-endian indexados por CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // fuera de rango se asigna a .notdef

Cuando se necesita una consulta cmap de TrueType, HotPDF recorre una cadena de contingencia en lugar de confiar en una única subtabla: las subtablas Unicode de Windows (formato 4, luego formato 12 para planos complementarios) van primero, luego 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), luego los formatos heredados 6 y 0. Las subtablas de formato 2 se excluyen deliberadamente de la interpretación: asignan páginas de códigos multibyte heredadas como Shift-JIS y Big5, no Unicode, y de todos modos las fuentes CJK modernas invariablemente incluyen una subtabla de formato 4 o formato 12. Cualquier código que no sobreviva a ninguna de estas rutas recurre al dibujo GDI para ese glifo individual, de modo que un carácter no asignable degrada un solo glifo, no toda la secuencia de texto

Lo que no hace la ruta de fuentes incrustadas

Vale la pena detallar con claridad los límites. No se ejecuta el "hinting" (sugerencias de renderizado): los contornos se rellenan tal como se definieron, lo que resulta indistinguible del resultado con hinting a 150 DPI o superior, pero puede diferir en un píxel respecto a un rasterizador con hinting a tamaños muy reducidos. Los programas originales Tipo 1 en FontFile (cadenas de caracteres encriptadas con eexec) no se interpretan, y no se aplican los ejes de fuentes variables de OpenType; en ambos casos, al igual que 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 provocar un fallo en la página. El mismo enfoque de fidelidad ante todo se extiende a otras partes del renderizador: los patrones de sombreado axial y radial reciben el mismo tratamiento que merecen los degradados, y la parte de generación tiene sus propias particularidades con las fuentes en cómo EndDoc ordena los subconjuntos de fuentes

El uso de esta línea de procesamiento no requiere ningún código específico para fuentes: cada mecanismo anterior 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
      // Las fuentes incrustadas TrueType, CFF y Tipo 3 se renderizan desde el
      // propio archivo — no es necesario instalar nada en esta máquina
      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 lo que más le importa a su buzón de soporte: un PDF que incluye 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 tenido instalada esa tipografía. La línea de procesamiento de renderizado de glifos incrustados se distribuye como parte de HotPDF Component para Delphi y C++Builder, una biblioteca VCL nativa que cubre la creación, edición, extracción de texto y renderizado de páginas PDF sin dependencias de DLL externas