Das PDF-Katalog-Dictionary (Catalog) hat genau einen erforderlichen Navigationsschlüssel: /Pages. Dieser Schlüssel muss auf ein indirektes Objekt vom Typ /Pages zeigen, welches wiederum das /Kids-Array und die gesamte /Count (Anzahl) der Seiten enthält. Nimmt man diesen Zeiger weg, kann kein konformer Reader auch nur eine einzige Seite in der Datei ausfindig machen. ISO 32000-1 §7.7.2 ist in diesem Punkt unmissverständlich: Der Katalog muss einen /Pages-Eintrag haben, und das referenzierte Objekt muss den Typ /Pages aufweisen. Dateien, die diese Anforderung verletzen, sind nicht nur nicht konform; sie sind strukturell derart defekt, dass die meisten Parser schlecht damit umgehen können
Was die Spezifikation tatsächlich sagt
Ein minimales konformes PDF hat mindestens drei Objekte. Objekt 1 ist der Katalog, Objekt 2 ist der Pages-Root, und ab Objekt 3 folgen die einzelnen Seiten-Dictionaries. Der Katalog verweist auf den Pages-Root; der Pages-Root listet seine Kinder unter /Kids auf; jede Seite trägt eine /Parent-Rückreferenz. Die gesamte Kette ist per Design bidirektional, sodass ein Parser an beiden Enden beginnen und zu jeder beliebigen Seite in O(log n)-Zeit (für ausbalancierte Bäume) navigieren kann
% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
Der Seitenbaum kann verschachtelt sein. Bei einem Dokument mit Tausenden von Seiten werden die Seiten typischerweise in Zwischenknoten-Objekten gruppiert, die ebenfalls den Typ /Pages aufweisen, jedes mit seinen eigenen /Kids und einem /Count, das den darunter liegenden Teilbaum widerspiegelt. Der /Count des Wurzelknotens entspricht immer der gesamten Seitenanzahl. Dieser Wert ist es, was Viewer im Feld für die Seitenzahl anzeigen, noch bevor sie eine einzige Seite geparst haben, da das Lesen einer einzelnen Ganzzahl aus Objekt 2 wesentlich kostengünstiger ist, als den gesamten Baum zu durchlaufen
Wie eine Datei ohne Pages aussieht
Dateien, denen das Pages-Dictionary fehlt, stammen typischerweise von PDF-Generatoren, die Seitenobjekte direkt schreiben, ohne sie zu einem Baum zusammenzufügen, oder von Datenbeschädigungen, die den Wurzelknoten entfernen, während die als Blatt fungierenden Seitenobjekte intakt bleiben. Der Katalog in solch einer Datei besitzt entweder den Schlüssel /Pages gar nicht, oder er enthält eine Referenz auf ein Objekt, das in der Querverweistabelle nicht mehr existiert
% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj
% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj
25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj
Ein Parser, der der Spezifikation folgt, wird den Katalog lesen, versuchen, /Pages aufzulösen, nichts (oder eine tote Referenz) finden und dann entweder einen Fehler werfen oder null Seiten melden. Was er nicht tun darf, ist fortzufahren, als ob die Datei null Seiten hätte, und stillschweigend Erfolge zu melden; dies erzeugt eine leere Ausgabe, die für automatisierte Tools korrekt aussieht, aber für jeden Menschen, der sie öffnet, falsch ist
Warum Parser abstürzen
Die meisten PDF-Parser reservieren ihre interne Seitentabelle beim Ladevorgang basierend auf dem /Count-Wert aus dem Pages-Root. Wenn dieser Root fehlt, liest der Parser entweder null, reserviert nichts und dereferenziert dann beim ersten Abruf von Seite 1 einen Nullzeiger, oder er liest Datenmüll und reserviert einen völlig falschen Puffer. Keines dieser Ergebnisse ist elegant. Die Zugriffsverletzung (Access Violation) bei 0x008E5D78, die in Absturzprotokollen bei der Verarbeitung einer solchen Datei auftaucht, ist genau das: eine Nullzeiger-Dereferenzierung innerhalb des Seitenzugriffspfads, ausgelöst durch das Fehlen der Struktur, von der der Parser ausging, dass sie immer da sein würde
Die zugrunde liegende Entwurfsannahme ist vernünftig. Die überwiegende Mehrheit aller existierenden PDFs besitzt ein Pages-Dictionary. Parser, die die Existenzprüfung überspringen, um ein paar Anweisungen zu sparen, handeln nicht leichtsinnig; sie optimieren für den Normalfall. Die Dateien, die diese Optimierung bestrafen, sind so selten, dass Produktivcode möglicherweise nie einer begegnet, bis es dann doch passiert – an diesem Punkt ist der Absturz reproduzierbar und für den Entwickler, der §7.7.2 nicht gelesen hat, rätselhaft
Wiederherstellung ohne einen Seitenbaum
Wenn ein Parser solche Dateien verarbeiten muss, statt sie abzulehnen, folgt die Wiederherstellung einem vorhersehbaren Pfad: Scanne jedes indirekte Objekt in der Querverweistabelle, sammle diejenigen mit /Type /Page und sortiere sie nach Objektnummer. Die Objektnummernreihenfolge ist in der Spezifikation zwar nicht als Lesereihenfolge garantiert, aber in der Praxis neigen Generatoren, die den Seitenbaum weglassen, dazu, Seiten sequenziell auszugeben, sodass die Objektnummernreihenfolge öfter korrekt als falsch ist
Die Prüfung selbst ist günstig. Bevor man dem /Pages-Zeiger des Katalogs folgt, ist zu bestätigen, dass der Zeiger existiert, dass er sich zu einem echten Objekt auflöst und dass der /Type des aufgelösten Objekts gleich /Pages ist. Wenn eine dieser drei Bedingungen nicht erfüllt ist, fange dies durch den linearen Scan ab. Dieser Scan ist bei großen Dokumenten langsamer als die Traversierung des Baums, weil er jeden Objekt-Header liest, anstatt einem balancierten Pfad zu folgen, aber er funktioniert, und bei einer ohnehin fehlerhaften Datei hat Korrektheit Vorrang vor Geschwindigkeit
Ein Randfall, den der lineare Scan nicht automatisch löst: die Seitenreihenfolge. Ohne ein /Kids-Array, das die Sequenz definiert, ist die "korrekte" Reihenfolge durch die Spezifikation nicht definiert. Die Objektnummernreihenfolge ist der pragmatische Standard; falls die Datei wichtig genug ist, um sie sorgfältig zu verarbeiten, lohnt sich der zusätzliche Aufwand zu prüfen, ob die Seitenobjekte explizite /StructParents oder Annotationsreferenzen tragen, die eine Lesereihenfolge implizieren
Implikationen für PDF-Generatoren
Für jeden, der eher einen PDF-Generator als einen Parser schreibt, ist die Lehre eng gefasst: Gib immer den Pages-Root aus, bevor du die Datei schließt. Ein Katalog ohne einen /Pages-Eintrag ist unter keiner Revision der Spezifikation ein gültiges PDF. Generatoren, die Seitenobjekte on-the-fly erstellen und den Baum bei der Finalisierung zusammenstellen (der Ansatz, den die meisten Streaming-Writer nutzen), sind in Ordnung, solange die Finalisierung tatsächlich durchläuft. Der häufige Fehlermodus ist eine Ausnahme oder ein vorzeitiger Rücksprung, der den Schreibvorgang abbricht, bevor der Trailer komplett ist, was eine Datei zurücklässt, die sich in einigen Viewern (die über Wiederherstellungsheuristiken verfügen) öffnet und in anderen (die diese nicht haben) fehlschlägt
PDF/A und PDF/UA erlegen dem Seitenbaum zusätzliche Einschränkungen auf, die über die Anforderungen der Basisspezifikation hinausgehen, aber keines davon lockert die /Pages-Vorgabe auf. Ein Validator, der die Konformität mit ISO 19005 oder ISO 14289 prüft, wird ein fehlendes Pages-Dictionary als Verstoß gegen die Basisspezifikation abfangen, noch bevor er die profilspezifischen Regeln erreicht