Ein PDF-Reader beginnt nicht am Anfang der Datei. Er beginnt am Ende. Die letzten paar Bytes halten die Adresse von allem anderen, und ein Parser, der diese Reihenfolge nicht versteht, wird das Format von der ersten Zeile an falsch lesen. Der nützlichste Weg, PDF auf der Festplatte zu lernen, ist daher, es so zu lernen, wie ein Reader es tut: zuerst das Ende, dann rückwärts zur Karte springen, dann die Objekte auflösen, auf die die Karte zeigt
Die Bytes selbst sind schlicht genug, um sie in einem Texteditor zu lesen, wenn nichts komprimiert ist. Ein minimales einseitiges Dokument, das „Hello, World!“ zeichnet, passt in unter fünfhundert Bytes, und jedes Strukturelement des Formats ist darin sichtbar. Hier ist die ganze Datei, mit den vier Teilen markiert:
%PDF-1.0 % Header
%âãÏÓ
1 0 obj % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj
2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj
3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj
4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream
endobj
5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj
xref % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n
trailer % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF
Vier Teile, in der Datei stets in dieser Reihenfolge: ein Header, ein Body aus Objekten, eine Querverweistabelle und ein Trailer. Der Haken ist, dass man sie in nahezu umgekehrter Reihenfolge liest. ISO 32000-2 §7.5.1 legt dieselbe vierteilige Anatomie dar, und der Grund für den Zugriff von hinten nach vorn ist rein praktisch: Ein Reader, der direkt zu dem Objekt springt, das er braucht, ist viel schneller als einer, der jedes Byte von oben scannt, und genau dieser wahlfreie Zugriff ist es, wofür Trailer und Querverweistabelle existieren
Der Header sind zwei Zeilen, und die zweite zählt
Die erste Zeile ist %PDF-1.0. Das Prozentzeichen macht sie syntaktisch zu einem Kommentar, aber Reader behandeln sie als Dateisignatur und ziehen die Versionsnummer daraus. Die Versionsbehandlung ist in der Praxis locker. Ein für PDF 2.0 gebauter Reader öffnet bereitwillig eine Datei, die 1.0 behauptet, und die meisten Reader versuchen es auch mit einer Datei, deren deklarierte Version falsch ist oder deren Versionszeile ein Stück in die Datei hinein vergraben liegt statt bei Byte null. Die Zahl ist ein Hinweis darauf, welche Funktionen zu erwarten sind, kein Torwächter
Die zweite Zeile ist die, die Leute versehentlich löschen und dann einen Nachmittag lang debuggen. Auch sie ist ein Kommentar, aber ihre Nutzlast sind vier Bytes oberhalb von ASCII 127. Sie existieren, damit alles, was die Datei im „Textmodus“ bewegt, sie als binär erkennt und aufhört, Zeilenenden umzuschreiben. Ein PDF trägt komprimierte Streams, deren Bytes zufällig einem Wagenrücklauf oder Zeilenvorschub entsprechen können; schreibt ein Transferwerkzeug diese um, passt die im Dictionary verzeichnete Stream-Länge nicht mehr zu den Bytes auf der Festplatte, und die Datei ist korrupt. Der High-Byte-Kommentar ist eine vierzig Jahre alte Verteidigung gegen FTP im ASCII-Modus, und er steckt noch immer in jeder Datei, die ein ernsthaftes Werkzeug schreibt, denn der Fehler, den er verhindert, ist lautlos und total
Der Body hält die Objekte, jedes nummeriert
Alles, was das Dokument ausmacht, lebt im Body als flache Folge indirekter Objekte. Jedes öffnet mit zwei Ganzzahlen und dem Schlüsselwort obj, hält seinen Inhalt und schließt mit endobj. Objekt 1 im Beispiel oben ist der Seitenbaumknoten: 1 0 obj, dann ein Dictionary, dann endobj. Die erste Ganzzahl ist die Objektnummer, die zweite die Generationsnummer. Die Generation ist in einer frisch geschriebenen Datei fast immer null; sie steigt nur, wenn eine Objektnummer über Bearbeitungen hinweg wiederverwendet wird, was selten genug ist, dass man eine Generation ungleich null als Zeichen werten kann, dass die Datei inkrementelle Updates durchlaufen hat. Der Inhalt zwischen den Schlüsselwörtern ist hier ein Dictionary, geschrieben zwischen << und >>, könnte aber genauso gut eine Zahl, eine Zeichenfolge, ein Array oder ein Stream sein
Was daraus einen Graphen statt einer Liste macht, ist das Referenztoken 2 0 R. Es bedeutet „Objekt 2, Generation 0, wo auch immer es in der Datei lebt“. Der Seitenbaumknoten oben enthält seine Seite nicht; er zeigt auf Objekt 2, das nach demselben Mechanismus auf seine Ressourcen und seinen Content-Stream zeigt. Der Body ist in der Reihenfolge angelegt, die dem Writer gerade passte, und die Referenzen nähen ihn zu einem Baum zusammen, der im Katalog wurzelt. Die Position in der Datei trägt keine Bedeutung. Identität kommt von der Objektnummer, der Ort aus der Querverweistabelle
Die Querverweistabelle ist ein Index aus Byte-Offsets
Die Xref-Tabelle ist das, was Objektnummern in Dateipositionen verwandelt. Sie ist der Grund, warum ein Reader ein tausendseitiges Dokument öffnen und Seite 850 rendern kann, ohne die 849 Seiten davor zu parsen. Jeder Eintrag verzeichnet exakt, wo sein Objekt beginnt, gezählt in Bytes vom Dateianfang:
xref
0 6 % 6 entries, starting at object 0
0000000000 65535 f % entry 0: head of the free list
0000000015 00000 n % object 1 begins at byte 15
0000000074 00000 n % object 2 begins at byte 74
0000000192 00000 n % object 3 begins at byte 192
0000000291 00000 n % object 4 begins at byte 291
0000000409 00000 n % object 5 begins at byte 409
Die feste Breite ist Absicht. Jeder Eintrag ist exakt zwanzig Bytes: ein zehnstelliger Offset, ein Leerzeichen, eine fünfstellige Generation, ein Leerzeichen, ein einbuchstabiger Typ und ein Zwei-Byte-Zeilenende. Weil die Zeilen einheitlich sind, kann ein Reader per Arithmetik statt per Scan direkt zum Eintrag für Objekt n indizieren — die Tabelle, die wahlfreien Zugriff auf den Body gibt, ist also selbst wahlfrei zugreifbar. Die Zeile 0 6 ist ein Unterabschnittskopf: Sie besagt, dass die nächsten Einträge sechs Objekte ab Nummer 0 beschreiben
Objekt 0 ist besonders und immer vorhanden. Sein Typ ist f für frei, seine Generation ist 65535, und es führt die verkettete Liste freier Objektnummern an. In einer nie bearbeiteten Datei ist die Freiliste nur dieser eine Eintrag, eine Formalität. Ihren Nutzen verdient sie sich bei inkrementellen Updates, wenn das Löschen eines Objekts dessen Nummer in diese Liste einträgt, damit eine spätere Bearbeitung sie zurückfordern kann. Die anderen Einträge sind Typ n für in Gebrauch, und ihre zehnstellige Zahl ist der Offset, zu dem man springen würde, um die Definition dieses Objekts zu lesen
Der Trailer ist der Einstiegspunkt, und er sitzt am Ende
Der Trailer ist das Erste, was ein Reader tatsächlich konsumiert, obwohl er zuletzt geschrieben wird. Ein Parser öffnet die Datei, springt ans Ende und läuft rückwärts auf der Suche nach %%EOF. Direkt darüber sitzt startxref gefolgt von einer einzigen Zahl, und diese Zahl ist der Byte-Offset des Schlüsselworts xref. Damit springt der Reader direkt zur Querverweistabelle, ohne ein einziges Objekt gescannt zu haben:
trailer
<<
/Root 5 0 R % the document catalog
/Size 6 % one more than the highest object number
>>
startxref
459 % byte offset of the xref table
%%EOF
Das Trailer-Dictionary trägt die zwei Werte, die ein Reader braucht, bevor er sonst etwas tun kann. /Root zeigt auf den Dokumentkatalog, hier Objekt 5, die Spitze des Objektgraphen und der Weg zum Seitenbaum. /Size ist die Zahl der Einträge, die die Querverweistabelle enthalten soll — eins mehr als die höchste Objektnummer wegen des freien Eintrags auf Slot null. Von %%EOF aus ergibt sich die ganze Lesesequenz: den Marker finden, startxref lesen, um die Tabelle zu lokalisieren, die Tabelle laden, um zu erfahren, wo jedes Objekt lebt, /Root lesen, um den Katalog zu finden, und von dort Objekte bei Bedarf auflösen. Der Header ganz oben wird bis spät kaum konsultiert. Die Karte unten ist das, was der Reader zuerst braucht
Inkrementelles Update hängt eine zweite Karte an, statt neu zu schreiben
Dieses Ende-zuerst-Design zahlt sich aus, wenn sich eine Datei ändert. Ein PDF kann bearbeitet werden, ohne irgendein bereits auf der Festplatte liegendes Byte neu zu schreiben. Neue und geänderte Objekte werden ans Ende angehängt, gefolgt von einem frischen Querverweisabschnitt und einem frischen Trailer, und die Originaldatei darunter bleibt unberührt. Das eine neue Stück Buchführung ist ein /Prev-Eintrag im neuen Trailer, der den Byte-Offset der vorigen Querverweistabelle hält:
% ... original file, unchanged, ends here ...
6 0 obj % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj
xref % a second xref section, for the new object only
6 1
0000000612 00000 n
trailer
<<
/Root 5 0 R
/Size 7
/Prev 459 % byte offset of the earlier xref table
>>
startxref
680 % offset of this new xref section
%%EOF
Ein Reader beginnt weiterhin beim letzten %%EOF, folgt weiterhin startxref zur jüngsten Tabelle, folgt nun aber der /Prev-Kette rückwärts zu älteren Tabellen und führt sie zusammen, sodass für jede Objektnummer der neueste Eintrag gewinnt. Die Querverweisabschnitte bilden eine verkettete Liste durch die Datei hinab, jeder überschreibt für die von ihm berührten Objekte den vorherigen. Ein Objekt, das eine Bearbeitung ersetzt hat, existiert physisch weiterhin an seinem alten Offset; es ist bloß nicht mehr erreichbar, weil ein späterer Xref-Eintrag auf etwas Neueres zeigt
Das ist der Mechanismus, der signierte PDFs verifizierbar macht. Eine digitale Signatur deckt einen Bytebereich der Datei ab, und weil ein inkrementelles Update nur je anhängt, bewegen sich die signierten Bytes nie. Die Signatur validiert weiterhin gegen den ursprünglichen Bereich, während spätere Revisionen dahinter liegen, jede mit eigenem Xref und Trailer. Es ist auch der Grund, warum ein PDF wiederherstellbare Historie tragen kann: Jedes ersetzte Objekt liegt unter einem früheren Querverweisabschnitt noch auf der Festplatte — ein Feature für Versionsverfolgung und eine Haftung für jeden, der dachte, „löschen“ hieße, die Bytes seien weg
Der Preis ist Wachstum. Jede Bearbeitung hängt an; nichts wird an Ort und Stelle zurückgewonnen, eine vielfach überarbeitete Datei sammelt also tote Objekte und eine lange Kette von Xref-Abschnitten an. Das Heilmittel ist ein vollständiges Neuschreiben: das Dokument laden und frisch speichern, was die überlebenden Objekte neu nummeriert, die unerreichbaren fallen lässt und eine einzige saubere Querverweistabelle ausgibt. Die beiden Strategien stehen direkt gegeneinander. Anhängen ist schnell und bewahrt Signaturen und Historie; Neuschreiben ist langsamer und verwirft beides, im Tausch gegen eine kompakte Datei
Die vier Teile in der Praxis lesen
Das Layout zu kennen genügt, um die meisten „diese Datei öffnet nicht“-Probleme von Hand zu debuggen. Weist ein Reader ein PDF zurück, sitzen die üblichen Verdächtigen an den beiden Enden, nicht in der Mitte. Ein abgeschnittener Download verliert den Trailer, startxref oder %%EOF fehlt also und der Reader hat keinen Einstiegspunkt; tolerante Reader fallen darauf zurück, die ganze Datei zu scannen, um das Xref neu aufzubauen — genau der langsame Pfad, den die Tabelle vermeiden sollte. Ein verpfuschter Textmodus-Transfer korrumpiert Stream-Bytes, oder die Offsets stimmen nicht mehr mit der Realität überein, und Objekte laden von der falschen Position. Wenn die Offsets in der Tabelle nicht mehr auf echte obj-Schlüsselwörter zeigen, ist die Datei strukturell kaputt, selbst wenn jedes Objekt für sich in Ordnung ist
Für neuen Code lautet die Lehre aus dem Layout, die Byte-Buchführung einer Bibliothek zu überlassen. Die Offsets in der Querverweistabelle müssen mit den tatsächlichen Positionen jedes Objekts auf das Byte genau übereinstimmen, der Trailer muss auf die richtige Tabelle zeigen, und inkrementelle Updates müssen sich korrekt über /Prev verketten. Eine native Komponente wie die HotPDF Component für Delphi und C++Builder handhabt all das beim Schreiben einer Datei, einschließlich der Wahl zwischen dem Anhängen einer inkrementellen Revision und dem Neuschreiben einer kompakten. Wenn Sie dieselbe Struktur lieber von Grund auf aufgebaut statt seziert sehen wollen, geht der Begleitartikel über den Aufbau eines PDF-Dokuments von Grund auf das Ausgeben von Header, Objekten, Xref und Trailer der Reihe nach durch