PDF-Coding · Datei-Struktur

Cross-Reference-Streams und PDF-2.0-Optimierungen

Die klassische xref-Tabelle am Ende einer PDF-Datei sieht aus wie eine Reliquie aus den 1990ern, und genau das ist sie auch. Seit PDF 1.5 darf sie durch einen kompakten, komprimierten Cross-Reference-Stream ersetzt werden; in PDF 2.0 ist das der Normalfall. Diese Seite erklärt, was sich technisch ändert, warum es sich ändert und worauf du beim Lesen und Schreiben von PDFs achten musst.

  • 9 Minuten Lesezeit
  • Stand: Mai 2026

Was ein Cross-Reference-Stream ist

Jede PDF-Datei braucht ein Verzeichnis, das sagt: „Objekt Nr. 42 beginnt an Byte-Offset 18 245." Ohne dieses Verzeichnis findet kein Reader nichts, denn PDF wird nicht von vorn nach hinten gelesen, sondern vom Ende her: der Trailer verweist auf das Cross-Reference , das Cross-Reference verweist auf die Objekte.

Bis PDF 1.4 war dieses Verzeichnis eine textuelle Tabelle, eingeleitet mit dem Schlüsselwort xref und jeder Zeile fest formatiert (20 Bytes). Mit PDF 1.5 wurde eine Alternative eingeführt: ein Cross-Reference-Stream. Statt einer Klartext-Tabelle steht ein Stream-Objekt mit binär kodiertem Inhalt und einem Dictionary-Header, in dem genau festgelegt ist, wie die Bytes zu lesen sind. PDF 2.0 (ISO 32000-2) hebt das auf den Default an: Neue PDF-Werkzeuge schreiben standardmäßig xref-Streams, die alte Tabelle taucht nur noch in Backward-Compatibility-Szenarien auf.

Von der klassischen xref-Tabelle zum xref-Stream Ein Vorher-Nachher-Vergleich. Links unter „Bis PDF 1.4: xref-Tabelle" ein Kasten mit mehreren festen Textzeilen aus Zahlen, beschriftet mit Klartext, 20 Bytes pro Zeile, unkomprimiert. Ein Pfeil führt nach rechts zu einem Kasten unter „Ab PDF 1.5: xref-Stream", der ein Stream-Objekt mit einem Dictionary-Kopf und einem binär kodierten, komprimierten Datenblock zeigt, beschriftet mit Stream-Objekt, binär, FlateDecode-komprimiert. Eine Notiz weist darauf hin, dass PDF 2.0 den xref-Stream zum Standard macht. DASSELBE VERZEICHNIS, ZWEI FORMEN BIS PDF 1.4: xref-TABELLE 0000018245 00000 n 0000019033 00000 n 0000019921 00000 n Klartext · 20 Bytes/Zeile · unkomprimiert AB PDF 1.5: xref-STREAM << /Type /XRef /W [1 2 1] … >> 01 47 23 00 02 00 01 9A 0C … (binär) Stream-Objekt · binär · FlateDecode-komprimiert PDF 2.0 macht den xref-Stream zum Standard; die Tabelle bleibt für Alt-Reader.
Beide Formen beantworten dieselbe Frage „Wo liegt welches Objekt?": links als feste Klartext-Tabelle, rechts als komprimiertes Stream-Objekt mit Dictionary-Kopf.

Warum die xref-Tabelle abgelöst wird

Die klassische xref-Tabelle hat drei strukturelle Schwächen, die in modernen PDFs zunehmend stören:

  • Sie ist groß. 20 Bytes pro Objekt klingt wenig, summiert sich aber bei zehntausenden Objekten zu mehreren hundert Kilobyte. Ein Cross-Reference-Stream ist FlateDecode-komprimiert und schrumpft auf einen Bruchteil davon zusammen.
  • Sie kann nicht in den Datei-Inhalt. Die Tabelle muss textuell und unkomprimiert am Ende der Datei stehen. Damit wandert sie an Object-Streams (Punkt 3) vorbei, die eigentlich die größte Kompressions-Hebelwirkung haben.
  • Sie zwingt zur 4-GB-Grenze. Die textuelle Form kodiert Offsets als 10-stellige Zahlen, was de facto die Datei auf knapp 10 Milliarden Bytes begrenzt. xref-Streams kodieren Offsets binär in beliebig vielen Bytes; die offizielle qpdf-Dokumentation nennt 10 GB als praktische Schwelle, ab der xref-Streams alternativlos werden.

Dazu kommt: xref-Streams sind Stream-Objekte. Das heißt, sie folgen demselben Lese- und Schreibprotokoll wie alle anderen Objekte im PDF. Wer einen PDF-Parser baut, hat damit einen Code-Pfad weniger: die textuelle Tabelle erforderte einen Sonderfall, der mit dem Rest der Datei nichts gemeinsam hatte.

Lese-Reihenfolge einer PDF-Datei mit xref-Stream Ein Schaubild zeigt die fünf Stationen eines PDF-Readers von rechts nach links: 1. Datei-Ende mit dem Schlüsselwort startxref und einem Byte-Offset. 2. Das Cross-Reference-Stream-Objekt mit Dictionary und komprimierten Daten. 3. Die Trailer-Dictionary-Einträge wie /Root und /Size, die im selben Stream-Dictionary stehen. 4. Das Catalog-Objekt, das im PDF ganz vorn die Seitenstruktur einleitet. 5. Die einzelnen Seiten und Inhalts-Objekte. Pfeile verbinden die Stationen in Lese-Reihenfolge. Datei-Ende startxref 18245 xref-Stream Dictionary + komprimierte Entries Trailer-Keys /Root /Size im Stream-Dict Catalog Wurzel-Obj. /Pages, /Lang Seiten Content, Bilder, Tags LESE-REIHENFOLGE EINES PDF-READERS Schritt 1 → Schritt 5, von rechts nach links durch die Datei Schritt 1 Schritt 2 Schritt 3 Schritt 4 Schritt 5
Lese-Reihenfolge eines PDF-Readers mit xref-Stream: startxref am Datei-Ende → Cross-Reference-Stream mit Dictionary → Trailer-Keys im selben Dictionary → Catalog → einzelne Seiten. Der ehemalige separate trailer -Block ist Teil des xref-Stream-Dictionaries geworden.

Aufbau einer xref-Stream-Entry

Ein Cross-Reference-Stream besteht aus einem Stream-Dictionary und einem binären Datenblock. Im Dictionary stehen die üblichen Trailer-Schlüssel ( /Root , /Size , /Info , /Encrypt ) plus drei stream-spezifische Felder: /Type /XRef , ein /W -Array, das die Byte-Breite jeder Entry-Spalte definiert, und ein /Index -Array, das die Objekt-Nummern benennt, die abgedeckt werden.

xref-stream Header (vereinfacht)

15 0 obj
<< /Type /XRef
   /Size 16
   /W [ 1 4 2 ]
   /Root 1 0 R
   /Filter /FlateDecode
   /Length 67
>>
stream
...binäre Entries...
endstream
endobj
startxref
18245
%%EOF

Die binären Entries werden in Big-Endian-Reihenfolge konkateniert. Jede Entry besteht laut PDF-2.0-Spezifikation (Tabelle 17, Klausel 7.5.8) aus genau drei Feldern: einem Typ-Byte, einem Offset-Feld (variable Breite, von /W gesteuert) und einem Generations-Feld. Das Typ-Byte sagt, was die Entry beschreibt:

Die drei Entry-Typen im xref-Stream (ISO 32000-2 §7.5.8)
Typ-Byte Bedeutung Zweites Feld Drittes Feld
0 Freie Entry (Objekt gelöscht oder noch nie vergeben) Objekt-Nummer der nächsten freien Entry Generations-Nummer für die nächste Wiederverwendung
1 Belegte Entry, Objekt liegt direkt in der Datei Byte-Offset des Objekts in der Datei Generations-Nummer (meist 0 )
2 Komprimierte Entry, Objekt liegt in einem Object-Stream Objekt-Nummer des Object-Streams Index innerhalb des Object-Streams

Typ 2 ist der eigentliche Hebel: Er erlaubt, ein PDF-Objekt nicht direkt in der Datei, sondern in einem komprimierten Object-Stream zu speichern. Das ist genau die Kombination, die moderne PDFs so klein macht: Hunderte von kleinen Dictionary-Objekten landen gebündelt in einem einzigen Flate-komprimierten Stream.

Object-Streams: der natürliche Partner

Ein Object-Stream ist ein Container, der mehrere andere PDF-Objekte enthält, komprimiert. Er hat selbst eine Objekt-Nummer und einen eigenen Eintrag im xref-Stream (Typ 1). Im Header des Object-Streams steht, welche Objekt-Nummern darin liegen und an welchen Offsets im Stream-Inhalt sie beginnen.

Wichtig ist: Nur Objekte ohne eigenen Stream-Inhalt (also Dictionary- und Array-Objekte) dürfen in Object-Streams. Stream-Objekte selbst, also Content-Streams einer Seite, Bilder und eingebettete Schriften, bleiben außerhalb. Das ist auch der Grund, warum die Datei trotz aller Object-Stream-Konsolidierung viele Top-Level-Objekte enthält: jedes Bild, jeder Content-Stream ist eins davon.

Was in einen Object-Stream darf und was draußen bleibt Links ein großer Container mit der Beschriftung Object-Stream, komprimiert, in dem mehrere kleine Kästchen liegen, die mit Dictionary- und Array-Objekten beschriftet sind. Rechts liegen separat und außerhalb des Containers drei eigenständige Objekte mit der Beschriftung Stream-Objekte, die draußen bleiben: ein Content-Stream, ein Bild und eine eingebettete Schrift. Eine Notiz erklärt, dass nur Objekte ohne eigenen Stream-Inhalt in den Object-Stream dürfen. OBJECT-STREAM: WAS HINEINDARF, WAS DRAUSSEN BLEIBT DARF HINEIN Object-Stream (komprimiert) Dictionary Array Dictionary Objekte ohne eigenen Stream-Inhalt BLEIBT DRAUSSEN Content-Stream einer Seite Bild (Image-XObject) eingebettete Schrift Stream-Objekte: bleiben Top-Level
In einen Object-Stream wandern nur Objekte ohne eigenen Stream-Inhalt (Dictionaries, Arrays). Content-Streams, Bilder und Schriften bleiben eigenständige Top-Level-Objekte.

Hybrid-Files und Kompatibilität

Wer mit gemischten Reader-Versionen umgehen muss, kann eine Hybrid-Datei bauen: Sie enthält sowohl eine klassische xref-Tabelle als auch einen xref-Stream. Ältere Reader (PDF 1.4 und früher) finden die textuelle Tabelle und ignorieren den Stream; neuere Reader sehen im Trailer der Tabelle den Eintrag /XRefStm mit dem Byte-Offset zum Stream und nehmen diesen.

Hybrid-Files haben drei Nachteile, die in der Praxis fast immer gegen sie sprechen: Sie sind größer als reine xref-Stream-Dateien, sie verdoppeln die Wartungs-Logik beim Schreiben, und sie werden durch Tools, die nur das eine oder das andere kennen, leicht inkonsistent. PDF 2.0 erlaubt sie weiterhin, der ISO-Norm-Text betont aber, dass neue PDF-Werkzeuge sie nur in expliziten Kompatibilitäts-Szenarien erzeugen sollen.

Bei inkrementellen Updates, also wenn eine bestehende PDF-Datei um neue Objekte erweitert wird, ohne den Rest neu zu schreiben, hängt sich der neue xref-Stream einfach hinten an. Sein Dictionary verweist über /Prev auf den Byte-Offset des vorherigen xref-Streams (oder der vorherigen Tabelle). Reader folgen dieser Kette rückwärts, bis sie an einer Entry ohne /Prev ankommen.

Hybrid-Datei: zwei Reader, zwei Wege zum Verzeichnis In der Mitte eine Hybrid-PDF-Datei, die sowohl eine klassische xref-Tabelle als auch einen xref-Stream enthält. Von oben kommt ein älterer Reader bis PDF 1.4, dessen Pfeil zur klassischen xref-Tabelle führt; den xref-Stream ignoriert er. Von unten kommt ein neuerer Reader, dessen Pfeil über den Trailer-Eintrag mit Schrägstrich XRefStm zum xref-Stream führt. Eine Notiz erklärt, dass beide Reader dieselbe Datei öffnen können, Hybrid-Dateien aber größer sind und meist vermieden werden. HYBRID-DATEI: KOMPATIBILITÄT FÜR ZWEI READER-GENERATIONEN Alter Reader (bis PDF 1.4) Neuer Reader (1.5+) HYBRID-PDF klassische xref-Tabelle xref-Stream Trailer: /XRefStm → findet die Tabelle folgt /XRefStm zum Stream
Eine Hybrid-Datei trägt beide Verzeichnisse: Alt-Reader nehmen die klassische Tabelle, neue Reader folgen dem Trailer-Eintrag /XRefStm zum Stream. Praktisch, aber größer und daher meist vermieden.

Tooling: qpdf und pikepdf

In der Praxis musst du xref-Streams selten von Hand parsen, denn die etablierten Open-Source-Bibliotheken erledigen das automatisch. Trotzdem hilft es, zu wissen, welche Hebel sie anbieten.

qpdf: der Schreib-Modus

qpdf(in C++ geschrieben, mit CLI und C-API) bietet drei Modi für das Schreiben von Object-Streams, jeweils mit passender xref-Variante:

  • --object-streams=disable : keine Object-Streams, und zugleich eine klassische xref-Tabelle. Erzeugt PDFs, die auch alte Reader öffnen können.
  • --object-streams=preserve : Default. Übernimmt das Format der Quell-Datei.
  • --object-streams=generate : bündelt Objekte in Object-Streams (Gruppen von max. 100), erzeugt einen xref-Stream und hebt die PDF-Version auf mindestens 1.5 an.

pikepdf: die Python-Sicht

pikepdf wickelt qpdf in Python-Bindings ein. Object- und xref-Streams werden beim Öffnen automatisch dekomprimiert; im Python-Code arbeitet man mit normalen pikepdf.Object - Instanzen. Beim Speichern lässt sich das Stream-Verhalten über die Optionen von Pdf.save() steuern. Ein praktisches Detail für Forensik-Tools: Pdf.open(..., ignore_xref_streams=True) zwingt pikepdf, eventuell vorhandene xref-Streams zu ignorieren und ausschließlich der textuellen Tabelle zu folgen.

pikepdf: Stream-Verhalten steuern

 # Beim Schreiben Object-Streams erzwingen (kompakte PDFs) 
import pikepdf
with pikepdf.open("eingabe.pdf") as pdf:
    pdf.save(
        "ausgabe.pdf",
        object_stream_mode=pikepdf.ObjectStreamMode.generate,
        compress_streams=True,
    ) # Beim Öffnen xref-Streams ignorieren (für Forensik) 
pdf = pikepdf.open("verdaechtig.pdf", ignore_xref_streams=True)

Mythen-Prüfung