Ein Rechnungsarchiv kann sauber rendern, drucken, jede Schrift einbetten und einen gültigen Output Intent tragen, und ein PDF/A-Validator weist es trotzdem zurück, weil ein Resource-Dictionary 4.096 Einträge hält. Die Decke liegt bei 4.095. PDF/A erbt eine kleine Tabelle von Implementierungsgrenzen aus der PDF Reference, und eine Datei, die um eine Einheit über einer davon liegt, fällt bei der Konformität durch, obwohl kein Viewer sich beschwert
Der andere Fehlschlag, der im selben "sieht aber ok aus"-Fach landet, ist eine symbolische TrueType-Schrift, die einen /Encoding-Eintrag trägt. Kontrollkästchen, Barcodes und Dingbats kommen auf dem Bildschirm richtig heraus, und der Validator markiert trotzdem das Font-Dictionary. Beide Probleme leben im Objektgraph statt in irgendetwas, das ein Mensch auf der Seite sehen kann, also lohnt es sich, sie mechanisch zu prüfen, bevor eine Datei in ein Archiv geht
Die Zahlen, die PDF/A erbt, und wo die Grenze verläuft
PDFium Component prüft bei jeder PDF/A-Validierung vier Container-Grenzen: Ein Name-Token darf höchstens 127 Bytes lang sein, ein Array höchstens 8.191 Elemente halten, ein Dictionary höchstens 4.095 Einträge, und Arrays und Dictionaries dürfen höchstens 28 Ebenen tief verschachtelt sein. Alle vier Grenzen sind inklusiv. Ein 127-Byte-Name besteht und ein 128-Byte-Name fällt durch; ein Array mit exakt 8.191 Elementen besteht und ein Element mehr lässt es fallen
Die Namen-, Array- und Dictionary-Grenzen stammen aus der Ära, in der ein konformer Reader nur Strukturen bis zu diesen Größen beherrschen musste. Ein aktueller Desktop-Viewer öffnet vielleicht ein viel größeres Array ohne Murren, aber eine Archivdatei soll sich in Readern öffnen, gegen die noch niemand getestet hat, eingeschlossen alte, die ein Records-System noch laufen haben kann. Deshalb behält PDF/A die historischen Decken bei, statt sie zu lockern
Inklusiv versus exklusiv ist die Stelle, an der handgestrickte Prüfungen meist scheitern. Ein als Count >= 8191 statt Count > 8191 geschriebener Test weist Dateien zurück, die konform sind, und ein Validator, der konforme Dateien zurückweist, wird trotzdem geglaubt, also wird der Generator "repariert" für ein Problem, das er nie hatte. Die Funktion ValidatePdfACompliance in der Unit FPdfPdfa zählt strikt jenseits der Grenze und liest die Datei-Bytes direkt, also läuft die Prüfung, ohne die PDFium-DLL zu laden
uses
System.Classes, FPdfPdfa;
procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Res: TPdfAValidationResult;
begin
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfACompliance(Src); // Byte-Scan, keine PDFium-DLL nötig
if pvaiNameOverLimit in Res.Issues then
Log.Add('A name token is longer than 127 bytes');
if pvaiArrayOverLimit in Res.Issues then
Log.Add('An array holds more than 8191 elements');
if pvaiDictOverLimit in Res.Issues then
Log.Add('A dictionary holds more than 4095 entries');
if pvaiNestingOverLimit in Res.Issues then
Log.Add('Arrays and dictionaries nest deeper than 28 levels');
if Res.IsCompliant then
Log.Add('No PDF/A findings');
finally
Src.Free;
end;
end;
Die vier Flags sind Member der Issues-Menge an TPdfAValidationResult, neben allen anderen PDF/A-Fundstellen, und IsCompliant gibt nur dann True zurück, wenn eine PDF/A-Identifikation gefunden wurde und die Menge leer ist. Eine Datei ohne jegliche PDF/A-Identifikation wird gegen den strengen PDF/A-1-Regelsatz geprüft, also gelten die Container-Grenzen auch für sie
Welche Generatoren laufen in diese Grenzen?
Fast immer diejenigen, die Struktur in einer Schleife aufbauen. Ein Formular mit mehreren Tausend Feldern erzeugt ein AcroForm-/Fields-Array oder ein Seiten-/Annots-Array, das über 8.191 Einträge hinwegschreitet. Eine Report-Engine, die je erzeugtem Objekt eine Font- oder Bild-Ressource registriert und nie einen bestehenden Eintrag wiederverwendet, wächst ein /Resources-Sub-Dictionary über 4.095 Schlüssel hinaus. Ein getagtes Dokument, dessen Strukturbaum durch Rekursion über ein verschachteltes Datenmodell entsteht, kann 28 Ebenen passieren, ohne dass jemand hinschaut
Lange Namen haben einen anderen Ursprung: Daten, die in Name-Tokens verpackt werden. Ein Separations-Colorant, benannt nach einer Kundenreferenz, eine Optional-Content-Gruppe, benannt nach einem Dateipfad, ein Feld-Label, zusammengesetzt aus sechs Hierarchie-Ebenen. Namen kosten nichts zu erzeugen, und 127 Bytes sind schnell verbraucht, sobald ein UTF-8-Label mit akzentuierten Zeichen im Spiel ist, denn jedes dieser Zeichen nimmt zwei oder drei Bytes. Die 127 Bytes werden nach dem /-Einleiter gezählt, bis zum nächsten Trenner oder Whitespace
Die Abhilfe ist jedes Mal strukturell. Das Array splitten, das Dictionary splitten, die Rekursion abflachen, den Namen kürzen. PDF/A-Marker in die Datei zu injizieren kann bei keiner dieser Fundstellen helfen, denn das Problem ist die Form des Objektgraphs, und die XMP-Metadaten sagen nichts darüber
Wie der Scan Elemente und Einträge zählt
Die Zählregel zählt, wenn Sie eine Fundstelle in einem Hex-Editor nachstellen wollen. Ein verschachteltes Array oder Dictionary zählt als ein Element seines Elternteils, so ist die Grenze formuliert: Ein 10-Elemente-Array aus 1.000-Elemente-Arrays ist in Ordnung, denn kein einzelnes Array übersteigt 8.191. Innerhalb eines Dictionaries werden nur die Schlüssel gezählt, also bläht ein Wert, der zufällig ein Name ist, die Eintragssumme nicht auf. Hex-Strings wie <FEFF0041> werden als einzelne Tokens übersprungen, also werden ihre spitzen Klammern nie mit Dictionary-Verschachtelung verwechselt
Der Scan deckt die Objektstruktur der ganzen Datei ab, eingeschlossen Objekte, die in komprimierten Object-Streams gespeichert sind, die vor dem Zählen expandiert werden. Die Stream-Körper selbst, etwa Seiteninhalte und Bilddaten, werden zuerst ausgeblendet, also kann eine Folge von Binärbytes, die zufällig ein / oder [ enthält, keinen falschen Namen- oder Array-Befund auslösen
Es gibt eine benachbarte numerische Grenze, die dieselbe Validierung unter ihrem eigenen Flag meldet. Deklariert die Datei PDF/A-2 oder einen späteren Part, wird pvaiIntegerImplementationLimit für jedes Integer-Token außerhalb des 32-Bit-Signed-Bereichs ausgelöst, also außerhalb von -2.147.483.648 bis 2.147.483.647. Generatoren, die Byte-Offsets oder große Zähler als schlichte Integer schreiben, sind die übliche Quelle
Warum muss eine symbolische TrueType-Schrift /Encoding abgeben?
ISO 19005-1 §6.3.7 lässt als Zeichen-Mapping einer symbolischen TrueType-Schrift nur die eingebaute cmap des Font-Programms zu. Eine symbolische Schrift mappt Codes nach eigenen Regeln auf Glyphen; genau das erklärt das Symbolic-Flag. Fügt man einen /Encoding-Eintrag hinzu, gibt die Datei nun zwei Antworten auf die Frage "welche Glyphe wählt das Byte 0x41", ohne dass etwas in der Datei sagt, welche gewinnt. Reader lösen den Konflikt unterschiedlich, und dasselbe Kontrollkästchen kann im einen Viewer als Häkchen und im anderen als Buchstabe A herauskommen
ValidatePdfACompliance findet jedes Font-Dictionary mit /Subtype /TrueType, prüft, ob es einen /Encoding-Schlüssel hat, und liest dann Bit 3 (Wert 4) des /Flags-Eintrags im Font-Deskriptor. Dem Deskriptor wird gefolgt, ob er inline geschrieben oder als indirektes Objekt referenziert ist. Eine nicht-symbolische TrueType-Schrift, die /WinAnsiEncoding oder /MacRomanEncoding behält, wird von dieser Prüfung nicht markiert, denn für nicht-symbolische Schriften erwartet PDF/A exakt diese Kodierung
// Res kommt aus ValidatePdfACompliance, wie im ersten Beispiel
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
'the font''s built-in cmap (ISO 19005-1 6.3.7)');
// Eine benachbarte Font-Regel mit anderem Auslöser: eine NICHT-symbolische
// TrueType-Schrift, deren Encoding-Dictionary /Differences trägt
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');
Die Schriften, die diese Rule triggern, sind vorhersagbar: Symbol, Wingdings, Barcode-Schriften, Icon-Schriften. Sie tauchen in Geschäftsdokumenten als Kontrollkästchen, Bewertungssterne und Barcodes auf, und ein Subsetting-Schritt, der jede TrueType-Schrift gleich behandelt, schreibt auch für sie eine Kodierung. Sie sind auch die Schriften, denen niemand Beachtung schenkt, wenn ein Validator "Font-Problem" sagt, denn die Fließtext-Schriften werden zuerst geprüft
Der umgekehrte Fehler taucht ebenfalls in der Wildnis auf: Ein Produzent setzt das Symbolic-Flag an einer gewöhnlichen Textschrift, die in Wahrheit WinAnsi-kodiert ist. Die PDF/A-Prüfung folgt der Klausel, wie sie geschrieben ist, und meldet diese Schrift, und hier gehört die Reparatur in den Deskriptor: das Flag im Deskriptor korrigieren statt die Kodierung zu löschen. /Encoding aus einer falsch etikettierten Textschrift zu entfernen ließe den Text davon abhängen, welche cmap das Subset zufällig enthält, und das ist eine schlechtere Datei als die Ausgangsdatei. Vor der Wahl, welche Hälfte des Widerspruchs entfernt wird: erst den Glyphensatz der Schrift ansehen
Fundstellen im Preflight-Report lesen
Die Preflight-Report-Unit kommt mit der Enterprise Edition, während ValidatePdfACompliance in jeder Edition steckt. Der Report in der Unit FPdfPreflightReport sortiert die vier Container-Grenzen in die Kategorie Structure und die symbolische-TrueType-Encoding-Fundstelle in die Kategorie Content. Diese Aufteilung ist mit Absicht. Structure-Fundstellen gehen meist zurück an denjenigen, der den Generator pflegt, während Content-Fundstellen meist an denjenigen gehen, der Schriften und Vorlagen geliefert hat, und ein Report, der beide mischt, schickt beide Personen Problemen hinterher, die nicht ihre sind
Jede Fundstelle trägt ihren Enum-Namen als Code und eine Empfehlung, die die Abhilfe in konkreten Worten nennt, etwa "Dictionaries splitten, sodass keines mehr als 4095 Einträge trägt". Beachten Sie, dass die Kategorie Structure auch zwei weitere Fundstellen hält, eine fehlende getaggte Struktur bei einem Level-A-Anspruch und PDF-1.5-Cross-Reference- oder Object-Streams in einer PDF/A-1-Datei, also nach Code filtern, wenn Sie nur die vier Grenzen wollen
uses
System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;
procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
Pdf: TPdf;
Report: TPdfPreflightReport;
Issues: TPdfPreflightIssues;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // dieser Pfad lädt die PDFium-DLL
if not Pdf.Active then
Exit;
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
Issues := Report.FilterByCategory('Structure').FlattenIssues;
for I := 0 to High(Issues) do
Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
finally
Pdf.Free;
end;
end;
Die beiden Pfade beantworten leicht verschiedene Fragen. BuildPdfPreflightReport geht über TPdf.ValidatePdfA, das das offene Dokument in eine temporäre Kopie speichert, auf dieser Kopie denselben Byte-Scan laufen lässt und dann Prüfungen hinzufügt, die die PDFium-Engine brauchen, etwa Schriften, die auf einer Seite verwendet, aber nicht eingebettet sind. ValidatePdfACompliance urteilt über die exakten Bytes auf der Platte. Für Implementierungsgrenzen ist der Byte-Pfad auf der Originaldatei der vertrauenswürdige: Er meldet über die Datei, die Sie gleich archivieren, und er läuft in einem Build-Schritt oder einem abgeschotteten Dienst, in dem sich keine native DLL deployen lässt. Den Report-Pfad verwenden, wenn Sie zusätzlich die Engine-Prüfungen und die JSON-, SARIF- oder JUnit-Ausgabe für eine Pipeline wollen
Wie es weitergeht
Diese Prüfungen über einen Ordner laufen zu lassen, zu entscheiden, welche Fundstellen einen Release blockieren, und das Ergebnis in einen Report zu verwandeln, den Leute lesen, ist im Durchgang durch PDF/A-Preflight-Validierung in Delphi und in der Batch-Preflight-Report-CLI behandelt. Welchen PDF/A-Part und welches Konformitätslevel man überhaupt anzielt, ist Thema der Hinweise zu PDF/A-Archiv-Konformität; diese Wahl entscheidet unter anderem, ob die Integer-Grenze gilt
Die hier gezeigten Validatoren kommen mit PDFium Component für Delphi, C++Builder und Lazarus, neben Rendering, Formular-Füllung und Bearbeitung auf der PDFium-Engine. Die Produktseite von PDFium Component listet die unterstützten Konformitätsstandards, Editionen und Compiler-Versionen auf