技術記事

PDF論理オブジェクトモデル: 型、参照、構造

PDFファイルは、本質的には互いを指し示すオブジェクトの集まりです。圧縮、相互参照のブックキーピング、バイトオフセットを取り除くと残るのは、型指定された値の小さなセットが参照によって接続され、リーダーが見つけ方を知っている単一のオブジェクトをルートとするグラフです。テキストの段落から埋め込まれたフォント、デジタル署名まで、PDFが表現できるすべてのものは、8つのプリミティブなオブジェクト型と、1つのオブジェクトが別のオブジェクトを参照できるようにするルールから構築されています。これらを学べば、フォーマットの残りの部分は謎ではなく構成として読み解くことができます

これは、ISO 32000-1 の第7.3項で定義されているPDFの論理レイヤーであり、物理的なファイルレイアウト(ヘッダー、本文、相互参照テーブル、トレイラー。PDFファイル構造の技術的概要で独立したテーマとして扱われています)の1つ上のレベルに位置します。論理モデルとは、解析された後のそれらのバイトが何を意味するかです。ビューアはファイルを逆向きに読み取ってトレイラーを見つけ、そこからルートをたどり、そこからドキュメントはオブジェクトを参照するオブジェクトとして展開されます。これは、不正なページをデバッグしたり、パーサーを作成したり、ドキュメントの組み立てをライブラリに任せたりする際に推論する部分です

8つのオブジェクト型、他には何もない

PDFは正確に8つの基本的なオブジェクト型を定義しています。ドキュメント内のすべての値はそのいずれかであり、これがフォーマットの普及にもかかわらず扱いやすさを保っている理由です

ブール値 (Booleans) は、キーワード true および false です。これらは、注釈を印刷するかどうかなどのフラグをオンまたはオフにします

数値 (Numbers) には、仕様では1つの型として扱われる2つの種類があります。42 のような整数と、3.14-0.002 のような実数です。PDFには指数表記がないため、準拠しているファイルで 1e6 を目にすることはありません。座標、フォントサイズ、回転角度はすべて数値です

文字列 (Strings) はバイトのシーケンスを保持し、括弧内の (Hello) として、または山括弧内の16進数 <48656C6C6F> として記述されます。どちらの表記も同一のコンテンツをエンコードします。16進数は、括弧内では扱いにくいバイトの回避策です。文字列はテキストを運びますが、最初はバイトであり、ASCII以上のものを扱う瞬間に重要になります

名前 (Names) は、スラッシュによって導入されるアトミックなトークンです:/Type/Pages/MediaBox。名前は文字列ではありません。それは識別子であり、辞書のキーまたは列挙値として使用され、2つの名前はバイト単位で一致する場合にのみ等しくなります。スラッシュは構文であり、名前の一部ではありません。これは、/Times-Roman と文字列 (Times-Roman) を互換性があるものとして扱う初心者を混乱させますが、フォーマットではそうではありません

配列 (Arrays) は、角括弧内の順序付けられた異種リストです。[0 0 612 792] はページの長方形であり、配列は他のオブジェクトへの参照を含め、型を自由に混在させることができます。辞書 (Dictionaries) は働き者です。<<>> の間に記述され、辞書は名前のキーを任意の型の値にマップします。PDFのほぼすべての意味のある構造(ページ、カタログ、フォント、注釈)は、それが何であるかを宣言する /Type キーを持つ辞書です

ストリーム (Streams) は、stream キーワードと endstream キーワードの間に生のバイトの末尾を持つ辞書です。辞書はバイト(その長さ、およびそれらを圧縮する FlateDecode などのフィルター)を記述し、バイトは大きなペイロード(ページコンテンツの命令、埋め込みフォントプログラム、画像)を運びます。ストリームは、PDFがインラインに配置するには大きすぎるか、バイナリすぎるものを配置する場所です

8番目の型は null オブジェクト で、キーワードは null です。これは、キーが存在しないこととは異なる、実際の値です。null に設定された辞書エントリは、存在しないかのように扱われ、存在しないオブジェクトに解決される参照もエラーではなく null を生成します。その寛大な動作は意図的なものです。これにより、破損したファイルが開くことを拒否するのではなく、機能低下させることができます。9番目の型はありません。PDFが表現するすべては、これら8つがどのように組み合わされるかから生じます

直接値、インダイレクトオブジェクト、および参照

これら8つの型のいずれも、2つの方法で現れることができます。直接 (direct) オブジェクトは、MediaBox配列内の 612 のように、その場に記述されます。インダイレクト (indirect) オブジェクトには、他のオブジェクトがそれを指し示せるようにアイデンティティが与えられます。オブジェクト番号と世代番号の2つの整数であり、定義を objendobj でラップします:

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

これはオブジェクト12、世代0、フォント辞書です。ファイル内の他の場所では、別のオブジェクトが インダイレクト参照 (indirect reference) でそれを参照します。同じ2つの数字の後にキーワード R が続く 12 0 R です。参照はポインターです。ページのリソース辞書に /Font << /F1 12 0 R >> とある場合、フォントの定義をページにコピーすることなく、リソース名 /F1 の背後にあるフォントとしてオブジェクト12を指定します

世代番号は、削除と再利用のために存在します。オブジェクトが解放されてそのスロットが再利用されると、古い 12 0 R がスロット12の新しいテナントに解決されないように世代が増加します。新しく書き込まれたファイルはほとんどすべて世代0ですが、頻繁に編集されるファイルはより大きな番号を持つ可能性があり、世代を無視するパーサーは最終的に間違ったオブジェクトを読み取ることになります

インダイレクションは、PDFを効率的で編集可能なものにするものです。1つのフォント、画像、または色空間を一度定義して、100のページから参照することができます。小さな変更は、ファイルを書き直すのではなく、単一のオブジェクトを置き換える新しいリビジョンとして追加できます。相互参照テーブルはオブジェクト番号をバイトオフセットに変換するインデックスであるため、リーダーはスキャンせずに 12 0 obj に直接ジャンプしますが、これは物理的な最適化です。論理的には、12 0 R が「12 0 として識別されるオブジェクト」を意味することを知っておくだけで十分です

カタログ: すべてのドキュメントの始まり

参照の解決はどこかから開始する必要があり、その場所がトレイラーの /Root エントリです。これは ドキュメントカタログ (document catalog)、つまりオブジェクトグラフのルートであり、/Type /Catalog を持つ辞書を指します。トレイラーが最初に見つかるため、リーダーは最初にそこに到達し、そこからドキュメントの他のすべての部分には参照をたどることで到達できます

カタログには、厳密に必要なエントリが2つだけ含まれています。それ自体の /Type と、ページツリールートへのインダイレクト参照である /Pages です。残りはオプションであり、コンテンツではなくドキュメント全体の動作を記述します。/Outlines はブックマークツリーを指し、/Names は文字列をキーとする名前ツリーを保持し、/Metadata は XMP メタデータストリームを参照し、/PageMode/PageLayout はビューアがドキュメントをどのように開くべきかを提案します。これらはどれもページをレンダリングするためには必要ありません。それらはページの周囲のエクスペリエンスを構成します。カタログからぶら下がっているブックマーク、メタデータ、注釈の構造については、PDFのメタデータ、ブックマーク、注釈 に関する記事で取り上げています

以下の図は、周囲のファイル内のどこにオブジェクトの本文が配置されているかを示しています。カタログとページツリーは通常のインダイレクトオブジェクトとしてその本文内に存在します。それらの周りのヘッダー、相互参照テーブル、およびトレイラーは、リーダーがそれらを見つけるための物理的な足場です

PDFファイルの4つの物理セクションの図: バージョンヘッダー、カタログとページツリーを含むドキュメントオブジェクトを保持する本文、オブジェクトオフセットの相互参照テーブル、ルートを指すトレイラー

ページツリー: バランスの取れたページの階層

ドキュメントは /Pages からページツリーへと分岐し、ここでPDFがフラットなリストではなくグラフを選択したことが報われます。ページは単純なシーケンスとして保存されるのではありません。ページはツリーからぶら下がっており、その内部ノードは ページツリーノード/Type /Pages)であり、その葉は ページオブジェクト/Type /Page)です。内部ノードはその子を /Kids 配列にリストし、/Count に、その下にいくつのリーフページがあるかを記録します。ルート以外のすべてのノードには上へ戻る /Parent 参照が含まれているため、ツリーはどちらの方向にも移動できます

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

ここでオブジェクト2はルートであり、その下には3つのページがあります。リーフページの3と、内部ノード4を介して到達できるさらに2つのページです。ルートの /Count の3は、その下の総リーフ数と等しくなければならず、実際の構造と一致しないカウントは、手作業で編集されたファイルが問題を起こす一般的な原因です。ツリーのポイントはアクセスの局所性です。1,000ページのドキュメントの900ページ目を開くリーダーは、900のオブジェクトをたどることはありません。適切に形成されたツリーは浅くバランスが保たれているため、少数のノードを下るだけです。手作業でこのようなツリーを構築することは、最初から最後まで見る価値があるほど面倒であり、ゼロからのPDFドキュメントの構築 のウォークスルーでこれを行っています

ツリーはその2つ目の役割を 継承 (inheritance) を通じて果たします。少数のページ属性(/Resources/MediaBox/CropBox、および /Rotate)は内部ノードに設定し、個々のページから省略することができます。省略されたページは、最も近い祖先の値を継承します。ルートに一度 /MediaBox を設定すれば、すべての葉は繰り返すことなく同じページサイズを取得します。異なる必要があるページは、独自のものとして宣言します。これは、オブジェクトモデルの中で、値の意味がそれ自身のコンテンツだけでなく、ツリー内でのオブジェクトの位置に依存する唯一の場所です

リーフページが実際に保持しているもの

ページオブジェクトは、構造モデルと目に見えるコンテンツの間の接点です。その /Contents エントリは1つ以上のコンテンツストリーム、つまりテキストやグラフィックをページに描画する描画演算子を参照します。その /Resources 辞書は、これらの演算子が依存するフォント、画像、色空間に名前を付け、各エントリはページ間で共有されるオブジェクトへのインダイレクト参照です。/MediaBox はポイント(1/72インチ)でページの長方形を提供し、/Rotate/CropBox などのエントリはページの表示方法を調整します

この分業は、モデル全体の縮図です。ページ辞書は構造です。つまり、ページが何であり、何を使って描画するのかを示す、型指定されたエントリと参照です。コンテンツストリームは命令です。つまり、どのように描画するかを示す、独立した圧縮可能なブロブ(データの塊)です。/F1 の背後にあるフォントは共有リソースであり、一度定義され、それが使用される場所ならどこからでも参照されます。辞書、ストリーム、参照が連携して1つのページをレンダリングし、同じパターンがドキュメント全体に拡大適用されます。そのブロブ内のコンテンツストリームの演算子については、テキストとフォント および グラフィックと視覚要素 で個別に取り上げています

このモデルを知る価値がある理由

ほとんどの開発者がオブジェクトモデルに遭遇するのは、何かが壊れたときだけです。/Contents 参照が未定義であるためにページが空白でレンダリングされたり、フォントリソースが埋め込まれていないためにテキストが四角形で表示されたり、見つかったページ数と一致しない /Count をツールが報告したりする場合です。これらはそれぞれグラフに関するステートメントであり、グラフを直接読み取ることは推測に勝ります。8つの型と参照ルールは、頭に入れておくのに十分なほど小さな語彙であり、PDFを「オブジェクトを指し示すオブジェクト」として捉えられるようになれば、不正なファイルも分かりにくいものではなくなります

そうは言っても、学習目的以外でこのモデルを手書きすることが正しい判断になることはめったにありません。編集全体で相互参照オフセット、世代番号、ページツリーのカウント、ストリーム長の一貫性を保つことは、ライブラリが処理するために存在しているようなブックキーピングです。本番環境では、成熟したPDF開発ライブラリ がオブジェクトグラフを管理し、開発者はページやコンテンツのレベルで考えることができます。それでもモデルを知っておくことは役立ちます。ライブラリがその下で何を構築しているのか、そしてなぜそうするのかを理解できるからです