Technischer Artikel

Font-Subsetting und EndDoc-Finalisierungsreihenfolge

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

HotPDF: Font-Subsetting führt eine vollständige TrueType-Schrift durch eine verwendete-Glyphen-Abschlussmenge und schreibt glyf und loca neu, bevor die FontFile2-, Length1- und BaseFont-Objekte im Speicher aktualisiert werden
Subsetting baut glyf und loca um die Glyphen neu, die ein Dokument tatsächlich verwendet, und aktualisiert dann /FontFile2, /Length1 und das /BaseFont-Präfix im Speicher. Alles muss stehen, bevor irgendein Byte geschrieben 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

HotPDF-Zeitleiste: Vergleich einer EndDoc-Reihenfolge, in der BuildAndApplyUnicodeFontSubset nach SaveToStream läuft, mit der korrekten Reihenfolge, in der das Subsetting vor der Serialisierung läuft
Einen Aufruf vor das Schreiben zu ziehen ändert nichts am aufrufenden Code, entscheidet aber, ob die gespeicherten Bytes eine vollständige Schrift oder eine echte Teilmenge enthalten. Ein Schritt auf der falschen Seite der Serialisierung kehrt sauber zurück und tut nichts
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

HotPDF: vier in EndDoc eingereihte Finalisierungsschritte, darunter Font-Subsetting, CIDSet-Ausgabe und PDF/UA-Tabs-Stempeln, alle speisen den Serialisierer, bevor ein einziges Byte existiert
Subsetting, die /CIDSet-Ausgabe und der /Tabs /S-Stempel teilen sich einen Finalisierungs-Slot vor der Serialisierung. Ein falsch platzierter Aufruf kann sowohl den Schrift-Subset als auch eine Barrierefreiheitsanforderung fallen lassen, während jede Routine weiterhin Erfolg meldet

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