PDFファイルの仕組み
PDFは、ページ、フォント、画像といった番号付きオブジェクトから成るグラフであり、それぞれのバイトオフセットがファイル末尾のクロスリファレンステーブルに記録されています。ビューアーはトレーラーを読み、ページツリーをたどり、描画します。オフセットが破損すると、寛容なビューアーは走査によってテーブルを再構築し、厳格なビューアーはファイルを拒否します。
PDFファイルの仕組みとは
PDFはページ記述形式に見えますし、実際そのとおりです――しかし土台には小さなデータベースが潜んでいます。番号を振られたオブジェクトが並び、どのオブジェクトも他のオブジェクトを参照できます。40ページで使うフォントは1回だけ格納され、使い回されるヘッダー画像も1回だけ。ページそのものもツリーのノードで、何をどんな順で表示するかを定めています。
この形式は2008年からISO標準です(ISO 32000、2017年に改訂)。しかし中核モデルは1993年の初版から動いていません。ヘッダー、オブジェクトの本体、各オブジェクトの開始位置を記録するクロスリファレンステーブル、そしてルートを指すトレーラー。20年分の機能がこの土台を動かすことなく積み重なってきたからこそ、今日のビューアーで1998年のスキャンを開けるのです。
構造を理解すれば、すぐに見返りがあります。結合したファイルが問題なく動く理由、削除したページがファイル内に居座り続ける理由、破損したドキュメントへの扱いがビューアーごとに違う理由、そして修復が実際に何をしているのかが分かるからです。ここでリンクされているすべてのツール――結合、分割――は、そのオブジェクトの手術をブラウザ内でローカルに実行します。
結合は、この構造のレッスンを目に見える形にしたものです。結合とはオブジェクトグラフの組み直しにほかなりません。実際に見てみましょう――PDF結合ツールに複数のファイルを読み込ませれば、新しいページツリーノードが書き込まれ、新しいxrefテーブルが敷かれていく様子を観察できます。
取り出しは、同じ操作を逆向きに行うものです――残すページに必要なオブジェクトを判断します。そしてPDF分割ツールは、必要なオブジェクトだけを、専用のクロスリファレンステーブルを持つ新しいファイルに書き出します。元のファイルには一切手を触れません。
番号付きオブジェクトのグラフ
PDFファイルは%PDF-1.7のようなヘッダーで始まり、その後はオブジェクトばかりです。オブジェクトとは、番号と世代番号、キーワードobjで始まり、内容を運び、endobjで終わるブロックのこと。オブジェクト1はドキュメントのルートであるカタログ、オブジェクト2はページツリー、オブジェクト9は1ページ分の描画コマンドを収めたコンテンツストリーム、といった具合です。どのオブジェクトも、番号と文字Rを並べて12 0 Rのように書けば、別のオブジェクトを指せます。これが間接参照です。
間接参照こそが設計のすべてです。すべてが参照として表現されるため、構造はコピーではなく共有されます。カタログは1つ、リソースディクショナリは共有され、300ページのドキュメントでも、同じ埋め込みフォントを1つのオブジェクト経由で参照します。おかげでPDFは本物のグラフになります。PDFを操作するツールがテキスト処理ではなくグラフの手術を行っているのは、まさにそのためです。
PDFの最初の2つのオブジェクト
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
参照がどう解決されるか
catalog (object 1) └─ page tree (object 2) └─ page (object 3)
ページツリー
ページはリストではなくツリーに収められています。型がPagesのルートノードがKidsという配列を持ち、その要素はPageオブジェクトか、同じ要領で入れ子になったPagesノードです。すべての子は上へ向かうParent参照を持っています。ビューアーはカタログから最初の枝をたどって1ページ目を見つけます。ノードはサブツリーになれるので、40ページの章の挿入は、新しいノード1つとKids配列の編集1回で済みます。
継承もこのツリーで行われます。MediaBox(ページのサイズ)、Rotate(90度単位の表示回転)、Resources(フォントと画像)は、Pagesノードで1度宣言すれば、上書きしないすべての子孫へ流れ落ちます。全ページがA4のドキュメントなら、そのサイズはルートで1度宣言するだけです。ページオブジェクト1つがこれほど短く済むのはこのためです。
継承のあるページツリー
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 595 842] >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /Contents 9 0 R >> endobj
ページオブジェクト3が継承するもの
MediaBox [0 0 595 842] (A4, inherited) Contents 9 0 R (its own)
xrefテーブルとトレーラー
ファイルの末尾近くにはクロスリファレンステーブルがあります。オブジェクトごとに1行が割り当てられ、そのオブジェクトが始まるバイトオフセットを記録します。0000000123 00000 nというエントリーは、あるオブジェクトがファイルの先頭から123バイトの位置で始まることを意味します。nは使用中、fは空きスロットの印です。続いてトレーラーディクショナリが置かれ、Rootエントリーがカタログを指します。さらにstartxrefがテーブル自体の開始位置を記録し、ファイルは%%EOFで閉じられます。
狙いはランダムアクセスです。240ページ目が欲しいビューアーは、先頭から読みません。末尾へシークし、startxrefをたどってテーブルを見つけ、必要なバイトへ直接ジャンプします。この形式の末尾構造に関する一切――末尾へのデータ追記が安全で、先頭への追加が安全でない理由を含めて――は、地図を領土の後ろに置いたことに由来します。
| 構成要素 | 見た目 | 役割 |
|---|---|---|
| ヘッダー | %PDF-1.7 | バージョンの印。ファイルの1行目 |
| オブジェクト | 12 0 obj … endobj | 番号付きの内容ノード1つ |
| 間接参照 | 12 0 R | オブジェクトから別のオブジェクトへのポインター |
| xrefエントリー | 0000000123 00000 n | オブジェクトの始まるバイトオフセット |
| トレーラー | << /Root 1 0 R >> | カタログを指す。ドキュメントのルート |
| startxref | startxref 12054 | xrefテーブル自体のバイトオフセット |
| EOFマーカー | %%EOF | 最後の更新セクションの終わり |
インクリメンタル更新:追記する、書き換えない
PDFが編集されるとき――フォームに記入される、ページが削除される――行儀の良いライターはファイルを書き換えません。追記します。新しいオブジェクトと変更されたオブジェクトが末尾に置かれ、更新後の状態を記述する新しいxrefテーブルとトレーラーが続き、さらに別の%%EOFが書かれます。元のバイト列はそのまま残り、新しいテーブルが古いテーブルに取って代わります。
この設計は記録として美しく、実世界の3つの現象を説明してくれます。署名付きドキュメントは、ツールが書き換えると壊れます。署名は特定のバイト列を対象とするからで、インクリメンタル更新なら有効性を保てますが、書き換えでは保てません。削除した内容は居座ります。取り除いたページはたいていまだファイルの中にあり、最新のテーブルからは参照されなくても、走査する人には読めてしまいます。そして保存のたびにファイルは育っていきます。完全な書き換えを行う――クリーンアップやリニアライズがまさにそれです――まで、何も回収されないからです。
なぜビューアーによっては壊れたファイルを修復できるのか
xrefテーブルは、この形式の耐力壁であり、もっとも好んで壊れる場所でもあります。テキストとしてメール送信されたファイル(バイナリのバイトは破壊されます)、転送失敗で途中までしか届かなかったファイル、大げさな約束をするツールが書き出したファイルには、間違った場所を指すオフセットが残ることがあります。ビューアーがstartxrefをたどってナンセンスなものを見つけたら、そのファイルは規則どおり壊れています。
その後に何が起きるかは、仕様の要求ではなくポリシーの判断です。寛容なビューアーは再構築を行います。ファイル全体を走査して、有効なオブジェクトヘッダーの形をしたもの(番号、世代、キーワードobj)をすべて集め、見つかったものから新しいメモリ上のxrefを作り、ドキュメントをそれでも開きます。復旧は大きなファイルでは遅く、曖昧なオブジェクトを誤判定することもありますが、たいていうまくいきます。だからこそAcrobatは修復を申し出て、より厳格なビューアーはただ拒否するのです。PDFを書き出し直すものは何であれ、保持したオブジェクトだけの正確なテーブルを作ることで、この曖昧さに終止符を打ちます。
| ビューアーの挙動 | 何をするか | トレードオフ |
|---|---|---|
| 厳格 | xrefやstartxrefが不正ならファイルを拒否する | 速く、正直で、復旧には無力 |
| 寛容 | objマーカーを走査し、メモリ上でテーブルを再構築する | 大きなファイルでは遅い。曖昧なものは推測になる |
| 書き出しツール | 保持したオブジェクトの上に新しいxrefを書き込む | 曖昧さがなくなる。新しいファイル、新しいオフセット |
リニアライズとフォントサブセット
絵を完成させる改善が2つあります。リニアライズされたPDF――Fast Web Viewの名で売り出されてきたもの――は、1ページ目のオブジェクトが先に来るようオブジェクトを物理的に並べ替え、さらに残りが届く前に描画を始められるヒントテーブルを持ちます。ドキュメントの論理は何ひとつ変わらず、ディスク上の配置だけが変わります。リニアライズが完全な書き換えであり、低速な回線やバイトサービング対応サーバーで初回描画時間を縮めるのは、そのためです。
フォントも同じ共有の思想に従います。埋め込みフォントは1つのオブジェクトで、それを使う全ページから参照されます。行儀の良いライターは、実際に使われるグリフだけを埋め込みます――これがサブセットです。40ページのレポートでも、フォントデータはメガバイトではなく数十キロバイトで済むことがよくあります。代償は、テキスト抽出がサブセットのToUnicodeマップに依存することです。マップがなければ、ビューアーはグリフを完璧に表示するのに、コピーペーストすると無意味な文字の羅列が得られます。
よくある質問
ここでPDFを結合したり分割したりすると、PDFはアップロードされますか?
いいえ。解析、ページツリーの手術、xrefの書き換えは、すべてお使いのブラウザのタブ内で起きます。ファイルがマシンを離れることはありません。DevToolsで確かめられます。Networkパネルを開き、2つのファイルを結合すると、リクエスト一覧が空のまま変化しないのを見られます。サイトのContent-Security-Policyも、ページスクリプトによる外向きの接続をブロックしています。
ページを削除したのに、PDFが大きくなったのはなぜですか?
行儀の良いエディターは書き換えではなく追記を行うからです。新しいxrefテーブルはそのページを無いものとして扱いますが、対応するオブジェクトはまだファイルの中に、参照されないまま回収されずに座っています。削除された内容は、古いバイト列から復元できる場合もあります――ドキュメントがかつて機密を含んでいたなら知っておく価値のある点です。実際にファイルを縮めるのは、完全な書き換えです。
破損したPDFを修復できますか?
しばしば可能です。破損の大半はxrefテーブルかstartxrefポインターの破損で、寛容なビューアーは有効なオブジェクトヘッダーを探す走査でテーブルを再構築します。回復できないのは、走査が見つけられないものです。バイナリ破損で上書きされたオブジェクトや、本体の途中で切れたファイルは、単純に失われています。修復が部分的なドキュメントしか生まないことがあるのはこのため――走査の正直な限界です。
一部のPDFでコピーペーストが文字化けするのはなぜですか?
テキストは文字ではなくグリフコードで描画されており、文字を復元するには、埋め込みフォントに付属するToUnicodeマップが必要です。良いマップのないサブセットフォントは、表示は美しいのにペーストすると意味不明になり、スキャンページはOCRが文字をでっち上げるまでテキストをまったく持ちません。PDFからうまくコピーできないのであれば、表示は正常で、欠けているのはグリフと文字の対応表だということです。
リニアライズされたPDFとは何ですか?必要ですか?
リニアライズ(Fast Web View)は、1ページ目のオブジェクトが先に来るようファイルを並べ替え、ヒントテーブルを追加します。ビューアーはファイル全体が届く前に描画を始められます。変わるのは配信であって内容ではありません。意味を持つのは、バイトサービングつきのネットワーク経由でPDFが提供されるときです。ローカルで開く分には見た目は何も変わりません。ただし副作用として、きちんと整理された、完全に書き直されたファイルが手に入ります。
同じPDFがビューアーによって違って見えるのはなぜですか?
ISO 32000は意図的に裁量の余地を残しています。埋め込みフォントが欠けているときのフォント代替、破損した構造のビューアーごとの扱い、カラーマネジメントの既定値、透明部分の描画です。サブセットが埋め込まれ、破損のないPDFは、ほぼどこでも同じに表示されます。ずれが現れるのは、ローカルフォントに依存するファイルや、各ビューアーがそれぞれの流儀で解決する構造上の問題を抱えたファイルです。