Artigo Técnico

Emoji coloridos em PDF: COLR v1, SVG e bitmaps no Delphi

O HotPDF desenha emoji coloridos num PDF por meio do THotPDF.DrawRegisteredColorGlyph, que lê os dados de cor de uma fonte registrada com RegisterUnicodeTTF e os emite como gráficos PDF nativos: camadas COLR v0 como contornos de glyph preenchidos, grafos de paint COLR v1 como clips, shadings e blend modes, glyphs SVG como Form XObjects, e bitmaps CBDT ou sbix como imagens. O que não dá para mapear nativamente vai para o evento OnColorGlyphRasterize em vez de virar silenciosamente uma silhueta preta

Essa última frase é a razão de existir deste código. Embuta uma fonte emoji do jeito comum e o viewer recebe o contorno do glyf ou do CFF, preenchido com a cor de preenchimento corrente que por acaso estiver valendo. A carinha sorridente chega como uma mancha preta, a bandeira como um retângulo, e nada no pipeline reclama

Por que um emoji colorido sai como silhueta preta no PDF?

Um programa de fonte PDF não tem noção de glyphs coloridos. A ISO 32000-1 trata um glyph como uma forma pintada com a cor corrente, e as tabelas de cor que o OpenType adicionou depois, COLR/CPAL, SVG , CBDT/CBLC e sbix, não fazem parte do imaging model do PDF, então nenhum viewer é obrigado a lê-las de uma fonte embutida. A cor tem que ser traduzida para conteúdo da página no momento da geração, enquanto o produtor ainda tem os bytes da fonte e sabe qual glyph quer. Essa tradução difere por formato, e fontes emoji na natureza usam todas: vetores em camadas, grafos de paint com gradientes, documentos SVG embutidos e strikes PNG. O HotPDF reporta o resultado como THPDFOpenTypeColorFormat, com os valores otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG e otcfSBIX, e faz a sondagem da fonte numa prioridade fixa: COLR primeiro, depois SVG, depois CBDT, depois sbix. Dados vetores vencem bitmaps sempre que a fonte carrega ambos, que é o que você quer num documento que pode ser ampliado ou impresso

Diagrama da sondagem de glyphs coloridos do HotPDF: um programa de fonte PDF pinta contornos de glyph com a cor corrente, então as tabelas de cor OpenType COLR, SVG, CBDT e sbix precisam ser traduzidas para conteúdo da página no momento da geração, e o HotPDF sonda uma fonte registrada na prioridade fixa COLR, depois SVG, depois CBDT, depois sbix, reportando THPDFOpenTypeColorFormat de otcfCOLRv0 a otcfSBIX
Dados vetores vencem bitmaps sempre que a fonte carrega ambos, que é o que você quer num documento que pode ser ampliado ou impresso, e um glyph sem caminho de cor fica por conta do seu fallback

Uma chamada, cinco formatos: resolvendo e desenhando um glyph colorido

O THotPDF.GetRegisteredColorGlyphInfo responde qual caminho um code point vai tomar, e o DrawRegisteredColorGlyph o executa. Ambos procuram o code point no character map da fonte passada mais recentemente ao RegisterUnicodeTTF, então a fonte colorida precisa ser a fonte Unicode registrada no momento da chamada. A função de desenho retorna False quando o glyph não tem dados de cor ou nenhum caminho conseguiu renderizá-lo, e deixa o fallback por sua conta

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 mais próximo de 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
      // Sem dados de cor: cai no contorno monocromático
      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;

Dois parâmetros merecem atenção. O PaletteIndex seleciona uma paleta CPAL, então uma fonte que vem com uma paleta de fundo escuro pode ser trocada sem tocar no glyph. O TargetPixelsPerEm só importa para fontes de bitmap; deixado em zero, o default é Round(FontSize * 96 / 72), uma resolução de tela, e é por isso que o exemplo pede 300 para output de impressão. O limite honesto está na assinatura: a chamada recebe um code point e o mapeia só pelo cmap. Sequências ZWJ, modificadores de tom de pele e bandeiras de regional indicator são ligaduras GSUB, então compô-las é um problema de shaping do tipo coberto no artigo sobre alternates GSUB do OpenType, não algo que este ponto de entrada faça por você

COLR v0: camadas de glyph empilhadas com cores de paleta

O COLR v0 é o caso simples e o HotPDF o renderiza direto: cada glyph base lista glyphs de camada com uma entrada de cor CPAL, e cada camada vira uma operação text-showing comum com a cor de preenchimento própria, empilhada na ordem da tabela. Uma camada com alpha abaixo de 255 ganha um dicionário de parâmetros de graphics state com /ca e /CA correspondentes (ISO 32000-1 §8.4.5), e todo glyph de camada é marcado como usado para que o subsetter mantenha o contorno dele mesmo que nenhum code point o mapeie diretamente. Um detalhe surpreende as pessoas: o índice de entrada de paleta 0xFFFF significa "use a cor de primeiro plano do texto" na especificação OpenType, e o HotPDF o resolve para preto em vez de para a cor de preenchimento corrente da página. Para fontes emoji isso raramente importa; para icon fonts que dependem da entrada de primeiro plano para tingir um glyph, confira o output antes de presumir que ele vai seguir a cor do seu texto

Como o HotPDF transforma um grafo de paint COLR v1 em operadores PDF?

Parseando as tabelas de paint primeiro num grafo plano e limitado, e só depois mapeando cada nó a um constructo PDF. Um glyph COLR v1 não é uma lista de camadas, e sim um grafo acíclico direcionado de paint records, em que nós podem ser compartilhados via PaintColrLayers e PaintColrGlyph. O parser limita a 4096 nós de paint, 64 níveis de profundidade e 1024 color stops, e acompanha cada nó como ativo ou concluído, de modo que uma referência de volta a um nó ativo — um ciclo que uma fonte maliciosa pode montar reutilizando camadas — é rejeitada em vez de recursada. As bases dos offsets são onde uma primeira implementação erra. Os offsets de BaseGlyphPaintRecord são relativos ao início de BaseGlyphList, os offsets de paint de LayerList são relativos à LayerList, e cada Offset24 dentro de uma tabela de paint é relativo à própria tabela de paint. Resolva os três contra a mesma base e glyphs perfeitamente legais falham na checagem de limites, o que parece exatamente uma fonte corrompida. Uma vez que o grafo está montado, o mapeamento é direto:

  • O PaintGlyph define o contorno do glyph como clip com text rendering mode 7 (ISO 32000-1 §9.3.6) e pinta o filho dentro dele
  • Paints sólidos preenchem um retângulo clipado; gradientes lineares viram axial shadings com múltiplos stops e gradientes radiais viram radial shadings de duas cores (§8.7.4.5)
  • Gradientes sweep não têm equivalente em PDF, então o HotPDF os aproxima com 96 fatias de cor plana, cada uma amostrada da color line
  • Transforms são emitidas como cm, conjugadas em torno da origem da baseline do glyph, com translações escaladas por FontSize / UnitsPerEm
  • Os modos 13 a 27 do PaintComposite mapeiam para os PDF blend modes separáveis e não separáveis como /Multiply, /Screen e /Luminosity (§11.3.5), definidos via uma entrada /BM de ExtGState

A fronteira é explícita. Os modos Porter-Duff 5 a 12 (src_in, xor, plus e os demais) não têm contraparte de blend mode em PDF, os extend modes repeat e reflect em gradientes lineares e radiais não são emitidos, e gradientes cujos stops carregam valores de alpha diferentes não são falsificados com uma opacidade única. Gradientes radiais com mais de dois stops mantêm só a primeira e a última cor. O HotPDF confere o grafo inteiro contra esse subconjunto suportado antes de escrever um único operador, então um glyph não suportado deixa a página intacta e segue para o fallback raster em vez de deixar meio desenho para trás

Diagrama de conversão COLR v1 do HotPDF: o grafo de paint é parseado num grafo limitado com teto de 4096 nós, 64 níveis de profundidade e 1024 color stops com rejeição de ciclos, então o PaintGlyph vira um clip de modo 7, gradientes lineares e radiais viram axial e radial shadings, gradientes sweep viram 96 fatias, e os modos 13 a 27 do PaintComposite viram PDF blend modes
O grafo inteiro é conferido contra o subconjunto suportado antes de o primeiro operador ser escrito, então um glyph não suportado deixa a página intacta e segue para o fallback raster em vez de deixar um meio desenho

Glyphs SVG e strikes de bitmap

Glyphs SVG passam pelo mesmo builder limitado que o HotPDF usa para arquivos SVG importados, e o resultado é registrado como um Form XObject (§8.10), exatamente como descrito no artigo sobre SVG para Form XObject. O documento na tabela SVG pode estar comprimido com gzip; a descompressão roda em chunks de 8 KB e para assim que o tamanho expandido passasse de 32 MB, em vez de inflar primeiro e checar depois, e o input comprimido em si tem teto de 8 MB. O perfil é restritivo de propósito: scripts, imagens embutidas, URLs externas, URIs data: e referências não locais falham fechadas. O form é escalado para que o lado mais longo dele seja igual ao tamanho da fonte e ancorado na baseline, o que mapeia o sistema de coordenadas do SVG (y para baixo) no do PDF (y para cima). Fique ciente de que o builder recebe o documento SVG inteiro do glyph, sem seleção do elemento glyphNNN, então fontes que empacotam muitos glyphs num documento compartilhado único merecem teste antes de você depender delas

Fontes de bitmap são uma questão de escolha e posicionamento de strike. Para CBDT, o HotPDF escolhe o tamanho CBLC cujo ppem vertical é o mais próximo do TargetPixelsPerEm, aceita os formatos de imagem 17, 18 e 19, e lê as métricas do formato 19 da subtable de índice do CBLC porque esse formato não guarda as dele. Para sbix, os offsets de strike são relativos à tabela e os offsets de glyph relativos ao strike, e um record dupe reutiliza o gráfico de outro glyph mantendo os offsets de origem próprios; deixar a recursão sobrescrever a origem externa desloca a imagem. Payloads PNG e JPEG são decodificados internamente, escalados por FontSize / PixelsPerEmY em vez de esticados para o tamanho da fonte, e escritos com um soft mask (§11.6.5.3) sempre que algum pixel não for totalmente opaco. Payloads TIFF de sbix não são decodificados e vão para o evento

O que acontece quando um glyph não pode ser desenhado nativamente?

O HotPDF dispara o OnColorGlyphRasterize e posiciona o bitmap RGBA que o seu handler devolver; se nada estiver atribuído, ou o handler deixar o Handled em false, o DrawRegisteredColorGlyph retorna False e a página permanece inalterada. O evento dispara para um grafo COLR v1 fora do subconjunto suportado, um documento SVG que o builder seguro recusou, e um payload de bitmap que os decoders internos não leem. O handler recebe o formato, os bytes crus da fonte, o asset extraído (o documento SVG, possivelmente ainda gzipado, ou os bytes do bitmap; vazio para COLR v1), o glyph ID, a paleta e o tamanho de pixel alvo

Diagrama do fallback raster do HotPDF: o OnColorGlyphRasterize dispara para um grafo COLR v1 fora do subconjunto suportado, um documento SVG que o builder seguro recusou ou um payload de bitmap que os decoders não leem, passando formato, bytes da fonte, asset, GlyphID, PaletteIndex e PixelSize, e o buffer RGBA devolvido só é aceito quando o comprimento dele é exatamente Width vezes Height vezes 4
Tamanhos zero, comprimento de buffer errado ou dimensões que estouram são rejeitados antes de a página ser tocada, e sem handler ou com Handled false a chamada retorna False e a página permanece inalterada
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 é o seu rasterizador, não uma API do HotPDF.
  // Ele precisa retornar exatamente Width * Height * 4 bytes de RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Ligação
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;

O HotPDF valida o output do handler antes de tocar na página: tamanhos zero, um buffer cujo comprimento não é exatamente Width * Height * 4, ou dimensões grandes o bastante para estourar são rejeitados e a chamada retorna False. Um fallback raster ainda é um raster, então um emoji renderizado assim perde a nitidez vetorial; peça um PixelSize que case com a resolução do seu output. Combine o caminho de cor com checagens de cobertura na hora do desenho do artigo sobre rastreamento de glyphs faltantes e um pipeline que lida com texto arbitrário de usuário consegue reportar tanto glyphs faltantes quanto glyphs que perderam a cor

O renderizador de glyphs coloridos, a stack de shaping OpenType e o builder SVG seguro entram todos no componente HotPDF PDF para Delphi, disponível para Delphi e C++Builder