Ein Grinning-Face-Emoji, das mit einem gewöhnlichen Text-Aufruf ins PDF geschrieben wird, mit Segoe UI Emoji als Font, kommt als flache schwarze Silhouette heraus: richtige Form, kein Gelb, keine Schattierung. Nimmt man stattdessen einen CBDT-Emoji-Font, zeichnet derselbe Aufruf unter Umständen gar nichts, weil dieser Font seine Bilder als PNG-Dateien mitbringt und wenig oder keine Outline-Daten hat. Kopiert man die schwarze Form aus dem Viewer heraus, hält die Zwischenablage weiterhin U+1F600: Das Zeichen ist richtig, nur die Color-Tabellen wurden nie gelesen
Warum erscheint ein Color-Emoji im PDF als schwarze Silhouette?
Weil ein PDF-Font-Programm den Begriff einer farbigen Glyphe nicht kennt. ISO 32000-1 behandelt jede Glyphe als Form, die mit der aktuellen Füllfarbe gemalt wird, und die Color-Tabellen, die OpenType später hinzugefügt hat (COLR/CPAL, SVG , CBDT/CBLC und sbix), sind nicht Teil des PDF-Imaging-Modells, also hat kein Viewer einen Grund, sie aus einem eingebetteten Font zu lesen. Welche Farbe das Emoji auch hat, sie muss während des Schreibens des PDF in Seiteninhalt übersetzt werden, an einem Punkt, an dem der Producer die Font-Bytes noch in der Hand hat und genau weiß, welche Glyphe er will. Diese Übersetzung ist je Format verschieden, und echte Emoji-Fonts nutzen alle vier: gestapelte Vektor-Layer, Gradient-Paint-Graphs, ganze SVG-Dokumente und PNG-Strikes
HotPDF erledigt diese Übersetzung in THotPDF.DrawRegisteredColorGlyph. Es meldet, zu welchem Format eine Glyphe aufgelöst hat, als THPDFOpenTypeColorFormat, mit den Werten otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG und otcfSBIX, und sondiert den Font in fester Reihenfolge: COLR, dann SVG, dann CBDT, dann sbix. Ein Font, der sowohl Vektoren als auch Bitmaps trägt, bekommt also den Vektorpfad. Für alles, was gezoomt oder gedruckt wird, ist das die richtige Reihenfolge
Einen einzelnen Codepoint auflösen und zeichnen
THotPDF.GetRegisteredColorGlyphInfo verrät, welchen Pfad ein Codepoint nehmen wird, und DrawRegisteredColorGlyph nimmt ihn. Beide schlagen den Codepoint in der Character Map des Fonts nach, der zuletzt an RegisterUnicodeTTF übergeben wurde, der Emoji-Font muss beim Aufruf also der registrierte Unicode-Font sein. Die Zeichenfunktion gibt False zurück, wenn die Glyphe keine Color-Daten hat oder kein Pfad sie rendern konnte, und die Seite bleibt in dem Fall unangetastet
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';
// Paletten-Eintrag 0xFFFF („Text-Vordergrund“) löst zu dieser Farbe auf
Pdf.ColorGlyphForegroundColor := clNavy;
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\seguiemj.ttf');
// U+1F600, CPAL-Palette 0, Bitmap-Strike am nächsten an 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
// Kein Color-Pfad: auf die monochrome Outline ausweichen
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 wählt eine CPAL-Palette, ein Font mit Dark-Background-Palette lässt sich also umschalten, ohne die Glyphe anzufassen. Der Paletten-Eintrag 0xFFFF ist in der OpenType-Spezifikation etwas Besonderes: Er bedeutet „egal, welche Farbe der Text hat“, und HotPDF füllt ihn aus ColorGlyphForegroundColor und ColorGlyphForegroundAlpha (standardmäßig Schwarz und 255). Emoji-Fonts benutzen diesen Eintrag selten, Icon-Fonts schon, und wenn Ihr Font Glyphen darüber einfärbt, stellen Sie die Eigenschaft vor dem Zeichnen auf Ihre Textfarbe
TargetPixelsPerEm zählt nur bei Bitmap-Fonts, und sein Standardwert ist der Parameter, den man am ehesten überschreiben will. Auf null gelassen wird er zu Round(FontSize * 96 / 72), einer Bildschirmauflösung: 48 Pixel für ein 36-Punkt-Emoji. Bei einem Font, der sbix-Strikes bei 20, 32, 40, 48, 64, 96 und 160 ppem deklariert, wählt dieser Standard den 48-Pixel-Strike, und ein 600-dpi-Drucker muss ihn dann rund sechsfach vergrößern. Fordert man 300 an, was 36 Punkte bei 600 dpi brauchen, nimmt derselbe Aufruf den 160er-Strike, den größten, den der Font hat
Die Signatur legt auch den Geltungsbereich fest. Ein Codepoint geht hinein und wird durch cmap gemappt. ZWJ-Familiensequenzen, Skin-Tone-Modifikatoren und Regional-Indicator-Flaggen sind GSUB-Ligaturen, sie müssen also zuerst zu einer Glyphe geshaped werden, die Art von Arbeit, die der OpenType-GSUB-Alternates-Artikel behandelt
COLR-Layer und Paint-Graphs als PDF-Vektoren
COLR v0 ist das einfache Format. Jede Basis-Glyphe listet einen Stapel von Layer-Glyphen, jede mit einer CPAL-Farbe, und HotPDF füllt die Outline jedes Layers in Tabellenreihenfolge. Ein Layer, dessen CPAL-Farbe ein Alpha unter 255 trägt, bekommt ein eigenes /ca, sodass translucente Layer sich mit den darunterliegenden Layern genau so mischen, wie der Font-Designer es vorgesehen hat. In einem gewöhnlichen Dokument wird der ganze Stapel danach in eine isolierte Transparenzgruppe gefasst (ISO 32000-1 §11.4). Es geht dabei um das, was außerhalb der Glyphe passiert: Eine Opacity, ein Blend Mode oder eine Soft Mask, die man um das Emoji legt, wird einmal auf die fertige Glyphe angewandt, statt auf jedem Layer wiederholt zu werden und sich dort zu verdoppeln, wo Layer überlappen
COLR v1 ist ein anderes Tier: ein gerichteter azyklischer Graph von Paint-Records mit Gradients, Transforms, verschachtelten Glyph-Clips und Composite-Modi, in dem Knoten über PaintColrLayers und PaintColrGlyph geteilt werden können. HotPDF parst ihn zuerst in einen begrenzten Graph, gedeckelt bei 4096 Paint-Knoten, 96 Tiefenebenen und 1024 Color-Stops, und markiert jeden Knoten, solange er besucht wird, sodass eine Referenz zurück in den aktuellen Pfad, ein Zyklus, den ein bösartiger Font aus Layer-Wiederverwendung bauen kann, abgewiesen wird, statt hinein zu rekursieren
An den Offsets scheitern handgeschriebene COLR-v1-Parser üblicherweise. BaseGlyphPaintRecord-Offsets sind relativ zum Start von BaseGlyphList, LayerList-Paint-Offsets sind relativ zu LayerList, und jedes Offset24 in einer Paint-Tabelle ist relativ zu dieser Paint-Tabelle selbst. Löst man alle drei gegen dieselbe Basis auf, scheitern völlig legale Glyphen an der Bounds-Prüfung, was exakt so aussieht wie ein korrumpierter Font. Ist der Graph gebaut, ist das Mapping nach PDF größtenteils direkt:
PaintGlyphmacht die Glyph-Outline zum Clipping-Pfad, und sein Kind-Paint füllt die geclippte Fläche- Lineare Gradients werden zu Multi-Stop-axialen Shadings, radiale Gradients zu radialen Shadings (§8.7.4.5)
- Sweep-Gradients haben keinen PDF-Shading-Typ, also werden sie als 96 flach gefärbte Keile um das Zentrum gezeichnet, jeder aus der Color Line gesampelt
- Transforms werden zu
cm-Operatoren, mit Translationen skaliert umFontSize / UnitsPerEm PaintComposite-Modi 13 bis 27 mappen auf die PDF-Blend Modes desselben Namens,/Multiply,/Screen,/Luminosityund die restlichen (§11.3.5), gesetzt über einen ExtGState-/BM-Eintrag
Gradients mit translucenten Stops verbergen eine weitere Falle. COLR v1 interpoliert Farben premultiplied mit Alpha, während PDF straight color erwartet und Alpha selbst anwendet; ein Konverter, der die premultiplied Werte an PDF durchreicht, bekommt jeden semi-transparenten Streifen zweimal abgedunkelt. Die Farben müssen nach der Interpolation und vor dem Schreiben die Vormultiplikation wieder rückgängig gemacht werden
Was passiert mit COLR-v1-Composite-Modi unter PDF/A-1 oder PDF/X-1a?
Die Porter-Duff-Modi in PaintComposite (5 bis 12: src_in, dest_out, src_atop, xor, plus und ihre Geschwister) haben keinen PDF-Blend Mode desselben Namens, und die naheliegende Lesart der PDF-Spezifikation ist, dass sie sich nicht ausdrücken lassen. Diese Lesart ist zu streng. In/out-Modi sind eine Alpha-Soft Mask über einem Operanden; atop, xor und plus lassen sich aus isolierten Formen, Gruppen-Alpha, Luminosity-Masken und Blend-Arithmetik zusammenbauen. HotPDF baut sie so, diese Glyphen bleiben also Vektor
Der Preis dafür ist, dass die meisten dieser Konstruktionen PDF-Transparenz brauchen, und PDF/A-1, PDF/X-1a und PDF/X-3 verbieten sie. Ist PDFACompliance ein PDF/A-1-Wert oder ist PDFXCompliance 'X-1a' oder 'X-3', prüft HotPDF die Glyphe vor dem Zeichnen und leitet die Fälle, die sich ohne Transparenz nicht zeichnen lassen, an das Raster-Event weiter: atop-, xor- und plus-Composites, in/out-Composites, deren maskierter Operand mehr als ein einfaches opakes Paint ist, Solid-Paints mit Teil-Alpha, Gradients mit translucenten Stops und COLR-v0-Layer mit Alpha unter 255. Die in- und out-Modi bleiben Vektor, wenn die maskierte Seite eine schlichte Volltonfarbe oder ein Gradient ist, optional gefasst in einen Glyph-Clip oder Transform, denn dieser Fall braucht nur eine opake Graustufen-Maske. Dasselbe gilt für COLR v0, wenn StrictVersionLock die Datei unter PDF 1.4 festnagelt
Pdf.PDFACompliance := 'B'; // PDF/A-1b: keine Transparenz erlaubt
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF(EmojiFontPath);
if not Pdf.DrawRegisteredColorGlyph(Pdf.CurrentPage, $1F308,
72, 200, EmojiFontName, 36, 0, 300) then
// Ohne OnColorGlyphRasterize-Handler endet eine translucente
// oder komponierte Glyphe hier unter PDF/A-1
LogMissingColorGlyph($1F308); // eigene Logging-Routine
Muss das Archiv Emoji in Farbe zeigen, zielt man auf PDF/A-2b statt auf PDF/A-1b. Part 2 erlaubt Transparenz, jedes COLR-Konstrukt bleibt Vektor, und der Fallback-Handler wird zum Sicherheitsnetz statt zum Haupt-Renderer für jedes translucente Emoji. PDF/A-1 behält man für Workflows, die es beim Namen verlangen
Eine Einstellung auf der Leserseite gehört auch hierher. HotPDF komponiert COLR-v1-Gruppen in einem linear-light Blend Space; manche Renderer verlieren sehr dunkle Kanäle in solchen Gruppen und manche ignorieren Gruppen-Blend-Spaces völlig, daher lässt COLRv1DeviceSpaceGroupBlend (Standard False) die Gruppen im gewöhnlichen Device Space komponieren. Den Trade-off erklärt der Artikel zu COLR-v1-Repeat- und Reflect-Gradients; schaltet man ihn ein, wenn dunkle translucente Tints im Viewer verschwinden, den die Nutzer tatsächlich fahren
SVG-Glyphen und Bitmap-Strikes
SVG-Glyphen laufen durch denselben sicheren Builder, den HotPDF für importierte SVG-Dateien nutzt, und das Ergebnis wird als Form XObject registriert (§8.10), wie im Artikel SVG zu Form XObject beschrieben. Das Dokument in der SVG -Tabelle darf gzip-komprimiert sein. Komprimierter Input ist auf 8 MB gedeckelt, die Dekompression läuft in 8-KB-Blöcken und stoppt, sobald die entpackte Größe 32 MB überschreiten würde. Skripte, foreignObject, javascript:-URLs und @import-Stylesheets bringen den Builder dazu, das Dokument rundweg abzulehnen. Die Form wird skaliert, sodass ihre längere Seite der Font-Größe entspricht. Etwas, das man vor dem Produktivbetrieb ausprobieren sollte: Manche Fonts packen viele Glyphen in ein geteiltes SVG-Dokument, und der Builder bekommt dieses ganze Dokument pro Glyphe
Bitmap-Fonts laufen auf Strike-Wahl und Platzierung hinaus. Bei CBDT wählt HotPDF die CBLC-Größe, deren vertikales ppem TargetPixelsPerEm am nächsten liegt, akzeptiert die Bildformate 17, 18 und 19 und liest die Metriken von Format 19 aus der CBLC-Index-Subtabelle, weil dieses Format keine eigenen speichert. Bei sbix gewinnt der nächste Strike, der die Glyphe tatsächlich enthält; Strike-Offsets sind relativ zur Tabelle, Glyph-Offsets relativ zum Strike. Ein dupe-Record nutzt die Grafik einer anderen Glyphe wieder, behält aber seine eigenen Ursprungs-Offsets, und ein Parser, der das Lookup den äußeren Ursprung überschreiben lässt, schiebt das Bild zur Seite. PNG- und JPEG-Payloads werden intern dekodiert, um FontSize / PixelsPerEmY skaliert statt auf die Font-Größe gestreckt und mit einer Soft Mask geschrieben (§11.6.5.3), wann immer ein Pixel nicht voll opak ist. TIFF-Payloads in sbix gehen an das Event
Den Raster-Fallback-Handler schreiben
Wenn kein nativer Pfad greift, feuert HotPDF OnColorGlyphRasterize und platziert die RGBA-Bitmap, die der Handler zurückgibt. Ohne Handler, oder wenn Handled auf False bleibt, gibt DrawRegisteredColorGlyph False zurück und nichts wird gezeichnet. Der Handler bekommt das Format, die Font-Bytes, das extrahierte Asset (das SVG-Dokument, eventuell noch gzippt, oder die Bitmap-Bytes; leer bei COLR), die Glyph-ID, die Palette und die Ziel-Pixelgröße
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 ist Ihr Rasterizer, keine HotPDF-API.
// Er muss exakt Width * Height * 4 Bytes RGBA zurückgeben,
// mit geradem (nicht premultiplied) Alpha
Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;
// Verdrahtung
Pdf.OnColorGlyphRasterize := Fallback.Rasterize;
HotPDF validiert das Ergebnis, bevor die Seite es sieht: Null-Größen, ein Buffer, dessen Länge nicht exakt Width * Height * 4 ist, oder Dimensionen, groß genug zum Überlaufen, werden abgewiesen, und der Aufruf gibt False zurück. Bei einem variablen Color-Font bekommt der Handler die statische Instanz an den registrierten Achsenwerten; sollte er stattdessen einmal den originalen variablen Font erhalten, liefert ein Aufruf von GetRegisteredVariableFontAxes auf Sender (gegossen auf THotPDF) die Koordinaten, an denen zu rendern ist. PixelSize ist derselbe TargetPixelsPerEm-Wert, der Druckauflösungs-Ratschlag von oben gilt also auch hier
Eine Pipeline, die beliebigen Nutzertext druckt, muss zwei Arten von Verlust kennen: Glyphen, die der Font nie hatte, und Glyphen, die ihre Farbe verloren haben. Das Coverage-Tracking zur Zeichenzeit aus dem Artikel zum Tracking fehlender Glyphen meldet das erste; ein False von DrawRegisteredColorGlyph meldet das zweite
COLR-, SVG- und Bitmap-Emoji-Rendering, OpenType-Shaping und der sichere SVG-Builder sind allesamt Teil der HotPDF-Komponente für Delphi und C++Builder