はじめに
tosaken1116 Advent Calendar 2024 3日目担当の土佐犬です。
現在とあるプロジェクトに採択され、PDFの仕様理解をしています。
今回はその仕様理解で得られた知見のお話です。
PDFの仕様書はこちら
PDF v1.3
https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.3.pdf
PDFとは
PDFとはみなさんご存知のドキュメントファイルです。
どんなOSやブラウザで開いても同じ文書が表示されることが特徴の一つであり、表示環境に依存しない文書が作成できます。
Adobeが開発し、現在は国際規格となっています。
今やさまざまなアプリケーションがPDFの出力を可能としています。
そんなPDFですが中身がどのような構造になってるかみなさん見たことがありますでしょうか?
私も以前チラッと見たことがあるのですが基本的にはテキストとバイナリがごちゃ混ぜになったものです
PDFを読む
クロスリファレンステーブル
PDFの基本構造ですが読むのに必要なデータは以下の三つです。
- オブジェクト ページ情報や 画像の情報、フォントやテキストなどのコンテンツ情報
- クロスリファレンステーブル
-
- オブジェクトがファイル内のどのバイト位置にあるかを示すデータ
- クロスリファレンステーブルの参照位置
-
- クロスリファレンステーブルが定義されているバイト位置を示すデータ
手始めにこのPDFを読んでみましょう
This is a test
とだけ書かれた1ページのPDFです。
このPDFファイルをテキストエディターで無理やり開くと次のようなものが表示されます。
ちらほら読み取れる部分があったり読み取れない部分があったりします。
PDFを読むにはまずファイルの最後を見ます。
ファイルの最後には%%EOF
がありファイルの終端を示しています。
その少し前にstartxref
という文字列があると思います。
startxrefと%%EOFに挟まれた数字
9101
がクロスリファレンステーブルの定義が開始されているバイト位置です。
試しにddコマンドを使ってこのファイルの9101バイト目を読み取ります。
dd skip=9101 bs=1 if=sample.pdf
するとちょうどxref
という文字列からデータが始まることが見て取れます。
このxrefがクロスリファレンステーブルです。
ここからさらにオブジェクトにアクセスするためにクロスリファレンステーブルを読んでいきます。
まず1行目のxref
はここからクロスリファレンステーブルの定義が始まりますという宣言です。
次に0 15ですがこれはオブジェクトの数を示しています。
その次にオブジェクトのバイトオフセット,世代番号,使用状況が定義されています。
ここで1行目は0バイトから始まり世代番号が65535の特殊なオブジェクトなので読み飛ばします。
次に2行目はファイルの開始位置から320バイト
からデータ定義が始まることがわかります。
こちらも同様にddコマンドで見てみると次のようにデータが定義されています。
dd skip=320 bs=1 count=100 if=sample.pdf
何やら1 0 obj
という文字列から始まっていることが見て取れます。
そして同様に2行目3行目...と読んで行った後、最後にtrailer
というものがありその次の行には<<
から始まる文字列が続きます。
これがPDFにおけるメタデータを定義する辞書オブジェクト
と呼ばれるものです。
Catalog
<< /Size 15 /Root 10 0 R /Info 14 0 R /ID [ <d6919211d3ca8f74592e9deb2b573bd1>
<d6919211d3ca8f74592e9deb2b573bd1> ] >>
ここでRootという値に10 0 R
という値が振られていることがわかります。
ここの10というのは先ほどのクロスリファレンステーブルの10番目のデータであることを示し、10番目のデータから読み取っていくことでコンテンツの取得ができます。
クロスリファレンステーブルの10番目をみると
0000003327 00000 n
となっていることがわかります。
このことから3327バイト目を読み取ってみましょう。
dd skip=3327 bs=1 count=100 if=sample.pdf
するとこのように10番目のデータが取れていることがわかります。
ここで10番目のデータに関してみると以下のように辞書オブジェクトが定義されています。
Type: Catalog
Pages: 2 0 R
PDFにはCatalog,PageTree,Page,Resource,Contentが存在しそれぞれ次のような内包関係を持ちます。
さてここで察しのいい人はお気づきかもしれませんがPages
とはPageTreeを指し、その値の2 0 R
というのはクロスリファレンステーブルの2番目ということを指します。
PageTree
ではクロスリファレンステーブルの2番目をみてみましょう。
0000003244 00000 n
3244バイト目から読んでいきます。
dd skip=3244 bs=1 count=100 if=sample8.pdf
さて先ほどと同様に辞書オブジェクトがあります
Type: Pages
MediaBox: [0,0,960,540]
Count: 1
Kids: [1 0 R]
Typeとは先ほどのCatalogと同様に辞書オブジェクトのタイプです。
MediaBoxとはそのページの表示範囲を示します。
[x0 y0 x1 y1]の形で示されます。
ここではx:0=>960 y:0=>540となり、540*960の大きさの表示範囲となります
次にCountとKidsですがKidsはそのPageTreeが持つPageの参照、Countはその数です。
Page
このPDFには1ページしかないのでKidsの参照が1個しかありません。
ここで1 0 Rとは例にならってクロスリファレンステーブルの1番目のデータです。
0000000320 00000 n
dd skip=320 bs=1 count=100 if=sample.pdf
こちらも同様に
Type: Page
Parent: 2 0 R
Resources: 4 0 R
Contents: 3 0 R
Typeはこの辞書オブジェクトがPageであることを示しています。
Parentはこの辞書オブジェクトの親がクロスリファレンステーブルの2番目であることを示します。
新しくResourcesとContentsというものが出てきました。
Contentsはそのページに表示の仕方を示すオブジェクトへの参照。
ResourcesはContents内で使用される画像やフォントのデータへの参照です。
Parentは先ほどのPageTree辞書オブジェクトが返ってくるのでResourcesとContentsを見ます。
Resources
Resourcesは辞書オブジェクトであることがわかります。
ProcSet: [/PDF /Text]
ColorSpace: { /Cs1: 6 0 R}
Font: { /TT2: 8 0 R}
ProcSetはプロシージャーセットの略です。
ここではPDFとTextのプロシージャーを使用してこのページをレンダリングすることがわかります。
ColorSpaceは色空間の情報です。
Cs1
という色空観が6 0 R
に定義されています。
Fontはフォントの情報ですね。
TT2
というフォントが8 0 R
に定義されています。
Contents
さて一旦Resourceを置いておいてContentsを見ていきます。
Contentsは今までとは異なりどこか読み取れない文字がありますね。
これがバイナリです。
まずは辞書情報から読んでいきましょう
Filter: FlateDecode
Length: 226
これらはバイナリの圧縮形式および圧縮後のバイナリの長さを示しています。
ここではFlateDecode、つまりDeflate圧縮が行われています。
そして圧縮後の長さは226であることがわかります。
てことでデータを読み出して解凍します。
解凍は今回は雑にpythonでやります
dd skip=76 bs=1 count=226 if=sample8.pdf | python3 -c "import sys, zlib;
sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))"
このコマンドにおいてskipが76になっているのはContentsの参照位置+辞書オブジェクトの長さ+stream
という文字列の長さで76になっています
ということで意味ありげな文字列を取得できました
終わりに
ということでいいとこかもしれませんが今回はここで終わりです。
というのもある程度読めるようにするまでをここに記すには余白が狭すぎました。
というのと記事ネタが尽きないように
次回はテキストのデコードをします。