🔁Updates, kompakte Streams und schnelles Laden

Drei Erweiterungen der Dateistruktur: Änderungen anhängen statt überschreiben, Objekte und xref-Tabelle komprimieren (PDF 1.5) und die Datei so anordnen, dass Seite 1 erscheint, bevor der Download fertig ist.

➕Inkrementelles Update

Aus „Hello World“ wird „Hallo Welt“: Objekt 4 (Inhalt) und 6 (Titel) werden neu geschrieben – ans Ende angehängt. Die ersten 690 Bytes bleiben unverändert.
Original · 690 B
Update · 282 B
Angehängte Bytes (ab Byte 690)
4 0 obj
<< /Length 40 >>
stream
BT
/F1 24 Tf
72 72 Td
(Hallo Welt) Tj
ET
endstream
endobj
6 0 obj
<< /Title (Hallo Welt) /Producer (VisualPdf) >>
endobj
xref
4 1
0000000690 00000 n 
6 1
0000000780 00000 n 
trailer
<< /Size 7 /Root 1 0 R /Info 6 0 R /Prev 475 >>
startxref
843
%%EOF
Objekt 4 (neu) – geänderter Content-Stream Byte 690–779

Dieselbe Objektnummer 4 wird ans Ende angehängt, mit neuem Inhalt „Hallo Welt“. Das alte Objekt 4 bleibt als Bytes stehen, wird aber nicht mehr benutzt.

Objekt 6 (neu) – geänderte Metadaten Byte 780–842

Auch der Titel ändert sich – wieder als neue Version am Dateiende.

Neue xref-Sektion Byte 843–895

Enthält nur die geänderten bzw. neuen Objekte. Für alle anderen gilt weiterhin die ältere Tabelle.

Neuer Trailer mit /Prev Byte 896–951

/Prev 475 verweist auf die vorherige xref-Tabelle. Ein Leser folgt dieser Kette rückwärts; der jüngste Eintrag gewinnt.

startxref + zweites %%EOF Byte 952–971

Zeigt jetzt auf die neue Tabelle bei Byte 843. Die ursprünglichen Bytes davor bleiben unverändert – deshalb überleben digitale Signaturen ein Update.

startxref 843
   └─► xref (neu): Objekte 4, 6
         └─ /Prev 475 ─► xref (alt): Objekte 0–6
Objekt 4 → neue Version (Byte 690)
Objekt 3 → alte Tabelle  (Byte 121)
🔬 Im Byte-Explorer ansehen (Tab „Nach inkrementellem Update“)
💡 Warum anhängen?
Speichern geht schnell (nur neue Bytes schreiben), und jede frühere Fassung ist in der Datei noch enthalten.
✅ Signaturen bleiben gültig
Eine Signatur deckt einen festen Byte-Bereich ab. Ein Update danach ändert diese Bytes nicht – der Betrachter zeigt nur, dass es spätere Änderungen gibt.
⚠️ Datenschutz-Falle
„Gelöschter“ Text kann in älteren Revisionen weiterleben. Erst ein vollständiges Neuschreiben (z. B. qpdf in.pdf out.pdf) entfernt ihn.

🗜️Object Streams (PDF 1.5)

Viele kleine Dictionaries werden in einen gemeinsamen, komprimierten Stream gepackt. Echtes Beispiel: qpdf --object-streams=generate hello.pdf os.pdf. Bei unserer Mini-Datei wächst sie dabei sogar von 690 auf 746 Bytes (Overhead der Stream-Dictionaries) – der Gewinn zeigt sich erst bei Hunderten von Objekten.
Objekt 1 in os.pdf (entpackt)
<< /Filter /FlateDecode /First 26 /Length 215 /N 5 /Type /ObjStm >>
stream
2 0 3 34 4 78 5 191 6 246
<< /Pages 3 0 R /Type /Catalog >>
<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >>
<< /Contents 7 0 R /MediaBox [ 0 0 300 144 ] /Parent 3 0 R /Resources << /Font << /F1 5 0 R >> >> /Type /Page >>
<< /BaseFont /Helvetica /Subtype /Type1 /Type /Font >>
<< /Producer (VisualPdf) /Title (Hello World) >>
endstream

Der Kopf 2 0 3 34 4 78 5 191 6 246 besteht aus /N 5 Paaren „Objektnummer, Offset“. Offsets zählen ab /First 26:

ObjektOffsetPositionInhalt
2026<< /Pages 3 0 R /Type /Catalog >>
33460<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >>
478104<< /Contents 7 0 R /MediaBox [ 0 0 300 144 ] /Pa…
5191217<< /BaseFont /Helvetica /Subtype /Type1 /Type /F…
6246272<< /Producer (VisualPdf) /Title (Hello World) >>

Streams dürfen nicht in Object Streams liegen – der Content-Stream bleibt ein eigenes Objekt. Auch die Generationsnummer ist dort immer 0.

📇Cross-Reference-Streams (PDF 1.5)

Wo liegt Objekt 3, wenn es keinen Byte-Offset mehr hat? Die klassische xref-Tabelle kann das nicht ausdrücken. Ein xref-Stream ersetzt sie: binäre Zeilen fester Breite, dazu komprimiert.
<< /DecodeParms << /Columns 4 /Predictor 12 >> /Filter /FlateDecode /ID [ <53b8…> <53b8…> ] /Info 6 0 R /Length 32 /Root 2 0 R /Size 9 /Type /XRef /W [ 1 2 1 ] >>
Obj.gespeichert (Filter + 4 Bytes)dekodiertTypFeld 2Feld 3
002 00 00 00 0000 00 00 00000
102 01 00 0F 0001 00 0F 001150
202 01 00 F2 0002 00 01 00210
302 00 00 00 0102 00 01 01211
402 00 00 00 0102 00 01 02212
502 00 00 00 0102 00 01 03213
602 00 00 00 0102 00 01 04214
702 FF 01 49 FC01 01 4A 0013300
802 00 00 75 0001 01 BF 0014470
Zeile für Objekt 2

Filterbyte 02 = PNG-Filter „Up“: jedes Byte wird zum Byte darüber addiert (modulo 256).

darüber 01 00 0F 00
+ Zeile 01 00 F2 00
= Wert 02 00 01 00
W1 └─W2─┘ W3
Typ 2 = in Object Stream

2/0: compressed; stream = 1, index = 0

Diese Zeile ist Byte für Byte identisch mit der Ausgabe von qpdf --show-xref.

/W [1 2 1]: 1 Byte Typ, 2 Bytes Feld 2 (Offset bzw. Nummer des Object Streams), 1 Byte Feld 3 (Generation bzw. Index im Object Stream), Big Endian. Der Trailer steht jetzt im Stream-Dictionary (/Root, /Size, /Info), das Schlüsselwort trailer entfällt.

⚡Linearisierung („Fast Web View“)

Eine linearisierte Datei ist so sortiert, dass alle Objekte der ersten Seite vorn liegen. Ein Browser kann Seite 1 anzeigen, sobald /E Bytes geladen sind, und weitere Seiten per HTTP-Range-Anfrage nachladen (ISO 32000-1, Anhang F). Echtes Beispiel: qpdf --linearize hello.pdf lin.pdf.
Byte     Inhalt von lin.pdf  (qpdf --linearize hello.pdf lin.pdf)
    0    %PDF-1.7  + Binärkommentar
   15    3 0 obj  << /Linearized 1 /L 1305 /H [561 120] /O 6 /E 996 /N 1 /T 1127 >>
         xref 3 6 … trailer << … /Prev 1119 >>        ◄ xref der ersten Seite
  512    4 0 obj  Catalog
  561    5 0 obj  Hint-Stream (FlateDecode)            ◄ /H
  681    6 0 obj  Page (erste Seite)                    ◄ /O
  809    7 0 obj  Content-Stream
  926    8 0 obj  Font Helvetica
  ───── /E 996 ── bis hier: Seite 1 komplett ────────────────────
  996    1 0 obj  Pages
 1055    2 0 obj  Info
 1119    xref 0 3 … trailer                             ◄ Haupt-xref (/T 1127)
 1305    Dateiende                                      ◄ /L
EintragBedeutung
/Linearized 1Version der Linearisierung
/L 1305Dateilänge in Bytes – stimmt sie nicht, wurde die Datei nachträglich (inkrementell) geändert und gilt nicht mehr als linearisiert
/H [561 120]Offset und Länge des primären Hint-Streams (Objekt 5)
/O 6Objektnummer des Page-Objekts der ersten Seite
/E 996Offset des Endes der ersten Seite – bis hierher reicht alles, was zum Anzeigen von Seite 1 nötig ist
/N 1Anzahl der Seiten
/T 1127Offset des Leerzeichens vor dem ersten Eintrag der Haupt-xref-Tabelle

Prüfen: qpdf --check-linearization lin.pdf → „no linearization errors“; pdfinfo lin.pdf meldet Optimized: yes. Das Linearisierungs-Dictionary muss vollständig in den ersten 1024 Bytes stehen.