コンテンツにスキップ

ストリームとフィルター

Evidence: Standard-backed

実際の PDF のバイトの大半は ストリーム の中にあります。具体的には、ページコンテンツ、フォント、画像、そしてクロスリファレンスストリームそのものです。 それらのバイトが未加工のまま格納されることはほとんどありません。まず 1 つ以上の フィルター を通します。 このページでは、よく遭遇するフィルターの種類、それぞれの用途、トラブルが起きる箇所、そして NextPDF が同じ入力から常に同じバイトを生成するように圧縮を固定する理由を扱います。

ストリームとそのフィルターは契約です。「これらのバイトは deflate で圧縮され、その後 base-85 でエンコードされている。実データを得るにはその順序でデコードせよ」というものです。 /Filter エントリがバイトの実体と食い違っている場合、あるいは /Length が誤っている場合、あるいは 2 つのフィルターが誤った順序で列挙されている場合、ストリームはデコードできず、それが運んでいたオブジェクトは失われます。 リーダーはヒューリスティックに推測してはくれません。辞書が指示するとおりに動作します。

もう 1 つ、より見えにくいコストがあります。 ライブラリの圧縮器が非決定論的である場合、つまり zlib のビルドが異なる、レベルが異なる、内部のブロック境界が異なる場合には、本来は同一の PDF を生成するはずの 2 回の実行が、2 つの異なるファイルを生成してしまいます。 これはバイトレベルの再現性を破壊します。 再現性が破綻すると、今度はゴールデンファイルテスト、署名付きビルドの検証、そして出力を差分比較するあらゆるパイプラインが破綻します。 フィルターは、PDF が正しいかどうかと、PDF が 同一 かどうかの両方を決定づけます。

  • ストリームオブジェクト とは、辞書に 1 ブロックのバイト列を加えたものであり、streamendstream で囲まれ、/Length を持ち、通常は /Filter を伴います。
  • /Filter エントリは、デコードフィルター、または パイプライン として順に適用されるフィルターの 配列 を指定します。
  • フィルターは 2 つの系統に分かれます。圧縮(FlateDecode、LZWDecode、RunLengthDecode、DCTDecode、JPXDecode、JBIG2Decode)と ASCII トランスポート(ASCIIHexDecode、ASCII85Decode)、それに暗号化のための特別な Crypt フィルターです。
  • 最もよく目にするのは FlateDecode、つまり zlib/deflate です。 これはコンテンツ、フォント、クロスリファレンスストリームのデフォルトです。
  • NextPDF は Flate 出力を固定されたレベルとフォーマットで扱うため、同じ入力バイトは常に同じ出力バイトに圧縮されます。

NextPDF は、ストリームオブジェクトを 1 つのバッファヘルパー経由で出力し、1 つの固定された圧縮器を通して圧縮します。これは意図した設計です。

BinaryBuffer::writeStream()src/Support/BinaryBuffer.php)は、ストリームコンテンツをその辞書で包み、常に実際のバイト長と等しい /Length を書き込み、/Filter など呼び出し側が提供する追加エントリをマージします。 宣言される長さはコンテンツ文字列そのものから取られるため、書き込まれたバイトと食い違う長さが宣言される経路は存在しません。

圧縮は PinnedZlibCompressorsrc/Writer/PinnedZlibCompressor.php)を通ります。 このクラスが存在する理由はただ 1 つです。 レベルを明示しない gzcompress は zlib ランタイムのデフォルトに委ねられ、これは歴史的にビルドごとに変動してきました。 2 バイトの zlib ヘッダーは圧縮レベルまでも間接的にエンコードするため、「デフォルト」は安定した出力ではありません。 圧縮器はレベルを RFC 1951 の最大値に固定し、常に zlib でラップされた deflate(RFC 1950 ヘッダー + Adler-32 トレーラー)を出力します。これはまさに /Filter /FlateDecode が期待するものです。 zlib からのハード障害は、非圧縮出力へ暗黙にフォールバックするのではなく、型付き例外になります。ストリームが密かに生のまま出力されることは決してありません。

クロスリファレンスストリーム自体が、これらすべての実例になっています。CrossReferenceStreamsrc/Core/CrossReferenceStream.php)はバイナリのテーブルを構築し、それを圧縮し、/Type /XRef/W フィールド幅配列、そして /Filter /FlateDecode を持つストリームオブジェクトとして出力します。 リーダーがすべてのオブジェクトを見つけるためのインデックスそのものが、フィルター済みのストリームなのです。

フィルター系統用途問題が起きる箇所
FlateDecode圧縮zlib/deflate。コンテンツ、フォント、xref ストリームのデフォルト非決定論的な zlib ビルドは「同一」の PDF をバイト単位で食い違わせる
LZWDecode圧縮旧来の Lempel–Ziv–Welch 圧縮レガシー。Flate に取って代わられたが、古いファイルでまれに見られる
DCTDecode圧縮JPEG エンコードされた colour/grayscale 画像非可逆。すでに DCT 化された画像を再エンコードすると、さらに劣化する
JPXDecode圧縮JPEG 2000 ウェーブレット画像データ一部のアーカイブプロファイルでは許可されない。幅広いサポートはまちまち
JBIG2Decode圧縮2 値(1 ビット)画像の圧縮インライン画像と併用してはならない。非可逆モードはスキャン画像を改変しうる
RunLengthDecode圧縮バイト指向のランレングス圧縮同じバイトが長く連続するデータにしか効かない。他のデータは 増大 させうる
ASCIIHexDecodeトランスポートバイナリを 16 進数字として表現サイズが倍増する。7 ビットセーフなチャネル向けであり、サイズ削減用ではない
ASCII85Decodeトランスポートバイナリを base-85 ASCII として表現オーバーヘッドは約 25%。圧縮ではなくトランスポート上の利便性のため
Cryptセキュリティドキュメントのセキュリティハンドラーを適用クロスリファレンスストリームは Crypt フィルターを使用しては ならない

PDF 標準のフィルターセットを系統別に示し、それぞれに結びつく障害を併記したもの。 NextPDF はコンテンツ、フォント、そしてクロスリファレンスストリームに FlateDecode を書き込む。ASCII トランスポートフィルターは 7 ビットチャネル用であり、 サイズ削減用ではない。

フィルターのメカニズムは Spec: ISO 32000-2, §7.4 によって定義されています。 ストリームのフィルターはその辞書内の /Filter エントリで指定され、フィルターは カスケードしてパイプラインを形成 でき、ストリームを 2 つ以上のデコード変換に順次通すことができます。 標準内の例は、LZW の後に ASCII base-85 が続くもので、その順序でデコードされます。 ライターはストリームを圧縮するため、あるいは 7 ビットセーフにするためにエンコードします。 リーダーは対応するデコードフィルターを呼び出して元のデータを復元します。 Evidence: Standard-backed

標準のフィルター表は各フィルターを分類しています。 FlateDecode は zlib/deflate-encoded データを解凍し、元のテキストまたはバイナリデータを再現します。 DCTDecode は JPEG により、元を 近似する 画像サンプルを再現します — 「近似する(approximate)」という語が、それが非可逆であることを標準上で示しています。 LZWDecode、RunLengthDecode、JBIG2Decode、JPXDecode、そして Crypt フィルターも、同じ箇所でそれぞれ定義されており、JBIG2 はインライン画像での使用が明示的に禁じられています。

クロスリファレンスストリームは、フォーマット自身の仕組みを自分自身に適用したものです。それはストリームオブジェクト(/Type /XRef Spec: ISO 32000-2, §7.5.8 )であり、その /W 配列が デコード後のストリームにおける 各エントリフィールドのバイト幅を示します。 標準はそれが暗号化されておらず、Crypt フィルターを使用しないことを要求します。 NextPDF の CrossReferenceStream はこれに正確に従います。FlateDecode、 明示的な /W、暗号化なし。

Flate で圧縮されたページコンテンツストリーム。 これは圧倒的によく見られる形です。/Length/Filter を持つ辞書に続いて、streamendstream の間に圧縮済みバイトが置かれます。

<?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

リーダーはその逆を行います。/Length バイトを読み取り、/Filter がそう指示しているため FlateDecode を通し、元の演算子を取り戻します。 圧縮器を固定すれば、そのラウンドトリップは正しいだけではありません。 毎回 同一 でもあり、これこそゴールデンファイルと署名付きビルドのチェックが依拠するものです。

落とし穴は、ASCII フィルターを圧縮として扱ってしまうことです。 ASCIIHexDecode と ASCII85Decode はストリームを より大きく します。それぞれおおむね倍、おおむね 25% です。 これらは、7 ビットテキストでしか安全に扱えないチャネルを通してバイナリデータを移動させるために存在するのであり、容量を節約するためではありません。 PDF を「縮小」しようと ASCII85 を選ぶと、逆の結果になります。 同じ誤解の後半は、FlateDecode が画像に対して「コストなし」の可逆フィルターだと信じることです。 Flate は確かに可逆ですが、画像がすでに DCT(JPEG)でエンコードされていた場合、それを再びラップしたり非可逆フィルターを通して再変換したりすれば、Flate がその周りでどう働いても画像は劣化します。 フィルターパイプラインは与えられたものを正確に保持します。誤って与えてしまった再圧縮アーティファクトも含めて。

このページが扱うのは、フィルターがどのように宣言され適用されるかであり、各フィルター内部のビットレベルのアルゴリズムではありません。 決定性の保証は、具体的には NextPDF が書き出すストリームに対する Flate 出力についてのものです。 それは PHP のマイナーバージョンや標準に準拠した zlib ビルドをまたいでも成立しますが、標準は deflate エンコーダーが異なる内部ブロック境界を選ぶことを明示的に許可しているため、真に異なる zlib 実装(たとえば標準の zlib と zlib-ng)をまたいだバイト単位で同一の出力は保証されません。 ビルド環境はそのために固定されています。

NextPDF は、自身が出力するデータに対して FlateDecode と ASCII トランスポートフィルターを選択します。 これは画像トランスコーダーではありません。 任意の入力 JPEG2000 または JBIG2 ストリームを再パックすることを約束はしませんし、非可逆な画像のトレードオフはソースデータの性質であって、ライターが取り消せるものではありません。

なぜ FlateDecode は広く使われるのか。 可逆かつ汎用で、広くサポートされており、ほとんどの PDF のテキストと演算子のコンテンツによく適合するからです。 コンテンツストリーム、埋め込みフォント、そしてクロスリファレンスストリームにとって安全なデフォルトです。

圧縮をオフにできるか。 /Filter を省略して生のバイトを格納でき、リーダーはそれを受け入れます。 ファイルが大きくなるだけで、ほかに改善される点はありません。デバッグ以外で理由があることはまれです。

そもそもなぜ圧縮レベルを固定するのか。 出力を再現可能にするためです。 固定されていない圧縮レベル(あるいは zlib ビルド)は、解凍後のコンテンツを変えずに圧縮後のバイトを変えうるため、正しくても 同一 ではなくなり、バイトレベルの検証を台無しにします。

  • PDF とは実際に何か — このページのストリームが属するオブジェクトモデル。
  • フォント:難所 — 埋め込みフォントプログラムはフィルター済みのストリームであり、独自の障害モードを持つ。
  • PDF 2.0:何が変わったか — 2.0 ベースラインがストリーム、および NextPDF がデフォルトとするクロスリファレンスストリームをどう扱うか。
  • ストリームオブジェクト — 辞書に、streamendstream の間にある 1 ブロックのバイト列を加えたもの。/Length を保持し、通常は /Filter を伴う。
  • フィルター — リーダーがストリームのバイトに適用する、名前付きのデコード変換(たとえば FlateDecode)。
  • フィルターパイプライン — 順次適用されるフィルターの配列。配列の順序がデコードの順序。
  • FlateDecode — zlib/deflate フィルター。コンテンツ、フォント、クロスリファレンスストリームのデフォルト圧縮。
  • DCTDecode — JPEG 画像フィルター。非可逆 のため、再エンコードすると画像が再び劣化する。
  • ASCII トランスポートフィルター — ASCIIHexDecode / ASCII85Decode。圧縮ではなくサイズと引き換えにデータを 7 ビットセーフにする。
  • 決定論的圧縮 — 同一の入力に対してバイト単位で同一の圧縮出力を生成すること。圧縮器のレベルとフォーマットを固定することで達成される。