Technischer Artikel

PDF-Seitenreihenfolge: Wie der Seitenbaum die Seitenabfolge steuert

Objekt Nummer 1 ist nicht Seite 1. Diese einzige Tatsache bringt mehr PDF-Verarbeitungscode durcheinander als jeder andere Aspekt des Formats. Um zu verstehen, warum das so ist, muss man über das hinausblicken, was ein Viewer anzeigt, und sich den Objektgraphen ansehen, den der Viewer tatsächlich liest

Eine PDF-Datei ist eine Sammlung nummerierter indirekter Objekte. Seiten gehören zu diesen Objekten, aber ihre Anzeigereihenfolge hat nichts damit zu tun, wo sie sich in der Datei befinden oder welche Nummern sie tragen. Die Anzeigereihenfolge wird vollständig durch den /Pages-Baum bestimmt, eine verknüpfte Struktur, die im Dokumentkatalog verwurzelt ist. Wenn Sie den Baum ignorieren und Objekte numerisch scannen, fügen Sie die Seiten bei einem großen Teil realer Dateien in der falschen Reihenfolge zusammen

Der Seitenbaum: was die Reihenfolge tatsächlich bestimmt

Jedes PDF beginnt mit einem Dokumentkatalog (ISO 32000-2 §7.7.2). Der Katalog enthält einen /Pages-Eintrag, der auf den Wurzelknoten des Seitenbaums verweist. Dieser Wurzelknoten ist ein Wörterbuch mit /Type /Pages, einem /Kids-Array mit indirekten Referenzen und einem /Count, der die Gesamtanzahl der Blattseiten darunter angibt. Die Anzeigereihenfolge ist der tiefenbasierte Durchlauf dieses Baums von links nach rechts, Punkt

Eine minimale dreiseitige Datei macht dies konkret:

%PDF-1.7

1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [20 0 R  4 0 R  9 0 R] /Count 3 >>
endobj

% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

Das Array /Kids lautet [20 0 R 4 0 R 9 0 R], also ist Objekt 20 Seite 1, Objekt 4 ist Seite 2 und Objekt 9 ist Seite 3. Die Objektnummerierung ist irrelevant. Jeder Code, der Objekte in numerischer Reihenfolge durchläuft und diejenigen mit /Type /Page sammelt, erzeugt bei dieser Datei die falsche Sequenz

Warum erzeugen Generatoren nicht fortlaufende Layouts? Dafür gibt es mehrere Gründe. Eine Bibliothek, die Objektnummern für alle Seiten vorab zuweist, bevor sie deren Inhalt schreibt, nummeriert sie in der Reihenfolge ihrer Erstellung und schreibt die tatsächlichen Bytes dann in der Reihenfolge, die für den Serialisierer geeignet ist. Ein Zusammenführungstool, das Dokumente zusammenfügt, nummeriert Objekte aus jedem Quelldokument neu, um Kollisionen zu vermeiden. Die neu nummerierten Seitenobjekte werden am Ende über die kombinierte Objekttabelle verstreut, während das neue Root-/Kids-Array die richtige Anzeigereihenfolge enthält. Bei inkrementellen Aktualisierungen werden neue Objekte mit neuen Nummern am Ende der Datei angehängt. Daher befindet sich eine als Revision hinzugefügte Seite in der Nähe des Endes des Bytestreams, selbst wenn sie an Position 1 der Anzeigereihenfolge gehört

Flache Bäume und verschachtelte Teilbäume

Die Spezifikation lässt zwei Formen für den Seitenbaum zu. Einfache Generatoren erzeugen eine flache Struktur: ein Root-/Pages-Knoten, dessen /Kids-Array nur /Page-Blattobjekte enthält. Das lässt sich leicht durchlaufen: eine Ebene tief, ein Durchgang

Große Dokumente verwenden stattdessen routinemäßig einen ausgeglichenen Baum. Das /Kids-Array des Root-/Pages-Knotens enthält zwischengeschaltete /Pages-Knoten, von denen jeder wiederum sein eigenes /Kids-Array enthält. Der Wert /Count bei jedem Zwischenknoten gibt die Gesamtzahl der Blattseiten in seinem Teilbaum an, sodass ein Betrachter ganze Teilbäume überspringen kann, wenn er über den Index zu einer Seite springt, ohne jedes Objekt analysieren zu müssen. Bei einem Dokument mit 1.000 Seiten, das als ausgewogener Baum mit 10 Seiten pro Blattknoten strukturiert ist, kann Seite 750 durch binäre Suche in drei oder vier Wörterbuchsuchen gefunden werden, anstatt 750 /Kids-Einträge scannen zu müssen

Die Konsequenz für den Verarbeitungscode: Sie können nicht davon ausgehen, dass die erste Ebene der /Kids-Objekte des Typs /Page enthält. Jeder untergeordnete Knoten muss überprüft werden. Wenn der /Type /Pages ist, wird er rekursiv durchlaufen. Wenn der /Type /Page ist, handelt es sich um ein Blatt. Ein Anhalten auf der ersten Ebene führt dazu, dass bei jedem Dokument, das vom Generator verschachtelt wurde, stillschweigend ganze Teilbäume gelöscht werden

Vererbte Seitenattribute

Der Seitenbaum bietet auch einen Mechanismus zur gemeinsamen Nutzung von Ressourcen. Bestimmte Seitenattribute – /MediaBox, /CropBox, /Resources und /Rotate – sind vererbbar (ISO 32000-2 §7.7.3.4). Wenn in einem /Page-Wörterbuch eines dieser Attribute fehlt, geht ein Leser in der /Parent-Kette nach oben, bis er das Attribut findet oder zum Root-Knoten gelangt. Wenn ein freigegebenes Schriftartwörterbuch in den Root-/Pages-Knoten statt in jede Blattseite kopiert wird, kann die Dateigröße bei Dokumenten, in denen durchgehend dieselben Schriftarten verwendet werden, spürbar verringert werden

Die Vererbungsregel schafft eine Feinheit für Code, der Seiteneigenschaften liest. /MediaBox direkt von einem /Page-Objekt zu lesen und einen fehlenden Schlüssel als Fehler zu behandeln, ist falsch. Der Schlüssel kann auch einfach geerbt sein. Code, der die Seitengeometrie korrekt auflöst, muss der übergeordneten Kette folgen. Außerdem ist ein Zyklusschutz erforderlich: Eine beschädigte Datei kann eine /Parent-Referenz haben, die auf einen bereits besuchten Knoten zurückweist, was ohne eine Prüfung des besuchten Objekts zu einer Endlosschleife führen würde

Die Xref-Tabelle und Querverweis-Streams

Die Suche nach indirekten Objekten erfolgt über die Querverweistabelle (oder deren Nachfolger, den in PDF 1.5 eingeführten Querverweis-Stream). Die Xref ordnet jede Objektnummer einem Byte-Offset innerhalb der Datei zu. Ein konformer Reader verwendet die Xref, um direkt zu jedem beliebigen Objekt zu springen. Die Datei wird nicht sequenziell gescannt. Dieser Direktzugriff ermöglicht schnelle Seitensprünge: Der Viewer liest den Katalog, löst den /Pages-Verweis über die Xref auf, liest den /Pages-Root-Knoten, löst einen /Kids-Eintrag auf und so weiter, wobei er nur die benötigten Objekte berührt

Bei inkrementellen Updates wird am Ende der Datei ein neuer Xref-Abschnitt mit einem Trailer hinzugefügt, der auf den vorherigen verweist. Ein in einer Revision aktualisiertes Objekt erhält einen neuen Eintrag im angehängten Xref-Abschnitt. Die ursprünglichen Bytes bleiben an Ort und Stelle, werden aber überschrieben. Auf diese Weise bleiben digital signierte PDFs auch dann überprüfbar, wenn Anmerkungen oder Formularausfüllrevisionen hinzugefügt werden: Der signierte Bytebereich wird nie berührt, und der neue Inhalt befindet sich im angehängten Abschnitt. Der Seitenbaum kann ebenfalls aktualisiert werden, sodass das Hinzufügen oder Löschen von Seiten in einer Revision ein neues /Pages-Root mit einem überarbeiteten /Kids-Array erzeugt, während das alte Root-Objekt seine ursprüngliche Position in der Datei beibehält

Was ohne Baumtraversierung schiefgeht

Der Fehlermodus für Objekt-Scan-Ansätze ist unauffällig. Das Ausgabedokument sieht plausibel aus: Es hat die richtige Anzahl von Seiten und jede Seite enthält erkennbaren Inhalt. Nur die Reihenfolge ist falsch, und das auf eine Weise, die vom Generator, der Anzahl der Revisionen und davon abhängt, ob Seiten aus externen Quellen zusammengeführt wurden. Ein Testkorpus von Dateien, die von einem einzigen Tool erzeugt wurden, kann vollständig fehlerfrei passieren, Dateien von einem anderen Tool oder aus einem Merge-Workflow schlagen jedoch fehl. Diese Inkonsistenz ist der Grund, warum heuristische Fehlerbehebungen nie Bestand haben

Dateien mit inkrementellen Aktualisierungen sind hierfür besonders anfällig, da in späteren Überarbeitungen hinzugefügte oder neu angeordnete Seiten hohe Objektnummern erhalten, während die Anzeigereihenfolge vom aktualisierten /Kids-Array gesteuert wird. Ein Scan, der Objekte in numerischer Reihenfolge verarbeitet, platziert diese hoch nummerierten Seiten am Ende, unabhängig davon, wo der Baum sie vorsieht

Die Lösung ist nicht kompliziert. Beginnen Sie beim Katalog, lösen Sie die /Pages-Referenz auf, durchlaufen Sie das /Kids-Array rekursiv und geben Sie Blätter in der Reihenfolge aus, in der Sie auf sie stoßen. Das ist per Definition die Anzeigereihenfolge, unabhängig von Objektnummern, Byte-Offsets oder Dateistruktur. Die meisten ausgereiften PDF-Bibliotheken verfügen über eine Seitenanzahl und einen indizierten Seitenzugriff, die dies bereits korrekt ausführen. Das Risiko liegt im Code, der das Seitenmodell der Bibliothek umgeht und die Objektebene direkt berührt

Eine strukturelle Anomalie, die explizit behandelt werden sollte: Der Wert /Count bei einem zwischengeschalteten /Pages-Knoten kann in fehlerhaften Dateien falsch sein. Wenn man sich bei der Überprüfung der Grenzen auf /Count verlässt und dann vor einem vollständigen Durchlauf anhält, werden Seiten stillschweigend weggelassen, wenn der Zähler zu niedrig ist. Für wichtige Dokumente ist es sicherer, /Count nur als Leistungshinweis zur Vorabzuweisung von Kapazitäten oder zur binären Suche zu verwenden und die tatsächliche Anzahl aus dem Durchlauf abzuleiten

 Nächster Artikel