Een lachend gezicht als emoji die met een gewone tekstaanroep in een PDF wordt gezet, met Segoe UI Emoji als font, komt eruit als een effen zwart silhouet: de vorm klopt, geen geel, geen schaduwing. Wisselt u naar een CBDT-emoji-font, dan tekent dezelfde aanroep soms helemaal niets, want dat font bewaart zijn plaatjes als PNG-bestanden en levert weinig of geen outlinedata mee. Kopieert u de zwarte vorm uit de viewer, dan bevat het klembord nog steeds U+1F600: het teken klopt, alleen de kleurtabellen zijn nooit gelezen
Waarom verschijnt een kleur-emoji als zwart silhouet in een PDF?
Omdat een PDF-fontprogramma geen notie van een gekleurde glyph heeft. ISO 32000-1 behandelt elke glyph als een vorm die met de huidige vulkleur wordt geschilderd, en de kleurtabellen die OpenType later toevoegde (COLR/CPAL, SVG , CBDT/CBLC en sbix) maken geen deel uit van het PDF-imagingmodel, dus geen enkele viewer heeft ook maar een reden om ze uit een embedded font te lezen. Wat de emoji aan kleur heeft, moet worden omgezet naar pagina-inhoud terwijl de PDF wordt weggeschreven, op een moment dat de producent nog de fontbytes vasthoudt en precies weet welke glyph hij wil. Die vertaling verschilt per formaat, en echte emoji-fonts gebruiken ze alle vier: gestapelde vectorlagen, gradient-paint-grafen, complete SVG-documenten en PNG-strikes
HotPDF doet die vertaling in THotPDF.DrawRegisteredColorGlyph. Het meldt naar welk formaat een glyph is opgelost als THPDFOpenTypeColorFormat, met de waarden otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG en otcfSBIX, en het sondert het font in een vaste volgorde: eerst COLR, dan SVG, dan CBDT, dan sbix. Een font dat zowel vectoren als bitmaps meebrengt krijgt dus het vectorpad. Voor alles wat zal worden ingezoomd of geprint is dat de juiste volgorde
Eén codepunt oplossen en tekenen
THotPDF.GetRegisteredColorGlyphInfo antwoordt welk pad een codepunt zal nemen; DrawRegisteredColorGlyph neemt het. Beide zoeken het codepunt op in de character map van het font dat het laatst aan RegisterUnicodeTTF is meegegeven, dus het emoji-font moet op het moment van de aanroep het geregistreerde Unicode-font zijn. De tekenfunctie geeft False terug wanneer de glyph geen colordata heeft of niets hem kon renderen, en in dat geval blijft de pagina onaangetast
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';
// Palet-entry 0xFFFF ("tekstvoorgrond") lost op naar deze kleur
Pdf.ColorGlyphForegroundColor := clNavy;
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 kleurpad: 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;
PaletteIndex kiest een CPAL-palet, dus een font dat een palet voor donkere achtergronden meebrengt kan worden gewisseld zonder de glyph aan te raken. Palet-entry 0xFFFF is bijzonder in de OpenType-specificatie: hij betekent "wat de tekstkleur ook is", en HotPDF vult hem vanuit ColorGlyphForegroundColor en ColorGlyphForegroundAlpha (standaard zwart en 255). Emoji-fonts gebruiken die entry zelden. Icon fonts wel, en tint uw font glyphs via die entry, zet de property dan op uw tekstkleur voordat u tekent
TargetPixelsPerEm telt alleen voor bitmapfonts, en de standaardwaarde is de parameter die een eigen waarde het meest verdient. Op nul gelaten wordt hij Round(FontSize * 96 / 72), een schermresolutie: 48 pixels voor een emoji van 36 punten. Voor een font dat sbix-strikes aangeeft bij 20, 32, 40, 48, 64, 96 en 160 ppem selecteert die standaard dus de strike van 48 pixels, en een printer op 600 dpi moet die daarna zo'n zes keer vergroten. Vraagt u 300, wat 36 punten op 600 dpi nodig heeft, dan pakt dezelfde aanroep de strike van 160, de grootste die het font heeft
De signatuur fixeert ook de reikwijdte. Er gaat één codepunt in en dat wordt uitsluitend via cmap gemapt. ZWJ-familiereeksen, skin-tone-modifiers en regional-indicator-vlaggen zijn GSUB-ligaturen, dus die moeten eerst naar een glyph worden geshaped, werk van het soort dat in het artikel over OpenType GSUB-alternates wordt behandeld
COLR-lagen en paint-grafen als PDF-vectoren
COLR v0 is het eenvoudige formaat. Elke base glyph somt een stapel layer-glyphs op, elk met een CPAL-kleur, en HotPDF vult de outline van elke laag in tabelvolgorde. Een laag waarvan de CPAL-kleur een alpha onder 255 heeft, krijgt een eigen /ca, zodat transluciente lagen mengen met de lagen eronder precies zoals de fontontwerper het bedoelde. In een gewoon document wordt de hele stapel daarna opgevangen in één geïsoleerde transparantiegroep (ISO 32000-1 §11.4). Die groep gaat over wat er buiten de glyph gebeurt: een opacity, blend mode of soft mask die u om de emoji heen zet, wordt één keer toegepast op de afgewerkte glyph in plaats van op elke laag te worden herhaald en zichzelf te verdubbelen waar lagen overlappen
COLR v1 is een ander beest: een gerichte acyclische graaf van paint-records met gradients, transformaties, geneste glyph-clips en composite modes, waarin knooppunten via PaintColrLayers en PaintColrGlyph gedeeld kunnen worden. HotPDF parseert hem eerst tot een begrensde graaf, gecapteerd op 4096 paint-knooppunten, 96 diepteniveaus en 1024 color stops, en houdt elk knooppunt bij terwijl het wordt bezocht, zodat een verwijzing terug naar het huidige pad (een cyclus die een kwaadwillend font uit laaghergebruik kan bouwen) wordt geweigerd in plaats van in gerecursed
Offsets zijn waar handgeschreven COLR v1-parsers meestal stuklopen. 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 verloopt de mapping naar PDF grotendeels rechtstreeks:
PaintGlyphmaakt van de glyph-outline een clipping path en de paint van zijn kind vult het afgeknipte gebied- Lineaire gradients worden multi-stop axiale shadings en radiale gradients worden radiale shadings (§8.7.4.5)
- Sweep-gradients hebben geen PDF-shadingtype, dus ze worden getekend als 96 egale gekleurde taartpunten rond het middelpunt, elk gesampled van de kleurenlijn
- Transformaties worden
cm-operatoren, met translaties geschaald doorFontSize / UnitsPerEm PaintComposite-modi 13 tot 27 mappen op de PDF-blend modes met dezelfde naam,/Multiply,/Screen,/Luminosityen de rest (§11.3.5), via een ExtGState-/BM-entry
Gradients met transluciente stops verbergen nog een valkuil. COLR v1 interpoleert kleuren gepremultipliceerd door alpha, terwijl PDF rechte kleur verwacht en alpha zelf toepast, dus een converter die de gepremultipliceerde waarden aan PDF geeft, krijgt elke halftransparante band twee keer donkerder. Na de interpolatie en vóór het wegschrijven moeten de kleuren uit de premultiplicatie worden teruggehaald
Wat gebeurt er met COLR v1-composite modes onder PDF/A-1 of PDF/X-1a?
De Porter-Duff-modi in PaintComposite (5 tot 12: src_in, dest_out, src_atop, xor, plus en hun familie) hebben geen PDF-blend mode met dezelfde naam, en de voor de hand liggende lezing van de PDF-specificatie is dat ze niet uit te drukken zijn. Die lezing is te sterk. De in/out-modi zijn een alpha soft mask over één operand; atop, xor en plus zijn op te bouwen uit geïsoleerde formulieren, groeps-alpha, luminositeitsmaskers en blend-rekenkunde. HotPDF bouwt ze zo, dus deze glyphs blijven vector
De prijs is dat de meeste van die constructies PDF-transparantie nodig hebben, en PDF/A-1, PDF/X-1a en PDF/X-3 verbieden die. Als PDFACompliance een PDF/A-1-waarde is of PDFXCompliance 'X-1a' of 'X-3', toetst HotPDF de glyph vóór het tekenen en stuurt de gevallen die zonder transparantie niet te tekenen zijn naar het raster-event: atop-, xor- en plus-composites, in/out-composites waarvan de gemaskeerde operand meer is dan een simpele dekkende paint, solide paints met partieele alpha, gradients met transluciente stops en COLR v0-lagen met alpha onder 255. De in- en out-modi blijven vector zolang de gemaskeerde kant een gewone egale kleur of gradient is, optioneel verpakt in een glyph-clip of transformatie, want dat geval heeft alleen een dekkend grijswaardenmasker nodig. Hetzelfde geldt voor COLR v0 wanneer StrictVersionLock het bestand onder PDF 1.4 vastzet
Pdf.PDFACompliance := 'B'; // PDF/A-1b: geen transparantie toegestaan
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF(EmojiFontPath);
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F308,
72, 200, EmojiFontName, 36, 0, 300) then
// Zonder een OnColorGlyphRasterize-handler eindigt een transluciente
// of gecomposiete glyph hier onder PDF/A-1
LogMissingColorGlyph($1F308); // uw eigen logroutine
Moet het archief emoji in kleur tonen, richt u dan op PDF/A-2b in plaats van PDF/A-1b. Deel 2 staat transparantie toe, elke COLR-constructie blijft vector en de fallback-handler wordt een vangnet in plaats van de hoofdrenderer voor elke transluciente emoji. Houd PDF/A-1 voor workflows die dat bij naam voorschrijven
Eén instelling aan de lezerskant hoort hier ook thuis. HotPDF voegt COLR v1-groepen samen in een blendruimte met lineair licht; sommige renderers verliezen zeer donkere kanalen in zulke groepen en sommige negeren groeps-blendruimtes helemaal, dus met COLRv1DeviceSpaceGroupBlend (standaard False) worden de groepen in de gewone deviceruimte samengevoegd. De afweging staat uitgelegd in het artikel over COLR v1 repeat- en reflect-gradients; zet hem aan als donkere transluciente tinten verdwijnen in de viewer die uw gebruikers echt draaien
SVG-glyphs en bitmap-strikes
SVG-glyphs gaan door dezelfde veilige 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. Gecomprimeerde invoer is gecapteerd op 8 MB, decompressie verloopt in chunks van 8 KB en stopt zodra de uitgebreide grootte 32 MB zou passeren. Scripts, foreignObject, javascript:-URL's en @import-stylesheets doen de builder het document ronduit weigeren. De form wordt geschaald zodat zijn langste zijde gelijk is aan de fontgrootte. Eén ding om te testen vóór productie: sommige fonts proppen veel glyphs in één gedeeld SVG-document, en de builder ontvangt dat hele document voor elke glyph
Bitmapfonts komen neer op 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 wint de dichtstbijzijnde strike die de glyph daadwerkelijk bevat; strike-offsets zijn relatief aan de tabel en glyph-offsets aan de strike. Een dupe-record hergebruikt de afbeelding van een andere glyph maar houdt zijn eigen origin-offsets aan, en een parser die de lookup de buitenste origin laat overschrijven schuift de afbeelding opzij. 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 minder dan volledig dekkend is. TIFF-payloads in sbix gaan naar het event
De raster-fallback-handler schrijven
Als geen enkel natief pad werkt, vuurt HotPDF OnColorGlyphRasterize af en plaatst de RGBA-bitmap die uw handler teruggeeft. Zonder handler, of met Handled op False gelaten, geeft DrawRegisteredColorGlyph False terug en wordt er niets getekend. De handler krijgt het formaat, de fontbytes, de geëxtraheerde asset (het SVG-document, mogelijk nog gegzipt, of de bitmapbytes; leeg voor COLR), de glyph-ID, het palet en de doelpixelgrootte
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 eigen rasterizer, geen HotPDF-API.
// Hij moet exact Width * Height * 4 bytes RGBA teruggeven
// met rechte (niet-gepremultipliceerde) alpha
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Aansluiting
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF valideert de uitvoer voordat de pagina hem te zien krijgt: 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. Voor een variabel kleurfont krijgt de handler de statische instantie op de geregistreerde as-waarden; mocht hij ooit het originele variabele font ontvangen, dan geeft een aanroep van GetRegisteredVariableFontAxes op Sender (gecast naar THotPDF) de coördinaten terug om op te renderen. PixelSize is dezelfde waarde als TargetPixelsPerEm, dus het advies over printresolutie hierboven geldt hier ook
Een pipeline die willekeurige gebruikerstekst print moet van twee soorten verlies af weten: glyphs die het font nooit heeft gehad, en glyphs die hun kleur verloren. De draw-time-dekkingscontrole uit het artikel over het volgen van ontbrekende glyphs meldt de eerste; een False van DrawRegisteredColorGlyph meldt de tweede
COLR-, SVG- en bitmap-emoji-rendering, OpenType-shaping en de veilige SVG-builder zitten allemaal in de HotPDF component voor Delphi en C++Builder