Ein PDF ist im Kern ein Klartext-Container. Die meisten Dateien lassen sich im Hex-Editor oben lesen: ein Versionskommentar, dann eine Folge nummerierter Objekte, dann ein kleiner Index und ganz unten ein Zeiger, der dem Reader sagt, wo er beginnen soll. Ohne Komprimierung ist das Format zugänglich genug, um ein funktionierendes Dokument in einen Texteditor einzutippen und von einem Viewer öffnen zu lassen. Das einmalige Durchführen dieses Vorgangs lehrt mehr über den Aufbau eines PDFs als jede Menge Spezifikationslektüre, denn man muss die Objekte von Hand verdrahten, und die Datei verweigert das Öffnen, solange die Verdrahtung nicht stimmt.
Diese Anleitung erstellt das kleinstmögliche PDF, das tatsächlich etwas rendert: eine Seite, die Wörter "Hello, World!" in einer eingebetteten Schriftart auf US-Letter-Papier. Die fertige Datei benötigt genau fünf Objekte und einige Zeilen Verwaltungsstruktur drum herum. Zuerst werden die Objekte geschrieben, dann werden Header, Querverweis-Tabelle und Trailer zusammengesetzt, die alles zu einer Datei verbinden, die ein Reader akzeptiert.
Die fünf Objekte, auf die ein Viewer besteht
Ein Reader scannt ein PDF nicht von oben nach unten auf der Suche nach Inhalt. Er beginnt beim Trailer, folgt einem Verweis auf den Document Catalog und durchläuft eine Kette von Objekten von dort aus. Jedes Objekt in dieser Kette muss vorhanden sein, sonst schlägt das Öffnen fehl. Bei einem einseitigen Dokument ist die Kette kurz, und jedes Glied hat eine einzige Aufgabe:
- Catalog ist die Wurzel. Es ist das Objekt, auf das der Trailer zeigt, und sein einziger erforderlicher Eintrag hier ist ein Verweis auf den Page-Tree.
- Pages ist der Page-Tree-Knoten. Er listet die Seiten des Dokuments auf und meldet deren Anzahl.
- Page beschreibt eine physische Seite: ihre Größe, die Ressourcen, mit denen sie zeichnet, und den Content-Stream, der sie rendert.
- Content-Stream enthält die Zeichenoperatoren, die Postfix-Befehle, die Text und Grafiken auf diese Seite platzieren.
- Font deklariert die Schriftart, auf die der Content-Stream verweist. Bei Verwendung einer der 14 Standardschriften muss nichts eingebettet werden.
Jedes Objekt ist nummeriert und adressierbar. Ein indirektes Objekt wird als N 0 obj ... endobj geschrieben, wobei N die Objektnummer und die 0 die Generationsnummer ist (immer 0 in einer frisch geschriebenen Datei). An jeder anderen Stelle in der Datei verweist man mit einer Referenz auf dieses Objekt: 5 0 R bedeutet "Objekt 5." Diese Referenzen sind die Verdrahtung. Der Catalog enthält in unserer Nummerierung 2 0 R, um den Page-Tree zu erreichen, der Page-Tree enthält einen Verweis zurück auf die Seite usw. Stimmt eine Nummer nicht, folgt der Reader einem hängenden Zeiger ins Nichts.
Namen, Dictionaries und Streams
Drei Syntaxelemente tragen fast alles. Ein Name beginnt mit einem Schrägstrich: /Type, /Page, /F0. Namen sind Bezeichner, keine Strings, und PDF verwendet sie als Dictionary-Schlüssel und zur Kennzeichnung von Objekttypen. Ein Dictionary ist ein Satz von Schlüssel-Wert-Paaren in doppelten spitzen Klammern, wobei jeder Schlüssel ein Name ist: << /Type /Page /MediaBox [0 0 612 792] >>. Werte können Zahlen, Namen, Arrays in eckigen Klammern, Referenzen oder verschachtelte Dictionaries sein. Die meisten PDF-Objekte sind Dictionaries.
Ein Stream ist ein Dictionary, gefolgt von einem Byteblock zwischen den Schlüsselwörtern stream und endstream. Dort leben die Seitenzeichnungsoperatoren, und in echten Dateien auch komprimierte Bilder und eingebettete Schriften. Das Stream-Dictionary beschreibt die Bytes; in einer Produktionsdatei muss es einen /Length-Eintrag mit der exakten Byteanzahl enthalten und häufig ein /Filter wie /FlateDecode, wenn die Daten komprimiert sind. Bei der Länge werden wir auf ein Werkzeug zurückgreifen, denn Bytes von Hand zu zählen hat keinen pädagogischen Mehrwert und ein hohes Off-by-one-Risiko.
Die Objekte schreiben
Hier sind die fünf Objekte der Reihe nach. Wichtige Koordinatendetails für das Lesen des Content-Streams: PDF misst von der unteren linken Ecke der Seite in Punkten, wobei ein Punkt 1/72 Zoll beträgt, und Y wächst nach oben. Eine US-Letter-Seite ist 612 mal 792 Punkte groß, also sitzt 50 700 nahe der oberen linken Ecke, nicht unten.
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
Liest man die Referenzen, offenbart sich die Struktur. Objekt 1, der Catalog, verweist seinen /Pages-Eintrag auf Objekt 2. Objekt 2, der Page-Tree, listet Objekt 3 in /Kids und deklariert /Count 1. Objekt 3, die Seite, verweist /Parent zurück auf Objekt 2, dimensioniert sich selbst mit /MediaBox, stellt die Schrift unter dem lokalen Namen /F0 in seinen /Resources bereit und nennt Objekt 5 als seinen Inhalt. Objekt 4 ist die Schrift: /BaseFont /Helvetica wählt eine der 14 Standardschriften, die jeder konforme Reader bereits besitzt. Objekt 5 ist der Content-Stream.
Was der Content-Stream tatsächlich aussagt
Der Stream-Body ist ein kleines Programm in PDFs Seitenbeschreibungssprache, die postfix aufgebaut ist: erst Operanden, dann der Operator. Fünf Zeilen erledigen die Arbeit. BT und ET öffnen und schließen ein Text-Objekt; alles, was Text positioniert oder anzeigt, muss dazwischen stehen. /F0 36 Tf setzt die aktuelle Schrift auf die als /F0 referenzierte Ressource mit 36 Punkt (Tf steht für "set text font and size"). 50 700 Td verschiebt die Textposition auf (50, 700) in Seitenkoordinaten. (Hello, World!) Tj zeigt den String an, den PDF als Literal-Text in Klammern schreibt, und malt ihn mit Tj an der aktuellen Position. Lässt man BT/ET weg, lehnt ein strenger Reader die Textoperatoren ab; vergisst man, vor Tj eine Schrift zu setzen, gibt es keine aktuelle Schrift zum Zeichnen.
Die /Length 44 im Stream-Dictionary ist die Byteanzahl zwischen stream und endstream, und sie muss exakt stimmen. Diesen Wert an ein Werkzeug zu übergeben ist sinnvoller als Zeilenenden von Hand zu zählen, besonders weil die Gesamtzahl davon abhängt, ob der Editor LF oder CRLF als Zeilenende verwendet.
Header, Xref und Trailer
Die Objekte sind der Inhalt. Drei strukturelle Teile verwandeln sie in eine Datei. Das erste ist der Header, die allererste Zeile, die Format und Version benennt:
%PDF-1.7
Das % beginnt in der PDF-Syntax einen Kommentar, aber ein Reader behandelt diesen speziellen Kommentar als Formatsignatur und liest die Version daraus. Ein realer Writer folgt unmittelbar mit einer zweiten Kommentarzeile aus High-Byte-Zeichen, einem Hinweis für Dateiübertragungstools, dass die Datei binär ist und nicht als Text verändert werden darf.
Am Ende der Datei folgt die Querverweis-Tabelle, der Index, der wahlfreien Zugriff ermöglicht. Sie speichert den Byte-Offset jedes Objekts vom Dateianfang aus, sodass ein Reader direkt zu Objekt 3 springen kann, ohne die Objekte 1 und 2 zu parsen. Die Tabelle ist starr: Einträge haben eine feste Breite von 20 Bytes inklusive Zeilenende, formatiert als 10-stelliger Offset, 5-stellige Generation, ein Schlüsselwort (n für in Verwendung, f für frei) und ein zweiteiliger Abschluss. Eine korrekte Tabelle für unsere sechs Einträge (Objekt 0 ist immer der Kopf der Free-List) sieht so aus:
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
Diese Offsets sind der fragile Teil beim manuellen Schreiben von PDF. Jeder ist die exakte Byteposition, an der das entsprechende N 0 obj beginnt, und jeder Offset verschiebt sich, sobald man irgendwo darüber ein Zeichen einfügt. Der Trailer ist der Einstiegspunkt, den ein Reader zuletzt und zuerst verwendet: /Root 1 0 R benennt den Catalog, /Size 6 gibt die Objektanzahl an, und startxref 408 gibt den Byte-Offset des Wortes xref selbst an. Ein Reader öffnet die Datei, springt ans Ende, liest startxref, sucht die Querverweis-Tabelle auf und erreicht von dort den Catalog und alles darunter. %%EOF markiert das letzte Byte.
Die Byte-Zählung einem Werkzeug überlassen
Die oben angegebenen Offsets sind illustrativ; in der Praxis werden sie falsch sein, sobald man fertig getippt hat, da sie vom genauen Byte-Layout der Datei abhängen. Anstatt sie neu zu berechnen, schreibt man die Struktur mit Platzhalterwerten und lässt ein Hilfsprogramm die Querverweis-Tabelle und die Stream-Längen neu aufbauen. Das kostenlose, plattformübergreifende pdftk erledigt dies in einem Durchlauf:
pdftk hello-draft.pdf output hello.pdf
Es parst die Objekte, berechnet jeden Byte-Offset neu, trägt die korrekten /Length-Werte ein, schreibt eine gültige Xref-Tabelle und einen Trailer und gibt hello.pdf aus. Öffnet man diese Datei in einem Viewer, erhält man eine Seite mit "Hello, World!" in 36-Punkt-Helvetica nahe der Oberkante. Qpdf erledigt dieselbe Aufgabe, und viele Viewer reparieren eine leicht fehlerhafte Datei auch spontan. Den Offset-Berechnungen ein Werkzeug zu überlassen ist keine Faulheit; es liegt daran, dass die Offset-Arithmetik der einzige Teil des Formats ohne konzeptuellen Gehalt und mit der höchsten Fehlerrate ist.
Warum das auf echte Dokumente skaliert
An einem hundert Seiten starken Bericht ändert sich die eben aufgebaute Form nicht. Der Catalog sitzt weiterhin an der Wurzel, der Page-Tree sammelt weiterhin die Seiten, und jede Seite verweist weiterhin auf ihre Ressourcen und einen Content-Stream. Was wächst, ist die Breite, nicht das Gerüst: Der Page-Tree verzweigt sich, damit ein Reader ganze Teilbäume überspringen kann, Content-Streams tragen Hunderte von Operatoren statt fünf, Schriften werden als eigene Stream-Objekte mit Breitentabellen und Encodings eingebettet, und Bilder kommen als Streams mit bildspezifischen Filtern. Moderne Dateien packen viele Objekte in komprimierte Object-Streams und ersetzen die einfache Xref-Tabelle durch einen Cross-Reference-Stream, weshalb das Öffnen einer echten PDF-Datei im Texteditor meist eine Wand aus Binärdaten zeigt. Das darunter liegende Modell ist identisch mit dem in der handgeschriebenen Datei. Für das breitere Objektgraph und die Beziehungen zwischen Catalog, Page-Tree und Resource-Dictionaries in einem größeren Dokument setzt die ausführliche Beschreibung der PDF-Dokumentstruktur da an, wo diese Anleitung endet, und die Übersicht über die Dateistruktur behandelt inkrementelle Updates und die Verkettung des Trailers über mehrere Revisionen.
Vom manuellen Schreiben zu einer Bibliothek
Objekte von Hand einzutippen ist eine Lernübung, keine Produktionstechnik. Sobald echte Schriften, umgebrochener Text, Bilder oder mehr als eine triviale Seite benötigt werden, wird die Byte-Verwaltung, die pdftk übernommen hat, zur eigentlichen Hauptaufgabe, und eine Bibliothek, die sich darum kümmert, ist gefragt. Dieselben fünf Objekte werden weiterhin geschrieben, aber eine Bibliothek berechnet jeden Offset, verwaltet die Schrift- und Ressourcen-Dictionaries und komprimiert die Content-Streams, ohne dass man ein einzelnes Byte verfolgen muss. In Delphi und C++Builder reduziert die HotPDF-Komponente diese gesamte Datei auf einige wenige Aufrufe: das Dokument einrichten, BeginDoc, SetFont und TextOut zum Platzieren desselben Textes aufrufen, dann EndDoc, um einen korrekten Catalog, Page-Tree, Xref und Trailer zu schreiben. Das Verständnis der darunter liegenden Objekte ermöglicht es, die Ausgabe zu analysieren, wenn ein Dokument nicht wie erwartet gerendert wird.