Technischer Artikel

PDF/A-Implementierungsgrenzen und Font-Encoding-Prüfungen

PDFium Component validiert die Implementierungsgrenzen aus ISO 19005-1 Annex C — 127-Byte-Namenstokens, 8191-Array-Elemente, 4095-Dictionary-Einträge und 28 Ebenen Container-Verschachtelung — und meldet eine symbolische TrueType-Schrift, die einen /Encoding-Eintrag trägt. Beide Prüfungen laufen auf dem Byte-Scan-Pfad, sodass eine Delphi- oder Lazarus-Anwendung das Urteil erhält, ohne die PDFium-DLL überhaupt zu laden

Dies sind die Fehlschläge, die Leute am meisten ratlos machen, denn das Dokument sieht einwandfrei aus. Es rendert, es druckt, jede Schrift ist eingebettet, die Ausgabe-Absicht ist vorhanden. Dann lehnt ein Validator es über einem Dictionary ab, das 4096 Einträge hat, und nichts im sichtbaren Dokument erklärt, warum

Was schützen die Annex-C-Grenzen tatsächlich?

Interoperabilität mit Implementierungen, die älter sind als der eigene Generator. Annex C führt die Implementierungsgrenzen der PDF-Reference in jeden PDF/A-Part fort, und die Zahlen sind nicht willkürlich — sie beschreiben, was ein konformer Reader historisch zu bewältigen hatte. Eine Datei, die sie überschreitet, mag in einem modernen Betrachter einwandfrei öffnen und in dem Archiv-Reader scheitern, auf den ein Aktensystem vor fünfzehn Jahren standardisiert hat, was genau das Szenario ist, das PDF/A verhindern soll

Die vier Grenzen sind inklusiv. Ein Namenstoken von exakt 127 Bytes validiert; 128 nicht. Ein Array mit exakt 8191 Elementen validiert; 8192 nicht. PDFium Component pinnt beide Seiten jeder Grenze in seiner Test-Suite fest, denn ein Off-by-one in einer Grenz-Prüfung erzeugt die schlimmste Art Validator: eine, die konforme Dateien ablehnt und der trotzdem geglaubt wird

uses FPdfPdfa;

var
  Src: TFileStream;
  Res: TPdfAValidationResult;
begin
  Src := TFileStream.Create('archive.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfACompliance(Src);
    if pvaiArrayOverLimit in Res.Issues then
      Memo1.Lines.Add('An array carries more than 8191 elements');
    if pvaiDictOverLimit in Res.Issues then
      Memo1.Lines.Add('A dictionary carries more than 4095 entries');
    if pvaiNestingOverLimit in Res.Issues then
      Memo1.Lines.Add('Containers nest deeper than 28 levels');
    if pvaiNameOverLimit in Res.Issues then
      Memo1.Lines.Add('A name token is longer than 127 bytes');
  finally
    Src.Free;
  end;
end;

Welche Generatoren treffen diese Grenzen tatsächlich?

Welche, die Struktur programmatisch aufbauen, und das ist die meisten Line-of-Business-Ausgaben. Ein Formular mit mehreren tausend Feldern erzeugt ein /Annots-Array oder ein AcroForm-/Fields-Array, das über 8191 hinauswächst. Eine Seite, deren Ressourcen-Dictionary pro erzeugtem Bild oder pro Schrift-Instanz einen Eintrag akkumuliert, kreuzt 4095. Tief generierte Strukturbäume — ein getaggtes Dokument, das per Rekursion über ein verschachteltes Datenmodell gebaut wird — gehen über 28 Ebenen hinaus, ohne dass es jemand bemerkt, denn niemand schaut auf Verschachtelungstiefe

Lange Namen kommen aus einer anderen Gewohnheit: Daten in Namenstokens zu enkodieren. Ein Farbstoff-Name, der aus einer Kundenkennung gebaut ist, eine Optional-Content-Gruppe, die nach einem vollen Dateipfad benannt ist, ein Formularfeld, dessen vollqualifizierter Name sechs Ebenen einer Hierarchie aneinanderhängt. Namen sind billig zu erzeugen und leicht lang zu machen, und 127 Bytes verschwinden schneller, als man erwartet, sobald ein UTF-8-enkodiertes Label beteiligt ist

Die Abhilfe ist in jedem Fall strukturell. Das Array teilen, das Dictionary teilen, die Verschachtelung abflachen, den Namen kürzen — die Preflight-Empfehlung für jedes Issue benennt die konkrete Grenze statt mitzuteilen, die Datei sei ungültig. Marker-Injection hilft hier nicht: Dies sind keine Metadaten-Behauptungen, sondern die Form des Objekt-Graphen

Warum darf eine symbolische TrueType-Schrift kein /Encoding tragen?

Weil ISO 19005-1 §6.3.7 für symbolische TrueType-Schriften nur die eingebaute cmap der Schrift zulässt und ein /Encoding-Eintrag ihr widerspräche. Eine symbolische Schrift bildet Codes auf Glyphen auf ihre eigene Art ab — das ist es, was symbolisch bedeutet. Eine Enkodierungs-Tabelle hinzuzufügen liefert nun zwei Antworten auf die Frage „welchen Glyphen wählt Byte 0x41 aus", ohne eine Regel in der Datei, die entscheidet, welche gewinnt. Verschiedene Reader lösen das unterschiedlich, und ein Dokument, das in einem Betrachter als Text rendert, rendert in einem anderen als Dingbats

PDFium Component liest das symbolische Flag aus dem /FontDescriptor, gleich ob der Deskriptor inline im Schrift-Dictionary oder indirekt referenziert geschrieben ist. Eine nicht-symbolische TrueType-Schrift behält ihr gefordertes /WinAnsiEncoding oder /MacRomanEncoding, ohne beanstandet zu werden, denn für nicht-symbolische Schriften ist die Enkodierung genau das, was der Standard verlangt. Die Prüfung feuert auf den Widerspruch, nicht auf das Vorhandensein einer Enkodierung

if pvaiSymbolicTrueTypeEncoding in Res.Issues then
  Memo1.Lines.Add(
    'A symbolic TrueType font carries /Encoding; PDF/A admits only its ' +
    'built-in cmap (ISO 19005-1 6.3.7)');

Die praktische Quelle dieses Defekts ist Font-Subsetting durch einen Produzenten, der jede TrueType-Schrift gleich behandelt. Symbol, Wingdings, Barcode-Schriften und Icon-Schriften sind die üblichen Träger — genau die Schriften, die ein Geschäftsdokument für Checkboxen, Logos und Barcodes verwendet, und genau die, die niemand erneut prüft, wenn ein Dokument über „Schriften" an der Validierung scheitert

Wie die Issues in einen Preflight-Bericht gelangen

Die vier Container-Grenzen werden unter Struktur klassifiziert; das symbolische-TrueType-Enkodierungs-Issue wird unter Inhalt klassifiziert. Diese Aufteilung zählt, wenn ein Bericht an zwei verschiedene Personen geht: Struktur-Befunde gehören üblicherweise zu dem, der den Generator geschrieben hat, und Inhalts-Befunde zu dem, der die Assets geliefert hat

Jedes Issue trägt eine Empfehlung, die das Heilmittel in konkreten Begriffen nennt — Namenstokens auf 127 Bytes oder kürzer bringen, Arrays so teilen, dass keines mehr als 8191 Elemente trägt, /Encoding aus symbolischen TrueType-Schriften entfernen. Ein Bericht, der „nicht PDF/A-konform" sagt, beginnt eine Untersuchung. Ein Bericht, der sagt, welche Grenze überschritten wurde und wodurch, beendet eine

Validieren ohne die DLL, und warum das hier zählt

Alle obigen Prüfungen laufen gegen die Datei-Bytes, sodass sie in einem Dienst funktionieren, der keine PDFium-Binärdatei ausgeliefert hat, in einem Build-Schritt oder auf einer Maschine, auf der das Laden einer nativen DLL ein Policy-Problem ist. Das ist eine bewusste Design-Linie in PDFium Component: Die Prüfungen, die aus Struktur beantwortet werden können, werden aus Struktur beantwortet, und die DLL ist denjenigen vorbehalten, die genuinely eine Render-Engine brauchen

Für den umliegenden Workflow — Validierung über einen Ordner laufen lassen, Berichte erzeugen und entscheiden, was mit den Befunden geschieht — siehe die Erläuterungen zur PDF/A-Preflight-Validierung in Delphi und zum Batch-Preflight-Bericht-CLI. Für die Wahl des Archiv-Profils, die über allen diesen Prüfungen steht, behandeln die Hinweise zur PDF/A-Archiv-Konformität, welchen Part und welches Level man anstrebt, bevor man anfängt, Befunde zu beheben

PDFium Component hüllt die PDFium-Engine für Delphi, C++Builder und Lazarus mit einer High-Level-VCL-API und einer Reihe Konformitäts-Validatoren ein, die mit oder ohne DLL laufen — siehe die PDFium-Component-Produktseite für die unterstützten Standards und Plattformen