Articolo tecnico

Font emoji colorati in Delphi: COLR v1, SVG e bitmap

HotPDF disegna emoji a colori dentro un PDF attraverso THotPDF.DrawRegisteredColorGlyph, che legge i dati colore di un font registrato con RegisterUnicodeTTF e li emette come grafica PDF nativa: layer COLR v0 come contorni di glyph riempiti, grafi di paint COLR v1 come clip, shadings e blend mode, glyph SVG come Form XObject, e bitmap CBDT o sbix come immagini. Tutto ciò che non sa mappare nativamente va all'evento OnColorGlyphRasterize invece di trasformarsi in silenzio in una forma nera

Quella ultima clausola è l'intera ragione per cui questo codice esiste. Includi un font emoji nel modo ordinario e il viewer prende il contorno da glyf o CFF, riempito con qualunque sia il colore di fill corrente. La faccina arriva come chiazza nera, la bandiera come rettangolo, e nella pipeline non si lamenta nessuno

Perché un emoji a colori esce come silhouette nera in PDF?

Un font program PDF non ha nessuna nozione di color glyph. ISO 32000-1 tratta un glyph come una forma dipinta col colore corrente, e le tabelle colore che OpenType ha aggiunto dopo, cioè COLR/CPAL, SVG , CBDT/CBLC e sbix, non fanno parte del modello di imaging PDF, quindi nessun viewer è obbligato a leggerle da un font incluso. Il colore va tradotto in contenuto di pagina al momento della generazione, mentre il producer ha ancora i byte del font e sa quale glyph vuole. Quella traduzione cambia per formato, e i font emoji in giro li usano tutti: vettori a layer, grafi di paint a gradienti, documenti SVG inclusi e strike PNG. HotPDF riporta il risultato come THPDFOpenTypeColorFormat, con i valori otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG e otcfSBIX, e sonda il font in una priorità fissa: prima COLR, poi SVG, poi CBDT, poi sbix. I dati vettoriali battono le bitmap ogni volta che un font porta entrambi, che è ciò che vuoi in un documento che può essere ingrandito o stampato

Diagramma della sonda dei color glyph in HotPDF: un font program PDF dipinge i contorni dei glyph col colore corrente, quindi le tabelle colore OpenType COLR, SVG, CBDT e sbix vanno tradotte in contenuto di pagina al momento della generazione, e HotPDF sonda un font registrato nella priorità fissa COLR, poi SVG, poi CBDT, poi sbix, riportando THPDFOpenTypeColorFormat da otcfCOLRv0 a otcfSBIX
I dati vettoriali battono le bitmap ogni volta che un font porta entrambi, che è ciò che vuoi in un documento che può essere ingrandito o stampato, e un glyph senza percorso colore è lasciato al tuo fallback

Una chiamata, cinque formati: risolvere e disegnare un color glyph

THotPDF.GetRegisteredColorGlyphInfo risponde a quale percorso prenderà un code point, e DrawRegisteredColorGlyph lo prende. Entrambe cercano il code point nella character map del font passato più di recente a RegisterUnicodeTTF, quindi il font colore deve essere il font Unicode registrato al momento della chiamata. La funzione di disegno restituisce False quando il glyph non ha dati colore o nessun percorso può renderizzarlo, e lascia a te il fallback

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, palette CPAL 0, strike bitmap più vicina 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
      // Nessun dato colore: ripiega sul 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;

Due parametri meritano attenzione. PaletteIndex sceglie una palette CPAL, quindi un font che spedisce una palette per sfondo scuro può essere commutato senza toccare il glyph. TargetPixelsPerEm conta solo per i font bitmap; lasciato a zero fa default a Round(FontSize * 96 / 72), una risoluzione da schermo, ed è per questo che l'esempio chiede 300 per output di stampa. Il limite onesto sta nella firma: la chiamata prende un code point e lo mappa solo attraverso cmap. Le sequenze ZWJ, i modificatori di tono della pelle e le bandiere regional-indicator sono ligature GSUB, quindi comporle è un problema di shaping del tipo coperto in l'articolo sulle alternate OpenType GSUB, non qualcosa che questo punto d'ingresso fa per te

COLR v0: layer di glyph impilati con colori di palette

COLR v0 è il caso semplice e HotPDF lo renderizza direttamente: ogni glyph di base elenca glyph di layer con una voce colore CPAL, e ogni layer diventa una normale operazione text-showing col suo colore di fill, impilata in ordine di tabella. Un layer con alpha sotto 255 riceve un dizionario di graphics state parameter con /ca e /CA corrispondenti (ISO 32000-1 §8.4.5), e ogni glyph di layer viene marcato come usato così il subsetter ne conserva il contorno anche se nessun code point ci mappa direttamente. Un dettaglio sorprende la gente: l'indice di voce di palette 0xFFFF significa «usa il colore di primo piano del testo» nella specifica OpenType, e HotPDF lo risolve in nero anziché nel colore di fill corrente della pagina. Per i font emoji raramente conta; per i font di icone che contano sulla voce di foreground per tingere un glyph, controlla l'output prima di dare per scontato che seguirà il colore del tuo testo

Come trasforma HotPDF un grafo di paint COLR v1 in operatori PDF?

Parsificando prima le tabelle di paint in un grafo piatto e limitato, e solo dopo mappando ogni nodo a un costrutto PDF. Un glyph COLR v1 non è una lista di layer ma un grafo aciclico diretto di record di paint, dove i nodi possono essere condivisi attraverso PaintColrLayers e PaintColrGlyph. Il parser lo limita a 4096 nodi di paint, 64 livelli di profondità e 1024 color stop, e tiene traccia di ogni nodo come attivo o fatto, così un riferimento a un nodo attivo, un ciclo che un font malizioso può costruire riutilizzando i layer, viene rifiutato invece di essere attraversato in ricorsione. Le basi degli offset sono dove una prima implementazione sbaglia. Gli offset di BaseGlyphPaintRecord sono relativi all'inizio di BaseGlyphList, gli offset di paint di LayerList sono relativi a LayerList, e ogni Offset24 dentro una tabella di paint è relativo a quella tabella di paint stessa. Risolvine tre contro la stessa base e glyph perfettamente legali falliscono il controllo dei limiti, con un aspetto identico a quello di un font corrotto. Una volta costruito il grafo, la mappatura è diretta:

  • PaintGlyph imposta il contorno del glyph come clip con text rendering mode 7 (ISO 32000-1 §9.3.6), poi dipinge il suo figlio dentro
  • I paint solidi riempiono un rettangolo in clip; i gradienti lineari diventano axial shadings multi-stop e i gradienti radiali diventano radial shadings a due colori (§8.7.4.5)
  • I gradienti sweep non hanno equivalente PDF, quindi HotPDF li approssima con 96 spicchi a colore piatto, ognuno campionato dalla linea di colore
  • Le trasformazioni vengono emesse come cm, coniugate attorno all'origine della baseline del glyph, con le traslazioni scalate per FontSize / UnitsPerEm
  • Le modalità PaintComposite da 13 a 27 mappano ai PDF blend mode separabili e non separabili come /Multiply, /Screen e /Luminosity (§11.3.5), impostati attraverso una voce /BM di ExtGState

Il confine è esplicito. Le modalità Porter-Duff da 5 a 12 (src_in, xor, plus e le altre) non hanno controparte come PDF blend mode, le modalità di estensione repeat e reflect sui gradienti lineari e radiali non vengono emesse, e i gradienti i cui stop portano valori di alpha diversi non vengono fintati con una singola opacità. I gradienti radiali con più di due stop conservano solo il primo e l'ultimo colore. HotPDF controlla l'intero grafo contro questo sottoinsieme supportato prima di scrivere un solo operatore, così un glyph non supportato lascia la pagina intatta e passa al fallback raster invece di lasciarsi dietro mezzo disegno

Diagramma della conversione COLR v1 in HotPDF: il grafo di paint viene parsificato in un grafo limitato con tetto a 4096 nodi, 64 livelli di profondità e 1024 color stop con rifiuto dei cicli, poi PaintGlyph diventa una clip in modalità 7, i gradienti lineari e radiali diventano axial e radial shadings, i gradienti sweep diventano 96 spicchi, e le modalità PaintComposite da 13 a 27 diventano PDF blend mode
L'intero grafo viene controllato contro il sottoinsieme supportato prima che il primo operatore sia scritto, così un glyph non supportato lascia la pagina intatta e passa al fallback raster invece di lasciarsi dietro mezzo disegno

Glyph SVG e strike bitmap

I glyph SVG passano dallo stesso builder limitato che HotPDF usa per i file SVG importati, e il risultato viene registrato come Form XObject (§8.10), esattamente come descritto in l'articolo su SVG verso Form XObject. Il documento nella tabella SVG può essere compresso gzip; la decompressione gira a chunk da 8 KB e si ferma appena la dimensione espansa supererebbe i 32 MB, invece di inflare prima e controllare dopo, e l'input compresso in sé è limitato a 8 MB. Il profilo è restrittivo di proposito: script, immagini incluse, URL esterni, URI data: e riferimenti non locali falliscono chiudendo. La form viene scalata così che il suo lato lungo eguagli la dimensione del font e ancorata sulla baseline, il che mappa il sistema di coordinate y-in basso di SVG su quello y-in alto di PDF. Sappi che il builder riceve l'intero documento SVG del glyph, senza alcuna selezione dell'elemento glyphNNN, quindi i font che impacchettano molti glyph in un documento condiviso meritano un test prima di affidarsi a loro

I font bitmap sono una questione di scelta dello strike e di piazzamento. Per CBDT, HotPDF sceglie la dimensione CBLC il cui ppem verticale è più vicino a TargetPixelsPerEm, accetta i formati immagine 17, 18 e 19, e legge le metriche del formato 19 dalla subtable di indice CBLC perché quel formato non ne memorizza di proprie. Per sbix, gli offset degli strike sono relativi alla tabella e gli offset dei glyph allo strike, e un record dupe riusa la grafica di un altro glyph mantenendo i propri offset di origine; lasciare che la ricorsione sovrascriva l'origine esterna sposta l'immagine. I payload PNG e JPEG vengono decodificati internamente, scalati per FontSize / PixelsPerEmY anziché stirati alla dimensione del font, e scritti con una soft mask (§11.6.5.3) ogni volta che qualche pixel non è del tutto opaco. I payload TIFF di sbix non vengono decodificati e vanno all'evento

Che cosa accade quando un glyph non si può disegnare nativamente?

HotPDF alza OnColorGlyphRasterize e piazza qualunque bitmap RGBA restituisca il tuo handler; se nulla è assegnato, o l'handler lascia Handled false, DrawRegisteredColorGlyph restituisce False e la pagina resta invariata. L'evento scatta per un grafo COLR v1 fuori dal sottoinsieme supportato, un documento SVG che il builder sicuro ha rifiutato, e un payload bitmap che i decoder interni non leggono. L'handler riceve il formato, i byte grezzi del font, l'asset estratto (il documento SVG, forse ancora gzippato, o i byte della bitmap; vuoto per COLR v1), il glyph ID, la palette e la dimensione in pixel di destinazione

Diagramma del fallback raster in HotPDF: OnColorGlyphRasterize scatta per un grafo COLR v1 fuori dal sottoinsieme supportato, un documento SVG che il builder sicuro ha rifiutato o un payload bitmap che i decoder non leggono, passando formato, byte del font, asset, GlyphID, PaletteIndex e PixelSize, e il buffer RGBA restituito è accettato solo quando la sua lunghezza è esattamente Width per Height per 4
Dimensioni zero, una lunghezza di buffer sbagliata o dimensioni che vanno in overflow vengono rifiutate prima che la pagina sia toccata, e senza handler o con Handled false la chiamata restituisce False e la pagina resta invariata
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 è il tuo rasterizer, non una API HotPDF.
  // Deve restituire esattamente Width * Height * 4 byte di RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Cablaggio
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;

HotPDF valida l'output dell'handler prima di toccare la pagina: dimensioni zero, un buffer la cui lunghezza non è esattamente Width * Height * 4, o dimensioni abbastanza grandi da andare in overflow vengono rifiutate e la chiamata restituisce False. Un fallback raster resta un raster, quindi un emoji renderizzato così perde la nitidezza vettoriale; chiedi un PixelSize che combaci con la tua risoluzione di output. Accoppia il percorso colore ai controlli di copertura al momento del disegno tratti da l'articolo sul tracciamento dei glyph mancanti e una pipeline che gestisce testo utente arbitrario può riportare sia i glyph mancanti sia i glyph che hanno perso il colore

Il renderer di color glyph, lo stack di shaping OpenType e il builder SVG sicuro sono tutti nella PDF component HotPDF Delphi, disponibile per Delphi e C++Builder