endobj全体を間接オブジェクトと言います。次のように表されます。ドキュメントの中で、オブジェクト番号と世代番号の組で一意に間接オブジェクトを特定できなくてはなりません。このエントリーでは、世代番号については説明しません。Rとするということを覚えてください。>>>>endobjは、フォントを定義する辞書を3番の間接オブジェクトとして宣言します。辞書の値や配列の要素を間接オブジェクトとして切り出して、それを参照することが出来ます。>>>>endobjとしてもまったく意味は同じです。間接参照は、配列や辞書の値などに用いることが出来ます。R]次のような配列はどのように読むのでしょうか。いう3つの間接参照から成る配列です。間接参照を実践的に使用した例を見てみましょう。あるPDFファイル生成プログラムがストリームを吐いているとします。いう状況の時、ストリームの長さをとりあえず間接参照にしておき、後からそのオブジェクト番号で書き込むことができます。59endobj間接参照を理解することは、PDFのファイル構造を理解することといっても過言ではありません。次章では、PDFのファイル構造、すなわちPDFファイルはどのように成り立っているのかについて説明します。world!の一例を用いて、PDFファイル全体の構成を説明します。world!になります。お好きなテキストエディタで入力し、hello.pdfという名前で保存してください。Vimをお使いの方はvim-pdfというプラグインを使用し、適切に設定すると、いい感じに補完やインデントがされると思います。R>>startxref0%%EOF実はこれは有効なPDFではありません。手で入力するのは少し難しい、様々な情報が欠落しています。不完全なPDFファイルではあるものの、ファイルの構造を表すには十分な情報を提供しています。ReaderやPreview.appといった入力に寛容なPDFリーダーを使うと、上記のPDFファイルを開くことが出来ます。完全なPDFファイルのフォーマットを見てPDFファイルを理解すべきです。pdftkコマンドを使って不完全なPDFファイルから完全なPDFファイルを生成します。pdftkはオープンソースなソフトウェアで、様々なプラットフォームをサポートしており、PDFファイルに対して様々な処理を行うことが出来ます。不完全なPDFを完全なPDFに変換できることを覚えておいてください。上記のhello.pdfをpdftkで変換してみます。hello-out.pdf出力されるファイルは次のようになります。world!と表示されると思います。1つのPDFファイルは大きく分けて次の4つの要素によって成り立っています。(trailer)以下ではhello-out.pdfを例にとって、4つの要素を詳しく説明していきます。ヘッダPDFファイルは、2行のヘッダから始まります。%PDF-1.7%????この2行について説明します。%PDF-1.7PDFのバージョンは今のところ、1.0から1.7が存在します。厳密に言うと、このヘッダに書かれたバージョンで定義されていない要素が使われている場合、PDFリーダはその要素を読み込むのをやめることが出来ます。多くの寛容なPDFリーダは、ヘッダのバージョンに誤って古いものが書かれていても新しいバージョンとして読み込むことが多いようです。2行目は、テキストエディタによってどういう文字が見えるかは変わってきます。%から始まっていることから分かる通り、この行はコメントの一つです。私の手元のhello-out.pdfでは%\xe2\xe3\xcf\xd3となっていました。ファイル転送プログラムの中には、テキストファイルだと分かると勝手に改行コードを変換するものがあります。PDFファイルは改行コードを変換されると破損してしまいます。置くことが推奨されています。トレーラはPDFファイルの最後に書かれている次の部分です。6>>startxref440%%EOFPDFリーダーは、PDFファイルを最後から読みます。それに習って最後から説明したいと思います。ファイルの最後は次のマーカーで終わります。%%EOF次に、startxrefというキーワードと数字が書かれています。startxref440これは、相互参照テーブルがファイルの最初から数えて何バイト目から始まるかを表しています。試しにPDFファイルから440バイト落としてみます。bytes/sec)ちょうどxrefキーワードから始まる相互参照テーブルが表示されました。これは、どんなPDFファイルでも同じです。数MBくらいのPDFファイルを用意して、tailコマンドでファイルの最後を見て、ddでそのバイト数落としたものを表示してみてください。(そうでなければそのPDFファイルは破損しています)。実に当たり前のことですが、ほんの数十行くらいの小さなPDFファイルも、数MBもあるPDFも同じ仕組で動いていることが分かるとだんだん嬉しくなってきます。トレーラの先頭にはtrailerキーワードと一つの辞書が書かれています。6>>ここにはドキュメントカタログが1番の間接オブジェクトであることと、後述の相互参照テーブルのエントリーの数が6個あることが記述されています。ドキュメントカタログとはなにかについては後述します。ファイルのドキュメントカタログの番号が分かったところで、何MBもある巨大なPDFファイルからどうやってその間接オブジェクトを見つけるのでしょうか。賢いPDFリーダは、相互参照テーブルを使います。相互参照テーブルPDFファイルは、番号付けられた大量のオブジェクトと、それらに対する間接参照によって成り立っています。PDFファイルのサイズは、数MB、数十MB、時に数百MBになることもあります。objをどうやって見つけますか。ファイルを全てメモリーにロードして探索しますか。それぞれの間接オブジェクトがファイルの先頭から数えて何バイト目から始まるかを一覧にしたものです。この仕組みによって、PDFリーダーは任意の間接オブジェクトに効率よくアクセスできるようになっています。先ほどのhello-out.pdfの相互参照テーブルを見てみましょう。以下に示す部分が相互参照テーブルです。上から順番に説明します。相互参照テーブルはxrefキーワードから始まります。次の行には2つの数字が書かれています。6この2つの数字は、この相互参照テーブルは何番目から何番目の間接オブジェクトのバイトオフセットを持っているかを表しています。この例だと、0番から5番のオブジェクトの情報を持っています。1番の間接オブジェクトの情報を持っています。続くj行が相互参照テーブルの本体になります。今回の例だと6行あります。順番に0番から5番の間接オブジェクトがファイルの何バイト目から始まるかを表しています。試しに、ddコマンドでバイトオフセット分skipして表示してみます。objのように、間接オブジェクトの最初が表示されました。確かに各間接オブジェクトのバイトオフセットを表しているようです。ここでnnnnnnnnnnは10桁に0埋めされた数字で、gggggは5桁に0埋めされた数字、uはfかnのいづれかです。この行は、間接オブジェクトが何バイト目から始まるかを表しています。この時nnnnnnnnnnは、対象となる間接オブジェクトのバイトオフセットを表し、gggggは世代番号を表します。このエントリーに対する間接オブジェクトはありません。nnnnnnnnnnは次のフリーエントリーの番号を表し、gggggは世代番号を表します。先ほど「0番から5番の間接オブジェクト」と言いましたが、hello-out.pdfに0番の間接オブジェクトはありません。PDFの中で0番目のオブジェクトは特別な存在で、常にフリーなオブジェクトになっていますし、世代番号はほとんどのケースで65535になっています。フリーエントリーの詳しい挙動については仕様書を参照してください。このエントリーでは、複数のフリーエントリーや、世代番号、複数の相互参照テーブルなどは扱っていません。次の形であるということを覚えておけばよいでしょう。...aaaは1番の間接オブジェクトのバイトオフセット、bbbは2番の間接オブジェクトのバイトオフセット...です。注意深い人は、行末のスペースが気になったかもしれません。このスペースは絶対に省略してはいけません。実はスペースと改行の2文字は、次の3通りが可能です。\r""\r\n"改行コードは環境によって変わってくるため、何らかの制限によって\n以外の改行コードによってPDFが作られるかもしれません。\n"のようにスペースを入れることが義務付けられています。なることが保証されており、相互参照テーブルの中をランダムアクセスできるようになります。nバイト目から始まります。全ての相互参照テーブルをメモリーに持たなくても、任意の番号の間接オブジェクトのバイトオフセットの番号をアクセスし、そしてその間接オブジェクトにランダムアクセスできるようになっているというわけです。この決まりに従っているPDFに対しては、ファイルサイズを変更することなく、相互参照テーブルの改行コードの変更を行うことが出来ます。本体PDFファイルの本体は、間接オブジェクトが並んだこの部分です。略endobjこのPDFファイルには、間接オブジェクトが5つ存在します。これらの間接オブジェクトの間の関係を取り持っているのが、間接参照たちです。このPDFファイルの中の5つの間接オブジェクトとトレーラ辞書たちの参照関係を図に表すと、次のようになります。この図を参考にしながら、トップダウンで順番にオブジェクトを見て行きましょう。ファイルの最後に書かれているトレーラ辞書は次のようになっていました。6>>この/Rootには、PDFファイルの1番上の間接オブジェクトであるドキュメントカタログの番号を指定します。ドキュメントカタログとは、PDFファイルに1つ存在し、そのPDFファイルの間接オブジェクトのグラフの中で頂点に位置するオブジェクトです。今回ですと、ドキュメントカタログは次のようになっています。/Catalogを指定します。ページツリーのオブジェクトに対応する間接参照を指定します。ページツリーとは、簡単に言うとページの一覧を指定するオブジェクトです。hello-out.pdfのページツリーオブジェクトは次の部分です。/Pagesを指定します。このページツリーの直下のノードを配列で表します。このページツリーの直下にあるノードの数を表します。親ノードの参照を指定します。ドキュメントカタログの直下のページツリーならば指定不要です。単純なケースでは、ページツリーオブジェクトはファイルのページオブジェクトの配列を持っています。全てのページを1つのページツリーの/Kidsに指定するやり方は生成するのは簡単ですが、PDFリーダーからするとページツリーをアクセスする時の(空間)効率があまりよくありません。そこで、ページツリーの子ノードには他のページツリーを指定することもできるようになっています。(このエントリーではそういうページツリー構造については説明しません)。ページオブジェクトは次の部分です。このオブジェクトはPDFファイルの1ページと対応します。ページオブジェクトの各項目は次のようになっています。/Pageを指定します。親のページツリーノードを指定します。フォントやイメージといった、ページのリソースを指定します。これを省略すると、親のページツリーノードから継承されます。このページのコンテンツストリームに対する間接参照を指定します。このページの用紙サイズをポイントで指定します。これを省略すると親ノードから継承されます。world!で指定した項目だけ説明しました。様々な項目に関する説明は、『PDF構造解説』や仕様書を参照してください。ページオブジェクトに指定されたリソース辞書は、今回の場合は次のようになっています。今回はフォントが指定されています。Romanを割り当てています。ストリームから/F0という名前で該当フォントを参照することが出来ます。ストリームオブジェクトは5番の間接オブジェクトです。このストリームによって、ページに表示するテキストやグラフィックスを指定します。記述することで文字やグラフィックスを描いていきます。演算子は後置式で、引数を先に書きます。いう文法はPostScriptから受け継いでいます。(センチメートルではありません)。cmこの6つの数字は、次のようなアフィン変換に相当します。右方向に50ポイント、上方向に720ポイントの平行移動を表します。この6つの数字のうち最初の4つの数字を変えると、文字を拡大縮小したり、回転させたり、歪める事ができます。変化させて遊んでみてください。BT演算子とET演算子は、これらの中がテキストであることを意味しています。Textの頭文字です。BTテキストに関する演算子たちETTf演算子はフォントとフォントサイズを指定する演算子です。Tfここでは、/F0フォントを使用し、36ポイントのサイズで描画することを宣言しています。Romanになります。Tj演算子は文字列を表示する演算子です。何の略かは正直言って知りません。Tjテキストに関する演算子は他にもいくつもあります。ここで改行して新しい文字列を表示するためにはTL演算子とT*演算子を使用します。演算子は、縦方向の行間オフセットを指定します。今回フォントサイズを36ポイントに指定しているので、それよりも少し大きい40ポイントを指定してみました。T*演算子は次の行の先頭に移動する演算子です。先ほどはcm演算子で文字の位置をずらしました。cm演算子は図全体に適用される演算子ですが、もう少し細かく文字の位置を制御したいこともあります。変換方法はcm演算子と同じくアフィン変換です。TjETendstreamこれでもうあなたは、任意の位置に任意のサイズでテキストを配置できるようになりました。world!ファイルの内容を全て説明しました。PDFファイルの最も小さな単位である「オブジェクト」から、間接参照、ヘッダ、トレーラ、相互参照テーブルなどを説明してきました。次の章では、任意のテキストを与えてPDFファイルを生成するプログラムを書いてみます。world!ファイルを元にPDFの基礎的なオブジェクトからファイル構造まで説明してきました。ここまで説明すると、ある程度機械的に、すなわちプログラムからPDFファイルを生成できるようになります。この章では私が書いたテキスト→PDF変換プログラムを紹介し、そのプログラムの説明をしたいと思います。単行本(ソフトカバー)以下が私が書いたテキストPDF変換プログラムです。Haskellで書いてみました。texttopdf.hsという名前で保存してください。BS.getContentsこのプログラムを実行すると、標準出力にPDFが表示されます。manの出力をPDFに変換してみます。man.pdfman.pdfは次のようになります。任意のテキストからPDFファイルを生成できるようになった時の喜びはこの上ないものです。ここからはtexttopdf.hsの説明をしたいと思います。Haskellに興味が無いよ、あるいはヒント無しで自分で実装したいよという方は、スキップしてください。1行目ではOverloadedStrings言語拡張を使うことを宣言しています。型の値としても使用できるようになります。ByteStringといった効率のよい文字列型を扱うときは、この言語拡張が便利です。モジュールのインポートの説明は省略します。10行目からは、データ型の定義が始まります。以下のコードはPDFのオブジェクトを表しています。BS.ByteString参照もオブジェクトの1つとして実装すると統一的に扱えて便利です。型の設計ですが、Stringではなくて比較的効率のよいByteStringを用いています。続くrenderElem関数では、オブジェクトの表示方法を定義しています。型は次のようになっています。BuilderBuilderとは小さい文字列を集めて効率よく大きな文字列を構築するためのコンビネータです。詳しくはライブラリーのドキュメントを参照してください。Builderそのものは文字列というよりも文字列を構築するための関数と捉えたほうが正確ですが、OverloadedStrings言語拡張を使うと、あたかも文字列のように扱えます。"null"たった1行のコードですが、ここで何が起きているかを理解することは大切です。OverloadedStrings言語拡張を使わずに書くと次のようになります。Builderという型になっています。OverloadedStrings言語拡張は、型をちゃんと理解した上で使用してください。文字列の表示は次のようになっています。c丸括弧やエスケープ文字自身をエスケープしています。\(world\))のように変換されます。ストリームの表示は次のように定義しました。>>という辞書を作っています。以下のPdfObj型は、オブジェクトに番号を振った間接オブジェクトを表しています。"\nendobj\n"この関数によって、間接オブジェクトは次のように表示されます。obj対象のオブジェクトendobj面倒なので世代番号は常に0としています。ヘッダーには%PDF-M.Nといったバージョンと、4文字のバイナリ文字列をコメントで書いています。"\n"バイナリ文字は0x7fよりも大きな文字であれば何でも構いません。[PdfXrefEntry]その1つのエントリーは数字2つとエントリーが使用中かフリーかを表すフラグを持っています。InUseEntry表示するときは、0埋めが必要です。]最初の数字は10桁の0埋め、次の数字は5桁の0埋めです。行末のスペースを忘れないようにしてください。トレーラには、ドキュメントカタログのオブジェクト番号と、相互参照テーブルのエントリーの数、相互参照テーブルのバイトオフセットを渡します。"%%EOF\n"トレーラ辞書の描画には、最初の方で定義したrenderElem関数を用いることが出来ます。1つのPDFファイルは、PDFのバージョン、ドキュメントカタログの番号と間接オブジェクトの配列によって定義しました。[PdfObj]表示は、ヘッダーを表示し、各間接オブジェクトを表示し、相互参照テーブル、そしてトレーラを表示します。trailer相互参照テーブルにバイトオフセットを渡すために、BuilderをByteStringに変換して長さを計算しています。offsets)))バイトオフセットを足し上げるコードは、まさにscanlがぴったり当てはまる場面です。objectsそしてtextsToPdf関数は、テキストからPdfFileを構築する関数です。contents入力の型は、1行の文字列がBS.ByteString、1ページはそれが何行かあるので[BS.ByteString]、ファイル全体では何ページかあるので[[BS.ByteString]]となっています。間接オブジェクトの番号は次のように設計しました。ここでnはページの数を表します。コンテンツストリームこれらのコードは順番に辞書を組み立てていくだけなので説明を省略します。以下のsplitN関数は、引数nを取って配列をその長さごとに分割していく関数です。n)この関数を使ってmain関数が書かれています。BS.getContents関数結合演算子(.)が使われているときは右から読みます。標準入力から受け取って、行ごとに分割し、さらに45行ごとに分けます。stdoutで標準出力に出力します。単行本(ソフトカバー)これで一応ざっとtexttopdf.hsの説明をしてみました。基本的な流れは、PDFのオブジェクトや相互参照テーブルなどをデータ型で表現し、それを描画する関数を定義するだけです。雑に書いていくとスペースを忘れて結合してはいけないトークンがくっついてしまったり、余計な改行が入ってしまったりするので、出力を見ながら丁寧にバグを直します。textsToPdfを書くときは、まず間接オブジェクトの番号から設計します。上のプログラムtexttopdf.hsはHaskellで書いていますが、特にHaskellでなくては書けない難しいものではありません。私が何かプログラムを書こうとし始めた時に、最初に選んでしまう言語がHaskellなだけです。いうわけなので、ここで練習問題を出したいと思います。好きな言語で実装してください。括弧の中は、私が適当に点数を振ったものです。難易度の参考にしてください。あなたの好きな言語は何ですか。(30)最初は、有効なPDFファイルを出力するのは難しいかもしれません。相互参照テーブルをきちんと吐くのは難しいです。最初はpdftkコマンドで出力ファイルを完成させてもいいかもしれませんが、頑張ってきちんとしたPDFファイルを吐いて欲しいです。Readerでファイルを開くことです。Readerがお気に召さないPDFファイルであるということです。バイトオフセットはしっかりと計算してください。相互参照テーブルの開始位置は正しいですか。相互参照テーブルの数は正しいですか。各間接オブジェクトに対するバイトオフセットは正しいですか。\n"の形になっていますか。行末のスペースを忘れていませんか。Readerで開いても保存ダイアログなしにファイルを閉じることが出来ます。日本語の扱い方この章ではPDFでの日本語の扱い方について説明します。前章で作ったtexttopdf.hsは、残念ながらアスキー文字しか受けつけません。world!に直接日本語を書いてみたり、texttopdf.hsに日本語を流し込んでも、文字化けしてしまいます。ここでは日本語を含むPDFファイルのサンプルを例にとって、CIDフォントの指定の仕方を説明してみます。以下のファイルは日本語を含むPDFファイルです。できる限りhello.pdfを使いつつ拡張する形で書いてみました。hello-ja.pdfという名前で保存してください。R>>startxref0%%EOFこのPDFファイルは相互参照テーブルなどが欠けていますので、pdftkコマンドを使ってPDFファイルを完成させます。hello-ja-out.pdf出力されたhello-ja-out.pdfを開くと次のようになります。見事、日本語が表示されました!日本語を表示するために今回指定したフォントを説明する前に、これまで指定してきたフォントを思い出してみましょう。hello.pdfでは、フォントを次のように指定していました。1フォントを使うことを意味しています。1フォントとは、欧文フォントのためにPostScriptで採用されていたAdobeのフォントのことです。1フォントの/BaseFontには/Times-Romanや/Times-Italic、/Helvetica、/Courierなどを使用できます。一方で、今回作ったhello-ja.pdfで使ったフォントは次のようになっています。>>>>endobjここでは/Subtypeに/Type0が指定されています。ここでは子孫フォントとして、6番の間接オブジェクトに書かれているフォントが/DescendantFontsに指定されています。この指定されたフォントは次のようになっています。R>>endobjこのフォントは/Subtypeが/CIDFontType0となっています。いうフォントをCIDフォントと言います。番号と呼ばれる数字から文字の形への対応が指定されています。CID番号は、SJISやUTF-8と言った文字コードとは全く異なるコード体系なので、文字列の16進数表現からCID番号への変換方法を指定する必要があります。呼ばれるテキストコード変換テーブルを指定します。対応したCMAPです。ここではSJISエンコーディングで16進数表現した文字列リテラルを用いています。-p8e71894e93d0894b924396a48cdf96a2905c93d19cfa88e5もしSJISが嫌いでUTF-16BEで指定したいという方は、CMAPに/UniJIS-UTF16-Hを指定してください。iconvとxxdコマンドでUTF-16BEエンコードした16進数表現を調べます。-p005f0028003a0033300d002022200029005f/Encodingを/UniJIS-UTF16-Hにして、テキストリテラルを書き換えます。T*ETendstreamendobjこれをpdftkコマンドで変換してPDFビューワで表示すると、次のようになります。Descriptorと呼ばれる、CIDフォントのスタイルを指定する辞書が書かれていますが、この詳細については省略します。詳しくは『PDF構造解説』や仕様書を参照してください。日本語を含むPDFを生成するプログラム前章の内容を元に、先ほどのtexttopdf.hsを日本語対応させてみます。以下のプログラムをtexttopdf-ja.hsとして保存してください。BS.getContents実行してみましょう。test.pdfPDFリーダーで開いてみます。複雑な入力を与えてみます。wikipedia.pdf少し雑で横にはみ出ていたりMarkdown記法が見えていたりしますが、まったく文字化けすることなく日本語が表示されています。Readerを閉じるときに保存ダイアログが表示されることはありません。最初に書いたtexttopdf.hsとの差分を説明します。必要なモジュールを追加しています。IConvもしIConvモジュールがインストールされていない時は、iconvパッケージをインストールしてください。オブジェクトのデータ構造は変わっていませんが、PdfStringの表示方法が変わっています。">"このコードは、入力の文字列がUTF-8エンコードされているものと仮定しています。lazyByteStringHexはData.ByteString.Builderの関数です。Builderモジュールには便利な関数がいくつも用意されていますので、どうやってByteStringからBuilderに変換するんだろうと思った時は、ドキュメントをざっと眺めるとよいでしょう。0フォントにしたのと、CIDフォントの辞書の追加が行われています。Descriptorの辞書追加がされています。文字列の16進数表現をちゃんと書ければ大して難しいものではありません。先ほどの練習問題でtexttopdfを実装していただいた方は、ぜひ日本語対応させてください。フォントのオブジェクトの追加がありますので、オブジェクト番号をずらすのを忘れないようにしてください。グラフィックス前章までは、PDFファイルで文字を出力する方法を紹介してきました。この章では、線や四角や丸といったグラフィックスを表示する方法を説明します。テキストではTjやT*といった演算子を使いましたが、グラフィックスも演算子を使って描いていきます。PDFファイルのストリームでは、演算子とそれに対する引数で文字や絵を表現します。いう引数を渡してテキストを表示しています。Tj演算子が後置であることはもう慣れるしかありません。グラフィックスも、グラフィックスに関する演算子があり、それらに引数を渡していくことで描画していきます。次のようなストリームを書いてみます。SETこれは、次のように表示されます。ここでは3つの新しい演算子が出てきました。演算子で、描画開始の位置を変更します。演算子で、その位置までのパスを設定します。演算子で、それまでのパスを線で描画します。グラフィックスの演算子は非常にたくさんありますが、その一部を使って次のようなPDFを書いてみます。world!であるhello.pdfの5番の間接オブジェクトを次のように書き換えてください。f*ETendstreamendobjこのファイルをpdftkで変換したPDFファイルは、次のように出力されます。絵の基本となるいくつかの構成要素で描くことができました。演算子で、四角を作ります。dy)を対角線とする四角になります。Scはベジェ曲線を生成する演算子です。2つの制御点と目的の点という3つの点の座標を指定します。その次の円の描画には、4つのベジエ曲線が使われています。S色の変更には、rgやRG演算子を使います。rgrgは塗りつぶしに対する色で、RGはストロークに対する色です。どちらもRGBの順で0〜1の実数を指定します。gは塗りつぶしの色を灰色の0(黒)〜1(白)で指定する演算子で、Gはそのストローク版です。ストロークの決定は、いくつも演算子があります。(奇偶規則)これらの演算子を駆使すれば、様々な絵を描けるようになります。(renderElem関数まではtexttopdf.hsと全く同じです)。squaresPdf実行してみます。squares.pdfPDFビューワーで開くと、カラフルな四角が螺旋状に並んだ絵が表示されます。色を指定するrg演算子と、矩形パスを作るre、塗りつぶしを行うfしか使っていませんが、プログラムで位置を決めたりサイズを変更していくことで面白い絵が描かれました。(Data.Complexをインポートしてください)。2実行してPDFファイルを生成すると、次のようにヒルベルト曲線が描画されます。パスを描画しているだけですが、ほんの120行程度のプログラムでこれだけ様々な絵をPDFに吐けるようになると、楽しくなってきます。いうことにかかってきます。テキストと線を組み合わせて、例えばページの上下に横線を引いたり、枠線を作って領収書のようなものを作ることもできるでしょう。自由な発想でPDFのグラフィックスを楽しんでください。PDFを読むプログラムこの章では、PDFファイルを読むプログラムを書いてみます。実装したいところです。パーサーを書き始めるとどうしてもコードの書き方が言語やライブラリに依存してしまいます。そこで、ここで紹介するプログラムでは、相互参照テーブルを読み込んで、それぞれの間接オブジェクトの最初表示するだけにします。これまで何度も繰り返してきたとおり、PDFファイルは何MB、何十MBにもなります。いうファイルを一気にメモリーに読み込むのは賢いやり方ではありません。ファイルの中の間接オブジェクトにランダムアクセスするための方法が、相互参照テーブルです。PDFリーダーは、おおよそ次のようにファイルを読みます。最初の行からPDFのバージョンを調べるファイルの最後を読み、相互参照テーブルが何バイト目から始まるかを調べるトレーラを見て、ドキュメントカタログが何番かを調べる相互参照テーブルから、ドキュメントカタログが何バイト目から始まるかを調べるドキュメントカタログの本体にアクセスして、オブジェクトをパースしてページ構造を解釈していく間接参照があるときは、相互参照テーブルから対象となる間接オブジェクトが何バイト目から始まるかを調べて、アクセスする相互参照テーブルによって、PDFリーダーは必要なオブジェクトを必要なときにロードすることが出来ます。大きなstreamendstreamendobjこのようなページとコンテンツがあるとします。書き込まれているため、一般にストリームは大きくなることが多いです。今、PDFビューワーが1ページ目を表示しているとします。2番目の間接オブジェクトを見れば、1ページ目のページオブジェクトは4番、さらにコンテンツのオブジェクトは7番と分かります。従って、他のページのコンテンツのオブジェクト、8番・9番の間接オブジェクトは読み込む必要はありません。PDFファイルが何十MBもあったとしても、優秀なPDFビューワーは任意のページを一瞬で開くことができるでしょう。ビューワーを3ページ目までスクロールした時、9番の間接オブジェクトをメモリーに読み込みますが、逆に7番の間接オブジェクトは不要になるので、メモリーから解放します。サムネイル表示などをすると、多くのストリームを読み込む必要が有るため、非同期でロードするように実装されているかもしれません。したPDFリーダーを実装するのはかなりややこしそうです。そこで今回は、相互参照テーブルを読み込んで、それぞれの間接オブジェクトの最初の行を表示するだけのプログラムを書いてみました。以下がそのプログラムです。handleこのプログラムを実行してみます。objきちんと間接オブジェクトの最初が表示されました。PDFファイルでうまくいくはずです。(そういうケースはこのエントリーでは扱いません)。tableとりあえず、ファイルが壊れていることを疑ってみます。pdftkコマンドを使って修復してみます。obj...22MもあるPDFファイルでも、間接オブジェクトのリストが綺麗に表示されました。ファイルの全てをロードしているのではなく、ランダムアクセスして必要な分だけロードしているため高速に動作します。この方法でうまくいかなければ、そのPDFはこのエントリーで説明している範囲外の技術を使って保存されている可能性があります。(このエントリーでは扱いません)。順番にコードの説明をしましょう。mainの3つの関数が定義されています。xrefIndex関数は、ファイルハンドルを受け取って、相互参照テーブルが始まるバイトオフセットを返す関数です。型の設計をしてみましょう。Integer)を返します。Integer)型の設計は出来ました。関数の本体を書いていきます。100バイト取り込みます。handle)この2行のコードは、System.IOモジュールのドキュメントを読めば書くことが出来ます。contentsを単語毎に分割し、後ろから2番目の単語を数字にして返します。Monadのdoでパターンマッチさせています。xrefIdxStr1番最後の単語が%%EOF以外だったり、3番目がstartxref以外だったり、あるいはファイルが全体で1単語だったりすると、Nothingが返ってきます。%%EOFの後に100個の改行があるみたいな意地悪なPDFファイルでもNothingが返ってくるでしょう。次の関数printEachObjは、相互参照テーブルのバイトオフセットをもらって、各間接オブジェクトの1行目を表示します。読み込み位置を相互参照テーブルの最初に設定します。xrefIdxそして、1行読み込んでxrefキーワードかどうかチェックします。xrefキーワードの後は、必ず改行文字があることが仕様で決められています。table"次に、相互参照テーブルの中のエントリーがいくつあるかを読み取ります。]インデックスアクセスする時は、ちゃんと十分な長さあるかチェックして失敗系も書かなくてはなりませんが、とりあえず雑に書いています。1行表示して、位置を戻しています。handleそれが成功したら、そのバイトオフセットを先ほどのprintEachObj関数に渡します。index"そして、最後にファイルハンドルを閉じて、終わりです。handleプログラムの各行がそれぞれ重要な役割を果たしています。全体でも50行を切っており、かなりコンパクトに書かれていることが分かるでしょう。今回もHaskellで書いてしまいましたが、やはり特にHaskellでなくては書けないプログラムではありません。ファイルを読み込む手段があり、fseek相当の関数があるならば、どんな言語でも書けるはずです。ここで練習問題を出したいと思います。好きな言語で実装してみてください。あなたの好きな言語は何ですか。(5)その言語で、ファイルの最後の100バイトを読み込み表示するプログラムを書いてください。(10)先ほどの数字を使って、相互参照テーブルの最初の行を表示してください。(25)PDFファイルを読む練習問題は、以上です。上で私が示したプログラムでは、最後の2つができていません。オブジェクトを読み込んでパースするのは難易度が高いと思います。ここまで実装したら、PDFをもっと自由に扱えるプログラムを書きたくなってくるでしょう。(私は全然その域まで到達していません。これから頑張って色々と実装していきたいと思います)。その第一歩として、このエントリーを読んで頂けたのならば、これ以上嬉しい事はありません。あとがきPDFファイルの基本的な要素は単純明快ですし、ファイル構造を理解することも難しくありません。PDFファイルは今後もしばらくは広く使われるでしょうし、一度ファイル構造を覚えてしまえば長く役立つ知識になります。PDFの仕様は公開されていますし、なんといってもこの世の中にはPDFファイルはあふれています。それにもかかわらず、PDFファイルは理解し難いものだと思われている気がします。プログラマーとてPDFファイルは理解し得ない謎のバイナリフォーマットと考えている人が多いのではないでしょうか。多くの人に、PDFファイルのファイル構造を正しく理解して欲しい、その一心でこのエントリーを書きました。PDFのファイル構造の理解への第一歩を踏み出す一助になればと思います。PDFのファイル構造の全てを理解することは非常に時間がかかります。私も全てを理解しているわけではありません。PDFの仕様書は750ページもあり、全てを頭に入れるのは大変です。(タイトルにも書いているように)、PDFのファイル構造の入門をお伝えしたに過ぎません。次のような内容はこのエントリーには書いていません。埋め込みフォントどれも重要な内容ですし、一般に広く使われています。本気でPDFビューワーを作ろうと思ったら、これらに関してはもちろん理解しなくてはなりません。何らかの形式からPDFを生成するプログラムも、フルスクラッチで作り始めてしまったら様々な要求に対応するのが大変になっていくでしょう。PDFの構造を理解していき、様々なPDFに対する操作を行えるようになることは、とても楽しいことです。PDFのオブジェクトすら理解していなかった時は、PDFファイルをテキストエディタで開いても、謎のバイナリ列を前に絶望していたでしょう。それが様々なPDFを生成できるようになり、また既存のPDFファイルも解析できるようになる喜びは、この上ないものです。作るときは、まず信頼のおけるライブラリを探すべきでしょう。このエントリーは、PDFのほんの入口を覗いたに過ぎませんし、今からちゃんとしたPDFライブラリを1から作るのは多くの時間が必要です。例えそうだとしても、入門レベルすら知らないのと、自前でPDFを生成したことまである人では、やはりPDFに対する理解は全然違います。このエントリーは、最速でPDFの様々な概念を理解していただくため、若干言葉の定義や仕様が不正確ではない部分があります。さらなるPDFについての理解を求める人は、『PDF構造解説』の購入をお薦めします。単行本(ソフトカバー)また、正確な言葉の定義や仕様については、アドビシステムズより公開されている仕様書を参照してください。私も、この記事を書くために一通り目を通しました。はてなでは、たとえ業務と関係ない技術だとしても、自分自身が仕組みを理解していない技術に対し、積極的に調査して仕組みを理解し、実際にコードを書き、あるいは得られた知見を伝え広めたいと思っている、そんな好奇心旺盛で教えたがりなエンジニアを募集しています。10はてなブログをはじめよう!itchynyさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。