Erstellen Sie einen Bericht, betten Sie eine TrueType-Schriftart ein, und die Ausgabe öffnet sich korrekt in jedem Viewer, den Sie ausprobieren. Die Glyphen stimmen, der Text ist auswählbar, die Datei ist gültig. Das Einzige, was nicht stimmt, ist die Größe. Ein Dokument, das ein paar Dutzend lateinische Zeichen verwendet, trägt die gesamte 350 KB große Schriftart. Ein Dokument, das einen Absatz auf Chinesisch druckt, trägt eine 14 MB große CJK-Schriftart anstelle des benötigten halben Megabytes. Es wurde keine Ausnahme ausgelöst, keine Warnung protokolliert und die Datei hat die Validierung bestanden. So sieht ein fehlerhaft geordneter Finalisierungsschritt von außen aus: Nichts schlägt fehl, und der einzige Beweis ist eine Zahl, die zu groß ist
Es lohnt sich, dies als Lektion aufzuschreiben, denn die Form des Fehlers ist allgemein. Jede Dokumenten-Engine verfügt über eine Finalisierungsphase, die Objekte unmittelbar vor dem Schreiben verändert, und die Korrektheit dieser Phase hängt allein von der Reihenfolge ihrer Schritte relativ zur Serialisierung ab. Landet ein Schritt auf der falschen Seite des Schreibvorgangs, bewirkt er nichts – lautlos
Was das Font-Subsetting eigentlich tun soll
Eine Untergruppe (Subset) einer Schriftart ist der Teil einer TrueType-Datei, den ein Dokument tatsächlich verwendet. ISO 32000-1 §9.9 beschreibt, wie ein eingebettetes Schriftprogramm in einem Stream liegt, auf den der Schriftart-Deskriptor verweist, und für ein TrueType-Programm ist dieser Stream /FontFile2 mit einer /Length1, die die unkomprimierte Byte-Anzahl angibt. Das Subsetting schreibt die Tabellen glyf und loca so um, dass sie nur die vom Dokument referenzierten Glyphen enthalten, nummeriert die Glyphen-IDs neu und stellt dem Namen /BaseFont ein Präfix aus sechs Buchstaben wie ABCDEF+ voran, um die Schriftart als Subset zu kennzeichnen, genau wie es die Spezifikation verlangt. Eine lateinische Schriftart, die auf zehn oder fünfzehn Kilobyte reduziert wird, macht den Unterschied zwischen einem schlanken PDF und einem PDF aus, das eine ganze Schriftart für eine einzige Überschrift mitliefert
Der Zeitpunkt, an dem dies geschieht, ist entscheidend. Subsetting ist keine Transformation, die Sie auf bereits auf der Festplatte befindliche Bytes anwenden. Es bearbeitet den Objektgraphen im Speicher: Es verkleinert den Inhalt des /FontFile2-Streams, korrigiert /Length1 und schreibt den String /BaseFont um. All das muss vorhanden sein, wenn der Serialisierer den Graphen durchläuft und Bytes ausgibt. Wenn die Bearbeitungen erst nach dem Schreiben der Bytes erfolgen, aktualisieren sie Objekte, die niemand jemals lesen wird
Das Symptom, und warum sich nichts beschwert
Landet das Subsetting auf der falschen Seite des Schreibvorgangs, ist das Symptom eine Ausgabe mit vollständigen Schriftarten und ohne jede Diagnose. Registrieren Sie eine Unicode-TrueType-Schriftart, erzeugen Sie ein normales Dokument, und das eingebettete Schriftobjekt kommt mit derselben Länge heraus wie die ursprüngliche .ttf-Datei, ohne sechs Buchstaben umfassendes Subset-Präfix beim Namen /BaseFont. Die Ausgabe schrumpft nie, egal ob ein Durchlauf zehn Glyphen verwendet oder zehntausend
Das Fehlen jeglicher Fehlermeldung macht diese Art von Fehler so kostspielig. Eine Subsetting-Routine, die zum falschen Zeitpunkt läuft, läuft trotzdem. Sie durchläuft die gesammelte Codepoint-Nutzung, erstellt ein absolut korrektes Subset und wendet es auf den Objektgraphen im Speicher an. Intern ist die Arbeit erledigt und der Aufruf wird sauber beendet. Das Einzige, was nicht stimmt, ist, dass der bearbeitete Objektgraph nicht mehr das ist, was geschrieben wird, da der Schreiber bereits fertig war. Aus Sicht des Aufrufers wurde das Dokument ohne Zwischenfälle erstellt und gespeichert, was genau den Eindruck eines lautlosen Fehlers erweckt
Wo das Subsetting in der Finalisierungsreihenfolge sitzt
In HotPDF finden die Abschlussarbeiten innerhalb von EndDoc statt. Der Subsetting-Schritt ist eine interne Routine namens BuildAndApplyUnicodeFontSubset. Es liest die pro Dokument verwendete Menge an Codepoints, die in einer Bitmap gespeichert wird, die beim Zeichnen von Glyphen gefüllt wird. Sie ordnet jeden verwendeten Codepoint über die zwischengespeicherte Codepoint-zu-Glyphe-Tabelle einer echten Glyphen-ID zu und schreibt das Schriftprogramm entsprechend um. Wenn eine Unicode-TrueType-Schriftart registriert wird, setzt der Ausgabepfad ein Bit im Satz der verwendeten Codepoints für jedes gezeichnete Zeichen, sodass die Engine beim Schließen des Dokuments genau weiß, welche Glyphen das Subset behalten muss
Die Falle ist die Position dieses Aufrufs. Wird BuildAndApplyUnicodeFontSubset aufgerufen, nachdem SaveToStream oder SaveToFile das Dokument bereits serialisiert haben, werden die Änderungen des Subsetters an /FontFile2, seine korrigierte /Length1 und das sechs Buchstaben umfassende /BaseFont-Präfix alle gegen einen Objektgraphen berechnet, der bereits in Bytes umgewandelt wurde. Die korrekte Sequenz führt zuerst den Subsetter aus und serialisiert danach, sodass der Schreiber die subsetierte Schriftart statt der ursprünglichen ausgibt. Es richtig zu machen ist eine Frage der Position eines einzigen Aufrufs, nicht mehr
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // das Subsetting läuft hier, vor dem Schreiben
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Nichts davon ist im aufrufenden Code sichtbar. Das Subsetting ist standardmäßig aktiviert, sobald eine Unicode-TrueType-Schriftart registriert wurde. Sie registrieren die Schriftart, beginnen das Dokument, zeichnen und beenden es, und das Subset wird aus den von Ihnen verwendeten Glyphen erstellt, bevor die Bytes den Speicher verlassen
Warum ein einziger falsch platzierter Schritt eine ganze Kategorie darstellt
Der Grund, warum dies eine Lektion und keine Fußnote wert ist, liegt darin, dass EndDoc eine Liste von Abschlussschritten ausgibt, von denen jeder einzelne empfindlich auf seine Position relativ zum Schreibvorgang reagiert. Das Font-Subsetting ist einer davon. Die PDF/A-Ausgabe erfordert einen /CIDSet-Stream, der genau die im Subset vorhandenen Glyphen-IDs auflistet - eine Einschränkung, die ISO 19005 auferlegt, damit ein Validator bestätigen kann, dass das eingebettete Programm mit den Angaben des Schriftart-Deskriptors übereinstimmt. Dieser Stream wird im selben Finalisierungsfenster ausgegeben und hängt davon ab, dass das Subset zuerst erstellt wurde. PDF/UA-1 erfordert gemäß ISO 14289-1 §7.18.3, dass jede Seite, die eine Anmerkung trägt, /Tabs mit dem Wert /S deklariert, und eine interne Routine namens EnsurePDFUATabsOnAnnotatedPages stempelt diesen Schlüssel während derselben Phase auf. Dort laufen auch Kontrollen zur Ausgabeabsicht (Output Intents)
Derselbe Reihenfolgefehler, der das Subsetting wirkungslos macht, würde auch den PDF/UA-Tabulatorreihenfolgen-Schlüssel auf Seiten mit Anmerkungen fallen lassen, denn dieser Schritt sitzt im selben Fenster. veraPDF und PAC melden ein fehlendes /Tabs /S als Verletzung des Matterhorn-Protokoll-Prüfpunkts 21-001. Ein einziger falsch platzierter Aufruf bläht also nicht bloß die Dateigröße auf; er bricht gleichzeitig stille eine Anforderung an die Barrierefreiheit, mit demselben Fehlen jeglicher Fehlermeldung. Das ist die Gefahr einer Finalisierungsphase: Ihre Schritte teilen sich eine Vorbedingung, und ein einziger Reihenfolgefehler kann mehrere von ihnen gleichzeitig außer Kraft setzen, während jeder Aufruf weiterhin Erfolg zurückgibt
Wie ein lautloser Ausgabefehler tatsächlich erkannt wird
Ein Ausfall, der keine Ausnahme auslöst, wird nicht durch Ausführen des Programms erkannt. Er wird erkannt, indem man die Ausgabe überprüft und mit dem vergleicht, was die Eingabe hätte erzeugen sollen. Beim Font-Subsetting sind die Prüfungen konkret. Vergleichen Sie die Ausgabedateigröße mit einer groben Erwartung: Ein Dokument, das nur eine Handvoll Glyphen verwendet, sollte nicht die Größe einer vollständigen Schriftart haben. Öffnen Sie das eingebettete Schriftobjekt und lesen Sie dessen Bytelänge; ein subsetiertes /FontFile2 für eine lateinische Schriftart ist ein kleiner Bruchteil der Quelldatei. Lesen Sie den Namen /BaseFont und bestätigen Sie, dass das sechs Buchstaben umfassende Präfix vorhanden ist, denn sein Fehlen ist ein direktes Signal dafür, dass kein Subset angewendet wurde
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// Ein paar Glyphen aus einem ~700 KB großen Font dürfen keinen Stream von mehreren hundert KB ergeben
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Für die PDF/A-Ausgabe ist die Prüfung noch strenger, da ein Validator die Arbeit für Sie erledigt. Legen Sie die Konformitätsstufe fest und führen Sie das Ergebnis durch veraPDF aus: Ein fehlendes /CIDSet oder ein Subset, das nicht mit dem Deskriptor übereinstimmt, wird als fehlgeschlagene Klausel gemeldet und bleibt nicht Ihrer visuellen Prüfung überlassen. Die Konformitätsschalter, die diese Finalisierungsarbeit steuern, sind Eigenschaften des Dokuments. PDFACompliance akzeptiert einen String wie '2B' für PDF/A-2 Level B, und PDFUACompliance ist ein Boolean, der die Anforderungen für getaggtes PDF und die Tabulatorreihenfolge einschaltet
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
Die Lektion für Entwickler
Daraus ergeben sich zwei Regeln. Die erste: Jeder Finalisierungsschritt, der Objekte verändert, muss ausgeführt werden, bevor diese Objekte serialisiert werden, und die Abschlussphase einer Dokumenten-Engine sollte als geordnete Pipeline gelesen werden, in der die Serialisierung die letzte Aktion ist und nicht eine Aktion unter mehreren. Die zweite ist die, die am leichtesten vergessen wird: Bei einem Ausgabeschritt ist das Fehlen eines Fehlers kein Beweis für den Erfolg. Eine Routine, die das richtige Subset erstellt und es auf den falschen, bereits geschriebenen Graphen anwendet, meldet nichts, denn aus ihrer eigenen Sicht war nichts falsch. Die Überprüfung muss das Artefakt betrachten, nicht den Rückgabecode. Prüfen Sie die Ausgabegröße, lesen Sie die Bytelängen der eingebetteten Schriftart und ihr /BaseFont-Präfix, und lassen Sie veraPDF die PDF/A-Ausgabe beurteilen, wo ein fehlendes /CIDSet ein lautloses Defizit in einen benannten Fehler verwandelt
Die Erstellerseite der Schriftarthandhabung, also wie Schriftarten registriert und für die Berichtsausgabe eingebettet werden, wird in unserem Artikel über Schriftarten und Bilder in der Berichtsausgabe behandelt. Die Validierungsseite, auf der diese Finalisierungsschritte mit den Standards abgeglichen werden, wird in der Anleitung zur PDF/A- und PDF/UA-Validierung beschrieben. Beide ergänzen sich mit der hier beschriebenen Subsetting- und Konformitätsarbeit, die als Teil der HotPDF-Delphi-Komponente für Delphi und C++Builder zusammen mit den APIs zum Laden, Bearbeiten, Verschlüsseln und Signieren ausgeliefert wird, die an anderer Stelle auf diesem Blog behandelt werden