Artigo Técnico

Fontes emoji a cores em PDF no Delphi: COLR v1, SVG, bitmaps

O HotPDF desenha emoji a cores dentro de um PDF através do THotPDF.DrawRegisteredColorGlyph, que lê os dados de cor de uma fonte registada com RegisterUnicodeTTF e emite-os como gráficos PDF nativos: camadas COLR v0 como contornos de glifo preenchidos, grafos de paint COLR v1 como clips, shadings e blend modes, glifos SVG como Form XObjects, e bitmaps CBDT ou sbix como imagens. Tudo o que não conseguir mapear nativamente vai para o evento OnColorGlyphRasterize em vez de se transformar em silêncio numa forma preta

Essa última cláusula é toda a razão de este código existir. Incorpore uma fonte emoji da maneira vulgar e o visualizador recebe o contorno de glyf ou CFF, preenchido com a cor de preenchimento corrente que for. A cara sorridente chega como uma mancha preta, a bandeira como um retângulo, e nada no pipeline se queixa

Porque é que um emoji a cores imprime como uma silhueta preta em PDF?

Um programa de fonte PDF não tem noção de glifos a cores. A ISO 32000-1 trata um glifo como uma forma pintada com a cor corrente, e as tabelas de cor que o OpenType acrescentou mais tarde, nomeadamente COLR/CPAL, SVG , CBDT/CBLC e sbix, não fazem parte do modelo de imagem do PDF, por isso nenhum visualizador é obrigado a lê-las de uma fonte incorporada. A cor tem de ser traduzida para conteúdo de página em tempo de geração, enquanto o produtor ainda tem os bytes da fonte e sabe que glifo quer. Essa tradução difere por formato, e as fontes emoji na natureza usam todos: vetores em camadas, grafos de paint com gradientes, documentos SVG incorporados e strikes PNG. O HotPDF reporta o resultado como THPDFOpenTypeColorFormat, com os valores otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG e otcfSBIX, e sonda a fonte por uma prioridade fixa: COLR primeiro, depois SVG, depois CBDT, depois sbix. Dados vetoriais ganham a bitmaps sempre que uma fonte tem ambos, que é o que se quer num documento que pode ser ampliado ou impresso

Diagrama da sonda de glifos a cores no HotPDF: um programa de fonte PDF pinta contornos de glifos com a cor corrente, por isso as tabelas de cor OpenType COLR, SVG, CBDT e sbix têm de ser traduzidas para conteúdo de página em tempo de geração, e o HotPDF sonda uma fonte registada pela prioridade fixa COLR, depois SVG, depois CBDT, depois sbix, reportando THPDFOpenTypeColorFormat de otcfCOLRv0 a otcfSBIX
Dados vetoriais ganham a bitmaps sempre que uma fonte tem ambos, que é o que se quer num documento que pode ser ampliado ou impresso, e um glifo sem caminho de cor fica entregue ao seu fallback

Uma chamada, cinco formatos: resolver e desenhar um glifo a cores

O THotPDF.GetRegisteredColorGlyphInfo responde a que caminho um code point vai seguir, e o DrawRegisteredColorGlyph segue-a. Ambos procuram o code point no character map da fonte passada mais recentemente a RegisterUnicodeTTF, por isso a fonte a cores tem de ser a fonte Unicode registada no momento da chamada. A função de desenho devolve False quando o glifo não tem dados de cor ou nenhum caminho o conseguiu renderizar, e deixa o fallback consigo

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: recuar 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;

Dois parâmetros merecem atenção. O PaletteIndex seleciona uma paleta CPAL, por isso uma fonte que traga uma paleta para fundos escuros pode ser trocada sem tocar no glifo. O TargetPixelsPerEm só interessa para fontes de bitmap; deixado a zero assume o valor por defeito Round(FontSize * 96 / 72), uma resolução de ecrã, 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 mapeia-o só pelo cmap. Sequências ZWJ, modificadores de tom de pele e bandeiras de indicadores regionais são ligaduras GSUB, por isso compô-las é um problema de shaping do género coberto em o artigo das alternativas GSUB do OpenType, e não algo que este ponto de entrada faça por si

COLR v0: camadas de glifos empilhadas com cores de paleta

O COLR v0 é o caso simples e o HotPDF renderiza-o diretamente: cada glifo base lista glifos de camada com uma entrada de cor CPAL, e cada camada torna-se uma operação de mostrar texto vulgar com a sua própria cor de preenchimento, empilhada pela ordem da tabela. Uma camada com alfa abaixo de 255 recebe um dicionário de parâmetros de estado gráfico com /ca e /CA correspondentes (ISO 32000-1 §8.4.5), e cada glifo de camada é marcado como usado para que o subsetter guarde o seu contorno mesmo que nenhum code point lhe corresponda diretamente. Um detalhe surpreende as pessoas: o índice de entrada de paleta 0xFFFF significa «usar a cor de primeiro plano do texto» na especificação OpenType, e o HotPDF resolve-o para preto em vez de para a cor de preenchimento corrente da página. Para fontes emoji isto raramente interessa; para fontes de ícones que dependam da entrada de primeiro plano para tingir um glifo, verifique o output antes de presumir que vai seguir a cor do seu texto

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

Analisando as tabelas de paint num grafo plano e limitado primeiro, e só depois mapeando cada nó num construto PDF. Um glifo COLR v1 não é uma lista de camadas mas um grafo acíclico dirigido de registos de paint, em que nós podem ser partilhados através de PaintColrLayers e PaintColrGlyph. O parser limita-o a 4096 nós de paint, 64 níveis de profundidade e 1024 color stops, e segue cada nó como ativo ou concluído, por isso uma referência de volta a um nó ativo, um ciclo que uma fonte maliciosa pode construir com reutilização de camadas, é rejeitada em vez de ser percorrida por recursão. As bases de 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 a LayerList, e cada Offset24 dentro de uma tabela de paint é relativo à própria tabela de paint. Resolver os três contra a mesma base faz glifos perfeitamente legais falharem a verificação de limites, o que parece exatamente uma fonte corrompida. Construído o grafo, o mapeamento é direto:

  • O PaintGlyph define o contorno do glifo como clip com modo de renderização de texto 7 (ISO 32000-1 §9.3.6), e depois pinta o filho lá dentro
  • Paints sólidos preenchem um retângulo recortado; gradientes lineares tornam-se shadings axiais com múltiplos stops e gradientes radiais tornam-se shadings radiais de duas cores (§8.7.4.5)
  • Gradientes sweep não têm equivalente em PDF, por isso o HotPDF aproxima-os com 96 cunhas de cor única, cada uma amostrada da linha de cor
  • Transformações são emitidas como cm, conjugadas à volta da origem da linha de base do glifo, com translações escaladas por FontSize / UnitsPerEm
  • Modos PaintComposite 13 a 27 mapeiam para os PDF blend modes separáveis e não separáveis como /Multiply, /Screen e /Luminosity (§11.3.5), definidos através de uma entrada /BM num ExtGState

A fronteira é explícita. Os modos Porter-Duff 5 a 12 (src_in, xor, plus e os restantes) não têm contrapartida em PDF blend mode, os modos de extensão repeat e reflect em gradientes lineares e radiais não são emitidos, e gradientes cujos stops carregam valores de alfa diferentes não são fingidos com uma única opacidade. Gradientes radiais com mais de dois stops ficam-se pela primeira e última cores. O HotPDF verifica o grafo inteiro contra este subconjunto suportado antes de escrever um único operador, por isso um glifo não suportado deixa a página intacta e passa ao fallback de raster em vez de deixar meia ilustração para trás

Diagrama da conversão COLR v1 no HotPDF: o grafo de paint é analisado num grafo limitado a 4096 nós, 64 níveis de profundidade e 1024 color stops com rejeição de ciclos, depois o PaintGlyph torna-se um clip modo 7, gradientes lineares e radiais tornam-se shadings axiais e radiais, gradientes sweep tornam-se 96 cunhas, e modos PaintComposite 13 a 27 tornam-se PDF blend modes
O grafo inteiro é verificado contra o subconjunto suportado antes de o primeiro operador ser escrito, por isso um glifo não suportado deixa a página intacta e passa ao fallback de raster em vez de deixar meia ilustração

Glifos SVG e strikes de bitmap

Os glifos SVG passam pelo mesmo builder limitado que o HotPDF usa para ficheiros SVG importados, e o resultado é registado como Form XObject (§8.10), exatamente como descrito em o artigo de SVG para Form XObject. O documento na tabela SVG pode estar comprimido com gzip; a descompressão corre em blocos de 8 KB e para assim que o tamanho expandido passaria 32 MB, em vez de primeiro inflacionar e verificar depois, e o input comprimido em si está limitado a 8 MB. O perfil é restritivo de propósito: scripts, imagens incorporadas, URLs externos, URIs data: e referências não locais falham fechadas. O form é escalado para que o seu lado maior seja igual ao tamanho da fonte e ancorado na linha de base, o que mapeia o sistema de coordenadas SVG y-para-baixo no PDF y-para-cima. Atenção que o builder recebe o documento SVG inteiro do glifo, sem seleção do elemento glyphNNN, por isso fontes que empilhem muitos glifos num único documento partilhado merecem testes antes de neles confiar

Fontes de bitmap são uma questão de escolha de strike e colocação. Para CBDT, o HotPDF escolhe o tamanho CBLC cujo ppem vertical está mais próximo de TargetPixelsPerEm, aceita os formatos de imagem 17, 18 e 19, e lê as métricas do formato 19 da subtabela de índice CBLC porque esse formato não guarda nenhuma própria. Para sbix, os offsets de strike são relativos à tabela e os offsets de glifo relativos ao strike, e um registo dupe reutiliza o gráfico de outro glifo mantendo os seus próprios offsets de origem; deixar a recursão sobrescrever a origem externa desloca a imagem. Cargas PNG e JPEG são descodificadas internamente, escaladas por FontSize / PixelsPerEmY em vez de esticadas para o tamanho da fonte, e escritas com soft mask (§11.6.5.3) sempre que algum pixel não seja totalmente opaco. Cargas sbix TIFF não são descodificadas e vão para o evento

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

O HotPDF levanta OnColorGlyphRasterize e coloca o bitmap RGBA que o seu handler devolver; se nada estiver atribuído, ou o handler deixar Handled falso, o DrawRegisteredColorGlyph devolve False e a página fica como está. O evento dispara para um grafo COLR v1 fora do subconjunto suportado, um documento SVG que o builder seguro recusou, e uma carga de bitmap que os descodificadores internos não leiam. O handler recebe o formato, os bytes da fonte em bruto, o ativo extraído (o documento SVG, eventualmente ainda gzipped, ou os bytes do bitmap; vazio para COLR v1), o GlyphID, a paleta e o tamanho de pixel alvo

Diagrama do fallback de raster no HotPDF: o OnColorGlyphRasterize dispara para um grafo COLR v1 fora do subconjunto suportado, um documento SVG que o builder seguro recusou ou uma carga de bitmap que os descodificadores não leiam, passando formato, bytes da fonte, ativo, GlyphID, PaletteIndex e PixelSize, e o buffer RGBA devolvido só é aceite quando o seu comprimento é exatamente Width vezes Height vezes 4
Tamanhos zero, um comprimento de buffer errado ou dimensões com overflow são rejeitados antes de a página ser tocada, e sem handler ou com Handled falso a chamada devolve False e a página fica como está
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, e não uma API do HotPDF.
  // Tem de devolver exatamente Width * Height * 4 bytes de RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Ligações
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 seja exatamente Width * Height * 4, ou dimensões grandes ao ponto de transbordar são rejeitados e a chamada devolve False. Um fallback de raster continua a ser raster, por isso um emoji renderizado assim perde a nitidez vetorial; peça um PixelSize que corresponda à sua resolução de output. Emparelhe o caminho a cores com verificações de cobertura em tempo de desenho vindas de o artigo de rastreio de glifos em falta e um pipeline que trate texto arbitrário de utilizadores consegue reportar tanto glifos em falta como glifos que perderam a cor

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