Technischer Artikel

Color-Emoji-Fonts in PDF: COLR v1, SVG und Bitmaps in Delphi

HotPDF zeichnet Color-Emoji über THotPDF.DrawRegisteredColorGlyph in eine PDF, das die Color-Daten eines mit RegisterUnicodeTTF registrierten Fonts liest und sie als native PDF-Grafik ausgibt: COLR-v0-Layer als gefüllte Glyph-Outlines, COLR-v1-Paint-Graphs als Clips, Shadings und Blend Modes, SVG-Glyphen als Form XObjects und CBDT- oder sbix-Bitmaps als Bilder. Was es nicht nativ mappen kann, geht stattdessen ins OnColorGlyphRasterize-Event, statt stillschweigend zu einer schwarzen Form zu verkümmern

Diese letzte Klausel ist der ganze Grund, warum dieser Code existiert. Betten Sie einen Emoji-Font auf dem gewöhnlichen Weg ein, bekommt der Viewer die Outline aus glyf oder CFF, gefüllt mit dem, was zufällig die aktuelle Füllfarbe ist. Das grinsende Gesicht kommt als schwarzer Klecks an, die Flagge als Rechteck, und nichts in der Pipeline beschwert sich

Warum druckt ein Color Emoji in PDF als schwarze Silhouette?

Ein PDF-Font-Programm kennt keine Color-Glyphs. ISO 32000-1 behandelt eine Glyphe als Form, die mit der aktuellen Farbe gemalt wird, und die Color-Tabellen, die OpenType später hinzugefügt hat, nämlich COLR/CPAL, SVG , CBDT/CBLC und sbix, sind nicht Teil des PDF-Imaging-Modells, also ist kein Viewer verpflichtet, sie aus einem eingebetteten Font zu lesen. Die Farbe muss zur Generierungszeit in Seiteninhalt übersetzt werden, solange der Producer noch die Font-Bytes hat und weiß, welche Glyphe er will. Diese Übersetzung unterscheidet sich pro Format, und Emoji-Fonts in freier Wildbahn nutzen alle: geschichtete Vektoren, Gradient-Paint-Graphs, eingebettete SVG-Dokumente und PNG-Strikes. HotPDF meldet das Ergebnis als THPDFOpenTypeColorFormat, mit den Werten otcfNone, otcfCOLRv0, otcfCOLRv1, otcfCBDT, otcfSVG und otcfSBIX, und sondiert den Font in fester Priorität: zuerst COLR, dann SVG, dann CBDT, dann sbix. Vektordaten schlagen Bitmaps, wann immer ein Font beides trägt – genau das wollen Sie in einem Dokument, das gezoomt oder gedruckt werden kann

HotPDF-Diagramm der Color-Glyph-Sondierung: Ein PDF-Font-Programm malt Glyph-Outlines mit der aktuellen Farbe, also müssen die OpenType-Color-Tabellen COLR, SVG, CBDT und sbix zur Generierungszeit in Seiteninhalt übersetzt werden, und HotPDF sondiert einen registrierten Font in fester Priorität COLR, dann SVG, dann CBDT, dann sbix, gemeldet als THPDFOpenTypeColorFormat von otcfCOLRv0 bis otcfSBIX
Vektordaten schlagen Bitmaps, wann immer ein Font beides trägt, genau das wollen Sie in einem Dokument, das gezoomt oder gedruckt werden kann, und eine Glyphe ohne Color-Pfad bleibt Ihrem Fallback überlassen

Ein Aufruf, fünf Formate: eine Color-Glyphe auflösen und zeichnen

THotPDF.GetRegisteredColorGlyphInfo beantwortet, 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 ging, der Color-Font muss also zum Zeitpunkt des Aufrufs 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 überlässt den Fallback Ihnen

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-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
      // Keine Color-Daten: 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;

Zwei Parameter verdienen Aufmerksamkeit. PaletteIndex wählt eine CPAL-Palette, ein Font mit Dark-Background-Palette lässt sich also umschalten, ohne die Glyphe anzufassen. TargetPixelsPerEm zählt nur bei Bitmap-Fonts; auf null gelassen defaultet er auf Round(FontSize * 96 / 72), eine Bildschirmauflösung, weshalb das Beispiel für Druckausgabe 300 verlangt. Die ehrliche Grenze sitzt in der Signatur: Der Aufruf nimmt einen Codepoint und mappt ihn allein durch cmap. ZWJ-Sequenzen, Skin-Tone-Modifikatoren und Regional-Indicator-Flaggen sind GSUB-Ligaturen, ihr Zusammenbau ist also ein Shaping-Problem der Art, wie es der OpenType-GSUB-Alternates-Artikel behandelt – nichts, was dieser Einstiegspunkt für Sie erledigt

COLR v0: gestapelte Glyph-Layer mit Palettenfarben

COLR v0 ist der einfache Fall, und HotPDF rendert ihn direkt: Jede Basis-Glyphe listet Layer-Glyphen mit einem CPAL-Farbeintrag, und jeder Layer wird zu einer gewöhnlichen Text-Show-Operation mit eigener Füllfarbe, gestapelt in Tabellenreihenfolge. Ein Layer mit Alpha unter 255 bekommt ein Graphics-State-Parameter-Dictionary mit passendem /ca und /CA (ISO 32000-1 §8.4.5), und jede Layer-Glyphe wird als benutzt markiert, damit der Subsetter ihre Outline behält, auch wenn kein Codepoint direkt auf sie mappt. Ein Detail überrascht Leute: Der Paletten-Eintragsindex 0xFFFF bedeutet in der OpenType-Spezifikation „nutze die Text-Vordergrundfarbe“, und HotPDF löst ihn zu Schwarz auf, nicht zur aktuellen Seitenfüllfarbe. Bei Emoji-Fonts spielt das selten eine Rolle; bei Icon-Fonts, die auf den Vordergrund-Eintrag setzen, um eine Glyphe zu tönen, prüfen Sie die Ausgabe, bevor Sie davon ausgehen, dass sie Ihrer Textfarbe folgt

Wie verwandelt HotPDF einen COLR-v1-Paint-Graph in PDF-Operatoren?

Indem es die Paint-Tabellen zuerst in einen flachen, begrenzten Graph parst und erst dann jeden Knoten auf ein PDF-Konstrukt mappt. Eine COLR-v1-Glyphe ist keine Layer-Liste, sondern ein gerichteter azyklischer Graph von Paint-Records, in dem Knoten über PaintColrLayers und PaintColrGlyph geteilt werden können. Der Parser deckelt ihn bei 4096 Paint-Knoten, 64 Tiefenebenen und 1024 Color-Stops und führt jeden Knoten als aktiv oder fertig, sodass eine Referenz zurück auf einen aktiven Knoten – ein Zyklus, den ein bösartiger Font aus Layer-Wiederverwendung bauen kann – zurückgewiesen wird, statt hinein zu rekursieren. An den Offset-Basen geht eine erste Implementation schief. 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. Alle drei gegen dieselbe Basis aufzulösen lässt völlig legale Glyphen an der Bounds-Prüfung scheitern, was exakt aussieht wie ein korrumpierter Font. Ist der Graph gebaut, ist das Mapping direkt:

  • PaintGlyph setzt die Glyph-Outline als Clip mit Text-Rendering-Mode 7 (ISO 32000-1 §9.3.6) und malt sein Kind darin
  • Solid-Paints füllen ein geclipptes Rechteck; lineare Gradients werden zu Multi-Stop-axialen Shadings und radiale Gradients zu Zweifarben-radialen Shadings (§8.7.4.5)
  • Sweep-Gradients haben kein PDF-Äquivalent, also nähert HotPDF sie mit 96 flach gefärbten Keilen an, die jeweils von der Color Line gesampelt sind
  • Transforms werden als cm ausgegeben, konjugiert um den Glyph-Baseline-Ursprung, mit Translationen skaliert um FontSize / UnitsPerEm
  • PaintComposite-Modi 13 bis 27 mappen auf die separierbaren und nicht-separierbaren PDF-Blend Modes wie /Multiply, /Screen und /Luminosity (§11.3.5), gesetzt über einen ExtGState-/BM-Eintrag

Die Grenze ist explizit. Porter-Duff-Modi 5 bis 12 (src_in, xor, plus und die restlichen) haben kein PDF-Blend-Mode-Pendant, Repeat- und Reflect-Extend-Modi an linearen und radialen Gradients werden nicht ausgegeben, und Gradients, deren Stops unterschiedliche Alphawerte tragen, werden nicht mit einer einzelnen Opacity gefälscht. Radiale Gradients mit mehr als zwei Stops behalten nur ihre erste und letzte Farbe. HotPDF prüft den ganzen Graph gegen diese unterstützte Teilmenge, bevor ein einziger Operator geschrieben wird, eine nicht unterstützte Glyphe lässt die Seite also unangetastet und geht zum Raster-Fallback weiter, statt eine halbe Zeichnung zurückzulassen

HotPDF-Diagramm der COLR-v1-Konvertierung: Der Paint-Graph wird in einen begrenzten Graph geparst, gedeckelt bei 4096 Knoten, 64 Tiefenebenen und 1024 Color-Stops mit Zyklus-Abweisung, dann wird PaintGlyph zum Mode-7-Clip, lineare und radiale Gradients werden zu axialen und radialen Shadings, Sweep-Gradients zu 96 Keilen, und PaintComposite-Modi 13 bis 27 werden zu PDF-Blend Modes
Der ganze Graph wird gegen die unterstützte Teilmenge geprüft, bevor der erste Operator geschrieben wird, eine nicht unterstützte Glyphe lässt die Seite also unangetastet und geht zum Raster-Fallback weiter, statt eine halbe Zeichnung zurückzulassen

SVG-Glyphen und Bitmap-Strikes

SVG-Glyphen laufen durch denselben begrenzten Builder, den HotPDF für importierte SVG-Dateien nutzt, und das Ergebnis wird als Form XObject registriert (§8.10), exakt wie im Artikel SVG zu Form XObject beschrieben. Das Dokument in der SVG -Tabelle darf gzip-komprimiert sein; die Dekompression läuft in 8-KB-Blöcken und stoppt, sobald die entpackte Größe 32 MB überschreiten würde, statt erst aufzublasen und danach zu prüfen, und der komprimierte Input selbst ist auf 8 MB gedeckelt. Das Profil ist absichtlich restriktiv: Skripte, eingebettete Bilder, externe URLs, data:-URIs und nicht-lokale Referenzen scheitern geschlossen. Die Form wird skaliert, sodass ihre längere Seite der Font-Größe entspricht, und an der Baseline verankert, was das y-abwärts-SVG-Koordinatensystem aufs y-aufwärts-PDF-Koordinatensystem mappt. Beachten Sie, dass der Builder das ganze SVG-Dokument der Glyphe bekommt, ohne Auswahl des glyphNNN-Elements – Fonts, die viele Glyphen in ein geteiltes Dokument packen, sollten Sie also testen, bevor Sie sich auf sie verlassen

Bitmap-Fonts sind eine Frage der Strike-Wahl und der Platzierung. 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 sind Strike-Offsets relativ zur Tabelle und Glyph-Offsets relativ zum Strike, und ein dupe-Record nutzt die Grafik einer anderen Glyphe wieder, während er seine eigenen Ursprungs-Offsets behält; lässt man die Rekursion den äußeren Ursprung überschreiben, wandert das Bild. 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. sbix-TIFF-Payloads werden nicht dekodiert und gehen an das Event

Was passiert, wenn eine Glyphe nicht nativ gezeichnet werden kann?

HotPDF feuert OnColorGlyphRasterize und platziert, was auch immer Ihre Handler-Funktion an RGBA-Bitmap zurückgibt; ist nichts zugewiesen oder lässt der Handler Handled auf false, gibt DrawRegisteredColorGlyph False zurück und die Seite bleibt unverändert. Das Event feuert für einen COLR-v1-Graph außerhalb der unterstützten Teilmenge, ein SVG-Dokument, das der sichere Builder abgelehnt hat, und eine Bitmap-Payload, die die internen Dekoder nicht lesen. Der Handler bekommt das Format, die rohen Font-Bytes, das extrahierte Asset (das SVG-Dokument, eventuell noch gzippt, oder die Bitmap-Bytes; leer bei COLR v1), die Glyph-ID, die Palette und die Ziel-Pixelgröße

HotPDF-Diagramm des Raster-Fallbacks: OnColorGlyphRasterize feuert für einen COLR-v1-Graph außerhalb der unterstützten Teilmenge, ein SVG-Dokument, das der sichere Builder abgelehnt hat, oder eine Bitmap-Payload, die die Dekoder nicht lesen, und übergibt Format, Font-Bytes, Asset, GlyphID, PaletteIndex und PixelSize, der zurückgegebene RGBA-Buffer wird nur akzeptiert, wenn seine Länge exakt Width mal Height mal 4 beträgt
Null-Größen, eine falsche Buffer-Länge oder überlaufende Dimensionen werden abgewiesen, bevor die Seite angefasst wird, und ohne Handler oder bei Handled false gibt der Aufruf False zurück und die Seite bleibt unverändert
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.
  Handled := RenderWithOwnEngine(Format, FontBytes, AssetData,
    GlyphID, PaletteIndex, PixelSize, Width, Height, RGBA);
end;

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

HotPDF validiert die Handler-Ausgabe, bevor es die Seite anfasst: 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. Ein Raster-Fallback bleibt ein Raster, ein so gerendertes Emoji verliert also seine Vektorschärfe; fordern Sie eine PixelSize an, die zu Ihrer Ausgabeauflösung passt. Kombinieren Sie den Color-Pfad mit den Coverage-Prüfungen zur Zeichenzeit aus dem Artikel zum Tracking fehlender Glyphen, und eine Pipeline, die beliebigen Nutzertext verarbeitet, kann sowohl fehlende Glyphen als auch Glyphen melden, die ihre Farbe verloren haben

Der Color-Glyph-Renderer, der OpenType-Shaping-Stack und der sichere SVG-Builder erscheinen alle in der HotPDF Delphi PDF component, verfügbar für Delphi und C++Builder