Teknisk artikel

Färgemoji i PDF: COLR v1, SVG och bitmaps i Delphi

HotPDF ritar färgemoji till en PDF genom THotPDF.DrawRegisteredColorGlyph, som läser färgdatan hos ett teckensnitt registrerat med RegisterUnicodeTTF och skickar ut den som inbyggd PDF-grafik: COLR v0-lager som fyllda glyfkonturer, COLR v1-paint-grafer som clips, shadings och blend modes, SVG-glyfer som Form XObjects, och CBDT- eller sbix-bitmaps som bilder. Allt den inte kan mappa nativt går till eventet OnColorGlyphRasterize i stället för att tyst förvandlas till en svart form

Den sista klausulen är hela skälet till att den här koden finns. Bädda in ett emoji-teckensnitt på vanligt sätt och viewern får konturen från glyf eller CFF, fylld med vilken aktuell fyllnadsfärg som råkar gälla. Det leende ansiktet anländer som en svart klicka, flaggan som en rektangel, och ingenting i pipelinen klagar

Varför skrivs en färgemoji ut som en svart silhuett i PDF?

Ett PDF-teckensnittsprogram har inget begrepp om färgglyfer. ISO 32000-1 behandlar en glyf som en form målad med aktuell färg, och de färgtabeller som OpenType lade till senare, nämligen COLR/CPAL, SVG , CBDT/CBLC och sbix, är inte del av PDF:s imaging-modell, så ingen viewer är skyldig att läsa dem från ett inbäddat teckensnitt. Färgen måste översättas till sidinnehåll vid genereringstiden, medan producenten fortfarande har teckensnittsbytena och vet vilken glyf den vill ha. Den översättningen skiljer sig per format, och emoji-teckensnitt i det vilda använder dem alla: lagerlagda vektorer, gradient-paint-grafer, inbäddade SVG-dokument och PNG-strikes. HotPDF rapporterar resultatet som THPDFOpenTypeColorFormat, med värdena otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG och otcfSBIX, och probar teckensnittet i fast prioritet: COLR först, sedan SVG, sedan CBDT, sedan sbix. Vektordata vinner över bitmaps närhelst ett teckensnitt bär båda, vilket är vad du vill ha i ett dokument som kan zoomas eller skrivas ut

HotPDF-diagram för färgglyfprobning: ett PDF-teckensnittsprogram målar glyfkonturer med aktuell färg, så OpenType-färgtabellerna COLR, SVG, CBDT och sbix måste översättas till sidinnehåll vid genereringstiden, och HotPDF probar ett registrerat teckensnitt i fast prioritet COLR, sedan SVG, sedan CBDT, sedan sbix, och rapporterar THPDFOpenTypeColorFormat från otcfCOLRv0 till otcfSBIX
Vektordata vinner över bitmaps närhelst ett teckensnitt bär båda, vilket är vad du vill ha i ett dokument som kan zoomas eller skrivas ut, och en glyf utan färgväg lämnas åt din fallback

Ett anrop, fem format: att lösa upp och rita en färgglyf

THotPDF.GetRegisteredColorGlyphInfo svarar på vilken väg en kodpunkt kommer att ta, och DrawRegisteredColorGlyph tar den. Båda slår upp kodpunkten i teckenmappningen hos det teckensnitt som senast skickades till RegisterUnicodeTTF, så färgteckensnittet måste vara det registrerade Unicode-teckensnittet vid anropstillfället. Rita-funktionen returnerar False när glyfen inte har färgdata eller ingen väg kunde rendera den, och lämnar fallbacken till dig

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ärmast 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 färgdata: fall tillbaka på den monokroma konturen
      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;

Två parametrar förtjänar uppmärksamhet. PaletteIndex väljer en CPAL-palett, så ett teckensnitt som levererar en mörkbakgrundspalett kan bytas utan att röra glyfen. TargetPixelsPerEm spelar bara roll för bitmap-teckensnitt; lämnat på noll blir det som standard Round(FontSize * 96 / 72), en skärmupplösning, vilket är varför exemplet ber om 300 för utskrift. Den ärliga begränsningen sitter i signaturen: anropet tar en kodpunkt och mappar den genom enbart cmap. ZWJ-sekvenser, hudtonmodifierare och regionala indikatorflaggor är GSUB-ligaturer, så att sätta samman dem är ett shaping-problem av det slag som täcks i artikeln om OpenType GSUB-alternativ, inte något den här ingångspunkten gör åt dig

COLR v0: staplade glyflager med palettfärger

COLR v0 är det enkla fallet och HotPDF renderar det direkt: varje basglyf listar lageryglyfer med en CPAL-färgpost, och varje lager blir en vanlig textvisningsoperation med sin egen fyllnadsfärg, staplad i tabellordning. Ett lager med alfa under 255 får en graphics state parameter dictionary med matchande /ca och /CA (ISO 32000-1 §8.4.5), och varje lageryglyf markeras som använd så att subsettern behåller dess kontur trots att ingen kodpunkt mappar till den direkt. En detalj överraskar folk: palettpostindexet 0xFFFF betyder "använd textens förgrundsfärg" i OpenType-specifikationen, och HotPDF löser upp det till svart i stället för till sidans aktuella fyllnadsfärg. För emoji-teckensnitt spelar det sällan roll; för ikonteckensnitt som förlitar sig på förgrundsposten för att tona en glyf, kontrollera utdatan innan du antar att den följer din textfärg

Hur gör HotPDF om en COLR v1-paint-graf till PDF-operatorer?

Genom att tolka paint-tabellerna till en platt, avgränsad graf först och först därefter mappa varje nod till en PDF-konstruktion. En COLR v1-glyf är inte en lista av lager utan en riktad acyklisk graf av paint-poster, där noder kan delas genom PaintColrLayers och PaintColrGlyph. Parsern takar den vid 4096 paint-noder, 64 djupnivåer och 1024 färgstopp, och spårar varje nod som aktiv eller klar så att en referens tillbaka till en aktiv nod, en cykel som en illvillig font kan bygga av lageråteranvändning, avvisas i stället för att rekurseras in i. Offsetbaserna är där en första implementation går fel. BaseGlyphPaintRecord-offsets är relativa till starten av BaseGlyphList, LayerList-paint-offsets är relativa till LayerList, och varje Offset24 inuti en paint-tabell är relativ till den paint-tabellen själv. Lös upp alla tre mot samma bas och helt lagliga glyfer faller på gränskontrollen, vilket ser ut precis som en korrupt font. När grafen väl är byggd är mappningen direkt:

  • PaintGlyph sätter glyfkonturen som ett clip med text rendering mode 7 (ISO 32000-1 §9.3.6) och målar sedan sitt barn inuti den
  • Solida paints fyller en klippt rektangel; linjära gradienter blir multi-stop axiella shadings och radiella gradienter blir tvåfärgade radiella shadings (§8.7.4.5)
  • Sweep-gradienter har ingen PDF-motsvarighet, så HotPDF approximerar dem med 96 enfärgade kilar, var och en samplad från färglinjen
  • Transformeringar skickas ut som cm, konjugerade runt glyfens baslinjeursprung, med translationer skalade med FontSize / UnitsPerEm
  • PaintComposite-lägena 13 till 27 mappas till de separerbara och icke-separerbara PDF-blend modes som /Multiply, /Screen och /Luminosity (§11.3.5), satta genom en ExtGState /BM-post

Gränsen är explicit. Porter-Duff-lägena 5 till 12 (src_in, xor, plus och resten) har ingen PDF-blend mode-motsvarighet, repeat- och reflect-utökningslägen på linjära och radiella gradienter skickas inte ut, och gradienter vars stopp bär olika alfavärden fejkas inte med en enda opacitet. Radiella gradienter med fler än två stopp behåller bara sin första och sista färg. HotPDF kontrollerar hela grafen mot den stödda delmängden innan en enda operator skrivs, så en ostödd glyf lämnar sidan orörd och går vidare till rasterfallbacken i stället för att lämna efter sig en halv ritning

HotPDF-diagram för COLR v1-konvertering: paint-grafen tolkas till en avgränsad graf takad vid 4096 noder, 64 djupnivåer och 1024 färgstopp med cykelavvisning, sedan blir PaintGlyph ett mode 7-clip, linjära och radiella gradienter blir axiella och radiella shadings, sweep-gradienter blir 96 kilar, och PaintComposite-lägena 13 till 27 blir PDF-blend modes
Hela grafen kontrolleras mot den stödda delmängden innan den första operatorn skrivs, så en ostödd glyf lämnar sidan orörd och går vidare till rasterfallbacken i stället för att lämna efter sig en halv ritning

SVG-glyfer och bitmap-strikes

SVG-glyfer går igenom samma avgränsade byggare som HotPDF använder för importerade SVG-filer, och resultatet registreras som en Form XObject (§8.10), exakt som beskrivs i artikeln SVG till Form XObject. Dokumentet i SVG -tabellen kan vara gzip-komprimerat; dekomprimeringen körs i 8 KB-chunks och stannar så snart den expanderade storleken skulle passera 32 MB, i stället för att först expandera och kontrollera efteråt, och den komprimerade indatan i sig är takad vid 8 MB. Profilen är restriktiv med flit: skript, inbäddade bilder, externa URL:er, data:-URI:er och icke-lokala referenser failar stängt. Formen skalas så att dess längsida är lika med teckensnittsstorleken och förankras på baslinjen, vilket mappar det y-ned-vända SVG-koordinatsystemet på det y-upp-vända PDF-systemet. Var medveten om att byggaren tar emot hela SVG-dokumentet för glyfen, utan något urval av glyphNNN-elementet, så teckensnitt som packar många glyfer i ett delat dokument är värda att testa innan du bygger på dem

Bitmap-teckensnitt är en fråga om strike-val och placering. För CBDT väljer HotPDF den CBLC-storlek vars vertikala ppem är närmast TargetPixelsPerEm, accepterar bildformaten 17, 18 och 19, och läser format 19-mått från CBLC-indexsubtabellen eftersom det formatet inte lagrar några egna. För sbix är strike-offsets relativa till tabellen och glyf-offsets till striken, och en dupe-post återanvänder en annan glyfs grafik medan den behåller sina egna ursprungsoffsets; att låta rekursionen skriva över det yttre ursprunget förskjuter bilden. PNG- och JPEG-payloads avkodas internt, skalas med FontSize / PixelsPerEmY i stället för att dras ut till teckensnittsstorleken, och skrivs med en soft mask (§11.6.5.3) närhelst någon pixel inte är helt opak. sbix-TIFF-payloads avkodas inte och går till eventet

Vad händer när en glyf inte kan ritas nativt?

HotPDF avfyrar OnColorGlyphRasterize och placerar vilken RGBA-bitmap din hanterare än returnerar; om inget är tilldelat, eller om hanteraren lämnar Handled falskt, returnerar DrawRegisteredColorGlyph False och sidan förblir oförändrad. Eventet avfyras för en COLR v1-graf utanför den stödda delmängden, ett SVG-dokument som den säkra byggaren vägrade, och en bitmap-payload de interna avkodarna inte läser. Hanteraren får formatet, råa fontbyte, det extraherade asset:et (SVG-dokumentet, möjligen fortfarande gzippat, eller bitmap-bytena; tomt för COLR v1), glyf-ID:t, paletten och målpixelstorleken

HotPDF-diagram för rasterfallback: OnColorGlyphRasterize avfyras för en COLR v1-graf utanför den stödda delmängden, ett SVG-dokument som den säkra byggaren vägrade eller en bitmap-payload avkodarna inte läser, och skickar med formatet, fontbyten, asset, GlyphID, PaletteIndex och PixelSize, och den returnerade RGBA-bufferten accepteras bara när dess längd är exakt Width gånger Height gånger 4
Nollstorlekar, en fel bufferlängd eller överflytande dimensioner avvisas innan sidan rörs, och utan hanterare eller med Handled falskt returnerar anropet False och sidan förblir oförändrad
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 är din rasterizer, inte ett HotPDF-API.
  // Den måste returnera exakt Width * Height * 4 byte RGBA.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

HotPDF validerar hanterarens utdata innan sidan rörs: nollstorlekar, en buffert vars längd inte är exakt Width * Height * 4, eller dimensioner stora nog att överflyta avvisas och anropet returnerar False. En rasterfallback är fortfarande en raster, så en emoji renderad på det sättet förlorar sin vektorskärpa; be om en PixelSize som matchar din utdataupplösning. Para färgvägen med draw-time-täckningskontroller från artikeln om spårning av saknade glyfer så kan en pipeline som hanterar godtycklig användartext rapportera både saknade glyfer och glyfer som tappat sin färg

Färgglyfrenderaren, OpenType-shaping-stacken och den säkra SVG-byggaren kommer alla i HotPDF Delphi PDF-komponenten, tillgänglig för Delphi och C++Builder