Un emoji de cara sonriente escrito en un PDF con una llamada de texto ordinaria, usando Segoe UI Emoji como fuente, sale como una silueta negra plana: la forma correcta, sin amarillo, sin sombreado. Cambia a una fuente emoji CBDT y la misma llamada puede no dibujar nada, porque esa fuente guarda sus imágenes como archivos PNG y trae pocos o ningún dato de contorno. Copia la forma negra fuera del visor y el portapapeles sigue conteniendo U+1F600: el carácter está bien, solo que las tablas de color nunca se leyeron
¿Por qué un emoji a color se imprime como silueta negra en PDF?
Porque un programa de fuente PDF no tiene concepto de glifo coloreado. ISO 32000-1 trata cada glifo como una forma pintada con el color de relleno actual, y las tablas de color que OpenType añadió después (COLR/CPAL, SVG , CBDT/CBLC y sbix) no son parte del modelo de imagen PDF, así que ningún visor tiene razón para leerlas de una fuente incrustada. El color que tenga el emoji tiene que convertirse en contenido de página mientras se escribe el PDF, en un punto donde el productor todavía sostiene los bytes de la fuente y sabe exactamente qué glifo quiere. Esa traducción es distinta para cada formato, y las fuentes emoji reales usan los cuatro: capas vectoriales apiladas, grafos de pintura con degradados, documentos SVG enteros y strikes PNG
HotPDF hace esa traducción en THotPDF.DrawRegisteredColorGlyph. Informa a qué formato resolvió un glifo como THPDFOpenTypeColorFormat, con los valores otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG y otcfSBIX, y sondea la fuente en un orden fijo: COLR, luego SVG, luego CBDT, luego sbix. Una fuente que lleva tanto vectores como bitmaps recibe pues la ruta vectorial. Para cualquier cosa que se vaya a ampliar o imprimir, ese es el orden correcto
Resolver y dibujar un único code point
THotPDF.GetRegisteredColorGlyphInfo te dice qué camino tomará un code point; DrawRegisteredColorGlyph lo recorre. Ambas buscan el code point en el character map de la fuente pasada más recientemente a RegisterUnicodeTTF, así que la fuente emoji tiene que ser la fuente Unicode registrada en el momento de la llamada. La función de dibujo devuelve False cuando el glifo no tiene datos de color o nada pudo renderizarlo, y la página se queda intacta en ese caso
const
FormatNames: array[THPDFOpenTypeColorFormat] of string =
('none', 'COLR v0', 'COLR v1', 'CBDT', 'SVG', 'sbix');
var
Pdf: THotPDF;
Info: THPDFOpenTypeColorGlyphInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'emoji.pdf';
// La entrada de paleta 0xFFFF ("text foreground") se resuelve a este color
Pdf.ColorGlyphForegroundColor := clNavy;
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\seguiemj.ttf');
// U+1F600, paleta CPAL 0, bitmap strike más cercano a 300 ppem
if Pdf.GetRegisteredColorGlyphInfo($1F600, 0, 300, Info) then
Writeln(Format('GID %d via %s',
[Info.GlyphID, FormatNames[Info.Format]]));
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F600,
72, 144, 'Segoe UI Emoji', 36, 0, 300) then
begin
// Sin ruta de color: cae al contorno monocromo
Pdf.CurrentPage.SetFont('Segoe UI Emoji', [], 36, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 144, 0, WideString(#$D83D#$DE00));
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
PaletteIndex elige una paleta CPAL, así que una fuente que trae una paleta para fondo oscuro se puede cambiar sin tocar el glifo. La entrada de paleta 0xFFFF es especial en la especificación OpenType: significa «el color que tenga el texto», y HotPDF la rellena desde ColorGlyphForegroundColor y ColorGlyphForegroundAlpha (negro y 255 por defecto). Las fuentes emoji rara vez usan esa entrada. Las fuentes de iconos sí, y si la tuya tiñe glifos a través de ella, pon la propiedad a tu color de texto antes de dibujar
TargetPixelsPerEm importa solo para fuentes bitmap, y su valor por defecto es el parámetro que más vale la pena sobrescribir. Dejado a cero se convierte en Round(FontSize * 96 / 72), una resolución de pantalla: 48 píxeles para un emoji de 36 puntos. Para una fuente que declara strikes sbix a 20, 32, 40, 48, 64, 96 y 160 ppem, ese valor por defecto elige el strike de 48 píxeles, y una impresora de 600 dpi tiene entonces que agrandarlo unas seis veces. Pide 300, que es lo que 36 puntos necesita a 600 dpi, y la misma llamada toma el strike de 160, el mayor que tiene la fuente
La firma también fija el alcance. Entra un solo code point y se mapea a través de cmap. Las secuencias de familia con ZWJ, los modificadores de tono de piel y las banderas de indicadores regionales son ligaduras GSUB, así que hay que darles forma hasta un glifo primero, el tipo de trabajo cubierto en el artículo de alternates GSUB de OpenType
Capas COLR y grafos de paint como vectores PDF
COLR v0 es el formato simple. Cada glifo base lista una pila de glifos de capa, cada uno con un color CPAL, y HotPDF rellena el contorno de cada capa en orden de tabla. Una capa cuyo color CPAL tiene alfa por debajo de 255 recibe su propio /ca, así que las capas translúcidas se mezclan con las capas de debajo exactamente como pretendía el diseñador de la fuente. En un documento ordinario toda la pila se captura después en un grupo de transparencia aislado (ISO 32000-1 §11.4). El grupo va de lo que pasa fuera del glifo: una opacidad, un modo de fusión o una máscara suave que pongas alrededor del emoji se aplica una vez al glifo terminado, en lugar de repetirse en cada capa y duplicarse donde las capas se solapan
COLR v1 es otra cosa: un grafo acíclico dirigido de registros de paint con degradados, transformaciones, recortes de glifo anidados y modos compuestos, donde los nodos pueden compartirse a través de PaintColrLayers y PaintColrGlyph. HotPDF lo analiza primero en un grafo acotado, con tope de 4096 nodos de paint, 96 niveles de profundidad y 1024 paradas de color, y sigue cada nodo mientras se visita, de modo que una referencia de vuelta al camino actual (un ciclo que una fuente hostil puede construir con reutilización de capas) se rechaza en lugar de recursarse en ella
Los offsets son donde los parsers de COLR v1 escritos a mano suelen romper. Los offsets de BaseGlyphPaintRecord son relativos al inicio de BaseGlyphList, los offsets de paint de LayerList a LayerList, y cada Offset24 dentro de una tabla de paint a esa tabla de paint misma. Resolver los tres contra una sola base hace que glifos perfectamente legales fallen sus comprobaciones de límites, lo que se ve exactamente como una fuente corrupta. Una vez construido el grafo, el mapeo a PDF es en su mayor parte directo:
PaintGlyphconvierte el contorno del glifo en una ruta de recorte, y su paint hijo rellena el área recortada- Los degradados lineales se convierten en shadings axiales multi-stop y los radiales en shadings radiales (§8.7.4.5)
- Los degradados sweep no tienen tipo de shading PDF, así que se dibujan como 96 cuñas de color plano alrededor del centro, cada una muestreada de la línea de color
- Las transformaciones se convierten en operadores
cm, con las traslaciones escaladas porFontSize / UnitsPerEm - Los modos 13 a 27 de
PaintCompositese mapean a los modos de fusión PDF del mismo nombre,/Multiply,/Screen,/Luminosityy el resto (§11.3.5), a través de una entrada/BMde ExtGState
Los degradados con paradas translúcidas esconden una trampa más. COLR v1 interpola los colores premultiplicados por alfa, mientras que PDF espera color recto y aplica el alfa por su cuenta, así que un convertidor que le entrega los valores premultiplicados a PDF oscurece dos veces cada banda semitransparente. Los colores tienen que des-premultiplicarse después de la interpolación y antes de escribirse
¿Qué pasa con los modos compuestos COLR v1 bajo PDF/A-1 o PDF/X-1a?
Los modos Porter-Duff de PaintComposite (5 a 12: src_in, dest_out, src_atop, xor, plus y sus hermanos) no tienen ningún modo de fusión PDF con el mismo nombre, y la lectura obvia de la especificación PDF es que no se pueden expresar. Esa lectura es demasiado fuerte. Los modos in/out son una máscara suave de alfa sobre un operando; atop, xor y plus pueden ensamblarse a partir de formas aisladas, alfa de grupo, máscaras de luminosidad y aritmética de fusión. HotPDF los construye así, así que esos glifos siguen siendo vectoriales
El coste es que la mayoría de esas construcciones necesita transparencia PDF, y PDF/A-1, PDF/X-1a y PDF/X-3 la prohíben. Cuando PDFACompliance es un valor de PDF/A-1 o PDFXCompliance es 'X-1a' o 'X-3', HotPDF comprueba el glifo antes de dibujarlo y encamina los casos que no se pueden dibujar sin transparencia al evento raster: compuestos atop, xor y plus, compuestos in/out cuyo operando enmascarado es más que un paint plano opaco, paints sólidos con alfa parcial, degradados con paradas translúcidas, y capas COLR v0 con alfa por debajo de 255. Los modos in y out siguen siendo vectoriales cuando el lado enmascarado es un color sólido o un degradado plano, opcionalmente envuelto en un recorte de glifo o una transformación, porque ese caso solo necesita una máscara de grises opaca. Lo mismo aplica a COLR v0 cuando StrictVersionLock fija el archivo por debajo de PDF 1.4
Pdf.PDFACompliance := 'B'; // PDF/A-1b: no se permite transparencia
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF(EmojiFontPath);
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F308,
72, 200, EmojiFontName, 36, 0, 300) then
// Sin un manejador OnColorGlyphRasterize, un glifo translúcido
// o compuesto acaba aquí bajo PDF/A-1
LogMissingColorGlyph($1F308); // tu propia rutina de logging
Si el archivo tiene que mostrar emoji a color, apunta a PDF/A-2b en lugar de PDF/A-1b. La parte 2 permite transparencia, todos los constructos COLR siguen siendo vectoriales, y el manejador de fallback pasa a ser una red de seguridad en lugar del renderizador principal de cada emoji translúcido. Guarda PDF/A-1 para los flujos que lo exigen por su nombre
Un ajuste del lado lector también pertenece aquí. HotPDF compone los grupos COLR v1 en un espacio de fusión de luz lineal; algunos renderizadores pierden canales muy oscuros en tales grupos y otros ignoran por completo los espacios de fusión de grupo, así que COLRv1DeviceSpaceGroupBlend (por defecto False) deja que los grupos se compongan en el espacio de dispositivo ordinario. La disyuntiva está explicada en el artículo de degradados repeat y reflect de COLR v1; actívalo si los tintes translúcidos oscuros desaparecen en el visor que tus usuarios usan de verdad
Glifos SVG y strikes bitmap
Los glifos SVG pasan por el mismo builder seguro que HotPDF usa para los archivos SVG importados, y el resultado se registra como un Form XObject (§8.10), como se describe en el artículo de SVG a Form XObject. El documento de la tabla SVG puede venir comprimido con gzip. La entrada comprimida tiene un tope de 8 MB, la descompresión corre en trozos de 8 KB, y se detiene tan pronto como el tamaño expandido pasaría de 32 MB. Scripts, foreignObject, URLs javascript: y hojas de estilo @import hacen que el builder rechace el documento sin más. El formulario se escala de modo que su lado mayor iguale el tamaño de fuente. Una cosa que probar antes de producción: algunas fuentes empacan muchos glifos en un solo documento SVG compartido, y el builder recibe ese documento entero por cada glifo
Las fuentes bitmap se reducen a elección de strike y colocación. Para CBDT, HotPDF elige el tamaño CBLC cuyo ppem vertical está más cerca de TargetPixelsPerEm, acepta los formatos de imagen 17, 18 y 19, y lee las métricas del formato 19 desde la subtabla de índice CBLC porque ese formato no guarda ninguna propia. Para sbix gana el strike más cercano que realmente contiene el glifo; los offsets de strike son relativos a la tabla y los offsets de glifo al strike. Un registro dupe reutiliza la imagen de otro glifo pero conserva sus propios offsets de origen, y un parser que deja que la búsqueda sobrescriba el origen exterior desplaza la imagen lateralmente. Los payloads PNG y JPEG se descodifican internamente, se escalan por FontSize / PixelsPerEmY en lugar de estirarse al tamaño de fuente, y se escriben con una máscara suave (§11.6.5.3) siempre que algún píxel sea menos que totalmente opaco. Los payloads TIFF de sbix van al evento
Escribir el manejador del fallback rasterizado
Cuando ninguna ruta nativa funciona, HotPDF lanza OnColorGlyphRasterize y coloca el bitmap RGBA que devuelva tu manejador. Sin manejador, o con Handled en False, DrawRegisteredColorGlyph devuelve False y no se dibuja nada. El manejador recibe el formato, los bytes de la fuente, el asset extraído (el documento SVG, posiblemente aún gzipeado, o los bytes del bitmap; vacío para COLR), el ID del glifo, la paleta y el tamaño de píxel objetivo
type
TEmojiFallback = class
public
procedure Rasterize(Sender: TObject;
Format: THPDFOpenTypeColorFormat; const FontBytes: TBytes;
const AssetData: TBytes; GlyphID: Word;
PaletteIndex, PixelSize: Integer;
out Width, Height: Integer; out RGBA: TBytes;
out Handled: Boolean);
end;
procedure TEmojiFallback.Rasterize(Sender: TObject;
Format: THPDFOpenTypeColorFormat; const FontBytes: TBytes;
const AssetData: TBytes; GlyphID: Word;
PaletteIndex, PixelSize: Integer;
out Width, Height: Integer; out RGBA: TBytes;
out Handled: Boolean);
begin
Width := 0;
Height := 0;
RGBA := nil;
// RenderWithOwnEngine es tu rasterizador, no una API de HotPDF.
// Debe devolver exactamente Width * Height * 4 bytes de RGBA
// con alfa recto (sin premultiplicar)
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Cableado
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF valida el resultado antes de que la página lo vea: tamaños cero, un buffer cuya longitud no es exactamente Width * Height * 4, o dimensiones lo bastante grandes como para desbordar se rechazan, y la llamada devuelve False. Para una fuente de color variable el manejador recibe la instancia estática a los valores de ejes registrados; si alguna vez recibe la fuente variable original en su lugar, llamar a GetRegisteredVariableFontAxes sobre Sender (convertido a THotPDF) devuelve las coordenadas a las que renderizar. PixelSize es el mismo valor de TargetPixelsPerEm, así que el consejo de resolución de impresión de arriba aplica aquí también
Un pipeline que imprime texto de usuario arbitrario necesita saber de dos clases de pérdida: glifos que la fuente nunca tuvo, y glifos que perdieron su color. El seguimiento de cobertura en el momento de dibujar del artículo de seguimiento de glifos ausentes informa del primero; un False de DrawRegisteredColorGlyph informa del segundo
El renderizado de emoji COLR, SVG y bitmap, el shaping OpenType y el builder seguro de SVG forman todos parte del componente HotPDF para Delphi y C++Builder