PDF は本質的にはプレーンテキストのコンテナです。ほとんどのファイルをバイナリエディタで開くと、一番上は読み取り可能です。バージョンのコメント、次に番号が付けられた一連のオブジェクト、そして小さなインデックス、そして一番下にはリーダーに開始位置を伝えるポインターがあります。圧縮を取り除くと、その形式は、機能するドキュメントをテキストエディタで入力してビューアーで開くことができるほど親しみやすいものです。これを一度行うことで、仕様をどれだけ読むよりも、PDF がどのように構成されているかについて深く学ぶことができます。なぜなら、オブジェクト同士を手作業で結び付ける必要があり、配線を正しく行わないとファイルが開かないからです
このチュートリアルでは、実際に何かをレンダリングする最小の PDF を構築します。1ページで、US レターサイズに、組み込みフォントで "Hello, World!" という単語が書かれています。完成したファイルには、正確に5つのオブジェクトと、その周りの数行の記録が必要です。まずオブジェクトを記述し、次にヘッダー、クロスリファレンステーブル、トレイラーを組み立てて、リーダーが受け入れるファイルに結合します
ビューアーが要求する5つのオブジェクト
リーダーは、コンテンツを探すために PDF を上から下までスキャンするわけではありません。トレイラーから始まり、ドキュメントカタログへの参照をたどり、そこからオブジェクトのチェーンを歩いていきます。そのチェーン上のすべてのオブジェクトが存在しないと、開くのに失敗します。1ページのドキュメントの場合、チェーンは短く、各リンクには単一の役割があります
- Catalog はルートです。これはトレイラーが指すオブジェクトであり、ここで必要なエントリはページツリーへの参照のみです
- Pages はページツリーのノードです。ドキュメント内のページをリストし、その数を報告します
- Page は1つの物理的なページを記述します。サイズ、描画に使用するリソース、どのコンテンツストリームがそれをペイントするかなどです
- Content stream は、テキストとグラフィックをそのページに配置するための後置コマンドである描画オペレーターを保持します
- Font は、コンテンツストリームが参照する書体を宣言します。14の標準フォントのいずれかを使用すれば、何も埋め込む必要はありません
各オブジェクトには番号が付けられ、アドレス指定が可能です。間接オブジェクトは N 0 obj ... endobj のように記述されます。ここで、N はオブジェクト番号で、0 はその生成番号(新しく作成するファイルでは常に 0)です。ファイル内の他の場所では、参照を使用してそのオブジェクトを指します。5 0 R は「オブジェクト 5」を意味します。これらの参照が配線です。カタログはページツリーに到達するために私たちの番号付けで 2 0 R を保持し、ページツリーはページへの逆参照を保持し、以下同様に続きます。番号を間違えると、リーダーはダングリングポインターをたどって無の世界に陥ります
名前、辞書、ストリーム
3つの構文の要素がほぼすべてを担っています。名前 はスラッシュで始まります。/Type、/Page、/F0 などです。名前は大文字と小文字を区別する識別子であり、文字列ではありません。PDF はそれらを辞書のキーとして、またオブジェクトが何であるかをタグ付けするために使用します。辞書 は二重の山括弧で囲まれたキーと値のペアのセットであり、すべてのキーは名前です。<< /Type /Page /MediaBox [0 0 612 792] >> のようになります。値には、数字、名前、角括弧で囲まれた配列、参照、またはネストされた辞書を指定できます。ほとんどの PDF オブジェクトは辞書です
ストリーム は、辞書の後に stream と endstream のキーワードで挟まれたバイトのブロックが続くものです。ここは、ページ描画のオペレーターが存在する場所であり、実際のファイルでは圧縮された画像や埋め込まれたフォントも存在します。ストリームの辞書はバイトを記述します。本番ファイルでは、正確なバイト数を示す /Length エントリを保持する必要があり、データが圧縮されている場合は /FlateDecode のような /Filter が頻繁に含まれます。手作業でバイト数を数えることは、この練習において教育的な見返りがなく、ファイルを破壊する1のずれ(off-by-one)の可能性が高いため、/Length の入力はツールに頼ることにします
オブジェクトの記述
以下は5つのオブジェクトの順番です。コンテンツストリームを読む前に留意すべき座標の詳細として、PDF はページの下の左隅からポイント単位で測定し、1ポイントは 1/72 インチで、Y は上に向かって増加します。US レターサイズは 612 x 792 ポイントなので、50 700 は下ではなく左上近くに位置します
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]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
参照を読めば構造が明らかになります。オブジェクト 1(カタログ)は、/Pages エントリでオブジェクト 2 を指します。オブジェクト 2(ページツリー)は、/Kids でオブジェクト 3 をリストし、/Count 1 を宣言します。オブジェクト 3(ページ)は、/Parent でオブジェクト 2 に逆参照し(ツリーとページは互いに参照し合う必要があり、これは必須です)、/MediaBox で自身のサイズを設定し、/Resources でローカル名 /F0 の下にフォントを公開し、オブジェクト 5 をコンテンツとして指定します。オブジェクト 4 はフォントです。/BaseFont /Helvetica は、準拠するすべてのリーダーが既に持っている14の標準書体のいずれかを選択するため、埋め込む必要はありません。オブジェクト 5 はコンテンツストリームです
コンテンツストリームが実際に記述していること
ストリームの本体は、PDF のページ記述言語の小さなプログラムであり、後置表記法です。最初にオペランドが来て、次にそれを消費するオペレーターが来ます。5行で作業を行います。BT と ET はテキストオブジェクトを開閉します。テキストを配置したり表示したりするものはすべて、この間に配置する必要があります。/F0 36 Tf は、現在のフォントを 36 ポイントで /F0 という名前のリソースに設定します(Tf は「テキストフォントとサイズの設定」です)。50 700 Td は、ページ座標の (50, 700) にテキスト位置を移動します。(Hello, World!) Tj は文字列を表示します。PDF は文字列を括弧内のリテラルテキストとして記述し、Tj を使用して現在位置に描画します。BT/ET を省略すると、厳密なリーダーはテキストオペレーターを拒否します。Tj の前にフォントを設定し忘れると、描画するための現在のフォントが存在しません
ストリームの辞書の /Length 44 は、stream と endstream の間のバイト数であり、正確である必要があります。手作業で改行を数えるよりも、ツールに任せる価値がある値です。特に、エディターが改行を LF として書き込むか CRLF として書き込むかによって合計が変わるためです
ヘッダー、xref、トレイラー
オブジェクトはコンテンツです。3つの構造的な要素が、それらをファイルに変えます。1つ目はヘッダーで、一番最初の行であり、フォーマットとバージョンを指定します
%PDF-1.7
% は PDF 構文におけるコメントの開始ですが、リーダーはこの特定のコメントをフォーマットの署名として扱い、そこからバージョンを読み取ります。実際のライターは、その直後に上位ビットのバイトで構成される2行目のコメントを続けます。これは、ファイルがバイナリであり、テキストとして破損してはならないことをファイル転送ツールに示すヒントとなります
ファイルの最後には、ランダムアクセスを可能にするインデックスであるクロスリファレンステーブルがあります。ファイルの先頭からすべてのオブジェクトへのバイトオフセットを記録しているため、リーダーはオブジェクト 1 と 2 を最初に解析しなくても、オブジェクト 3 に直接シークできます。テーブルは厳密です。エントリは固定幅で、行末を含めて各 20 バイトであり、10桁のオフセット、5桁の生成、キーワード(使用中の場合は n、空きの場合は f)、および2バイトのターミネータとしてフォーマットされます。(オブジェクト 0 は常に空きリストの先頭です)6つのエントリの正しいテーブルは次のようになります
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
それらのオフセットは、PDF を手書きする際の脆い部分です。それぞれが対応する N 0 obj が始まる正確なバイト位置であり、その上のどこかに1文字でも追加した瞬間にすべてのオフセットがずれます。トレイラーは、リーダーが最後と最初に使用するエントリーポイントです。/Root 1 0 R はカタログを指定し、/Size 6 はオブジェクト数を述べ、startxref 408 は xref という単語自体のバイトオフセットを提供します。リーダーはファイルを開き、最後までジャンプして startxref を読み、クロスリファレンステーブルをシークし、そこからカタログとその下にあるすべてのものに到達します。%%EOF は最後のバイトをマークします
ツールにバイト数を修正させる
上記のオフセットは例示です。ファイルの正確なバイトレイアウトに依存するため、実際には入力が終わるまでに間違っているでしょう。再計算するのではなく、プレースホルダーの値で構造を記述し、ユーティリティにクロスリファレンステーブルとストリームの長さを再構築させます。無料でクロスプラットフォームの pdftk は、これを1回のパスで行います
pdftk hello-draft.pdf output hello.pdf
これはオブジェクトを解析し、すべてのバイトオフセットを再計算し、正しい /Length の値を入力し、有効な xref テーブルとトレイラーを記述し、hello.pdf を出力します。これを任意のビューアーで開くと、一番上近くに 36 ポイントの Helvetica で "Hello, World!" と書かれた1ページが表示されます。Qpdf も同じ役割を果たします。また、多くのビューアーは少し不正な形式のファイルでもその場で修復します。ここでツールに頼ることは怠慢ではありません。オフセットの計算は、概念的な内容がゼロでエラー率が最も高いフォーマットの一部であるため、これを自動化することで、構造そのものを学習の対象として維持できるからです
これが実際のドキュメントにスケールする理由
100ページのレポートであっても、構築した形状を変えるものはありません。カタログは引き続きルートに配置され、ページツリーはページを収集し、各ページはそのリソースとコンテンツストリームを指し示します。増えるのは幅であり、背骨ではありません。リーダーがサブツリー全体をスキップできるようにページツリーは分岐し、コンテンツストリームは5つではなく数百のオペレーターを保持し、フォントは幅のテーブルやエンコーディングと共に独自のストリームオブジェクトとして埋め込まれ、画像は画像固有のフィルターが適用されたストリームとして到着します。また、最新のファイルは多くのオブジェクトを圧縮されたオブジェクトストリームに詰め込み、プレーンな xref テーブルをクロスリファレンスストリームに置き換える傾向があります。これが、実際の PDF をテキストエディタで開いたときに通常はバイナリの壁のように表示される理由です。根底にあるモデルは、あなたが手作りしたファイルとまったく同じです。より広範なオブジェクトグラフや、大きなドキュメント全体でのカタログ、ページツリー、リソース辞書の関連付けについては、PDF ドキュメント構造の詳細なツアーで続きを取り上げています。また、ファイル構造の概要では、増分更新やトレイラーがリビジョン間でどのように連鎖するかについてカバーしています
手書きからライブラリへ
手作業でオブジェクトを入力することは学習のための練習であり、本番の技術ではありません。本物のフォント、折り返しテキスト、画像、または些細なページ以上のものが必要になった瞬間、pdftk がパッチを当ててくれたバイトの記録が作業のすべてになり、それを管理するライブラリが必要になります。同じ5つのオブジェクトが書き込まれることに変わりはありませんが、ライブラリは、あなたが1バイトも追跡することなく、すべてのオフセットを計算し、フォントやリソースの辞書を管理し、コンテンツストリームを圧縮します。Delphi および C++Builder の場合、HotPDF Component はこのファイル全体を数回の呼び出しに短縮します。ドキュメントを設定し、BeginDoc、SetFont、および TextOut を呼び出して同じ挨拶を配置し、次に EndDoc を呼び出して正しいカタログ、ページツリー、xref、およびトレイラーを書き込みます。根底にあるオブジェクトを理解することで、ドキュメントが期待どおりにレンダリングされない場合に出力について推論できるようになります