Zum Inhalt springen

Streams und Filter

Evidence: Standard-backed

Die meisten Bytes eines echten PDFs befinden sich in Streams: Seiteninhalt, Schriften, Bilder, der Cross-Reference-Stream selbst. Kaum eines dieser Bytes wird roh gespeichert — die Bytes durchlaufen zuvor einen oder mehrere Filter. Diese Seite erklärt, welche Filter Ihnen begegnen, wofür jeder einzelne dient, wo sie Probleme verursachen und warum NextPDF seine Komprimierung festschreibt, sodass aus derselben Eingabe stets dieselben Bytes entstehen.

Ein Stream und seine Filter bilden einen Vertrag: “Diese Bytes sind deflate-komprimiert und anschließend base-85-kodiert — dekodieren Sie sie in dieser Reihenfolge, um die eigentlichen Daten zu erhalten.” Wenn der Eintrag /Filter nicht mit dem übereinstimmt, was die Bytes tatsächlich sind, die /Length falsch ist oder zwei Filter in der falschen Reihenfolge aufgeführt sind, ist der Stream nicht dekodierbar und das darin enthaltene Objekt geht verloren. Ein Reader rät nicht heuristisch; er tut, was das Dictionary ihm vorgibt.

Es gibt einen zweiten, weniger offensichtlichen Preis. Wenn der Kompressor einer Bibliothek nicht-deterministisch ist — anderer zlib-Build, anderes Level, andere interne Blockgrenzen — dann erzeugen zwei Durchläufe, die ein identisches PDF erzeugen sollten, zwei unterschiedliche Dateien. Das bricht die byteweise Reproduzierbarkeit. Fehlende Reproduzierbarkeit bricht wiederum Golden-File-Tests, die Verifizierung signierter Builds und jede Pipeline, die Ausgaben vergleicht. Filter entscheiden sowohl darüber, ob das PDF korrekt ist, als auch darüber, ob das PDF dasselbe ist.

  • Ein Stream-Objekt ist ein Dictionary plus ein Byteblock, eingehüllt in streamendstream, mit einer /Length und üblicherweise einem /Filter.
  • Der Eintrag /Filter benennt den Dekodierfilter — oder ein Array von Filtern, die als Pipeline der Reihe nach angewendet werden.
  • Die Filter teilen sich in zwei Familien auf: Komprimierung (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) und ASCII-Transport (ASCIIHexDecode, ASCII85Decode), dazu der spezielle Crypt-Filter für die Verschlüsselung.
  • Der Filter, dem Sie am häufigsten begegnen, ist FlateDecode — zlib/deflate. Er ist der Standard für Inhalt, Schriften und den Cross-Reference-Stream.
  • NextPDF schreibt seine Flate-Ausgabe auf ein festes Level und Format fest, sodass dieselben Eingabebytes stets zu denselben Ausgabebytes komprimiert werden.

NextPDF gibt Stream-Objekte über einen einzigen Buffer-Helfer aus und komprimiert über einen einzigen festgeschriebenen Kompressor — bewusst.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) hüllt den Stream-Inhalt in sein Dictionary, schreibt dabei stets eine /Length, die der tatsächlichen Byte-Länge entspricht, und führt alle zusätzlichen Einträge zusammen, die der Aufrufer bereitstellt, etwa /Filter. Dadurch kann die deklarierte Länge nicht von den geschriebenen Bytes abweichen, denn die Länge wird aus dem Inhalts-String selbst übernommen.

Die Komprimierung läuft über PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Diese Klasse existiert aus einem einzigen Grund. gzcompress ohne explizites Level überlässt die Entscheidung dem zlib-Laufzeitstandard, der in der Vergangenheit über verschiedene Builds hinweg variiert hat. Der 2-Byte-zlib-Header kodiert sogar das Level indirekt, sodass “der Standard” keine stabile Ausgabe ist. Der Kompressor schreibt das Level auf das Maximum von RFC 1951 fest und gibt stets zlib-eingehüllten Deflate aus (RFC 1950-Header + Adler-32-Trailer), was genau das ist, was /Filter /FlateDecode erwartet. Ein harter Fehler von zlib wird zu einer typisierten Ausnahme statt zu einem stillen Rückgriff auf unkomprimierte Ausgabe — ein Stream wird niemals stillschweigend roh ausgegeben.

Der Cross-Reference-Stream selbst ist ein konkretes Beispiel für all das: CrossReferenceStream (src/Core/CrossReferenceStream.php) baut eine Binärtabelle auf, komprimiert sie und gibt sie als Stream-Objekt mit /Type /XRef, einem /W-Feldbreiten-Array und /Filter /FlateDecode aus. Der Index, der einem Reader das Auffinden jedes Objekts ermöglicht, ist selbst ein gefilterter Stream.

FilterFamilieWofür er dientWo er schiefgeht
FlateDecodeKomprimierungzlib/deflate; der Standard für Inhalt, Schriften, XRef-StreamsEin nicht-deterministischer zlib-Build kann “identische” PDFs Byte für Byte voneinander abweichen lassen
LZWDecodeKomprimierungÄltere Lempel–Ziv–Welch-KomprimierungVeraltet; durch Flate abgelöst, gelegentlich noch in alten Dateien anzutreffen
DCTDecodeKomprimierungJPEG-kodierte Farb-/GraustufenbilderVerlustbehaftet — das erneute Kodieren eines bereits DCT-kodierten Bildes verschlechtert es abermals
JPXDecodeKomprimierungJPEG-2000-Wavelet-BilddatenVon einigen Archivierungsprofilen nicht zugelassen; die breite Unterstützung ist uneinheitlich
JBIG2DecodeKomprimierungBilevel-Bildkomprimierung (1 Bit)Darf nicht mit Inline-Bildern verwendet werden; verlustbehaftete Modi können Scans verändern
RunLengthDecodeKomprimierungByteorientierte LauflängenkodierungHilft nur bei Daten mit langen Einzelbyte-Läufen; kann andere Daten vergrößern
ASCIIHexDecodeTransportBinärdaten als Hex-ZiffernVerdoppelt die Größe; nur für 7-Bit-sichere Kanäle, niemals zur Größenreduktion
ASCII85DecodeTransportBinärdaten als Base-85-ASCII~25 % Overhead; eine Transporthilfe, keine Komprimierung
CryptSicherheitWendet den Sicherheits-Handler des Dokuments anEin Cross-Reference-Stream darf keinen Crypt-Filter verwenden

Der PDF-Standard-Filtersatz, nach Familie geordnet, mit dem typischen Fehlerfall jedes einzelnen Filters. NextPDF schreibt FlateDecode für Inhalt, Schriften und den Cross-Reference-Stream fest; die ASCII-Transportfilter sind für 7-Bit-Kanäle, niemals zur Größenreduktion.

Der Filtermechanismus wird durch Spec: ISO 32000-2, §7.4 definiert. Die Filter eines Streams werden durch den Eintrag /Filter in seinem Dictionary angegeben, und Filter dürfen zu einer Pipeline verkettet werden, die den Stream nacheinander durch zwei oder mehr Dekodiertransformationen führt. Das Beispiel des Standards ist LZW, gefolgt von ASCII-Base-85, dekodiert in genau dieser Reihenfolge. Ein Writer kodiert einen Stream, um ihn zu komprimieren oder ihn 7-Bit-sicher zu machen. Ein Reader ruft die entsprechenden Dekodierfilter auf, um die ursprünglichen Daten wiederherzustellen. Evidence: Standard-backed

Die Filtertabelle des Standards klassifiziert jeden Filter. FlateDecode dekomprimiert zlib/deflate-kodierte Daten und stellt den ursprünglichen Text oder die ursprünglichen Binärdaten wieder her. DCTDecode stellt Bild-Samples wieder her, die das Original mittels JPEG annähern — mit dem Wort “annähern” signalisiert der Standard, dass der Vorgang verlustbehaftet ist. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode und der Crypt-Filter sind dort jeweils ebenfalls definiert, wobei JBIG2 ausdrücklich von Inline-Bildern ausgeschlossen ist.

Der Cross-Reference-Stream wendet die formateigene Maschinerie auf sich selbst an: Er ist ein Stream-Objekt (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ), dessen /W-Array die Bytebreite jedes Eintragsfeldes im dekodierten Stream angibt. Der Standard verlangt, dass er nicht verschlüsselt ist und keinen Crypt-Filter verwendet. NextPDFs CrossReferenceStream befolgt dies exakt — FlateDecode, explizites /W, keine Verschlüsselung.

Ein Seiteninhalts-Stream, mit Flate komprimiert. Dies ist die mit Abstand häufigste Form: ein Dictionary mit /Length und /Filter, dann die komprimierten Bytes zwischen stream und endstream.

<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.
$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,
// fixed zlib-wrapped format. The same $content always yields the
// same $compressed bytes, on any supported PHP/zlib build.
$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of
// $compressed; /Filter names the decode the reader must apply.
// N 0 obj
// << /Length <strlen($compressed)> /Filter /FlateDecode >>
// stream
// <$compressed bytes>
// endstream
// endobj

Ein Reader macht das Umgekehrte: Er liest /Length Bytes, führt sie durch FlateDecode, weil /Filter es so vorgibt, und erhält die ursprünglichen Operatoren zurück. Wenn Sie den Kompressor festschreiben, ist dieser Hin- und Rückweg nicht nur korrekt, sondern jedes Mal identisch — darauf verlassen sich Golden-File- und Signed-Build-Prüfungen.

Die Falle besteht darin, die ASCII-Filter als Komprimierung zu behandeln. ASCIIHexDecode und ASCII85Decode machen einen Stream größer — etwa um das Doppelte beziehungsweise um etwa 25 %. Sie existieren, um Binärdaten durch einen Kanal zu bewegen, der nur für 7-Bit-Text sicher ist, nicht um Platz zu sparen. ASCII85 zu wählen, um ein PDF zu “schrumpfen”, bewirkt das Gegenteil. Der zweite Teil desselben Missverständnisses ist der Glaube, FlateDecode sei bei Bildern ohne Nebenwirkungen verlustfrei. Flate ist verlustfrei, doch wenn das Bild bereits DCT-(JPEG-)kodiert war, verschlechtern ein erneutes Einhüllen oder ein Transkodieren durch einen verlustbehafteten Filter das Bild, unabhängig davon, was Flate darum herum tut. Die Filter-Pipeline bewahrt genau das, was Sie ihr zuführen — einschließlich eines Rekompressions-Artefakts, das Sie ihr versehentlich zugeführt haben.

Diese Seite behandelt, wie Filter deklariert und angewendet werden, nicht den bitgenauen Algorithmus innerhalb jedes einzelnen Filters. Die Determinismus-Garantie bezieht sich speziell auf die Flate-Ausgabe von NextPDF für die Streams, die es schreibt. Sie gilt über PHP-Minor-Versionen und standardkonforme zlib-Builds hinweg, doch der Standard erlaubt einem Deflate-Encoder ausdrücklich, unterschiedliche interne Blockgrenzen zu wählen, sodass eine byte-identische Ausgabe über wirklich unterschiedliche zlib-Implementierungen hinweg (zum Beispiel ein Standard-zlib gegenüber zlib-ng) nicht zugesichert wird. Aus diesem Grund ist die Build-Umgebung festgeschrieben.

NextPDF verwendet FlateDecode und die ASCII-Transportfilter für die Daten, die es ausgibt. Es ist kein Bild-Transcoder. Es verspricht nicht, einen beliebigen eingehenden JPEG2000- oder JBIG2-Stream neu zu verpacken, und Kompromisse verlustbehafteter Bildkompression sind eine Eigenschaft der Ausgangsdaten, nicht etwas, das ein Writer rückgängig machen kann.

Warum ist FlateDecode überall? Es ist verlustfrei, universell einsetzbar, gut unterstützt und passt gut zu den Text- und Operatoreninhalten der meisten PDFs. Es ist der sichere Standard für Inhalts-Streams, eingebettete Schriften und den Cross-Reference-Stream.

Kann ich die Komprimierung abschalten? Sie können /Filter weglassen und rohe Bytes speichern, und ein Reader wird es akzeptieren. Die Datei wird größer, und sonst verbessert sich nichts; abseits des Debuggings gibt es selten einen Grund dafür.

Warum überhaupt das Komprimierungslevel festschreiben? Damit die Ausgabe reproduzierbar ist. Ein nicht festgeschriebenes Level (oder ein anderer zlib-Build) kann die komprimierten Bytes ändern, ohne den dekomprimierten Inhalt zu ändern — korrekt, aber nicht identisch, was die byteweise Verifizierung zunichtemacht.

  • Stream-Objekt — ein Dictionary plus ein Block von Bytes zwischen stream und endstream, der eine /Length und üblicherweise ein /Filter trägt.
  • Filter — eine benannte Dekodiertransformation, die ein Reader auf die Bytes eines Streams anwendet (zum Beispiel FlateDecode).
  • Filter-Pipeline — ein Array von Filtern, die der Reihe nach angewendet werden; die Array-Reihenfolge ist die Dekodierreihenfolge.
  • FlateDecode — der zlib/deflate-Filter; die Standardkomprimierung für Inhalt, Schriften und Cross-Reference-Streams.
  • DCTDecode — der JPEG-Bildfilter; verlustbehaftet, sodass ein erneutes Kodieren das Bild abermals verschlechtert.
  • ASCII-Transportfilter — ASCIIHexDecode / ASCII85Decode; machen Daten 7-Bit-sicher auf Kosten der Größe — keine Komprimierung.
  • Deterministische Komprimierung — die Erzeugung byte-identischer komprimierter Ausgabe für identische Eingabe, erreicht durch das Festschreiben von Level und Format des Kompressors.