Technisch artikel

Kleur-emoji-fonts in PDF: COLR v1, SVG en bitmaps in Delphi

HotPDF zet kleur-emoji in een PDF via THotPDF.DrawRegisteredColorGlyph, dat de colordata leest van een font dat bij RegisterUnicodeTTF is geregistreerd en die als native PDF-grafiek uitzendt: COLR v0-lagen als gevulde glyph-outlines, COLR v1-paint-grafen als clips, shadings en blend modes, SVG-glyphs als Form XObjects, en CBDT- of sbix-bitmaps als images. Wat hij niet native kan mappen gaat naar het event OnColorGlyphRasterize in plaats van geruisloos in een zwart silhouet te veranderen

Die laatste clausule is de hele reden dat deze code bestaat. Embedt u een emoji-font op de gewone manier, dan krijgt de viewer de outline uit glyf of CFF, gevuld met wat de huidige vulkleur toevallig is. Het lachende gezicht komt binnen als zwarte klodder, de vlag als rechthoek, en nergens in de pipeline klaagt iemand

Waarom verschijnt een kleur-emoji als zwart silhouet in een PDF?

Een PDF-fontprogramma heeft geen notie van kleurglyphs. ISO 32000-1 behandelt een glyph als een vorm die met de huidige kleur wordt geschilderd, en de kleurtabellen die OpenType later toevoegde, namelijk COLR/CPAL, SVG , CBDT/CBLC en sbix, maken geen deel uit van het PDF-imagingmodel, dus geen enkele viewer is verplicht ze uit een embedded font te lezen. De kleur moet bij het genereren worden vertaald naar pagina-inhoud, op het moment dat de producent nog de fontbytes heeft en weet welke glyph hij wil. Die vertaling verschilt per formaat, en emoji-fonts in het wild gebruiken ze allemaal: gelaagde vectoren, gradient-paint-grafen, embedded SVG-documenten en PNG-strikes. HotPDF meldt het resultaat als THPDFOpenTypeColorFormat, met de waarden otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG en otcfSBIX, en sondert het font in een vaste prioriteit: eerst COLR, dan SVG, dan CBDT, dan sbix. Vectordata wint het van bitmaps zodra een font allebei heeft, en dat is wat u wilt in een document dat misschien wordt ingezoomd of geprint

Diagram van de kleurglyph-sondering in HotPDF: een PDF-fontprogramma schildert glyph-outlines met de huidige kleur, dus de OpenType-kleurtabellen COLR, SVG, CBDT en sbix moeten bij het genereren worden vertaald naar pagina-inhoud, en HotPDF sondert een geregistreerd font in de vaste prioriteit eerst COLR, dan SVG, dan CBDT, dan sbix, met als melding THPDFOpenTypeColorFormat van otcfCOLRv0 tot otcfSBIX
Vectordata wint het van bitmaps zodra een font allebei heeft, wat u wilt in een document dat misschien wordt ingezoomd of geprint, en een glyph zonder kleurpad wordt aan uw fallback overgelaten

Eén aanroep, vijf formats: een kleurglyph oplossen en tekenen

THotPDF.GetRegisteredColorGlyphInfo antwoordt welk pad een codepunt zal nemen, en DrawRegisteredColorGlyph neemt het. Beide zoeken het codepunt op in de character map van het font dat het laatst aan RegisterUnicodeTTF werd meegegeven, dus het kleurfont moet op het moment van de aanroep het geregistreerde Unicode-font zijn. De tekenfunctie geeft False terug wanneer de glyph geen colordata heeft of geen pad hem kon renderen, en laat de fallback aan u over

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-palet 0, bitmap strike het dichtst bij 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
      // Geen colordata: terugvallen op de monochrome outline
      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;

Twee parameters verdienen aandacht. PaletteIndex kiest een CPAL-palet, dus een font met een palet voor donkere achtergronden kan worden gewisseld zonder de glyph aan te raken. TargetPixelsPerEm telt alleen voor bitmapfonts; op nul gelaten valt hij terug op Round(FontSize * 96 / 72), een schermresolutie, en daarom vraagt het voorbeeld 300 voor printuitvoer. De eerlijke grens zit in de signatuur: de aanroep neemt één codepunt en mapt het uitsluitend via cmap. ZWJ-reeksen, skin-tone-modifiers en regional-indicator-vlaggen zijn GSUB-ligaturen, dus ze samenstellen is een shaping-probleem van het soort dat in het artikel over OpenType GSUB-alternates wordt behandeld, niet iets wat dit entry point voor u doet

COLR v0: gestapelde glyph-lagen met paletkleuren

COLR v0 is het eenvoudige geval en HotPDF rendert hem rechtstreeks: elke base glyph somt layer-glyphs op met een CPAL-kleurenentry, en elke laag wordt één gewone text-showing-operatie met zijn eigen vulkleur, gestapeld in tabelvolgorde. Een laag met alpha onder 255 krijgt een graphics state parameter dictionary met matchende /ca en /CA (ISO 32000-1 §8.4.5), en elke layer-glyph wordt als gebruikt gemarkeerd zodat de subsetter zijn outline houdt ook al mapt geen enkel codepunt er rechtstreeks naartoe. Eén detail verrast mensen: de palet-entry-index 0xFFFF betekent "use the text foreground color" in de OpenType-specificatie, en HotPDF lost die op naar zwart in plaats van naar de huidige vulkleur van de pagina. Voor emoji-fonts maakt dat zelden uit; bij icon fonts die op de foreground-entry leunen om een glyph te tinten, controleert u de uitvoer voordat u ervan uitgaat dat hij uw tekstkleur volgt

Hoe vertaalt HotPDF een COLR v1-paint-graaf naar PDF-operators?

Door eerst de paint-tabellen te parsen naar een vlakke, begrensde graaf en pas daarna elk knooppunt naar een PDF-constructie te mappen. Een COLR v1-glyph is geen lijst van lagen maar een gerichte acyclische graaf van paint-records, waarin knooppunten via PaintColrLayers en PaintColrGlyph gedeeld kunnen worden. De parser capteert hem op 4096 paint-knooppunten, 64 diepteniveaus en 1024 color stops, en houdt elk knooppunt bij als actief of klaar, zodat een verwijzing terug naar een actief knooppunt, een cyclus die een kwaadwillend font uit laaghergebruik kan bouwen, wordt geweigerd in plaats van in gerecursed. De offset-bases zijn waar een eerste implementatie de mist in gaat. BaseGlyphPaintRecord-offsets zijn relatief aan het begin van BaseGlyphList, paint-offsets in LayerList zijn relatief aan LayerList, en elke Offset24 binnen een paint-tabel is relatief aan die paint-tabel zelf. Los je alle drie tegen dezelfde basis op dan zakken volstrekt legale glyphs door de bounds-check, wat er precies uitziet als een corrupt font. Is de graaf eenmaal gebouwd, dan is de mapping rechttoe rechtaan:

  • PaintGlyph zet de glyph-outline als clip met text rendering mode 7 (ISO 32000-1 §9.3.6) en schildert daarna zijn kind erin
  • Solide paints vullen een afgeknipte rechthoek; lineaire gradients worden multi-stop axiale shadings en radiale gradients worden tweekleurige radiale shadings (§8.7.4.5)
  • Sweep-gradients hebben geen PDF-equivalent, dus HotPDF benadert ze met 96 egale gekleurde taarten, elk gesampled van de kleurenlijn
  • Transformaties worden als cm uitgezonden, geconjugeerd rond de glyph-baseline-oorsprong, met translaties geschaald door FontSize / UnitsPerEm
  • PaintComposite-modi 13 tot 27 mappen op de scheidbare en niet-scheidbare PDF-blend modes zoals /Multiply, /Screen en /Luminosity (§11.3.5), gezet via een ExtGState-/BM-entry

De grens staat expliciet vast. Porter-Duff-modi 5 tot 12 (src_in, xor, plus en de rest) hebben geen PDF-tegenhanger als blend mode, de extend-modes repeat en reflect bij lineaire en radiale gradients worden niet uitgezonden, en gradients waarvan de stops verschillende alpha-waarden dragen worden niet geveinsd met één enkele opacity. Radiale gradients met meer dan twee stops houden alleen hun eerste en laatste kleur over. HotPDF toetst de hele graaf aan deze ondersteunde subset voordat er ook maar één operator wordt geschreven, dus een niet-ondersteunde glyph laat de pagina onaangetast en schuift door naar de raster-fallback in plaats van een halve tekening achter te laten

Diagram van de COLR v1-conversie in HotPDF: de paint-graaf wordt geparseerd tot een begrensde graaf gecapteerd op 4096 knooppunten, 64 diepteniveaus en 1024 color stops met cyclusweigering, daarna wordt PaintGlyph een mode 7-clip, worden lineaire en radiale gradients axiale en radiale shadings, worden sweep-gradients 96 taarten en worden PaintComposite-modi 13 tot 27 PDF-blend modes
De hele graaf wordt aan de ondersteunde subset getoetst voordat de eerste operator wordt geschreven, dus een niet-ondersteunde glyph laat de pagina onaangetast en schuift door naar de raster-fallback in plaats van een halve tekening achter te laten

SVG-glyphs en bitmap-strikes

SVG-glyphs gaan door dezelfde begrensde builder die HotPDF voor geïmporteerde SVG-bestanden gebruikt, en het resultaat wordt geregistreerd als Form XObject (§8.10), precies zoals beschreven in het artikel over SVG naar Form XObject. Het document in de SVG -tabel mag gzip-gecomprimeerd zijn; decompressie verloopt in chunks van 8 KB en stopt zodra de uitgebreide grootte 32 MB zou passeren, in plaats van eerst op te blazen en daarna te controleren, en de gecomprimeerde invoer zelf is gecapteerd op 8 MB. Het profiel is met opzet restrictief: scripts, embedded images, externe URL's, data:-URI's en niet-lokale referenties falen gesloten. De form wordt geschaald zodat zijn langste zijde gelijk is aan de fontgrootte en verankerd op de baseline, wat het y-omlaag SVG-coördinatensysteem op het y-omhoog PDF-systeem mapt. Wél weet dat de builder het hele SVG-document voor de glyph ontvangt, zonder selectie van het glyphNNN-element, dus fonts die veel glyphs in één gedeeld document proppen zijn het testen waard voordat u erop vertrouwt

Bitmapfonts zijn een kwestie van strikekeuze en plaatsing. Voor CBDT kiest HotPDF de CBLC-grootte waarvan de verticale ppem het dichtst bij TargetPixelsPerEm zit, accepteert imageformaten 17, 18 en 19, en leest de metrics van formaat 19 uit de CBLC-indexsubtabel omdat dat formaat er geen eigen opslaat. Voor sbix zijn strike-offsets relatief aan de tabel en glyph-offsets aan de strike, en een dupe-record hergebruikt de graphic van een andere glyph terwijl hij zijn eigen origin-offsets houdt; laat je de recursie de buitenste origin overschrijven dan schuift de image. PNG- en JPEG-payloads worden intern gedecodeerd, geschaald met FontSize / PixelsPerEmY in plaats van uitgerekt naar de fontgrootte, en weggeschreven met een soft mask (§11.6.5.3) zodra enige pixel niet volledig dekkend is. sbix-TIFF-payloads worden niet gedecodeerd en gaan naar het event

Wat gebeurt er wanneer een glyph niet native kan worden getekend?

HotPDF vuurt OnColorGlyphRasterize af en plaatst de RGBA-bitmap die uw handler teruggeeft; is er niets toegewezen, of laat de handler Handled op false staan, dan geeft DrawRegisteredColorGlyph False terug en blijft de pagina onveranderd. Het event gaat af voor een COLR v1-graaf buiten de ondersteunde subset, een SVG-document dat de veilige builder weigerde, en een bitmap-payload die de interne decoders niet lezen. De handler krijgt het formaat, de ruwe fontbytes, de geëxtraheerde asset (het SVG-document, mogelijk nog gegzipt, of de bitmapbytes; leeg voor COLR v1), de glyph-ID, het palet en de doelpixelgrootte

Diagram van de raster-fallback in HotPDF: OnColorGlyphRasterize gaat af voor een COLR v1-graaf buiten de ondersteunde subset, een SVG-document dat de veilige builder weigerde of een bitmap-payload die de decoders niet lezen, geeft het formaat, fontbytes, asset, GlyphID, PaletteIndex en PixelSize door, en de teruggegeven RGBA-buffer wordt alleen geaccepteerd als zijn lengte exact Width keer Height keer 4 is
Nulgroottes, een verkeerde bufferlengte of overlopende dimensies worden geweigerd voordat de pagina wordt aangeraakt, en zonder handler of met Handled false geeft de aanroep False terug en blijft de pagina onveranderd
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 is uw rasterizer, geen HotPDF-API.
  // Hij moet exact Width * Height * 4 bytes RGBA teruggeven.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

HotPDF valideert de uitvoer van de handler voordat hij de pagina aanraakt: nulgroottes, een buffer waarvan de lengte niet exact Width * Height * 4 is, of dimensies groot genoeg om te overlopen worden geweigerd en de aanroep geeft False terug. Een raster-fallback blijft een raster, dus een emoji die zo wordt gerenderd verliest zijn vectorscherpte; vraag een PixelSize die bij uw uitvoerresolutie past. Koppel het kleurpad aan de draw-time-dekkingscontroles uit het artikel over het volgen van ontbrekende glyphs en een pipeline die willekeurige gebruikerstekst verwerkt kan zowel ontbrekende glyphs als glyphs die hun kleur verloren melden

De kleurglyph-renderer, de OpenType-shaping-stack en de veilige SVG-builder zitten allemaal in de HotPDF Delphi PDF component, beschikbaar voor Delphi en C++Builder