Ein PDF ist im Kern ein Klartext-Container. Öffnet man die meisten Dateien in einem Hex-Editor, ist der Anfang lesbar: ein Versionskommentar, dann eine Folge nummerierter Objekte, dann ein kleiner Index und ganz unten ein Zeiger, der einem Reader sagt, wo er anfangen soll. Nimmt man die Kompression weg, ist das Format zugänglich genug, dass man ein funktionierendes Dokument in einen Texteditor tippen und von einem Viewer öffnen lassen kann. Das einmal zu tun lehrt mehr darüber, wie PDF zusammenhält, als jede Menge Spezifikationslektüre, denn man muss die Objekte von Hand miteinander verdrahten, und die Datei weigert sich zu öffnen, bis die Verdrahtung stimmt
Diese Anleitung baut das kleinste PDF, das tatsächlich etwas rendert: eine Seite, die Worte „Hello, World!“ in einer eingebauten Schrift, auf US-Letter-Papier. Die fertige Datei braucht genau fünf Objekte und ein paar Zeilen Buchführung darum herum. Wir schreiben zuerst die Objekte und montieren dann Header, Querverweistabelle und Trailer, die sie zu einer Datei binden, die ein Reader akzeptiert
Die fünf Objekte, auf denen ein Viewer besteht
Ein Reader durchsucht ein PDF nicht von oben nach unten nach Inhalt. Er beginnt beim Trailer, folgt einer Referenz zum Dokumentkatalog und läuft von dort eine Objektkette entlang. Jedes Objekt auf dieser Kette muss existieren, sonst schlägt das Öffnen fehl. Für ein einseitiges 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 hier erforderlicher Eintrag ist eine Referenz auf den Seitenbaum
- Pages ist der Seitenbaumknoten. Er listet die Seiten des Dokuments auf und meldet, wie viele es sind
- Page beschreibt eine physische Seite: ihre Größe, die Ressourcen, mit denen sie zeichnet, und welcher Content-Stream sie malt
- Content-Stream enthält die Zeichenoperatoren, die Postfix-Befehle, die Text und Grafik auf dieser Seite platzieren
- Font deklariert die Schrift, auf die sich der Content-Stream bezieht. Nutzt man eine der 14 Standardschriften, muss man nichts einbetten
Jedes Objekt ist nummeriert und adressierbar. Ein indirektes Objekt wird als N 0 obj ... endobj geschrieben, wobei N die Objektnummer ist und die 0 seine Generationsnummer (in einer frisch geschriebenen Datei immer 0). Überall sonst in der Datei zeigt man mit einer Referenz auf dieses Objekt: 5 0 R bedeutet „Objekt 5“. Diese Referenzen sind die Verdrahtung. Der Katalog hält in unserer Nummerierung 2 0 R, um den Seitenbaum zu erreichen, der Seitenbaum hält eine Referenz hinunter zur Seite, und so weiter. Stimmt eine Nummer nicht, folgt der Reader einem baumelnden Zeiger ins Nichts
Namen, Dictionaries und Streams
Drei Syntaxelemente tragen fast alles. Ein Name beginnt mit einem Schrägstrich: /Type, /Page, /F0. Namen sind case-sensitive Bezeichner, keine Zeichenfolgen, und PDF nutzt sie für Dictionary-Schlüssel und um zu markieren, was ein Objekt ist. Ein Dictionary ist eine Menge 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 Seitenzeichenoperatoren, 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 Bytezahl tragen und oft einen /Filter wie /FlateDecode, wenn die Daten komprimiert sind. Wir werden uns beim Ausfüllen von /Length auf ein Werkzeug stützen, denn Bytes von Hand zu zählen ist der Teil dieser Übung ohne Lerneffekt und mit hoher Chance auf einen Off-by-one-Fehler, der die Datei zerstört
Die Objekte schreiben
Hier sind die fünf Objekte der Reihe nach. Das Koordinatendetail, das man vor dem Lesen des Content-Streams im Kopf behalten sollte: PDF misst von der linken unteren Ecke der Seite in Punkten, wobei ein Punkt 1/72 Zoll ist, und Y wächst nach oben. Eine US-Letter-Seite misst 612 mal 792 Punkte, 50 700 sitzt also nahe der linken oberen 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, ergibt sich die Struktur von selbst. Objekt 1, der Katalog, richtet seinen /Pages-Eintrag auf Objekt 2. Objekt 2, der Seitenbaum, listet Objekt 3 in /Kids und deklariert /Count 1. Objekt 3, die Seite, zeigt mit /Parent zurück hinauf auf Objekt 2 (Baum und Seite referenzieren einander, was erforderlich ist), bemisst sich mit /MediaBox, stellt die Schrift unter dem lokalen Namen /F0 in seinen /Resources bereit und benennt Objekt 5 als seinen Inhalt. Objekt 4 ist die Schrift: /BaseFont /Helvetica wählt eine der 14 Standardschriften, die jeder konforme Reader bereits hat, es gibt also nichts einzubetten. Objekt 5 ist der Content-Stream
Was der Content-Stream tatsächlich sagt
Der Stream-Körper ist ein winziges Programm in der Seitenbeschreibungssprache von PDF, die Postfix ist: Erst kommen die Operanden, dann der Operator, der sie konsumiert. Fünf Zeilen erledigen die Arbeit. BT und ET öffnen und schließen ein Textobjekt; alles, was Text positioniert oder anzeigt, muss dazwischen stehen. /F0 36 Tf setzt die aktuelle Schrift auf die Ressource namens /F0 bei 36 Punkt (Tf ist „set text font and size“). 50 700 Td bewegt die Textposition auf (50, 700) in Seitenkoordinaten. (Hello, World!) Tj zeigt die Zeichenfolge, die PDF als Literaltext in Klammern schreibt, und malt sie mit Tj an der aktuellen Position. Lässt man BT/ET weg, weist ein strenger Reader die Textoperatoren zurück; vergisst man, vor Tj eine Schrift zu setzen, gibt es keine aktuelle Schrift zum Zeichnen
Das /Length 44 im Stream-Dictionary ist die Zahl der Bytes zwischen stream und endstream, und sie muss exakt stimmen. Das ist der Wert, den man besser einem Werkzeug überlässt, statt Zeilenumbrüche von Hand zu zählen — zumal es die Summe verändert, ob Ihr Editor Zeilenenden als LF oder CRLF schreibt
Header, Xref und Trailer
Die Objekte sind der Inhalt. Drei Strukturteile machen daraus 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 genau diesen Kommentar als Formatsignatur und liest daraus die Version. Ein echter Writer lässt unmittelbar darauf eine zweite Kommentarzeile aus High-Bit-Bytes folgen, einen Hinweis für Dateitransfer-Werkzeuge, dass die Datei binär ist und nicht als Text verstümmelt werden darf
Am Ende der Datei kommt die Querverweistabelle, der Index, der wahlfreien Zugriff möglich macht. Sie verzeichnet den Byte-Offset jedes Objekts vom Dateianfang, sodass ein Reader direkt zu Objekt 3 springen kann, ohne zuerst die Objekte 1 und 2 zu parsen. Die Tabelle ist starr: Einträge haben feste Breite, 20 Bytes einschließlich Zeilenende, formatiert als 10-stelliger Offset, 5-stellige Generation, ein Schlüsselwort (n für in Gebrauch, f für frei) und ein Zwei-Byte-Abschluss. Eine korrekte Tabelle für unsere sechs Einträge (Objekt 0 ist immer der Kopf der Freiliste) 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 des handschriftlichen PDF-Schreibens. Jeder ist die exakte Byteposition, an der das zugehörige N 0 obj beginnt, und jeder Offset verschiebt sich in dem Moment, in dem man irgendwo darüber ein Zeichen einfügt. Der Trailer ist der Einstiegspunkt, den ein Reader zuletzt und zuerst nutzt: /Root 1 0 R benennt den Katalog, /Size 6 gibt die Objektzahl an, und startxref 408 liefert den Byte-Offset des Wortes xref selbst. Ein Reader öffnet die Datei, springt ans Ende, liest startxref, sucht die Querverweistabelle auf und erreicht von dort den Katalog und alles darunter. %%EOF markiert das letzte Byte
Die Bytezählung einem Werkzeug überlassen
Die Offsets oben sind illustrativ; in der Praxis sind sie falsch, sobald Sie fertig getippt haben, denn sie hängen vom exakten Bytelayout Ihrer Datei ab. Statt sie neu zu berechnen, schreiben Sie die Struktur mit Platzhalterwerten und lassen ein Dienstprogramm die Querverweistabelle und Stream-Längen neu aufbauen. Das freie, plattformübergreifende pdftk erledigt das in einem Durchgang:
pdftk hello-draft.pdf output hello.pdf
Es parst Ihre Objekte, berechnet jeden Byte-Offset neu, füllt die korrekten /Length-Werte ein, schreibt eine gültige Xref-Tabelle samt Trailer und gibt hello.pdf aus. Öffnet man das in einem beliebigen Viewer, erhält man eine Seite mit „Hello, World!“ in 36-Punkt-Helvetica nahe dem oberen Rand. Qpdf leistet dieselbe Arbeit, und viele Viewer reparieren eine leicht fehlerhafte Datei auch on-the-fly. Der Sinn, sich hier auf ein Werkzeug zu stützen, ist keine Faulheit; die Offset-Arithmetik ist der eine Teil des Formats mit null konzeptionellem Gehalt und der höchsten Fehlerrate, ihre Automatisierung hält die Struktur als das, was Sie lernen
Warum das auf echte Dokumente skaliert
Nichts an einem hundertseitigen Bericht ändert die Form, die Sie gerade gebaut haben. Der Katalog sitzt weiter an der Wurzel, der Seitenbaum sammelt weiter die Seiten, und jede Seite zeigt weiter auf ihre Ressourcen und einen Content-Stream. Was wächst, ist die Breite, nicht das Rückgrat: Der Seitenbaum verzweigt sich, damit ein Reader ganze Teilbäume überspringen kann, Content-Streams tragen Hunderte Operatoren statt fünf, Schriften werden als eigene Stream-Objekte mit Breitentabellen und Kodierungen eingebettet, und Bilder kommen als Streams mit bildspezifischen Filtern an. Moderne Dateien packen zudem viele Objekte in komprimierte Objektstreams und ersetzen die einfache Xref-Tabelle durch einen Querverweis-Stream, weshalb das Öffnen eines echten PDFs im Texteditor meist eine Wand aus Binärdaten zeigt. Das Modell darunter ist identisch mit dem in Ihrer handgemachten Datei. Für den weiteren Objektgraphen und wie Katalog, Seitenbaum und Ressourcen-Dictionaries sich über ein größeres Dokument hinweg zueinander verhalten, setzt die Tiefenerkundung der PDF-Dokumentstruktur dort an, wo dies endet, und der Überblick über die Dateistruktur behandelt inkrementelle Updates und wie sich der Trailer über Revisionen hinweg verkettet
Vom Handschreiben zur Bibliothek
Objekte von Hand zu tippen ist eine Lernübung, keine Produktionstechnik. In dem Moment, in dem Sie echte Schriften, umbrochenen Text, Bilder oder mehr als eine triviale Seite brauchen, wird die Byte-Buchführung, die pdftk für Sie geflickt hat, zur eigentlichen Arbeit, und Sie wollen eine Bibliothek, die sie besitzt. 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 Sie ein einziges Byte verfolgen. In Delphi und C++Builder reduziert die HotPDF Component diese ganze Datei auf eine Handvoll Aufrufe: das Dokument einrichten, BeginDoc aufrufen, mit SetFont und TextOut denselben Gruß platzieren, dann mit EndDoc einen korrekten Katalog, Seitenbaum, Xref und Trailer schreiben. Die Objekte darunter zu verstehen ist das, was Sie über die Ausgabe schlussfolgern lässt, wenn ein Dokument nicht so rendert wie erwartet