Artículo técnico

Emoji a color en PDF: COLR v1, SVG y bitmaps en Delphi

HotPDF dibuja emoji a color en un PDF mediante THotPDF.DrawRegisteredColorGlyph, que lee los datos de color de una fuente registrada con RegisterUnicodeTTF y los emite como gráficos PDF nativos: las capas COLR v0 como contornos de glifo rellenos, los grafos de paint de COLR v1 como clips, shadings y blend modes, los glifos SVG como Form XObjects, y los bitmaps CBDT o sbix como imágenes. Lo que no puede mapear de forma nativa va al evento OnColorGlyphRasterize en lugar de convertirse silenciosamente en una forma negra

Esa última cláusula es toda la razón de que exista este código. Incruste una fuente emoji de la manera corriente y el visor recibe el contorno de glyf o CFF, relleno con el color de relleno que esté vigente. La carita sonriente llega como una mancha negra, la bandera como un rectángulo, y nada en el pipeline se queja

¿Por qué un emoji a color sale como una silueta negra en PDF?

Un programa de fuente PDF no conoce la noción de glifos a color. ISO 32000-1 trata un glifo como una forma pintada con el color vigente, y las tablas de color que OpenType agregó después, o sea COLR/CPAL, SVG , CBDT/CBLC y sbix, no son parte del modelo de imagen de PDF, así que ningún visor está obligado a leerlas de una fuente incrustada. El color tiene que traducirse al contenido de la página en el momento de generar, mientras el productor todavía tiene los bytes de la fuente y sabe qué glifo quiere. Esa traducción difiere por formato, y las fuentes emoji que hay allá afuera usan todas: vectores en capas, grafos de paint con gradientes, documentos SVG incrustados y strikes PNG. HotPDF reporta el resultado como THPDFOpenTypeColorFormat, con los valores otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG y otcfSBIX, y sondea la fuente con una prioridad fija: primero COLR, luego SVG, luego CBDT, luego sbix. Los datos vectoriales le ganan a los bitmaps siempre que la fuente traiga ambos, que es justo lo que usted quiere en un documento que puede ampliarse o imprimirse

Diagrama de sondeo de glifos a color de HotPDF: un programa de fuente PDF pinta contornos de glifo con el color vigente, así que las tablas de color de OpenType COLR, SVG, CBDT y sbix deben traducirse al contenido de la página al generar, y HotPDF sondea una fuente registrada con la prioridad fija COLR, luego SVG, luego CBDT, luego sbix, reportando THPDFOpenTypeColorFormat desde otcfCOLRv0 hasta otcfSBIX
Los datos vectoriales le ganan a los bitmaps siempre que la fuente traiga ambos, que es lo que usted quiere en un documento que puede ampliarse o imprimirse, y un glifo sin camino a color queda en manos de su fallback

Una llamada, cinco formatos: resolver y dibujar un glifo a color

THotPDF.GetRegisteredColorGlyphInfo responde qué camino tomará un code point, y DrawRegisteredColorGlyph lo toma. Ambos buscan el code point en el mapa de caracteres de la fuente pasada más recientemente a RegisterUnicodeTTF, así que la fuente a color 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 ningún camino pudo renderizarlo, y le deja el fallback a usted

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';
    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 datos de 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;

Dos parámetros merecen atención. PaletteIndex selecciona una paleta CPAL, así que una fuente que traiga una paleta para fondo oscuro puede cambiarse sin tocar el glifo. TargetPixelsPerEm solo importa en fuentes bitmap; dejado en cero hace default a Round(FontSize * 96 / 72), una resolución de pantalla, razón por la que el ejemplo pide 300 para salida de impresión. El límite honesto está en la firma: la llamada toma un code point y lo mapea solo vía cmap. Las secuencias ZWJ, los modificadores de tono de piel y las banderas de indicadores regionales son ligaduras GSUB, así que componerlas es un problema de shaping del tipo que cubre el artículo de alternates GSUB de OpenType, no algo que este punto de entrada haga por usted

COLR v0: capas de glifo apiladas con colores de paleta

COLR v0 es el caso simple y HotPDF lo renderiza directamente: cada glifo base lista glifos de capa con una entrada de color CPAL, y cada capa se vuelve una operación común de text-showing con su propio color de relleno, apilada en el orden de la tabla. Una capa con alfa menor que 255 recibe un diccionario de parámetros de estado gráfico con /ca y /CA correspondientes (ISO 32000-1 §8.4.5), y cada glifo de capa se marca como usado para que el subsetter conserve su contorno aunque ningún code point lo mapee directamente. Un detalle sorprende a la gente: el índice de entrada de paleta 0xFFFF significa “use el color de primer plano del texto” en la especificación OpenType, y HotPDF lo resuelve a negro en lugar del color de relleno vigente de la página. Para fuentes emoji esto rara vez importa; para fuentes de íconos que dependen de la entrada de primer plano para teñir un glifo, revise la salida antes de asumir que seguirá el color de su texto

¿Cómo convierte HotPDF un grafo de paint COLR v1 en operadores PDF?

Parseando las tablas de paint primero en un grafo plano y acotado, y solo entonces mapeando cada nodo a una construcción PDF. Un glifo COLR v1 no es una lista de capas sino un grafo acíclico dirigido de registros de paint, donde los nodos pueden compartirse vía PaintColrLayers y PaintColrGlyph. El parser lo acota a 4096 nodos de paint, 64 niveles de profundidad y 1024 color stops, y sigue cada nodo como activo o terminado, de modo que una referencia a un nodo activo, un ciclo que una fuente maliciosa puede armar reutilizando capas, se rechaza en vez de recursar dentro. Las bases de los offsets son donde una primera implementación se equivoca. Los offsets de BaseGlyphPaintRecord son relativos al inicio de BaseGlyphList, los offsets de paint de LayerList son relativos a LayerList, y cada Offset24 dentro de una tabla de paint es relativo a esa misma tabla. Resuelva los tres contra la misma base y glifos perfectamente legales fallan el chequeo de límites, cosa que se ve exactamente como una fuente corrupta. Una vez armado el grafo, el mapeo es directo:

  • PaintGlyph fija el contorno del glifo como clip con text rendering mode 7 (ISO 32000-1 §9.3.6), y luego pinta a su hijo dentro
  • Los paints sólidos rellenan un rectángulo recortado; los gradientes lineales se vuelven shadings axiales multi-stop y los radiales, shadings radiales de dos colores (§8.7.4.5)
  • Los sweep gradients no tienen equivalente en PDF, así que HotPDF los aproxima con 96 gajos de color plano, cada uno muestreado de la línea de color
  • Las transformaciones se emiten como cm, conjugadas alrededor del origen de la baseline del glifo, con traslaciones escaladas por FontSize / UnitsPerEm
  • Los modos 13 a 27 de PaintComposite se mapean a los blend modes PDF separables y no separables como /Multiply, /Screen y /Luminosity (§11.3.5), fijados mediante una entrada /BM de ExtGState

El límite es explícito. Los modos Porter-Duff 5 a 12 (src_in, xor, plus y el resto) no tienen contraparte de blend mode en PDF, los extend modes repeat y reflect en gradientes lineales y radiales no se emiten, y los gradientes cuyos stops llevan valores de alfa distintos no se falsifican con una única opacidad. Los gradientes radiales con más de dos stops conservan solo su primer y último color. HotPDF chequea el grafo completo contra este subconjunto soportado antes de escribir un solo operador, así que un glifo no soportado deja la página intacta y pasa al fallback raster en lugar de dejar medio dibujo tirado

Diagrama de conversión COLR v1 de HotPDF: el grafo de paint se parsea en un grafo acotado con tope de 4096 nodos, 64 niveles de profundidad y 1024 color stops con rechazo de ciclos, luego PaintGlyph se vuelve un clip de modo 7, los gradientes lineales y radiales se vuelven shadings axiales y radiales, los sweep gradients se vuelven 96 gajos, y los modos 13 a 27 de PaintComposite se vuelven blend modes PDF
El grafo completo se chequea contra el subconjunto soportado antes de escribir el primer operador, así que un glifo no soportado deja la página intacta y pasa al fallback raster en lugar de dejar medio dibujo

Glifos SVG y strikes de bitmap

Los glifos SVG pasan por el mismo builder acotado que HotPDF usa para archivos SVG importados, y el resultado se registra como Form XObject (§8.10), exactamente como describe el artículo de SVG a Form XObject. El documento en la tabla SVG puede venir comprimido con gzip; la descompresión corre en bloques de 8 KB y se detiene apenas el tamaño expandido pasaría de 32 MB, en lugar de inflar primero y chequear después, y la entrada comprimida misma tiene un tope de 8 MB. El perfil es restrictivo a propósito: scripts, imágenes incrustadas, URLs externas, URIs data: y referencias no locales fallan cerrado. La forma se escala de modo que su lado mayor iguale el tamaño de la fuente y se ancla en la baseline, lo que mapea el sistema de coordenadas SVG con y hacia abajo sobre el de PDF con y hacia arriba. Tenga presente que el builder recibe el documento SVG completo del glifo, sin selección del elemento glyphNNN, así que las fuentes que empaquetan muchos glifos en un documento compartido merecen una prueba antes de que usted dependa de ellas

Las fuentes bitmap son una cuestión de elección de strike y de 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, los offsets de strike son relativos a la tabla y los offsets de glifo al strike, y un registro dupe reutiliza el gráfico de otro glifo conservando sus propios offsets de origen; dejar que la recursión pise el origen externo desplaza la imagen. 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 soft mask (§11.6.5.3) siempre que algún píxel no sea totalmente opaco. Los payloads TIFF de sbix no se decodifican y van al evento

¿Qué pasa cuando un glifo no se puede dibujar de forma nativa?

HotPDF dispara OnColorGlyphRasterize y coloca el bitmap RGBA que su handler devuelva; si no hay nada asignado, o el handler deja Handled en falso, DrawRegisteredColorGlyph devuelve False y la página queda igual. El evento se dispara para un grafo COLR v1 fuera del subconjunto soportado, un documento SVG que el builder seguro rechazó, y un payload bitmap que los decodificadores internos no leen. El handler recibe el formato, los bytes crudos de la fuente, el asset extraído (el documento SVG, posiblemente aún gzipeado, o los bytes del bitmap; vacío para COLR v1), el ID del glifo, la paleta y el tamaño de píxel objetivo

Diagrama de fallback raster de HotPDF: OnColorGlyphRasterize se dispara ante un grafo COLR v1 fuera del subconjunto soportado, un documento SVG que el builder seguro rechazó o un payload bitmap que los decodificadores no leen, pasando el formato, los bytes de la fuente, el asset, GlyphID, PaletteIndex y PixelSize, y el buffer RGBA devuelto se acepta solo cuando su longitud es exactamente Width por Height por 4
Tamaños en cero, una longitud de buffer equivocada o dimensiones que desbordan se rechazan antes de tocar la página, y sin handler o con Handled en falso la llamada devuelve False y la página queda igual
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.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Conexión
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;

HotPDF valida la salida del handler antes de tocar la página: tamaños en cero, un buffer cuya longitud no sea exactamente Width * Height * 4, o dimensiones lo bastante grandes como para desbordar se rechazan y la llamada devuelve False. Un fallback raster sigue siendo un raster, así que un emoji renderizado así pierde su nitidez vectorial; pida un PixelSize que coincida con su resolución de salida. Combine el camino a color con los chequeos de cobertura al dibujar del artículo de seguimiento de glifos faltantes y un pipeline que procese texto de usuario arbitrario puede reportar tanto glifos faltantes como glifos que perdieron su color

El renderizador de glifos a color, la pila de shaping OpenType y el builder seguro de SVG vienen todos en el HotPDF Delphi PDF component, disponible para Delphi y C++Builder