Artigo Técnico

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

Um emoji de cara sorridente gravado em um PDF com uma chamada de texto comum, usando Segoe UI Emoji como a fonte, sai como uma silhueta preta e plana: forma certa, sem amarelo, sem sombreamento. Troque por uma fonte de emoji CBDT e a mesma chamada pode não desenhar nada, porque essa fonte guarda as imagens dela como arquivos PNG e vem com pouco ou nenhum dado de contorno. Copie a forma preta para fora do viewer e a área de transferência ainda guarda U+1F600: o caractere está certo, apenas as tabelas de cor nunca foram lidas

Por que um emoji colorido imprime como silhueta preta em PDF?

Porque um programa de fonte PDF não tem noção de glifo colorido. A ISO 32000-1 trata todo glifo como uma forma pintada com a cor de preenchimento atual, e as tabelas de cor que o OpenType adicionou depois (COLR/CPAL, SVG , CBDT/CBLC e sbix) não fazem parte do modelo de imagem do PDF, então nenhum viewer tem motivo para lê-las de uma fonte incorporada. Qualquer cor que o emoji tenha precisa virar conteúdo de página enquanto o PDF está sendo gravado, em um ponto em que o produtor ainda segura os bytes da fonte e sabe exatamente qual glifo quer. Essa tradução é diferente para cada formato, e fontes de emoji reais usam os quatro: camadas vetoriais empilhadas, grafos de pintura com gradiente, documentos SVG inteiros e strikes de PNG

O HotPDF faz essa tradução no THotPDF.DrawRegisteredColorGlyph. Ele reporta para qual formato um glifo resolveu como THPDFOpenTypeColorFormat, com os valores otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG e otcfSBIX, e sonda a fonte em uma ordem fixa: COLR, depois SVG, depois CBDT, depois sbix. Uma fonte que carrega vetores e bitmaps portanto recebe o caminho vetorial. Para qualquer coisa que será ampliada ou impressa, essa é a ordem certa

Diagrama de sonda de glifo colorido do HotPDF: um programa de fonte PDF pinta contornos de glifo com a cor atual, então as tabelas de cor OpenType COLR, SVG, CBDT e sbix precisam ser traduzidas para conteúdo de página na geração, e o HotPDF sonda uma fonte registrada na prioridade fixa COLR, depois SVG, depois CBDT, depois sbix, reportando THPDFOpenTypeColorFormat de otcfCOLRv0 até otcfSBIX
Dados vetoriais vencem bitmaps sempre que uma fonte carrega os dois, que é o que você quer em um documento que pode ser ampliado ou impresso, e um glifo sem caminho de cor fica para o seu fallback

Resolvendo e desenhando um único code point

O THotPDF.GetRegisteredColorGlyphInfo diz qual caminho um code point vai seguir; o DrawRegisteredColorGlyph o executa. Ambos consultam o code point no mapa de caracteres da fonte passada mais recentemente ao RegisterUnicodeTTF, então a fonte de emoji precisa ser a fonte Unicode registrada no momento da chamada. A função de desenho retorna False quando o glifo não tem dados de cor ou nada conseguiu renderizá-lo, e a página fica intocada nesse 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';
    // Entrada de paleta 0xFFFF ("texto em primeiro plano") resolve para esta cor
    Pdf.ColorGlyphForegroundColor := clNavy;
    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 caminho de cor: volte para o 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;

O PaletteIndex escolhe uma paleta CPAL, então uma fonte que vem com uma paleta de fundo escuro pode ser trocada sem tocar no glifo. A entrada de paleta 0xFFFF é especial na especificação OpenType: significa "qualquer que seja a cor do texto", e o HotPDF a preenche de ColorGlyphForegroundColor e ColorGlyphForegroundAlpha (preto e 255 por padrão). Fontes de emoji raramente usam essa entrada. Fontes de ícones usam, e se a sua tinge glifos por ela, defina a propriedade para a sua cor de texto antes de desenhar

O TargetPixelsPerEm importa apenas para fontes de bitmap, e o padrão dele é o parâmetro que mais vale sobrepor. Deixado em zero ele vira Round(FontSize * 96 / 72), uma resolução de tela: 48 pixels para um emoji de 36 pontos. Para uma fonte que declara strikes sbix a 20, 32, 40, 48, 64, 96 e 160 ppem, esse padrão seleciona o strike de 48 pixels, e uma impressora de 600 dpi precisa ampliá-lo cerca de seis vezes. Peça 300, que é o que 36 pontos precisam a 600 dpi, e a mesma chamada pega o strike de 160, o maior que a fonte tem

A assinatura também fixa o escopo. Um code point entra e é mapeado pelo cmap. Sequências de família com ZWJ, modificadores de tom de pele e bandeiras de regional indicator são ligaduras GSUB, então precisam ser shapadas para um glifo primeiro, o tipo de trabalho coberto em o artigo de alternates GSUB do OpenType

Camadas COLR e grafos de pintura como vetores PDF

COLR v0 é o formato simples. Cada glifo base lista uma pilha de glifos de camada, cada um com uma cor CPAL, e o HotPDF preenche o contorno de cada camada na ordem da tabela. Uma camada cuja cor CPAL tem alpha abaixo de 255 recebe o próprio /ca dela, então camadas translúcidas se misturam com as camadas embaixo exatamente como o designer da fonte pretendia. Em um documento comum a pilha inteira é então capturada em um grupo de transparência isolado (ISO 32000-1 §11.4). O grupo é sobre o que acontece fora do glifo: uma opacidade, blend mode ou soft mask que você definir em volta do emoji é aplicada uma vez ao glifo terminado, em vez de repetida em cada camada e dobrada onde as camadas se sobrepõem

COLR v1 é outro animal: um grafo acíclico dirigido de records de pintura com gradientes, transformações, clipes de glifo aninhados e modos compostos, em que nós podem ser compartilhados via PaintColrLayers e PaintColrGlyph. O HotPDF o interpreta primeiro em um grafo limitado, com teto de 4096 nós de pintura, 96 níveis de profundidade e 1024 color stops, e acompanha cada nó enquanto é visitado para que uma referência de volta ao caminho atual (um ciclo que uma fonte hostil pode construir com reuso de camadas) seja rejeitada em vez de recursada

Offsets são onde parsers de COLR v1 escritos à mão costumam quebrar. Offsets de BaseGlyphPaintRecord são relativos ao começo de BaseGlyphList, offsets de pintura de LayerList são relativos a LayerList, e cada Offset24 dentro de uma tabela de pintura é relativo àquela tabela de pintura em si. Resolva os três contra uma única base e glifos perfeitamente legais falham nas checagens de limites, o que parece exatamente uma fonte corrompida. Uma vez que o grafo está construído, o mapeamento para PDF é na maior parte direto:

  • PaintGlyph transforma o contorno do glifo em um caminho de recorte, e a pintura filha preenche a área recortada
  • Gradientes lineares viram shadings axiais de múltiplas paradas e gradientes radiais viram shadings radiais (§8.7.4.5)
  • Gradientes sweep não têm tipo de shading PDF, então são desenhados como 96 fatias de cor plana em volta do centro, cada uma amostrada da linha de cores
  • Transformações viram operadores cm, com translações escaladas por FontSize / UnitsPerEm
  • Modos PaintComposite de 13 a 27 mapeiam para os blend modes PDF de mesmo nome, /Multiply, /Screen, /Luminosity e o resto (§11.3.5), por uma entrada /BM de ExtGState

Gradientes com paradas translúcidas escondem mais uma armadilha. COLR v1 interpola cores pré-multiplicadas por alpha, enquanto o PDF espera cor reta e aplica o alpha ele mesmo, então um conversor que entrega os valores pré-multiplicados ao PDF escurece toda faixa semitransparente duas vezes. As cores precisam ser des-pré-multiplicadas depois da interpolação e antes de serem gravadas

Diagrama de conversão COLR v1 do HotPDF: o grafo de pintura é interpretado em um grafo limitado com teto de 4096 nós, 96 níveis de profundidade e 1024 color stops com rejeição de ciclos, então PaintGlyph vira um caminho de recorte de contorno, gradientes lineares e radiais viram shadings axiais e radiais, gradientes sweep viram 96 fatias, e modos PaintComposite de 13 a 27 viram blend modes PDF
O grafo inteiro é checado contra o subconjunto suportado antes de o primeiro operador ser gravado, então um glifo não suportado deixa a página intocada e segue para o fallback raster em vez de deixar meio desenho

O que acontece com modos compostos de COLR v1 sob PDF/A-1 ou PDF/X-1a?

Os modos Porter-Duff no PaintComposite (5 a 12: src_in, dest_out, src_atop, xor, plus e os irmãos deles) não têm blend mode PDF de mesmo nome, e a leitura óbvia da especificação PDF é que eles não podem ser expressos. Essa leitura é forte demais. Modos in/out são um soft mask de alpha sobre um operando; atop, xor e plus podem ser montados de formas isoladas, alpha de grupo, máscaras de luminosidade e aritmética de blend. O HotPDF os constrói desse jeito, então esses glifos continuam vetoriais

O custo é que a maior parte dessas construções precisa de transparência PDF, e PDF/A-1, PDF/X-1a e PDF/X-3 a proíbem. Quando o PDFACompliance é um valor de PDF/A-1 ou o PDFXCompliance é 'X-1a' ou 'X-3', o HotPDF checa o glifo antes de desenhar e encaminha os casos que não podem ser desenhados sem transparência para o evento raster: compostos atop, xor e plus, compostos in/out cujo operando mascarado é mais que uma pintura opaca simples, pinturas sólidas com alpha parcial, gradientes com paradas translúcidas, e camadas COLR v0 com alpha abaixo de 255. Os modos in e out continuam vetoriais quando o lado mascarado é uma cor sólida ou gradiente simples, opcionalmente embrulhado em um clipe de glifo ou transformação, porque esse caso precisa apenas de uma máscara de tons de cinza opaca. O mesmo vale para COLR v0 quando o StrictVersionLock prende o arquivo abaixo do PDF 1.4

Pdf.PDFACompliance := 'B';   // PDF/A-1b: transparência não é permitida
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF(EmojiFontPath);

if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F308,
  72, 200, EmojiFontName, 36, 0, 300) then
  // Sem um handler OnColorGlyphRasterize, um glifo translúcido
  // ou composto termina aqui sob PDF/A-1
  LogMissingColorGlyph($1F308);   // rotina de log sua

Se o arquivo precisa mostrar emoji em cores, mire em PDF/A-2b em vez de PDF/A-1b. A Parte 2 permite transparência, todo constructo COLR continua vetorial, e o handler de fallback vira uma rede de segurança em vez do renderizador principal para cada emoji translúcido. Mantenha o PDF/A-1 para fluxos que o exigem pelo nome

Uma configuração do lado do leitor também cabe aqui. O HotPDF compõe grupos COLR v1 em um espaço de blend de luz linear; alguns renderizadores perdem canais bem escuros em tais grupos e alguns ignoram espaços de blend de grupo por inteiro, então o COLRv1DeviceSpaceGroupBlend (padrão False) deixa os grupos comporem no espaço de dispositivo comum. O trade-off é explicado em o artigo de gradientes repeat e reflect do COLR v1; ligue-o se tintas translúcidas escuras somem no viewer que seus usuários realmente rodam

Glifos SVG e strikes de bitmap

Glifos SVG passam pelo mesmo builder seguro que o HotPDF usa para arquivos SVG importados, e o resultado é registrado como um Form XObject (§8.10), como descrito em o artigo de SVG para Form XObject. O documento na tabela SVG pode estar comprimido com gzip. Entrada comprimida tem teto de 8 MB, a descompressão roda em blocos de 8 KB, e ela para assim que o tamanho expandido passaria de 32 MB. Scripts, foreignObject, URLs javascript: e stylesheets @import fazem o builder recusar o documento de plano. O form é escalado para que o lado mais longo dele iguale o tamanho da fonte. Uma coisa para tentar antes da produção: algumas fontes empacotam muitos glifos em um único documento SVG compartilhado, e o builder recebe aquele documento inteiro para cada glifo

Fontes de bitmap se resumem à escolha de strike e posicionamento. Para CBDT, o HotPDF escolhe o tamanho CBLC cujo ppem vertical está mais próximo do TargetPixelsPerEm, aceita formatos de imagem 17, 18 e 19, e lê as métricas do formato 19 da subtable de índice CBLC porque esse formato não guarda nenhuma própria. Para sbix o strike mais próximo que realmente contém o glifo vence; offsets de strike são relativos à tabela e offsets de glifo ao strike. Um record dupe reutiliza a imagem de outro glifo mas mantém os próprios offsets de origem dele, e um parser que deixa a consulta sobrescrever a origem externa desloca a imagem para o lado. Payloads PNG e JPEG são decodificados internamente, escalados por FontSize / PixelsPerEmY em vez de esticados ao tamanho da fonte, e gravados com um soft mask (§11.6.5.3) sempre que algum pixel é menos que totalmente opaco. Payloads TIFF no sbix vão para o evento

Escrevendo o handler de fallback raster

Quando nenhum caminho nativo funciona, o HotPDF levanta OnColorGlyphRasterize e posiciona o bitmap RGBA que o seu handler retornar. Sem handler, ou com o Handled deixado False, o DrawRegisteredColorGlyph retorna False e nada é desenhado. O handler recebe o formato, os bytes da fonte, o asset extraído (o documento SVG, possivelmente ainda gzipado, ou os bytes de bitmap; vazio para COLR), o ID do glifo, a paleta e o tamanho de pixel alvo

Diagrama de fallback raster do HotPDF: 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 decodificadores não leem, passando o formato, os bytes da fonte, o asset, GlyphID, PaletteIndex e PixelSize, e o buffer RGBA retornado é aceito apenas 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
  // com alpha reto (não pré-multiplicado)
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

O HotPDF valida o resultado antes de a página vê-lo: tamanhos zero, um buffer cujo comprimento não é exatamente Width * Height * 4, ou dimensões grandes o suficiente para estourar são rejeitados, e a chamada retorna False. Para uma fonte de cores variável o handler recebe a instância estática nos valores de eixos registrados; se ele algum dia receber a fonte variável original em vez disso, chamar GetRegisteredVariableFontAxes no Sender (convertido para THotPDF) retorna as coordenadas para renderizar. O PixelSize é o mesmo valor de TargetPixelsPerEm, então o conselho de resolução de impressão acima vale aqui também

Um pipeline que imprime texto arbitrário de usuários precisa saber sobre dois tipos de perda: glifos que a fonte nunca teve, e glifos que perderam a cor. O acompanhamento de cobertura em tempo de desenho de o artigo de rastreamento de glifos ausentes reporta o primeiro; um False do DrawRegisteredColorGlyph reporta o segundo

Renderização de emoji COLR, SVG e bitmap, shaping OpenType e o builder SVG seguro são todos parte do HotPDF component para Delphi e C++Builder