Eine PDF-Datei ist im Kern eine Sammlung von Objekten, die aufeinander verweisen. Entfernt man Komprimierung, Querverweisbuchhaltung und Byte-Offsets, bleibt ein Graph übrig: eine kleine Menge typisierter Werte, durch Referenzen verbunden und an einem einzigen Objekt verwurzelt, das der Leser zu finden weiß. Alles, was ein PDF ausdrücken kann, von einem Textabsatz über eine eingebettete Schriftart bis hin zu einer digitalen Signatur, wird aus acht primitiven Objekttypen und der Regel aufgebaut, die einem Objekt erlaubt, auf ein anderes zu verweisen. Wer diese kennt, liest den Rest des Formats als Komposition, nicht als Rätsel.
Dies ist die logische Ebene von PDF, definiert in ISO 32000-1, Abschnitt 7.3, und sie liegt eine Ebene über dem physischen Datei-Layout (Header, Rumpf, Querverweistabelle und Trailer), das ein eigenes Thema in der technischen Übersicht zur PDF-Dateistruktur ist. Das logische Modell ist das, was diese Bytes bedeuten, sobald sie geparst wurden. Ein Betrachter liest die Datei rückwärts, um den Trailer zu finden, folgt ihm zum Wurzelobjekt, und von dort entfaltet sich das Dokument als Objekte, die auf Objekte verweisen. Dies ist der Teil, über den man nachdenkt, wenn man eine fehlerhafte Seite debuggt, einen Parser schreibt oder einer Bibliothek vertraut, ein Dokument zusammenzustellen.
Acht Objekttypen und nichts weiter
PDF definiert genau acht grundlegende Objekttypen. Jeder Wert in einem Dokument ist einer davon, was das Format trotz seiner Reichweite handhabbar hält.
Booleans sind die Schlüsselwörter true und false. Sie schalten Flags ein und aus, etwa ob eine Anmerkung gedruckt wird.
Zahlen gibt es in zwei Varianten, die die Spezifikation als einen Typ behandelt: Ganzzahlen wie 42 und Reellzahlen wie 3.14 oder -0.002. PDF kennt keine Exponentialschreibweise, sodass 1e6 in einer konformen Datei nie vorkommt. Koordinaten, Schriftgrößen und Rotationswinkel sind allesamt Zahlen.
Strings enthalten Byte-Sequenzen, entweder in Klammern geschrieben, (Hello), oder in spitzen Klammern als Hexadezimal, <48656C6C6F>. Beide Notationen kodieren identischen Inhalt; Hex ist die Fluchtoption für Bytes, die innerhalb von Klammern problematisch wären. Strings tragen Text, sind aber zunächst Bytes, was in dem Moment wichtig wird, in dem man Inhalte jenseits von ASCII verarbeitet.
Namen sind atomare Token, eingeleitet durch einen Schrägstrich: /Type, /Pages, /MediaBox. Ein Name ist kein String; er ist ein Bezeichner, der als Dictionary-Schlüssel oder aufgezählter Wert verwendet wird, und zwei Namen sind nur dann gleich, wenn sie Byte für Byte übereinstimmen. Der Schrägstrich ist Syntax, kein Bestandteil des Namens. Dies verwirrt Einsteiger, die /Times-Roman und den String (Times-Roman) für austauschbar halten; das Format tut es nicht.
Arrays sind geordnete, heterogene Listen in eckigen Klammern: [0 0 612 792] ist ein Seitenrechteck, und ein Array kann Typen frei mischen, einschließlich Referenzen auf andere Objekte. Dictionaries sind das Arbeitspferd. Geschrieben zwischen << und >>, bildet ein Dictionary Namensschlüssel auf Werte beliebigen Typs ab, und fast jede bedeutsame Struktur in PDF, Seite, Katalog, Schriftart, Anmerkung, ist ein Dictionary mit einem /Type-Schlüssel, der deklariert, was es ist.
Streams sind Dictionaries mit einem nachgestellten Block roher Bytes zwischen den Schlüsselwörtern stream und endstream. Das Dictionary beschreibt die Bytes (ihre Länge und etwaige Filter wie FlateDecode, der sie komprimiert), und die Bytes tragen die umfangreiche Nutzlast: Seiteninhaltsanweisungen, eingebettete Schriftprogramme, Bilder. Ein Stream ist das, wo PDF alles ablegt, was zu groß oder zu binär ist, um inline zu stehen.
Der achte Typ ist das Null-Objekt, das Schlüsselwort null. Es ist ein echter Wert, verschieden davon, dass ein Schlüssel fehlt. Ein Dictionary-Eintrag, der auf null gesetzt ist, wird behandelt, als wäre er nicht vorhanden, und eine Referenz, die zu einem nicht existierenden Objekt aufgelöst wird, liefert ebenfalls null statt eines Fehlers. Dieses tolerante Verhalten ist beabsichtigt: Es lässt eine beschädigte Datei degradieren, anstatt sich zu weigern zu öffnen. Es gibt keinen neunten Typ; alles, was PDF ausdrückt, entsteht daraus, wie diese acht kombiniert werden.
Direkte Werte, indirekte Objekte und Referenzen
Jeder dieser acht Typen kann auf zwei Arten erscheinen. Ein direktes Objekt wird an Ort und Stelle geschrieben, wie die 612 innerhalb eines MediaBox-Arrays. Ein indirektes Objekt erhält eine Identität, sodass andere Objekte darauf zeigen können: zwei Ganzzahlen, eine Objektnummer und eine Generationsnummer, die die Definition in obj und endobj einschließen:
12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
Dies ist Objekt 12, Generation 0, ein Schriftart-Dictionary. An jeder anderen Stelle in der Datei verweist ein anderes Objekt darauf mit einer indirekten Referenz: die gleichen zwei Zahlen gefolgt vom Schlüsselwort R, 12 0 R. Die Referenz ist ein Zeiger. Wenn das Resource-Dictionary einer Seite /Font << /F1 12 0 R >> enthält, benennt es Objekt 12 als die Schriftart hinter dem Ressourcennamen /F1, ohne die Definition der Schriftart in die Seite zu kopieren.
Die Generationsnummer existiert für Löschungen und Wiederverwendung. Wenn ein Objekt freigegeben und sein Slot wiederverwendet wird, erhöht sich die Generation, damit eine veraltete 12 0 R-Referenz nicht zum neuen Inhaber von Slot 12 aufgelöst werden kann. Frisch geschriebene Dateien haben fast ausnahmslos Generation 0, aber eine stark bearbeitete Datei kann höhere Zahlen aufweisen, und ein Parser, der die Generation ignoriert, liest irgendwann das falsche Objekt.
Indirektion macht PDF effizient und bearbeitbar. Eine Schriftart, ein Bild oder ein Farbraum kann einmal definiert und von hundert Seiten referenziert werden. Eine kleine Änderung kann als neue Revision angehängt werden, die nur ein einzelnes Objekt ersetzt, anstatt die Datei neu zu schreiben. Die Querverweistabelle ist der Index, der eine Objektnummer in einen Byte-Offset umwandelt, sodass der Leser direkt zu 12 0 obj springt, ohne zu suchen, aber das ist eine physische Optimierung. Logisch gesehen muss man nur wissen, dass 12 0 R bedeutet: „das Objekt mit der Kennung 12 0".
Der Katalog: wo jedes Dokument beginnt
Das Auflösen von Referenzen muss irgendwo beginnen, und dieser Ausgangspunkt ist der /Root-Eintrag des Trailers, der auf den Dokumentkatalog zeigt: die Wurzel des Objektgraphen, ein Dictionary mit /Type /Catalog. Der Leser erreicht ihn zuerst, weil der Trailer zuerst gefunden wird, und von dort aus ist jeder andere Teil des Dokuments durch das Verfolgen von Referenzen erreichbar.
Der Katalog enthält nur zwei zwingend erforderliche Einträge: seinen /Type und /Pages, eine indirekte Referenz auf die Wurzel des Seitenbaums. Der Rest ist optional und beschreibt dokumentweites Verhalten statt Inhalte: /Outlines zeigt auf den Lesezeichenbaum, /Names enthält nach Zeichenkette indizierte Namenbäume, /Metadata verweist auf einen XMP-Metadaten-Stream, und /PageMode sowie /PageLayout schlagen vor, wie ein Betrachter das Dokument öffnen soll. Keines davon ist zum Rendern einer Seite erforderlich; sie konfigurieren das Erlebnis rund um die Seiten. Die am Katalog hängenden Lesezeichen-, Metadaten- und Anmerkungsstrukturen werden im Artikel zu PDF-Metadaten, Lesezeichen und Anmerkungen behandelt.
Das Diagramm unten zeigt, wo der Objektrumpf in der umgebenden Datei liegt. Der Katalog und der Seitenbaum befinden sich als gewöhnliche indirekte Objekte in diesem Rumpf; der Header, die Querverweistabelle und der Trailer drumherum sind das physische Gerüst, das es einem Leser erlaubt, sie zu lokalisieren.

Der Seitenbaum: eine ausgeglichene Hierarchie von Seiten
Von /Pages aus verzweigt das Dokument in den Seitenbaum, wo sich PDFs Entscheidung für einen Graphen gegenüber einer flachen Liste auszahlt. Seiten werden nicht als einfache Folge gespeichert; sie hängen von einem Baum ab, dessen innere Knoten Seitenbaum-Knoten (/Type /Pages) sind und dessen Blätter Seitenobjekte (/Type /Page) sind. Ein innerer Knoten listet seine Kinder in einem /Kids-Array auf und verzeichnet in /Count, wie viele Blattseiten darunter liegen. Jeder Knoten außer der Wurzel trägt eine /Parent-Referenz nach oben, sodass der Baum in beide Richtungen durchlaufen werden kann.
2 0 obj % Wurzel des Seitenbaums
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj
3 0 obj % ein Blattknoten (Seite)
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F1 12 0 R >> >>
/Contents 5 0 R >>
endobj
4 0 obj % ein innerer Knoten, der zwei weitere Seiten gruppiert
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj
Hier ist Objekt 2 die Wurzel mit drei Seiten darunter: dem Blattknoten Seite 3 sowie zwei weiteren, die über den inneren Knoten 4 erreichbar sind. Das /Count der Wurzel mit dem Wert 3 muss gleich der Gesamtanzahl der darunter liegenden Blätter sein, und eine Zahl, die nicht mit der tatsächlichen Struktur übereinstimmt, ist ein häufiger Fehler bei manuell bearbeiteten Dateien. Der Zweck des Baums ist die Lokalität des Zugriffs. Ein Leser, der Seite 900 eines tausendseitigen Dokuments öffnet, durchläuft keine 900 Objekte; er steigt durch einige wenige Knoten ab, weil ein wohlgeformter Baum flach und ausgeglichen bleibt. Einen solchen Baum von Hand zu erstellen ist aufwändig genug, um ihn von Anfang bis Ende zu sehen, was die Anleitung zum Aufbau eines PDF-Dokuments von Grund auf zeigt.
Der Baum verdient seine zweite Berechtigung durch Vererbung. Einige wenige Seitenattribute, /Resources, /MediaBox, /CropBox und /Rotate, können auf einem inneren Knoten gesetzt und bei einzelnen Seiten weggelassen werden, die dann den Wert des nächsten Vorfahren erben. /MediaBox einmal auf der Wurzel zu setzen gibt jeder Blattseite dieselbe Seitengröße ohne Wiederholung; eine Seite, die abweichen muss, deklariert ihren eigenen Wert. Dies ist die einzige Stelle im Objektmodell, wo die Bedeutung eines Wertes von der Position eines Objekts im Baum abhängt, nicht nur von seinem eigenen Inhalt.
Was ein Blattknoten tatsächlich enthält
Ein Seitenobjekt ist der Verbindungspunkt zwischen dem strukturellen Modell und dem sichtbaren Inhalt. Sein /Contents-Eintrag verweist auf einen oder mehrere Content-Streams, die Zeichenoperatoren, die Text und Grafiken auf die Seite malen. Sein /Resources-Dictionary benennt die Schriftarten, Bilder und Farbräume, auf die diese Operatoren angewiesen sind, wobei jeder Eintrag eine indirekte Referenz auf ein seitenübergreifend gemeinsam genutztes Objekt ist. Die /MediaBox gibt das Seitenrechteck in Punkten (1/72 Zoll) an, und Einträge wie /Rotate und /CropBox passen die Darstellung an.
Diese Arbeitsteilung ist das gesamte Modell im Kleinen. Das Seiten-Dictionary ist Struktur: typisierte Einträge und Referenzen, die angeben, was die Seite ist und womit sie zeichnet. Der Content-Stream ist Anweisung: ein separates, komprimierbares Blob, das angibt, wie gezeichnet wird. Die Schriftart hinter /F1 ist eine gemeinsam genutzte Ressource, einmal definiert und überall verwendet, wo sie gebraucht wird. Dictionary, Stream und Referenz kooperieren, um eine Seite zu rendern, und dieselben Muster skalieren auf das gesamte Dokument. Die Content-Stream-Operatoren in diesem Blob werden separat für Text und Schriftarten sowie für Grafiken und visuelle Elemente behandelt.
Warum dieses Modell es wert ist, es zu kennen
Die meisten Entwickler begegnen dem Objektmodell erst, wenn etwas schiefgeht: Eine Seite bleibt leer, weil ihre /Contents-Referenz ins Leere zeigt, Text erscheint als Kästchen, weil eine Schriftressource nie eingebettet wurde, ein Tool meldet ein /Count, das nicht mit den gefundenen Seiten übereinstimmt. Jedes davon ist eine Aussage über den Graphen, und den Graphen direkt zu lesen ist besser als zu raten. Die acht Typen und die Referenzregel sind ein kleines genug Vokabular, um es im Kopf zu behalten, und sobald man eine PDF-Datei als Objekte sieht, die auf Objekte zeigen, hören fehlerhafte Dateien auf, undurchsichtig zu sein.
Das Modell von Hand zu schreiben ist jedoch selten die richtige Wahl, außer zum Lernen. Querverweisoffsets, Generationsnummern, Seitenbaum-Zähler und Stream-Längen über Bearbeitungen hinweg konsistent zu halten, ist genau die Buchhaltung, die eine Bibliothek übernimmt. Im Produktionseinsatz verwaltet eine ausgereifte PDF-Entwicklungsbibliothek den Objektgraphen, während man selbst in Seiten und Inhalten denkt. Das Modell zu kennen zahlt sich dennoch aus: Man versteht, was die Bibliothek darunter aufbaut und warum.