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. Cambie 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 contornos o ninguno. Copie 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 fuentes PDF no tiene noción de un glifo a color. ISO 32000-1 trata cada glifo como una forma pintada con el color de relleno actual, y las tablas de color que OpenType agregó después (COLR/CPAL, SVG , CBDT/CBLC y sbix) no son parte del modelo de imagen de PDF, así que ningún visor tiene razón alguna para leerlas de una fuente incrustada. El color que tenga el emoji debe convertirse en contenido de página mientras se escribe el PDF, en un punto donde el productor todavía tiene 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 las cuatro: capas vectoriales apiladas, grafos de pintura con degradados, documentos SVG completos y strikes PNG
HotPDF hace esa traducción en THotPDF.DrawRegisteredColorGlyph. Reporta a qué formato se 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 trae vectores y bitmaps toma entonces la ruta vectorial. Para cualquier cosa que vaya a ampliarse o imprimirse, ese es el orden correcto
Resolver y dibujar un solo code point
THotPDF.GetRegisteredColorGlyphInfo le dice qué ruta tomará un code point; DrawRegisteredColorGlyph la toma. Ambos buscan el code point en el mapa de caracteres de la fuente pasada más recientemente a RegisterUnicodeTTF, así que la fuente emoji tiene que ser la fuente Unicode registrada al 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 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 ("primer plano del texto") se resuelve a este color
Pdf.ColorGlyphForegroundColor := clNavy;
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\seguiemj.ttf');
// U+1F600, paleta CPAL 0, strike de bitmap 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 a color: caer 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 fondos oscuros puede cambiarse 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 suya tiñe glifos a través de ella, ponga la propiedad en su 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 anular. Dejado en cero se vuelve 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. Pida 300, que es lo que 36 puntos necesita a 600 dpi, y la misma llamada toma el strike de 160, el más grande que tiene la fuente
La firma también fija el alcance. Entra un code point y se mapea por cmap. Las secuencias de familia 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 que cubre 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 abajo exactamente como lo intentó el diseñador de la fuente. En un documento ordinario toda la pila se captura luego en un grupo de transparencia aislado (ISO 32000-1 §11.4). El grupo va sobre lo que pasa fuera del glifo: una opacidad, un modo de mezcla o una máscara suave que usted ponga alrededor del emoji se aplica una vez al glifo terminado, en lugar de repetirse en cada capa y duplicarse donde las capas se traslapan
COLR v1 es otro animal: un grafo acíclico dirigido de registros de paint con degradados, transformaciones, clips de glifo anidados y modos compuestos, donde los nodos pueden compartirse por PaintColrLayers y PaintColrGlyph. HotPDF lo analiza primero en un grafo acotado, con tope de 4096 nodos de paint, 96 niveles de profundidad y 1024 color stops, y lleva registro de cada nodo mientras se visita, de modo que una referencia de vuelta a la ruta actual (un ciclo que una fuente hostil puede armar con reutilización de capas) se rechaza en lugar de recursarse dentro
Los offsets son donde suelen romperse los parsers de COLR v1 escritos a mano. 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. Resuelva los tres contra una sola base y glifos perfectamente legales fallan sus comprobaciones de límites, cosa que se ve exactamente como una fuente corrupta. Una vez construido el grafo, el mapeo a PDF es casi directo:
PaintGlyphconvierte el contorno del glifo en una ruta de recorte, y su paint hijo rellena el área recortada- Los degradados lineales se vuelven shadings axiales multi-stop y los radiales se vuelven shadings radiales (§8.7.4.5)
- Los degradados sweep no tienen tipo de shading en PDF, así que se dibujan como 96 gajos de color plano alrededor del centro, cada uno muestreado de la línea de color
- Las transformaciones se vuelven operadores
cm, con las traslaciones escaladas porFontSize / UnitsPerEm - Los modos 13 a 27 de
PaintCompositese mapean a los modos de mezcla PDF del mismo nombre,/Multiply,/Screen,/Luminosityy el resto (§11.3.5), por una entrada/BMde ExtGState
Los degradados con stops translúcidos 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 obtiene cada banda semitransparente oscurecida dos veces. Los colores tienen que des-premultiplicarse después de la interpolación y antes de escribirse
¿Qué pasa con los modos compuestos de 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 modo de mezcla 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 se pueden armar a partir de formas aisladas, alfa de grupo, máscaras de luminosidad y aritmética de mezcla. HotPDF los construye así, así que esos glifos se quedan vectoriales
El costo 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 PDF/A-1 o PDFXCompliance es 'X-1a' o 'X-3', HotPDF comprueba el glifo antes de dibujar y enruta 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 opaco simple, paints sólidos con alfa parcial, degradados con stops translúcidos, y capas COLR v0 con alfa por debajo de 255. Los modos in y out se quedan vectoriales cuando el lado enmascarado es un color sólido o un degradado sencillo, opcionalmente envuelto en un clip 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 handler OnColorGlyphRasterize, un glifo translúcido
// o compuesto termina aquí bajo PDF/A-1
LogMissingColorGlyph($1F308); // su propia rutina de logging
Si el archivo debe mostrar emoji a color, apunte a PDF/A-2b en lugar de PDF/A-1b. La parte 2 permite transparencia, cada construcción COLR se queda vectorial, y el handler de alternativa se vuelve una red de seguridad en lugar del renderizador principal de cada emoji translúcido. Conserve PDF/A-1 para los flujos de trabajo que lo exigen por nombre
Un ajuste del lado lector también pertenece aquí. HotPDF compone los grupos COLR v1 en un espacio de mezcla de luz lineal; algunos renderizadores pierden los canales muy oscuros en esos grupos y otros ignoran por completo los espacios de mezcla de grupo, así que COLRv1DeviceSpaceGroupBlend (por defecto False) deja que los grupos se compongan en el espacio de dispositivo ordinario. La contrapartida se explica en el artículo de degradados repeat y reflect de COLR v1; actívelo si los tintes translúcidos oscuros desaparecen en el visor que sus usuarios de verdad usan
Glifos SVG y strikes de bitmap
Los glifos SVG pasan por el mismo builder seguro que HotPDF usa para archivos SVG importados, y el resultado se registra como un Form XObject (§8.10), como describe el artículo de SVG a Form XObject. El documento en la tabla SVG puede venir comprimido con gzip. La entrada comprimida tiene un tope de 8 MB, la descompresión corre en fragmentos de 8 KB, y se detiene apenas el tamaño expandido pasaría de 32 MB. Los scripts, foreignObject, URLs javascript: y hojas de estilo @import hacen que el builder rechace el documento sin más. La forma se escala de modo que su lado mayor iguale el tamaño de la fuente. Una cosa que probar antes de producción: algunas fuentes empaquetan muchos glifos en un solo documento SVG compartido, y el builder recibe ese documento completo 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 de verdad 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 pise el origen externo desplaza la imagen hacia un lado. Los payloads PNG y JPEG se decodifican internamente, se escalan por FontSize / PixelsPerEmY en lugar de estirarse al tamaño de la fuente, y se escriben con una máscara suave (§11.6.5.3) siempre que algún píxel esté menos que totalmente opaco. Los payloads TIFF de sbix van al evento
Escribir el handler de alternativa raster
Cuando ninguna ruta nativa funciona, HotPDF dispara OnColorGlyphRasterize y coloca el bitmap RGBA que su handler devuelva. Sin handler, o con Handled dejado en False, DrawRegisteredColorGlyph devuelve False y no se dibuja nada. El handler recibe el formato, los bytes de la fuente, el asset extraído (el documento SVG, posiblemente aún comprimido con gzip, 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 su rasterizador, no una API de HotPDF.
// Debe devolver exactamente Width * Height * 4 bytes de RGBA
// con alfa recto (no premultiplicado)
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Conexión
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF valida el resultado antes de que la página lo vea: tamaños en cero, un búfer cuya longitud no sea exactamente Width * Height * 4, o dimensiones lo bastante grandes como para desbordar se rechazan, y la llamada devuelve False. Para una fuente variable a color el handler recibe la instancia estática en los valores de ejes registrados; si alguna vez recibe la fuente variable original, 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 al dibujar de el artículo de seguimiento de glifos faltantes reporta lo primero; un False de DrawRegisteredColorGlyph reporta lo segundo
El renderizado de emoji COLR, SVG y bitmap, el shaping OpenType y el builder seguro de SVG son todos parte del componente HotPDF para Delphi y C++Builder