HotPDF rendert eingebettete PDF-Schriftarten in Delphi, ohne dass auf der Maschine etwas installiert werden muss: Die Pipeline zum Rendern eingebetteter Glyphen in HPDFGlyphRender.pas analysiert die im PDF selbst gespeicherten Schriftartprogramme – TrueType-glyf-Outlines aus FontFile2, CFF-Type-2-Charstrings aus FontFile3 und Type-3-Glyphen-Inhaltsströme – und gibt sie als gefüllte GDI-Vektorpfade wieder. Dieser Artikel ist eine tiefgehende Analyse der Schriftarttreue hinter dem Rendern von PDF-Seiten in eine TBitmap mit HotPDF: Jener Artikel behandelt den Renderer als Ganzes, dieser beschreibt, wie der Text auf diesen Seiten seine exakten Formen erhält
Warum wird eine PDF-Datei mit Kästchen anstelle von Text gerendert?
Kästchen, Leerstellen oder geringfügig falsche Zeichen in der gerenderten PDF-Ausgabe bedeuten fast immer, dass der Renderer das Betriebssystem nach einer Schriftart gefragt hat, anstatt die im Dokument eingebettete zu verwenden. Das Problem äußert sich jedes Mal gleich: Auf dem System, auf dem das Dokument erstellt wurde, sieht es perfekt aus, doch wenn ein Kunde es auf einem sauberen Server oder einem gesperrten Desktop öffnet, zeigt die japanische Rechnung Tofu-Kästchen (leere Quadrate) oder eine ersetzte, ähnlich aussehende Schriftart verschiebt jeden Zeilenumbruch. Die Schriftarten befanden sich nie auf dieser Maschine, sondern nur in der PDF-Datei – und ein Renderer, der bei der Ersetzung durch Systemschriftarten stehen bleibt, kann sie nicht lesen. Untergruppen-Schriftarten (Subset-Schriftarten) machen es noch schlimmer: Eine Untergruppe enthält möglicherweise nur vierzig Glyphen unter Zeichencodes, die privat nur dieser einen Datei zugewiesen sind – eine Zuweisung, die keine installierte Schriftart teilt
ISO 32000-1 §9.9 definiert drei Träger für ein eingebettetes Schriftartprogramm im Schriftart-Deskriptor: FontFile enthält ein ursprüngliches Type-1-Programm, FontFile2 ein TrueType-Programm und FontFile3 eine reine CFF- (Type1C oder CIDFontType0C) oder eine OpenType-Kapselung. Eine vierte Variante, die Type-3-Schriftart aus ISO 32000-1 §9.6.5, bettet überhaupt nichts Binäres ein – jede Glyphe ist ein kleiner PDF-Inhaltsstrom, der direkt ausgeführt wird. Die drei Träger unterscheiden sich in der Outline-Mathematik (quadratische B-Splines versus kubische Charstrings versus beliebige Seitenoperatoren), sodass ein originalgetreuer Renderer einen separaten Interpreter für jeden Typ benötigt, plus eine Kodierungsschicht, die Zeichencodes in den richtigen Glyphenindex übersetzt, bevor eine Outline verarbeitet wird
Wie konvertiert HotPDF TrueType-glyf-Outlines in GDI-Pfade?
THPDFEmbeddedTTF in HPDFGlyphRender.pas liest die loca-Tabelle, um jeden Glyphendatensatz zu lokalisieren, durchläuft die glyf-Konturen Punkt für Punkt und gibt einen GDI-Pfad aus. Zwei TrueType-Konventionen erfordern eine explizite Behandlung. Erstens implizieren aufeinanderfolgende Kontrollpunkte (Off-Curve-Points) einen Kurvenpunkt (On-Curve-Point) in ihrer Mitte, und eine Kontur, deren Punkte alle Kontrollpunkte sind, beginnt im Mittelpunkt ihres letzten und ersten Punktes. Wird eine dieser Regeln ignoriert, erhalten abgerundete Glyphen flache Kanten oder kollabieren. Zweitens sind TrueType-Kurven quadratische Bézier-Kurven, während das GDI-eigene PolyBezierTo kubische Kurven erwartet. Daher wird jedes quadratische Segment exakt gradmäßig angehoben (Degree Elevation), anstatt in gerade Liniensegmente zerlegt zu werden
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Die Graderhöhung ist verlustfrei: Die kubische Kurve zeichnet dieselbe Kurve nach, sodass die gerenderte Outline exakt dem entspricht, was ein konformer Viewer aus derselben Tabelle zeichnet – bei jedem Zoom. Die verbleibende Arbeit ist die Platzierung. Jede Glyphe wird in Schrifteinheiten definiert (typischerweise in einem Raster von 1000 oder 2048 Einheiten pro Geviert/em), und der Renderer setzt die Skalierungsmatrix, die Textmatrix und die aktuelle Transformationsmatrix zu einer Glyphe-zu-Gerät-Transformation zusammen, bevor der Pfad gefüllt wird. Die Reihenfolge ist hier wichtiger als es scheint: Setzt man dieselben drei Matrizen rückwärts zusammen, kollabiert jede Glyphe in Richtung des Ursprungs – eine fehlerhafte Darstellung, deren eigentliche Ursache nur eine einzige Zeile Matrixalgebra ist
Wie der Type-2-Charstring-Interpreter CFF-Schriftarten verarbeitet
THPDFEmbeddedCFF bietet für FontFile3-Programme einen echten Type-2-Charstring-Interpreter: Er parst die CFF-INDEX-Strukturen, das Top-DICT und Private-DICT, führt dann jeden Charstring aus und gibt die Pfadsegmente direkt an GDI aus. Eine OpenType-Kapselung (der OTTO-Container) wird zuerst entfernt, um an die reine CFF-Tabelle zu gelangen; nackte CIDFontType0C- und Type1C-Ströme werden direkt konsumiert. Charstrings are a compact stack language, and three of its conventions decide whether the interpreter stays in sync with the byte stream. The optional width prefix means the first stack-clearing operator may carry one extra leading operand. The hintmask operator implies a vstemhm when operands are still on the stack, and the number of mask bytes to skip depends on the accumulated stem count — get the count wrong once and every subsequent opcode is misread. And subroutine calls add a bias to their index (107, 1131, or 32768 depending on subroutine count) before lookup, so an unbiased call lands on the wrong subroutine entirely
CID-basierte CFF fügt eine Dereferenzierung hinzu, die naive Implementierungen stolpern lässt: Der Zeichencode wählt eine CID, aber der Charstring-Index ist eine GID, und der Zeichensatz (Charset) der Schriftart ordnet die GID der CID zu. Daher erstellt der Renderer vor dem Zeichnen die umgekehrte CID-zu-GID-Zuordnung und wählt das Private-DICT pro Glyphe über FDSelect für Schriftarten aus, die mehrere enthalten. Namensbasierte Type1C-Programme, der übliche Träger für einfache Type-1-Schriftarten, lösen stattdessen Ein-Byte-Codes über die integrierte Kodierung des CFF-Programms oder über den als nächstes beschriebenen PDF-Kodierungsmechanismus auf. Eine Einschränkung: Der Interpreter liest Hint-Operatoren, um den Datenstrom synchron zu halten, führt das Hinting jedoch nicht aus – eine Grenze, die am Ende besprochen wird
Was ist eine Type-3-Schriftart und wie wird sie gezeichnet?
Eine Type-3-Glyphe ist überhaupt keine Outline – ISO 32000-1 §9.6.5 definiert sie als einen Inhaltsstrom. HotPDF rendert sie daher, indem es den Grafikzustand sichert, die Schriftartmatrix, Schriftartgröße und Textmatrix mit der CTM verknüpft und die Glyphenprozedur über denselben Operator-Interpreter ausführt, der auch Seiten zeichnet, wobei die schrifteigenen /Resources im Scope liegen. Zwei Details der Spezifikation sind für die Korrektheit wichtig. Type-3-Breiten (/Widths) werden im Glyphenraum ausgedrückt und nicht im 1/1000-Textraum, den jeder andere Schriftarttyp verwendet. Daher müssen Vorschübe die /FontMatrix durchlaufen – Barcode-Schriftarten mit einer Matrix von 0.01 verschieben sich andernfalls um eine Größenordnung falsch. Und eine Glyphenprozedur, die mit dem d1-Operator beginnt, macht zwei Zusagen, die der Renderer durchsetzt: Das Zeichnen wird auf die deklarierte Bounding-Box begrenzt, und gemäß ISO 32000-1 §9.6.5.2 ignoriert die Glyphe ihre eigenen Farboperatoren und zeichnet mit der aktuellen Füllfarbe des Aufrufers, sodass rg, g, k und ihre Strichzeichnungs-Gegenstücke innerhalb der Prozedur für die Dauer dieser Glyphe unterdrückt werden. Ignoriert man die Farbregel, wird eine d1-Barcode-Schriftart, die von der Seite in Blau gestempelt wird, schwarz dargestellt; ignoriert man den Clip, zeichnet eine fehlerhafte Glyphe über ihre Zelle hinaus
Wie Zeichencodes zu Glyphen-IDs werden
Outline-Interpreter sind nur die halbe Miete, denn die Bytes in einer PDF-Textzeichenfolge sind Zeichencodes, keine Glyphenindizes, und ISO 32000-1 widmet der Zuordnung zwei Abschnitte. Für einfache Schriftarten, §9.6.6 schreibt eine strenge Priorität vor: Ein /Differences-Array überschreibt die Basiskodierung (WinAnsiEncoding, MacRomanEncoding oder StandardEncoding), welche wiederum die eigene Zuordnung des Schriftartprogramms überschreibt. HotPDF löst diese Kette in eine 256 Einträge umfassende Code-zu-GID-Tabelle auf und übersetzt Glyphennamen über drei Wege in Glyphenindizes: exakte Zeichensatzübereinstimmung in einem CFF-Programm, numerische gNN/glyphNN-Namen als literale Indizes und die Übersetzung von Namen zu Unicode gemäß Adobe Glyph List gefolgt von einem cmap-Lookup für TrueType-Programme. Bei zusammengesetzten Schriftarten übernimmt §9.7 die Kontrolle über CIDToGIDMap: Der Regelfall ist /Identity, aber der Eintrag kann auch ein Strom von Big-Endian-Paaren sein, die nach CID indiziert sind. Der Unicode-Ausgang von HotPDF selbst verwendet genau diese Stream-Form für kompakte Untergruppen, sodass der Stream-Pfad keine exotische Ausnahme darstellt
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
Wenn ein TrueType-cmap-Lookup erforderlich ist, durchläuft HotPDF eine Fallback-Kette, anstatt einer einzelnen Untertabelle zu vertrauen: Die Windows-Unicode-Untertabellen (Format 4, dann Format 12 für zusätzliche Ebenen) stehen an erster Stelle, gefolgt von der (3,0)-Symbol-Untertabelle mit ihrer F000-Private-Use-Area-Konvention, die auf das niederwertige Byte gespiegelt ist – der Grund, warum eine Symbolschriftart wie Wingdings auf einfache ASCII-Codes antwortet –, und schließlich die alten Formate 6 und 0. Untertabellen des Formats 2 werden bewusst nicht interpretiert: Sie ordnen veraltete Multibyte-Codeseiten wie Shift-JIS und Big5 zu, kein Unicode, und moderne CJK-Schriftarten enthalten ohnehin ausnahmslos eine Untertabelle des Formats 4 oder 12. Jeder Code, der keinen dieser Wege erfolgreich durchläuft, fällt auf das Zeichnen per GDI für diese einzelne Glyphe zurück, sodass ein nicht zuordbares Zeichen nur eine Glyphe und nicht den gesamten Textdurchlauf beeinträchtigt
Was der eingebettete Pfad nicht tut
Hinting wird nicht ausgeführt – Outlines werden so gefüllt, wie sie definiert sind. Dies ist ab 150 DPI aufwärts nicht von einer Ausgabe mit Hinting zu unterscheiden, kann aber bei sehr kleinen Größen um ein Pixel von einem Rasterizer mit Hinting abweichen. Ursprüngliche Type-1-Programme in FontFile (eexec-verschlüsselte Charstrings) werden nicht interpretiert, und Achsen für variable OpenType-Schriftarten werden nicht angewendet. In beiden Fällen, ebenso wie bei einem beschädigten Schriftartprogramm oder einer glyf-Tabelle ohne nutzbare cmap, wird auf das Zeichnen mit Systemschriftarten zurückgegriffen, anstatt dass die Seite fehlschlägt. Derselbe Ansatz zur Priorisierung der Originaltreue gilt auch an anderen Stellen des Renderers – axiale und radiale Schattierungsmuster erhalten dieselbe Behandlung, die Verläufen gebührt –, und die Generatorseite hat in der Art und Weise, wie EndDoc Schriftart-Untergruppen ordnet, ihre eigene Geschichte zu Schriftartdetails
Die Nutzung der Pipeline erfordert keinerlei schriftartspezifischen Code – jeder der oben genannten Mechanismen greift automatisch beim Aufruf des Seitenrenderings
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Die praktische Konsequenz ist diejenige, die Ihren Support betrifft: Eine PDF-Datei, die ihre Schriftarten mitführt, wird mit genau diesen Schriftarten gerendert – auf einem Build-Server, in einem Windows-Container oder auf einem Kunden-Desktop, auf dem die Schriftart nie installiert war. Die Pipeline zum Rendern eingebetteter Glyphen wird als Teil der HotPDF-Komponente für Delphi und C++Builder ausgeliefert – einer nativen VCL-Bibliothek, die PDF-Erstellung, -Bearbeitung, -Textextraktion und -Seitenrendering ohne externe DLL-Abhängigkeiten abdeckt