Technický článek

Color emoji fonty v PDF: COLR v1, SVG a bitmapy v Delphi

HotPDF kreslí color emoji do PDF přes THotPDF.DrawRegisteredColorGlyph, které čte color data fontu registrovaného přes RegisterUnicodeTTF a emituje je jako nativní PDF grafiku: COLR v0 vrstvy jako vyplněné obrysy glyfů, COLR v1 paint grafy jako clippy, shadings a blend módy, SVG glyfy jako Form XObjects a CBDT nebo sbix bitmapy jako obrázky. Cokoli, co nativně neumí namapovat, jde do eventu OnColorGlyphRasterize místo aby se potichu proměnilo v černou siluetu

Ta poslední věta je celý důvod, proč tenhle kód existuje. Vložte emoji font obyčejným způsobem a prohlížeč dostane obrys z glyf nebo CFF, vyplněný tím, co se zrovna nachází v aktuální barvě výplně. Usmívající obličej dorazí jako černá skvrna, vlajka jako obdélník a nic v pipeline si nestěžuje

Proč se color emoji v PDF tiskne jako černá silueta?

PDF font program nemá pojem color glyfů. ISO 32000-1 bere glyf jako tvar vybarvený aktuální barvou a color tabulky, které OpenType později přidal, totiž COLR/CPAL, SVG , CBDT/CBLC a sbix, nejsou součástí PDF imaging modelu, takže žádný prohlížeč není povinen je číst z vloženého fontu. Barvu je potřeba přeložit do obsahu stránky už v momentě generování, zatímco producent má ještě bajty fontu a ví, který glyf chce. Tenhle překlad se liší podle formátu a emoji fonty ve volné přírodě používají všechny: vrstvené vektory, gradient paint grafy, vložené SVG dokumenty a PNG strikes. HotPDF hlásí výsledek jako THPDFOpenTypeColorFormat s hodnotami otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG a otcfSBIX a sonduje font v fixní prioritě: nejdřív COLR, pak SVG, pak CBDT, pak sbix. Vektorová data vyhrávají nad bitmapami, kdykoli je font nese obojí, což je v dokumentu, který se může zoomovat nebo tisknout, přesně to, co chcete

Diagram sondy color glyfů HotPDF: PDF font program vybarvuje obrysy glyfů aktuální barvou, takže OpenType color tabulky COLR, SVG, CBDT a sbix je potřeba přeložit do obsahu stránky už při generování a HotPDF sonduje registrovaný font v fixní prioritě COLR, pak SVG, pak CBDT, pak sbix, s hlášením THPDFOpenTypeColorFormat od otcfCOLRv0 po otcfSBIX
Vektorová data vyhrávají nad bitmapami, kdykoli je font nese obojí, což je v dokumentu, který se může zoomovat nebo tisknout, přesně to, co chcete, a glyf bez color cesty se nechá na vašem fallbacku

Jedno volání, pět formátů: resolve a kreslení color glyfu

THotPDF.GetRegisteredColorGlyphInfo odpoví, kterou cestou code point půjde, a DrawRegisteredColorGlyph ji vezme. Obě hledají code point v character mapě fontu, naposledy předaného RegisterUnicodeTTF, takže color font musí být v momentě volání registrovaným Unicode fontem. Draw funkce vrátí False, když glyf nemá color data nebo žádná cesta ho nezvládne vyrenderovat, a fallback nechává na vás

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, CPAL paleta 0, bitmap strike nejblíž 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
      // Žádná color data: fallback na monochromatický obrys
      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;

Dva parametry si zaslouží pozornost. PaletteIndex vybírá CPAL paletu, takže font, který veze paletu pro tmavé pozadí, se přepne bez dotknutí glyfu. TargetPixelsPerEm hraje roli jen u bitmapových fontů; na nule defaultně nabere Round(FontSize * 96 / 72), tedy obrazovkové rozlišení, a proto příklad pro tiskový výstup žádá 300. Upřímný limit sedí v signatuře: volání bere jeden code point a mapuje ho jen přes cmap. ZWJ sekvence, skin-tone modifikátory a vlajky z regional-indicatorů jsou GSUB ligatury, takže jejich skládání je shaping problém typu popsaného v článku o OpenType GSUB alternates, ne něco, co tenhle vstupní bod udělá za vás

COLR v0: vrstvené glyfové vrstvy s palette barvami

COLR v0 je jednoduchý případ a HotPDF ho renderuje přímo: každý base glyph vyjmenovává layer glyfy s CPAL color položkou a každá vrstva se stane jednou obyčejnou text-showing operací s vlastní barvou výplně, skládanou v pořadí tabulky. Vrstva s alpha pod 255 dostane graphics state parameter slovník se souhlasnými /ca a /CA (ISO 32000-1 §8.4.5) a každý layer glyf se označí jako použitý, aby ho subsetter podržel, i když na něj žádný code point nemapuje přímo. Jeden detail lidi překvapí: index palette položky 0xFFFF znamená v OpenType specifikaci „použij barvu popředí textu“ a HotPDF ho resolveuje na černou, ne na aktuální barvu výplně stránky. U emoji fontů to málokdy hraje roli; u icon fontů, které spoléhají na položku popředí při tónování glyfu, zkontrolujte výstup, než budete předpokládat, že bude následovat barvu vašeho textu

Jak převádí HotPDF COLR v1 paint graf na PDF operátory?

Tím, že nejdřív parsuje paint tabulky do plochého, ohraničeného grafu a teprve pak mapuje každý uzel na PDF konstrukt. COLR v1 glyf není seznam vrstev, ale orientovaný acyklický graf paint záznamů, kde se uzly můžou sdílet přes PaintColrLayers a PaintColrGlyph. Parser ho zastropuje na 4096 paint uzlů, 64 úrovních hloubky a 1024 color stopech a vede si o každém uzlu, jestli je aktivní, nebo hotový, takže reference zpátky na aktivní uzel, cyklus, který si zlobivý font postaví ze sdílení vrstev, se odmítne místo rekurze do něj. Na offsetových základech se poprvé zpotí implementace. Offsety BaseGlyphPaintRecord jsou relativní ke startu BaseGlyphList, paint offsety LayerList jsou relativní k LayerList a každý Offset24 uvnitř paint tabulky je relativní k té paint tabulce samotné. Resolveujte všechny tři proti stejnému základu a úplně legální glyfy neprojdou bounds kontrolou, což vypadá přesně jako corrupt font. Jakmile je graf postavený, mapování je přímé:

  • PaintGlyph postaví obrys glyfu jako clip s text rendering mode 7 (ISO 32000-1 §9.3.6) a pak vybarví své dítě uvnitř něj
  • Solid painty vyplní oclipovaný obdélník; lineární gradienty se stanou multi-stop axial shadings a radiální gradienty dvoubarevnými radial shadings (§8.7.4.5)
  • Sweep gradienty nemají PDF ekvivalent, takže je HotPDF aproximuje 96 ploše vybarvenými klíny, každý vzorkovaný z color line
  • Transformy se emitují jako cm, konjugované kolem baseline originu glyfu, s translacemi škálovanými FontSize / UnitsPerEm
  • PaintComposite módy 13 až 27 se mapují na separabilní a neseparabilní PDF blend módy, jako /Multiply, /Screen a /Luminosity (§11.3.5), nastavené přes položku /BM v ExtGState

Hranice je explicitní. Porter-Duff módy 5 až 12 (src_in, xor, plus a zbytek) nemají PDF blend-mode protějšek, repeat a reflect extend módy na lineárních a radiálních gradientech se neemitují a gradienty, jejichž stopy nesou různé alpha hodnoty, se nepředstírají jedinou opacitou. Radiální gradienty s víc než dvěma stopy nechají jen svou první a poslední barvu. HotPDF zkontroluje celý graf proti téhle podporované podmnožině, než napíše jediný operátor, takže nepodporovaný glyf nechá stránku nedotčenou a jde dál na raster fallback, místo aby za sebou nechal půlku kresby

Diagram konverze COLR v1 v HotPDF: paint graf se parsuje do ohraničeného grafu zastropovaného na 4096 uzlů, 64 úrovní hloubky a 1024 color stopů s odmítnutím cyklů, pak PaintGlyph se stane clipem módu 7, lineární a radiální gradienty se stanou axial a radial shadings, sweep gradienty 96 klínů a PaintComposite módy 13 až 27 se stanou PDF blend módy
Celý graf se zkontroluje proti podporované podmnožině, než se zapíše první operátor, takže nepodporovaný glyf nechá stránku nedotčenou a jde dál na raster fallback, místo aby zanechal půl kresby

SVG glyfy a bitmap strikes

SVG glyfy jdou přes stejný ohraničený builder, jaký HotPDF používá pro importované SVG soubory, a výsledek se registruje jako Form XObject (§8.10), přesně jak popisuje článek o převodu SVG na Form XObject. Dokument v tabulce SVG může být gzip komprimovaný; dekomprese běží po 8 KB blocích a zastaví se, jakmile by rozbalená velikost překročila 32 MB, místo aby nejdřív nafukovala a pak kontrolovala, a samotný komprimovaný vstup je zastropován na 8 MB. Profil je záměrně restriktivní: skripty, vložené obrázky, externí URL, URI data: a nelokální reference failují zavřeně. Form se škáluje tak, aby jeho delší strana rovnala velikosti fontu, a kotví na baseline, což přemapuje y-dole SVG souřadnicový systém na y-nahoru PDF systém. Počítejte s tím, že builder dostává celý SVG dokument pro glyf, bez výběru elementu glyphNNN, takže fonty, které cpe mnoho glyfů do jednoho sdíleného dokumentu, stojí za otestování, než se na ně spolehnete

Bitmapové fonty jsou otázka volby strike a umístění. U CBDT vybere HotPDF CBLC velikost, jejíž vertikální ppem je nejblíž TargetPixelsPerEm, přijímá image formáty 17, 18 a 19 a metriky formátu 19 čte z CBLC index subtabulky, protože tenhle formát neukládá žádné vlastní. U sbix jsou strike offsety relativní k tabulce a glyfové offsety ke strike a záznam dupe znovu použije grafiku jiného glyfu, zatímco drží své vlastní origin offsety; nechat rekurzi přepsat vnější origin posune obrázek. PNG a JPEG payloady se dekódují interně, škálují se FontSize / PixelsPerEmY místo natahování na velikost fontu a zapisují se s soft mask (§11.6.5.3), kdykoli kterýkoli pixel není úplně neprůhledný. sbix TIFF payloady se nedekódují a jdou do eventu

Co se stane, když se glyf nedá kreslit nativně?

HotPDF spustí OnColorGlyphRasterize a umístí jakoukoli RGBA bitmapu, kterou váš handler vrátí; pokud není nic přiřazené, nebo handler nechá Handled false, DrawRegisteredColorGlyph vrátí False a stránka zůstane nezměněná. Event se spustí pro COLR v1 graf mimo podporovanou podmnožinu, pro SVG dokument, který safe builder odmítl, a pro bitmapový payload, který interní dekodéry nečtou. Handler dostane formát, syrové bajty fontu, extrahovaný asset (SVG dokument, možná pořád gzipped, nebo bitmapové bajty; prázdné pro COLR v1), ID glyfu, paletu a cílovou pixelovou velikost

Diagram raster fallbacku HotPDF: OnColorGlyphRasterize se spustí pro COLR v1 graf mimo podporovanou podmnožinu, pro SVG dokument, který safe builder odmítl, nebo bitmapový payload, který dekodéry nečtou, a předává formát, bajty fontu, asset, GlyphID, PaletteIndex a PixelSize, přičemž vrácený RGBA buffer se přijme jen když jeho délka je přesně Width krát Height krát 4
Nulové velikosti, špatná délka bufferu nebo přetékající rozměry se odmítnou, než se stránka dotkne, a bez handleru nebo s Handled false vrátí volání False a stránka zůstane nezměněná
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 je váš rasterizer, ne API HotPDF.
  // Musí vrátit přesně Width * Height * 4 bajtů RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

// Zapojení
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;

HotPDF validuje výstup handleru, než sáhne na stránku: nulové velikosti, buffer, jehož délka není přesně Width * Height * 4, nebo rozměry dost velké na přetečení se odmítnou a volání vrátí False. Raster fallback je pořád raster, takže emoji vyrenderované takhle ztrácí vektorovou ostrost; žádejte PixelSize odpovídající vašemu výstupnímu rozlišení. Zkombinujte color cestu s draw-time kontrolami pokrytí z článku o trackování chybějících glyfů a pipeline zvládající libovolný uživatelský text dokáže hlásit obojí, chybějící glyfy i glyfy, které ztratily barvu

Renderer color glyfů, OpenType shaping stack i safe SVG builder všechny vycházejí v HotPDF Delphi PDF component, dostupném pro Delphi a C++Builder