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
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
PaintGlyphdefine 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 porFontSize / UnitsPerEm - Modos
PaintComposite13 a 27 mapeiam para os PDF blend modes separáveis e não separáveis como/Multiply,/Screene/Luminosity(§11.3.5), definidos através de uma entrada/BMnum 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
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
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