Tehnički članak

Emoji u boji u PDF-u: COLR v1, SVG i bitmape u Delphiju

HotPDF crta emoji u boji u PDF kroz THotPDF.DrawRegisteredColorGlyph, koji čita podatke o bojama fonta registriranog s RegisterUnicodeTTF i ispisuje ih kao nativne PDF grafike: COLR v0 slojevi kao ispunjene konture glifova, COLR v1 paint grafovi kao clipovi, shadingovi i blend modovi, SVG glifovi kao Form XObjecti, a CBDT ili sbix bitmape kao slike. Sve što ne može nativno preslikati odlazi na event OnColorGlyphRasterize umjesto da tiho postane crni lik

Zadnja rečenica razlog je postojanja cijelog ovog koda. Ugradite emoji font običnim putem i preglednik dobije konturu iz glyf ili CFF, ispunjenu o čemu god da je trenutačna boja ispune. Nasmišljeno lice stiže kao crna mrlja, zastava kao pravokutnik, i ništa u cjevovodu ne prigovara

Zašto se emoji u boji u PDF-u ispisuje kao crna silueta?

PDF font program nema pojma o glifovima u boji. ISO 32000-1 tretira glif kao lik naslikan trenutačnom bojom, a tablice boja koje je OpenType kasnije dodao — COLR/CPAL, SVG , CBDT/CBLC i sbix — nisu dio PDF imaging modela, pa nijedan preglednik nije dužan čitati ih iz ugrađenog fonta. Boja se mora prevesti u sadržaj stranice u trenutku generiranja, dok producent još drži bajtove fonta i zna koji glif želi. Taj prijevod razlikuje se po formatu, a emoji fontovi u divljini koriste sve: slojevite vektore, gradient paint grafove, ugrađene SVG dokumente i PNG strikeove. HotPDF javlja rezultat kao THPDFOpenTypeColorFormat, s vrijednostima otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG i otcfSBIX, i sonduje font u fiksnom prioritetu: prvo COLR, pa SVG, pa CBDT, pa sbix. Vektorski podaci pobjeđuju bitmape kad font nosi oba, a to je upravo ono što želite u dokumentu koji se može zumirati ili ispisati

Dijagram sonde glifova u boji u HotPDF-u: PDF font program slika konture glifova trenutačnom bojom, pa se OpenType tablice boja COLR, SVG, CBDT i sbix moraju prevesti u sadržaj stranice u trenutku generiranja, a HotPDF sondira registrirani font u fiksnom prioritetu COLR, pa SVG, pa CBDT, pa sbix, javljajući THPDFOpenTypeColorFormat od otcfCOLRv0 do otcfSBIX
Vektorski podaci pobjeđuju bitmape kad font nosi oba, što je upravo ono što želite u dokumentu koji se može zumirati ili ispisati, a glif bez putanje u boji prepušten je vašem fallbacku

Jedan poziv, pet formata: razrješavanje i crtanje glifa u boji

THotPDF.GetRegisteredColorGlyphInfo odgovara koju će putanju code point uzeti, a DrawRegisteredColorGlyph je vodi. Oba traže code point u character mapi fonta koji je zadnji proslijeđen RegisterUnicodeTTF, pa color font mora biti registrirani Unicode font u trenutku poziva. Draw funkcija vraća False kad glif nema podataka o boji ili nijedna putanja ne može renderirati, i fallback prepušta vama

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 najbliži 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
      // Nema podataka o boji: vrati se na monokromnu konturu
      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 parametra zaslužuju pažnju. PaletteIndex bira CPAL paletu, pa font koji isporučuje paletu za tamnu podlogu može se prebaciti bez diranja glifa. TargetPixelsPerEm važan je samo za bitmap fontove; ostavljen na nuli po zadanom je Round(FontSize * 96 / 72), rezolucija zaslona, pa primjer traži 300 za ispisni izlaz. Iskrena granica sjedi u potpisu: poziv uzima jedan code point i preslikava ga kroz sam cmap. ZWJ sekvence, modifikatori tona kože i regional-indicator zastave GSUB su ligature, pa je njihovo slaganje problem oblikovanja vrste pokrivene u članku o OpenType GSUB alternativama, a ne nešto što ovaj ulazni punkt radi za vas

COLR v0: naslagani slojevi glifova s bojama palete

COLR v0 jednostavan je slučaj i HotPDF ga renderira izravno: svaki base glif nabroji slojne glifove s CPAL unosom boje, i svaki sloj postane jedna obična text-showing operacija sa svojom bojom ispune, naslagana u redoslijedu tablice. Sloj s alfa ispod 255 dobiva rječnik parametara grafičkog stanja s odgovarajućim /ca i /CA (ISO 32000-1 §8.4.5), i svaki se slojni glif označi kao korišten pa subsetter zadrži njegovu konturu iako se na njega ne preslikava nijedan code point. Jedan detalj iznenađuje: indeks unosa palete 0xFFFF znači "koristi boju prednjeg plana teksta" u OpenType specifikaciji, a HotPDF ga razriješi u crnu, a ne u trenutačnu boju ispune stranice. Za emoji fontove to rijetko ima veze; za icon fontove koji se na prednji plan oslanjaju da oboje glif, provjerite izlaz prije nego pretpostavite da će pratiti boju teksta

Kako HotPDF pretvara COLR v1 paint graf u PDF operatore?

Tako da se paint tablice najprije parsiraju u ravan, ograničen graf, i tek onda svaki čvor preslika u PDF konstrukt. COLR v1 glif nije lista slojeva nego usmjeren aciklički graf paint zapisa, u kojem se čvorovi mogu dijeliti kroz PaintColrLayers i PaintColrGlyph. Parser ga ograničava na 4096 paint čvorova, 64 razine dubine i 1024 color stopa, i vodi svaki čvor kao aktivan ili gotov, pa se referenca natrag na aktivni čvor — ciklus koji zlonamjerni font može sagraditi iz ponovne uporabe slojeva — odbija umjesto da se u nju uroni. Offset baze su mjesto gdje prva implementacija griješi. BaseGlyphPaintRecord offseti relativni su na početak BaseGlyphList, LayerList paint offseti relativni su na LayerList, a svaki Offset24 unutar paint tablice relativan je na samu tu paint tablicu. Razriješite li sva tri protiv iste baze, posve legalni glifovi padaju na provjeri granica, što izgleda točno kao pokvaren font. Jednom kad je graf sagrađen, preslikavanje je izravno:

  • PaintGlyph postavlja konturu glifa kao clip s text rendering modom 7 (ISO 32000-1 §9.3.6), pa unutra slika svoje dijete
  • Solid paintovi pune isječeni pravokutnik; linearni gradijenti postaju multi-stop aksijalni shadingovi, a radijalni gradijenti dvobojni radijalni shadingovi (§8.7.4.5)
  • Sweep gradijenti nemaju PDF ekvivalent, pa ih HotPDF aproksimira s 96 klinova jednobojno obojanih, svaki uzorkovan s boje linije
  • Transforme ispisuju se kao cm, konjugirane oko ishodišta baselinea glifa, s translacijama skaliranim s FontSize / UnitsPerEm
  • PaintComposite modovi 13 do 27 preslikavaju se na separabilne i neseparabilne PDF blend modove poput /Multiply, /Screen i /Luminosity (§11.3.5), postavljene kroz ExtGState unos /BM

Granica je izričita. Porter-Duff modovi 5 do 12 (src_in, xor, plus i ostali) nemaju PDF blend-mod parnjaka, repeat i reflect extend modovi na linearnim i radijalnim gradijentima ne ispisuju se, i gradijenti čiji stopovi nose različite alfa vrijednosti ne lažiraju se jednom opacityem. Radijalni gradijenti s više od dva stopa zadrže samo prvu i zadnju boju. HotPDF provjerava cijeli graf protiv ovog podržanog podskupa prije nego napiše jedan jedini operator, pa nepodržani glif ostavi stranicu netaknutom i prijeđe na raster fallback umjesto da iza sebe ostavi pola crteža

Dijagram konverzije COLR v1 u HotPDF-u: paint graf parsira se u ograničeni graf s plafonom od 4096 čvorova, 64 razina dubine i 1024 color stopa uz odbijanje ciklusa, pa PaintGlyph postaje mode 7 clip, linearni i radijalni gradijenti postaju aksijalni i radijalni shadingovi, sweep gradijenti postaju 96 klinova, a PaintComposite modovi 13 do 27 postaju PDF blend modovi
Cijeli se graf provjerava protiv podržanog podskupa prije nego se napiše prvi operator, pa nepodržani glif ostavlja stranicu netaknutom i prelazi na raster fallback umjesto da ostavi pola crteža

SVG glifovi i bitmap strikeovi

SVG glifovi prolaze kroz isti ograničeni builder koji HotPDF koristi za uvezene SVG datoteke, a rezultat se registrira kao Form XObject (§8.10), točno kako opisuje članak o pretvorbi SVG u Form XObject. Dokument u tablici SVG može biti gzip-komprimiran; dekompresija ide u chunkovima od 8 KB i staje čim proširena veličina prijeđe 32 MB, umjesto da prvo inflatira pa provjeri, a sam komprimirani ulaz ograničen je na 8 MB. Profil je restriktivan namjerno: skripte, ugrađene slike, vanjski URL-ovi, data: URI-jevi i nelokalne reference padaju zatvoreno. Forma se skalira tako da joj duža stranica bude jednaka veličini fonta i sidri se na baselineu, čime se y-prema-dolje SVG koordinatni sustav preslikava na y-prema-gore PDF-ov. Znajte da builder prima cijeli SVG dokument za glif, bez odabira elementa glyphNNN, pa fontovi koji pakuju mnogo glifova u jedan zajednički dokument vrijedni su testiranja prije nego se na njih oslonite

Bitmap fontovi pitanje su izbora strikea i pozicije. Za CBDT, HotPDF bira CBLC veličinu čiji je vertikalni ppem najbliži TargetPixelsPerEm, prima slikovne formate 17, 18 i 19, i za format 19 čita metrike iz CBLC index subtablice jer ih taj format ne sprema vlastite. Za sbix, strike offseti relativni su na tablicu, a offseti glifova na strike, i dupe zapis ponovno koristi grafiku drugog glifa čuvajući vlastite origin offsete; pustite li rekurziju da pregazi vanjski origin, slika se pomakne. PNG i JPEG payloadi dekodiraju se interno, skaliraju se s FontSize / PixelsPerEmY umjesto da se razastežu na veličinu fonta, i zapisuju se s soft maskom (§11.6.5.3) svaki put kad ijedan piksel nije posve neproziran. sbix TIFF payloadi ne dekodiraju se i idu na event

Što se dogodi kad se glif ne može nacrtati nativno?

HotPDF okida OnColorGlyphRasterize i smjesti ono što vaš handler vrati kao RGBA bitmapu; ako ništa nije dodijeljeno, ili handler ostavi Handled false, DrawRegisteredColorGlyph vraća False i stranica ostaje nepromijenjena. Event opali za COLR v1 graf izvan podržanog podskupa, SVG dokument koji je sigurni builder odbio, i bitmap payload koji interni dekoderi ne čitaju. Handler dobiva format, sirove bajtove fonta, izvučeni asset (SVG dokument, moguće još u gzipu, ili bitmap bajtovi; prazno za COLR v1), GlyphID, paletu i ciljanu veličinu piksela

Dijagram raster fallbacka u HotPDF-u: OnColorGlyphRasterize opali za COLR v1 graf izvan podržanog podskupa, SVG dokument koji je sigurni builder odbio ili bitmap payload koji dekoderi ne čitaju, predajući format, bajtove fonta, asset, GlyphID, PaletteIndex i PixelSize, a vraćeni RGBA buffer prima se samo kad mu je duljina točno Width puta Height puta 4
Nulte veličine, kriva duljina buffera i dimenzije koje se prelijevaju odbijaju se prije nego se stranica dira, a bez handlera ili s Handled false poziv vraća False i stranica ostaje nepromijenjena
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 vaš rasterizer, nije HotPDF API.
  // Mora vratiti točno Width * Height * 4 bajtova RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

HotPDF validira izlaz handlera prije nego dira stranicu: nulte veličine, buffer čija duljina nije točno Width * Height * 4, ili dimenzije dovoljno velike da se preliju odbijaju se i poziv vraća False. Raster fallback i dalje je raster, pa emoji ovako renderiran gubi vektorsku oštrinu; tražite PixelSize koji odgovara vašoj izlaznoj rezoluciji. Uparite li putanju u boji s provjerama pokrivenosti pri crtanju iz članka o praćenju nedostajućih glifova, cjevovod koji rukuje proizvoljnim korisničkim tekstom može javiti i nedostajuće glifove i glifove koji su izgubili boju

Renderer glifova u boji, OpenType shaping stack i sigurni SVG builder svi isporučuju se u HotPDF Delphi PDF komponenti, dostupnoj za Delphi i C++Builder