Teknisk artikkel

Farge-emoji-fonter i PDF: COLR v1, SVG og bitmaps i Delphi

HotPDF tegner farge-emoji inn i en PDF gjennom THotPDF.DrawRegisteredColorGlyph, som leser fargedataene til en font registrert med RegisterUnicodeTTF og sender dem ut som native PDF-grafikk: COLR v0-lag som fylte glyffomrisser, COLR v1 paint-grafer som klipp, shadings og blend modes, SVG-glyffer som Form XObjects, og CBDT- eller sbix-bitmapper som bilder. Alt den ikke kan mappe nativt, går til OnColorGlyphRasterize-eventet i stedet for å stille bli til en svart form

Den siste leddsetningen er hele grunnen til at denne koden finnes. Bygg inn en emoji-font på den vanlige måten, så får visningsprogrammet omrissen fra glyf eller CFF, fylt med hva enn gjeldende fyllfarge tilfeldigvis er. Det smilende ansiktet ankommer som en svart klump, flagget som et rektangel, og ingenting i pipelinen klager

Hvorfor skrives en farge-emoji ut som en svart silhuett i PDF?

Et PDF-fontprogram har ingen forestilling om fargeglyffer. ISO 32000-1 behandler en glyff som en form malt med gjeldende farge, og fargetabellene OpenType la til senere, nemlig COLR/CPAL, SVG , CBDT/CBLC og sbix, er ikke del av PDF-bildemodellen, så ingen leser er forpliktet til å lese dem fra en innebygget font. Fargen må oversettes til sideinnhold ved genereringstid, mens produsenten fortsatt har fontbytene og vet hvilken glyff den vil ha. Den oversettelsen er forskjellig per format, og emoji-fonter der ute bruker alle: lagdelte vektorer, gradient-paint-grafer, innebygde SVG-dokumenter og PNG-strikes. HotPDF rapporterer resultatet som THPDFOpenTypeColorFormat, med verdiene otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG og otcfSBIX, og sonderer fonten i en fast prioritet: COLR først, så SVG, så CBDT, så sbix. Vektordata vinner over bitmapper når en font bærer begge, noe som er det du vil ha i et dokument som kan zoomes eller skrives ut

Diagram over HotPDF fargeglyff-sondering: et PDF-fontprogram maler glyffomrisser med gjeldende farge, så OpenType-fargetabellene COLR, SVG, CBDT og sbix må oversettes til sideinnhold ved genereringstid, og HotPDF sonderer en registrert font i fast prioritet COLR, så SVG, så CBDT, så sbix, og rapporterer THPDFOpenTypeColorFormat fra otcfCOLRv0 til otcfSBIX
Vektordata vinner over bitmapper når en font bærer begge, noe som er det du vil ha i et dokument som kan zoomes eller skrives ut, og en glyff uten fargesti overlates til din fallback

Ett kall, fem formater: å løse opp og tegne en fargeglyff

THotPDF.GetRegisteredColorGlyphInfo svarer hvilken sti et kodepunkt vil ta, og DrawRegisteredColorGlyph tar den. Begge slår kodepunktet opp i tegntabellen til fonten som sist ble sendt til RegisterUnicodeTTF, så fargefonten må være den registrerte Unicode-fonten i kallets øyeblikk. Tegnefunksjonen returnerer False når glyffen ikke har fargedata eller ingen sti kunne rendre den, og overlater fallbacken til deg

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-palett 0, bitmap strike nærmest 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
      // Ingen fargedata: fall tilbake til den monokrome omrissen
      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;

To parametre fortjener oppmerksomhet. PaletteIndex velger en CPAL-palett, så en font som leverer en mørk-bakgrunn-palett kan byttes uten å røre glyffen. TargetPixelsPerEm betyr bare noe for bitmapfonter; latt på null, får den standardverdien Round(FontSize * 96 / 72), en skjermoppløsning, noe som er grunnen til at eksemplet ber om 300 for utskrift. Den ærlige grensen sitter i signaturen: kallet tar ett kodepunkt og mapper det gjennom cmap alene. ZWJ-sekvenser, hudtonemodifikatorer og regional-indikator-flagg er GSUB-ligaturer, så å sette dem sammen er et shaping-problem av den typen som dekkes i artikkelen om OpenType GSUB-alternativer, ikke noe dette inngangspunktet gjør for deg

COLR v0: stablede glyfflag med palettfarger

COLR v0 er det enkle tilfellet, og HotPDF rendrer det direkte: hver basisglyff lister lagglyffer med en CPAL-fargeoppføring, og hvert lag blir én ordinær tekstvisende operasjon med sin egen fyllfarge, stablet i tabellrekkefølge. Et lag med alfa under 255 får en grafikktilstandsparameterordbok med matchende /ca og /CA (ISO 32000-1 §8.4.5), og hver lagglyff merkes som brukt slik at subsetteren beholder omrissen selv om intet kodepunkt mapper til den direkte. Én detalj overrasker folk: palettoppføringsindeksen 0xFFFF betyr «bruk tekstens forgrunnsfarge» i OpenType-spesifikasjonen, og HotPDF løser den til svart snarere enn til sidens gjeldende fyllfarge. For emoji-fonter betyr det sjelden noe; for ikonfonter som stoler på forgrunnsoppføringen for å tone en glyff, sjekk resultatet før du antar at det følger tekstfargen din

Hvordan gjør HotPDF en COLR v1 paint-graf om til PDF-operatorer?

Ved å parse paint-tabellene inn i en flat, avgrenset graf først, og først deretter mappe hver node til et PDF-konstruksjon. En COLR v1-glyff er ikke en liste over lag, men en rettet akyclisk graf av paint-poster, der noder kan deles gjennom PaintColrLayers og PaintColrGlyph. Parseren takserer den til 4096 paint-noder, 64 nivåer av dybde og 1024 fargestopp, og sporer hver node som aktiv eller ferdig, slik at en referanse tilbake til en aktiv node, en syklus en ondsinnet font kan bygge av laggjenbruk, avvises i stedet for å rekurseres inn i. Offset-basene er der en første implementering tar feil. BaseGlyphPaintRecord-offsets er relative til starten av BaseGlyphList, LayerList-paint-offsets er relative til LayerList, og hver Offset24 inne i en paint-tabell er relativ til den paint-tabellen selv. Løs alle tre mot samme base, og fullt lovlige glyffer feiler grensesjekken, noe som ser nøyaktig ut som en korrupt font. Når grafen er bygget, er mappingen direkte:

  • PaintGlyph setter glyffomrisset som et klipp med tekstvisningsmodus 7 (ISO 32000-1 §9.3.6), og maler så barnet sitt inni den
  • Solide malinger fyller et klipt rektangel; lineære gradienter blir multi-stop aksiale shadings og radielle gradienter blir tokfargede radielle shadings (§8.7.4.5)
  • Sweep-gradienter har ingen PDF-ekvivalent, så HotPDF tilnærmer dem med 96 flatfargede kiler, hver samplet fra fargelinjen
  • Transformasjoner sendes ut som cm, konjugert rundt glyffens grunnlinje-origo, med translasjoner skalert av FontSize / UnitsPerEm
  • PaintComposite-moduser 13 til 27 mapper til de separable og ikke-separable PDF blend modes som /Multiply, /Screen og /Luminosity (§11.3.5), satt gjennom en ExtGState /BM-oppføring

Grensen er eksplisitt. Porter-Duff-moduser 5 til 12 (src_in, xor, plus og resten) har ingen PDF blend mode-motstykke, repeat- og reflect-utvidelsesmoduser på lineære og radielle gradienter sendes ikke ut, og gradienter hvis stopp bærer forskjellige alfaverdier, fakeres ikke med én enkelt opasitet. Radielle gradienter med mer enn to stopp beholder bare sine første og siste farger. HotPDF sjekker hele grafen mot dette støttede delsettet før en eneste operator skrives, så en ustøttet glyff lar siden urørt og går videre til raster-fallbacken i stedet for å etterlate en halv tegning

Diagram over HotPDF COLR v1-konvertering: paint-grafen parses inn i en avgrenset graf taksert til 4096 noder, 64 dybdenivåer og 1024 fargestopp med syklusavvisning, så blir PaintGlyph et modus 7-klipp, lineære og radielle gradienter blir aksiale og radielle shadings, sweep-gradienter blir 96 kiler, og PaintComposite-modusene 13 til 27 blir PDF blend modes
Hele grafen sjekkes mot det støttede delsettet før den første operatoren skrives, så en ustøttet glyff lar siden urørt og går videre til raster-fallbacken i stedet for å etterlate en halv tegning

SVG-glyffer og bitmap-strikes

SVG-glyffer går gjennom den samme avgrensede byggeren HotPDF bruker for importerte SVG-filer, og resultatet registreres som et Form XObject (§8.10), nøyaktig som beskrevet i artikkelen om SVG til Form XObject. Dokumentet i SVG -tabellen kan være gzip-komprimert; dekompresjonen kjører i 8 KB-biter og stopper så snart den utvidede størrelsen ville passere 32 MB, i stedet for å blåse opp først og sjekke etterpå, og den komprimerte inputen er i seg selv taksert til 8 MB. Profilen er restriktiv med hensikt: skript, innebygde bilder, eksterne URL-er, data:-URIer og ikke-lokale referanser feiler lukket. Formen skaleres slik at dens lengste side er lik fontstørrelsen og forankret på grunnlinjen, noe som mapper det y-ned SVG-koordinatsystemet over på det y-opp PDF-en. Vær oppmerksom på at byggeren mottar hele SVG-dokumentet for glyffen, uten utvalg av glyphNNN-elementet, så fonter som pakker mange glyffer inn i ett delt dokument, er verdt å teste før du støtter deg til dem

Bitmapfonter er et spørsmål om strike-valg og plassering. For CBDT velger HotPDF CBLC-størrelsen hvis vertikale ppem er nærmest TargetPixelsPerEm, godtar bildeformatene 17, 18 og 19, og leser format 19-metrikkene fra CBLC-indeksundertabellen fordi det formatet ikke lagrer egne. For sbix er strike-offsets relative til tabellen og glyff-offsets til striken, og en dupe-post gjenbruker en annen glyffs grafikk mens den beholder sine egne origo-offsets; å la rekursjonen overskrive det ytre origo flytter bildet. PNG- og JPEG-laster dekodes internt, skaleres av FontSize / PixelsPerEmY i stedet for å strekkes til fontstørrelsen, og skrives med en soft maske (§11.6.5.3) når som helst piksel ikke er fullstendig ugjennomsiktig. sbix TIFF-laster dekodes ikke og går til eventet

Hva skjer når en glyff ikke kan tegnes nativt?

HotPDF reiser OnColorGlyphRasterize og plasserer hva enn RGBA-bitmap handleren din returnerer; hvis ingenting er tildelt, eller handleren lar Handled stå usann, returnerer DrawRegisteredColorGlyph False og siden forblir uendret. Eventet utløses for en COLR v1-graf utenfor det støttede delsettet, et SVG-dokument den trygge byggeren nektet, og en bitmap-last de interne dekoderne ikke leser. Handleren får formatet, rå fontbytes, den utpakkede ressursen (SVG-dokumentet, muligens fortsatt gzippet, eller bitmap-bytene; tom for COLR v1), glyff-ID-en, paletten og målpikselstørrelsen

Diagram over HotPDF raster-fallback: OnColorGlyphRasterize utløses for en COLR v1-graf utenfor det støttede delsettet, et SVG-dokument den trygge byggeren nektet eller en bitmap-last dekoderne ikke leser, og sender formatet, fontbytes, ressursen, GlyphID, PaletteIndex og PixelSize, og den returnerte RGBA-bufferen aksepteres bare når lengden er nøyaktig Width ganger Height ganger 4
Nullstørrelser, feil bufferlengde eller overløpende dimensjoner avvises før siden røres, og uten handler eller med Handled usann returnerer kallet False og siden forblir uendret
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 er din rasterizer, ikke et HotPDF-API.
  // Den må returnere nøyaktig Width * Height * 4 byte med RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

HotPDF validerer handlerens resultat før den rører siden: nullstørrelser, en buffer hvis lengde ikke er nøyaktig Width * Height * 4, eller dimensjoner store nok til å flyte over, avvises og kallet returnerer False. En raster-fallback er fortsatt en raster, så en emoji rendret slik mister vektorskarpheten sin; be om en PixelSize som matcher utskriftsoppløsningen din. Par fargestien med draw-time dekningssjekker fra artikkelen om sporing av manglende glyffer, og en pipeline som håndterer vilkårlig brukertekst kan rapportere både manglende glyffer og glyffer som mistet fargen sin

Fargeglyff-rendereren, OpenType shaping-stakken og den trygge SVG-byggeren leveres alle i HotPDF Delphi PDF-komponenten, tilgjengelig for Delphi og C++Builder