コンテンツにスキップ

PDF とは実際のところ何か

Evidence: Standard-backed

PDF は、たまたまファイルに収められたページ記述ではありません。プリンターを取り付けた小さなグラフデータベースです。このページでは、すべての PDF が持つ 4 つの部分 — ヘッダー、ボディ、相互参照テーブル、トレーラー — と、リーダーが推測に頼らずあらゆるオブジェクトを見つけられるよう、NextPDF がそれらをどう書き出すかを説明します。

ほとんどの PDF のバグはレンダリングのバグではありません。構造 のバグです。たとえば、本来指すべきオブジェクトより 1 文字先を指すバイトオフセット、誤ったルートを名指すトレーラー、オブジェクトの実際の位置と食い違う相互参照エントリーといったものです。これらはいずれも、リーダーがファイル内で別の経路をたどり、末尾で破綻するまでは、ページの見た目を変えません。

PDF を不透明なものとして扱うと、こうした不具合はランダムに見えます。オブジェクトモデルを理解していれば、それらは本来の姿、つまり位置と一致しない数値として見えてきます。フォーマットを読み解けることが、「PDF が壊れている」と言うのか、「オブジェクト 14 のオフセットが古いのは、ライターがストリーム長の確定前に測定したためだ」と言えるのかの違いになります。

PDF はファイル上で、次の 4 つの部分から成ります。

  1. ヘッダー — バージョンを示す 1 行(%PDF-2.0)。
  2. ボディ — 番号付きの 間接オブジェクト の並び。すなわち辞書、ストリーム、配列、数値、文字列、名前です。
  3. 相互参照テーブル(PDF 2.0 では相互参照 ストリーム)— オブジェクト番号からバイトオフセットへの索引で、ファイルを走査せずに任意のオブジェクトへ到達できます。
  4. トレーラー — ドキュメントのルートオブジェクトを名指し、相互参照セクションの開始位置を指し示す小さな辞書です。

リーダーは PDF を先頭から末尾へ順に読むわけではありません。まず 最後 の行を読み、startxref を見つけ、相互参照セクションへ飛び、それをボディへのインデックスとして使います。このフォーマットは後ろから読まれるように設計されています。その 1 つの事実が、その設計のほとんどを説明します。

NextPDF は、フォーマットが読まれるとおりに PDF を構築します。つまり、オブジェクトを先に、オフセットを後から記録し、テーブルを最後に書き出します。

すべての間接オブジェクトには、単一のレジストリ(src/Core/ObjectRegistry.php)によって番号が割り当てられます。レジストリは allocate() を通じて連番を払い出し、オブジェクトのバイト列が出力バッファに書き込まれた 後でregister() を通じてバイトオフセットを記録します。オフセットが前もって推測されることは決してありません。オフセットは、オブジェクトヘッダーが出力された瞬間に BinaryBuffer::getOffset() から観測されます。これが、NextPDF の相互参照エントリーが、対象のオブジェクトからずれない理由です。つまり、オフセットはバッファの実際の位置そのものだからです。

ボディが完成すると、バージョン固有の シリアライズ戦略src/Writer/PdfSerializationStrategy.php)が相互参照セクションとトレーラーを書き出します。

  • Pdf20StreamStrategy は、圧縮された相互参照 ストリーム/Type /XRef)を出力します。これは PDF 2.0 でのデフォルトです。
  • Pdf17TableStrategyPdf14TableStrategy は、従来の 20 バイトの相互参照 テーブル と、別個のトレーラー辞書を出力します。これは、より古いファイル構造を要求する PDF/A プロファイルで必要とされます。

戦略は出力プロファイルによって選択され、推論されることはありません。いずれの場合でも、最終的なバイト列の形は同じです。すなわち、相互参照セクション、続いて startxref、続いてバイトオフセット、続いて %%EOF です。その末尾こそ、リーダーが最初に見つけるものです。

  1. Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
  2. Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
  3. Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
  4. Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
リーダーが NextPDF ファイル内のオブジェクトをどう解決するか、そして各ステップを定義する ISO 32000-2 の節。それはファイルの末尾から始まり、内側へと進みます。

この 4 部構成は NextPDF 独自の取り決めではありません。 Spec: ISO 32000-2, §7.5 のファイル構造に関する節そのものです。規格では PDF を、ヘッダー、オブジェクトのボディ、相互参照テーブル、トレーラーとして定義し、リーダーはファイルの末尾から構文解析すべきだと述べています。最後の行は %%EOF であり、その前の 2 行は startxref キーワードと、相互参照セクションへのバイトオフセットです。

Evidence: Standard-backed

間接オブジェクト は、オブジェクト番号と世代番号を空白で区切り、続いてキーワード objendobj で挟んだオブジェクトの値として定義されます。オブジェクト番号と世代番号の組み合わせは、そのオブジェクトを一意に識別します。その間接 参照 は、オブジェクト番号、世代番号、キーワード R として書かれます。NextPDF の ObjectRegistry はこの構造をそのまま反映します。すなわち、連番、新たに書かれるオブジェクトには世代 0、そして記録されたオフセットです。

PDF 1.5 以降では、オブジェクトを オブジェクトストリーム の内部に置くことも許容されています。その場合、オブジェクトは obj/endobj キーワードなしで格納され、世代はゼロでなければなりません。相互参照 ストリーム/Type /XRef Spec: ISO 32000-2, §7.5.8 )は PDF 2.0 の通常のオブジェクトと、これらの圧縮されたオブジェクトの両方をインデックス化する仕組みです。 NextPDF の CrossReferenceStream は、/W フィールド幅配列と FlateDecode 圧縮を用いてこの仕組みを構築します。

これは、最小限の PDF ボディとそのトレーラーの形です。相互参照セクション内の数値はバイトオフセットです。これらは厳密に正しくなければならず、だからこそ NextPDF はそれらを計算するのではなくバッファから記録します。

%PDF-2.0
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] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000122 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
196
%%EOF

リーダーはこれを末尾から開きます。すなわち %%EOF、続いて startxref 196、続いてバイト 196 へシークして xref の開始位置へ移動し、オブジェクト 1 がバイト 9 にあると読み取り、/Root 1 0 R をたどってカタログに至り、そこからページツリーをたどります。オブジェクト 0 は常にフリーリストの先頭であり、世代は 65535 です。これはフォーマットの最初期の設計から受け継がれた名残であり、リーダーがそれを期待するため忠実に再現されています。

落とし穴は、PDF がソースコードのように上から下へ読まれると思い込むことです。そうではありません。ボディ内のオブジェクト順は任意で構いません。オブジェクト番号はファイル内で連続している必要はなく、リーダーがそれを前提にすることも決してありません。権威ある索引は 唯一、相互参照セクションであり、それを見つける唯一の方法は末尾のトレーラーです。完全に正しいボディを持ちながら startxref 内の数値が 1 つでも誤っている PDF は、読めません。オブジェクトがばらばらの順序で書かれていても、相互参照テーブルが正しい PDF は問題ありません。位置には意味がなく、記録された 位置がすべてです。

このページはファイル構造を説明するものであり、ページコンテンツを説明するものではありません。マークがどのようにページ上に置かれるか — コンテンツストリーム、グラフィックス演算子、テキスト表示 — は別の話題です。また、ファイルが書き出された 後で 変更されたときに何が起こるかも扱いません。それは増分更新の役割であり、そこでは 2 つ目の相互参照セクションが追記され、トレーラーが後方へと連鎖します。

NextPDF は ライター です。ここで説明する挙動は、自ら構築したドキュメントをどうシリアライズするかというものです。これは汎用的な PDF パーサーや修復ツールではありません。損傷した相互参照テーブルを持つ任意のサードパーティ製ファイルを読み取り、再構築し、あるいは救出することは約束しません。この保証は狭く、かつ意図的なものです。NextPDF が 書き出す ファイルは、オフセットが一致します。なぜなら、それらは予測値ではなく測定値だからです。

新規ファイルが常に 0 を使うなら、なぜ世代番号があるのか? 世代番号は、更新をまたぐオブジェクトの再利用のために存在します。新しく書き出されたファイルでは、すべてのオブジェクトが世代 0 です。0 以外の世代は、ファイルが増分更新され、オブジェクト番号が再利用された場合にのみ現れます。

2 つのオブジェクトが同じ番号を持てるか? 単一の相互参照セクション内では、持てません。増分更新をまたぐと、ファイルには物理的に同じオブジェクト番号のコピーが複数含まれ得ます。最も新しい相互参照エントリーが優先されます。これは次のページの主題です。

ファイル内のオブジェクト順は出力に影響するか? いいえ。NextPDF は再現可能なビルドのためにオブジェクトを決定的な順序で書き出しますが、リーダーはすべてを相互参照セクションを通じて解決するため、物理的な順序は意味論的に重要ではありません。

  • 間接オブジェクト — ボディ内の番号付きオブジェクト。N G obj … endobj として書かれ、N はオブジェクト番号、G は世代番号です。
  • 間接参照 — 間接オブジェクトへのポインター。N G R と書かれます。
  • 相互参照テーブル(xref) — オブジェクト番号からバイトオフセットへの索引。PDF 2.0 では、これは通常、従来の 1 エントリー 20 バイトのテキストテーブルではなく 相互参照ストリーム/Type /XRef)です。
  • トレーラー — 相互参照セクションの末尾にある辞書で、/Root(ドキュメントカタログ)と /Size を名指し、startxref オフセットを通じて見つけられます。
  • オブジェクトストリーム — それ自身が他の間接オブジェクトを(まとめて圧縮して)含むストリームオブジェクト。メンバーは obj/endobj を持たず、世代はゼロです。
  • ドキュメントカタログ/Root によって名指されるオブジェクト。ページツリーとドキュメント内の他のすべてへ至る入口です。