PDFが読めるようになるお話

日々の生活において、PDFというファイルは意外と私たちを支えてくれています。貰い物の家電でエラーが出た場合には型番を検索して説明書を読んだ方も多いはずです。
私はほぼ全ての家電が貰い物なのでいつもお世話になっています。

さぁ私と一緒にPDFを楽しく読みましょう。

PDFとは

Portable Document Format(ポータブル・ドキュメント・フォーマット、略称:PDF)は、アドビシステムズが開発および提唱する、電子上の文書に関するファイルフォーマットである。1993年に発売されたAdobe Acrobatで採用された。

Portable Document Format – Wikipedia https://ja.wikipedia.org/wiki/Portable_Document_Format

ということで文書を読むためのフォーマットです。
作った通りの見た目を相手に届けられる便利なファイルです。
デザイン業界とかでも最後はこの形式にして印刷することも多いです。

今回読むPDF

以下のPDFを読みたいと思います。

A4サイズにHello Worldと書かれているだけです。

では早速読みましょう

このPDFをテキストエディタで開いてみます。

%PDF-1.6
%����

中略

startxref
606961
%%EOF

全部で2897行あるうちの先頭・末尾のみ抜き出しました。

最初の2行を読む

PDFでは%から始まる行はコメント行と定義されているようです。
1行目は%PDF-1.6となっています。
これはPDFのバージョンを表しています。このファイルは1.6ですね。
一番新しいのは1.7かな?

2行目のコメントは化けてますね。
ちょっとバイナリを見てみましょう。
xxdコマンドはhexダンプしてくれる便利なやつです。

$ xxd -g 1 -l 100 helloworld.pdf
00000000: 25 50 44 46 2d 31 2e 36 0d 25 e2 e3 cf d3 0d 0a  %PDF-1.6.%......
00000010: 31 20 30 20 6f 62 6a 0d 3c 3c 2f 4d 65 74 61 64  1 0 obj.<</Metad
00000020: 61 74 61 20 32 20 30 20 52 2f 4f 75 74 70 75 74  ata 2 0 R/Output
00000030: 49 6e 74 65 6e 74 73 5b 3c 3c 2f 44 65 73 74 4f  Intents[<</DestO
00000040: 75 74 70 75 74 50 72 6f 66 69 6c 65 20 35 20 30  utputProfile 5 0
00000050: 20 52 2f 49 6e 66 6f 28 4a 61 70 61 6e 20 43 6f   R/Info(Japan Co
00000060: 6c 6f 72 20                                      lor

2行目の%の後にはe2 e3 cf d3が記述されているのがわかります。
これはファイルがテキストでなくバイナリであることを示すために存在しているようです。

これで最初の2行を読むことができました。
あぁ楽しいですね。見た目はHello Worldって書かれているだけなのに、全然そこまで届きそうにないこの感じ。ようわからんものを1つずつ、1文字ずつ、1byteずつ読んで理解するこの感じ。ゾクゾクします!

最後の3行を読む

最終行の%%EOFは読んで字の如くEnd of Fileです。
PDFは%PDF-1.nから%%EOFまでに内容が記述されることがわかりました。

お次は%%EOFの直前の

startxref
606961

startxrefとあります。xrefがスタートするようです。
それが606961だそうです。さて、xrefってなんだろね?

xrefってなんだ?

クロスリファレンステーブルのことです。
クロスリファレンステーブルってのはPDF内のオブジェクトがファイルのどの位置に記述されているかが記述されている表のことです。
PDFはオブジェクトの塊です。文字も図形も画像も全てオブジェクトです。

それが606961の位置から書かれてるよーってことになります。
ではその位置をxxdで見てみます。

 $ xxd -g 1 -l 100 -s 606961 helloworld.pdf
000942f1: 78 72 65 66 0d 0a 30 20 31 37 0d 0a 30 30 30 30  xref..0 17..0000
00094301: 30 30 30 30 30 30 20 36 35 35 33 35 20 66 0d 0a  000000 65535 f..
00094311: 30 30 30 30 30 30 30 30 31 36 20 30 30 30 30 30  0000000016 00000
00094321: 20 6e 0d 0a 30 30 30 30 30 30 30 32 36 38 20 30   n..0000000268 0
00094331: 30 30 30 30 20 6e 0d 0a 30 30 30 30 30 34 36 39  0000 n..00000469
00094341: 36 35 20 30 30 30 30 30 20 6e 0d 0a 30 30 30 30  65 00000 n..0000
00094351: 30 30 30 30                                      0000

xrefから始まってます。テキストエディタで見てみます。

xref
0 17

2873行目にありました。
直後に0 17とあります。これはこの表には17個(0番から16番)のオブジェクトの位置を記述していることを意味します。

ってことで、ここから17行見ればオブジェクトの位置を知ることができるって寸法です。じゃ見てみましょ。

xref
0 17
0000000000 65535 f
0000000016 00000 n
0000000268 00000 n
0000046965 00000 n
0000000000 00000 f
0000049508 00000 n
0000047016 00000 n
0000047319 00000 n
0000049396 00000 n
0000047527 00000 n
0000047648 00000 n
0000047673 00000 n
0000047866 00000 n
0000047933 00000 n
0000048218 00000 n
0000048306 00000 n
0000606732 00000 n

書いてますねー。それっぽいのが書いてますねー。
この1行には
[オブジェクトの書かれている位置] [世代番号] n
あるいは
[次のフリーなオブジェクトの番号] [世代番号] f
が記されています。
nのオブジェクトが表示されるオブジェクトです。
フリーなオブジェクトというのは使用されていないオブジェクトを意味しています。今はとりあえずnのものを見ればいいでしょう。

ちなみに0番のオブジェクトは特別なオブジェクトです。
この特別なオブジェクトは世代番号65535が与えられるのが基本です。

世代番号ってのはオブジェクトが更新された時に値が増加するようです。
おおむね0です。今は無視します。

長くなりましたね。
それじゃ1番目のオブジェクト0000000016 00000 nを見ましょう。16バイト目から書かれているようです。

オブジェクトを読む

$ xxd -g 1 -l 100 -s 16 helloworld.pdf
00000010: 31 20 30 20 6f 62 6a 0d 3c 3c 2f 4d 65 74 61 64  1 0 obj.<</Metad
00000020: 61 74 61 20 32 20 30 20 52 2f 4f 75 74 70 75 74  ata 2 0 R/Output
00000030: 49 6e 74 65 6e 74 73 5b 3c 3c 2f 44 65 73 74 4f  Intents[<</DestO
00000040: 75 74 70 75 74 50 72 6f 66 69 6c 65 20 35 20 30  utputProfile 5 0
00000050: 20 52 2f 49 6e 66 6f 28 4a 61 70 61 6e 20 43 6f   R/Info(Japan Co
00000060: 6c 6f 72 20 32 30 30 31 20 43 6f 61 74 65 64 29  lor 2001 Coated)
00000070: 2f 4f 75 74                                      /Out

1 0 objから始まってますね。
PDFのオブジェクトは[オブジェクト番号] [世代番号] objから始まり、endobjまで記述されます。
テキストエディタから持ってきましょう。

1 0 obj
<</Metadata 2 0 R/OutputIntents[<</DestOutputProfile 5 0 R/Info(Japan Color 2001 Coated)/OutputCondition()/OutputConditionIdentifier(JC200103)/RegistryName(http://www.color.org)/S/GTS_PDFX/Type/OutputIntent>>]/Pages 3 0 R/Type/Catalog>>
endobj

3行目から5行目ですね。

さて、実はPDFには普通のプログラムのようにデータ型が存在します。
この1番のオブジェクトにはその1つである辞書型(ディクショナリ型)が書かれているのです。

PDFのデータ型

  • 真偽値 bool
  • 文字列 string(binary)
  • 数値 number (int, real)
  • 名前 name
  • 辞書 dictionary
  • 配列 array
  • オブジェクト参照 ref(これは勝手に名付けました)

があります。

真偽値はtruefalseです。

文字列リテラルは丸括弧()で囲われたものです。(string value dayo)"string value dayo"を意味します。

文字列のうちバイナリリテラルというのもあり、<>で囲われたものです。
<A1BF24A93D>みたいな感じで記述されます。

数値は123とか+.123とか49.342のように記述されます。

他の言語にはないものとして名前という型があります。
/から始まるもので、何か意味のある文字が書かれます。/Lengthみたいなね。

辞書は名前をキーとしてどんな値も保存できます。<<>>で囲われます。
<< /Length 32 /Foo /Hoge /Dict <</Type /SubDict>> /Array [12 (str)] >>みたいな感じです。JSONで書くと

{
  "/Length": 32,
  "/Foo": "/Hoge",
  "/Dict": {
    "/Type": "/SubDict"
  },
  "/Array": [12 "str"],
}

って感じかな?

配列はその通りデータの配列です。だいたいどんなデータでも入れられます。
配列はその順番に意味を持ちます。

オブジェクト参照は[オブジェクト番号] [世代番号] Rで記述されます。
他のオブジェクトの値を参照します。

改めて1番のオブジェクトを読もう

1 0 obj
<</Metadata 2 0 R/OutputIntents[<</DestOutputProfile 5 0 R/Info(Japan Color 2001 Coated)/OutputCondition()/OutputConditionIdentifier(JC200103)/RegistryName(http://www.color.org)/S/GTS_PDFX/Type/OutputIntent>>]/Pages 3 0 R/Type/Catalog>>
endobj

不思議とみなさんはもう読めるでしょう。
さっきまで理解できなかったものが、今は理解できるこの感覚が最高に楽しいですよね!
それじゃこのディクショナリをjsっぽい仮想言語で記述してみましょう。

var 1_0_obj = {
  "/Metadata": 2_0_obj,
  "/OutputIntents": [
    {
      "/DestOutputProfile": 5_0_obj,
      "/Info": "Japan Color 2001 Coated",
      "/OutputCondition": "",
      "/OutputConditionIdentifier": "JC200103",
      "/RegistryName": "http://www.color.org",
      "/S": "/GTS_PDFX",
      "/Type": "/OutputIntent",
    }
  ],
  "/Pages": 3_0_obj,
  "/Type": "/Catalog"
}

このオブジェクトが何を表しているかは/Typeを見ればだいたいわかります。
/Typeが無いものはただ参照されるだけの値です。
このオブジェクトは/Catalogオブジェクトのようです。

/CatalogオブジェクトはPDFのオブジェクト階層において最上層に位置します。

このオブジェクトの/Pagesには3 0 Rつまり3 0 objを参照しています。クロスリファレンステーブルで位置を確認して見にいきましょう。

ページツリー(/Type = /Pages)

xref
0 17
0000000000 65535 f < 0番
0000000016 00000 n < 1番
0000000268 00000 n < 2番
0000046965 00000 n < 3番 これだ!

先頭から46965バイト目から書かれているらしいですね。

$ xxd -g 1 -l 100 -s 46965 helloworld.pdf
0000b775: 33 20 30 20 6f 62 6a 0d 3c 3c 2f 43 6f 75 6e 74  3 0 obj.<</Count
0000b785: 20 31 2f 4b 69 64 73 5b 36 20 30 20 52 5d 2f 54   1/Kids[6 0 R]/T
0000b795: 79 70 65 2f 50 61 67 65 73 3e 3e 0d 65 6e 64 6f  ype/Pages>>.endo
0000b7a5: 62 6a 0d 36 20 30 20 6f 62 6a 0d 3c 3c 2f 42 6c  bj.6 0 obj.<</Bl
0000b7b5: 65 65 64 42 6f 78 5b 30 2e 30 20 30 2e 30 20 35  eedBox[0.0 0.0 5
0000b7c5: 39 35 2e 32 37 36 20 38 34 31 2e 38 39 5d 2f 43  95.276 841.89]/C
0000b7d5: 6f 6e 74 65                                      onte

あったあった。

3 0 obj
<</Count 1/Kids[6 0 R]/Type/Pages>>
endobj

/CountはこのPDFのページ数です。このファイルは当たり前ですが1ページです。お次は/Kids。これはページのことが書かれたオブジェクトへの参照です。
6 0 objに書かれています。さぁ見に行こう!

ページ (/Type /Page)

6 0 obj
<</BleedBox[0.0 0.0 595.276 841.89]/Contents 7 0 R/CropBox[0.0 0.0 595.276 841.89]/LastModified(D:20200119182202+09'00')/MediaBox[0.0 0.0 595.276 841.89]/Parent 3 0 R/Resources<</ExtGState<</GS0 8 0 R>>/Font<</C0_0 9 0 R>>/ProcSet[/PDF/Text]>>/TrimBox[0.0 0.0 595.276 841.89]/Type/Page>>
endobj

/BleedBox(裁ち落としサイズ)とか/TrimBox(仕上がりサイズ)とか、ページのサイズ情報が多く含まれてます。[0.0 0.0 595.276 841.89]はA4サイズのことだろうなと直感でわかりますね。
このページに何が書かれているかは/Contentsにあり、7 0 objへの参照となっています。追いかけっこは楽しいですね!

ページコンテンツ

7 0 obj
<</Filter/FlateDecode/Length 140>>stream
H�4��
AD�����=��t@<��U�����+:����	U䥞�N�(��+S
�ʹ�;MSA��|�j��C~�?�+�'����E��H
(q���7�;�:X�D8�h\(���(�{��p�t�E�5}&%�
endstream
endobj

おっと、化けまくってますね!
ってかstreamとか言うものが現れました。
ストリームを含むオブジェクトはストリームオブジェクトと呼びます。

ストリームオブジェクトは

<</Length ストリームの長さ>>stream
stream内容
endstream

とストリームに関する情報が記述された辞書の後にstreamから始まりendstreamまで内容が書かれます。
この辞書には必ず/Lengthがあります。

/Filterってなんだ

/Filterはストリームが圧縮・エンコードされていることを意味します。
このストリームは/FlateDecodeとあるのでdeflateで圧縮されていることになります。

Deflate(デフレート)とはLZ77ハフマン符号化を組み合わせた可逆データ圧縮アルゴリズムフィル・カッツが開発した圧縮ツールPKZIPのバージョン2で使われていた。ZIPgzipなどで使われている。1996年5月に RFC 1951 としてドキュメント化された。ヘッダーやフッターをつけた zlib (RFC 1950) 形式や gzip (RFC 1952) 形式とともに使われる事が多い。

Deflate – Wikipedia https://ja.wikipedia.org/wiki/Deflate

っていう圧縮形式です。
世に出回っているPDFの多くは圧縮していることが多いです。そうじゃないとベラボーに重たくなりますからね。

展開してみる

pdftkというコマンドラインツールを使用して展開(uncompress)します。
そしてこのストリームに値する部分を抜粋してみました。
展開によってちょっと内容が変わっているかもしれませんが、概ね同じでしょう。

stream
q
0 841.89 595.276 -841.89 re
W n
BT
0 0 0 1 k
/Perceptual ri
/GS0 gs
/C0_0 1 Tf
60.2094 0 0 60.2094 21.2749 763.499 Tm
<00290046004D004D00500001003800500053004D0045>Tj
ET
Q

endstream

q 〜 Qはqの直前までの設定をQの後に元に戻すみたいな感じです。
スコープみたいな感じと捉えてます。

このstreamの内容はPostScript言語のPDF用サブセットらしいです。
PostScriptは図やテキストを表すのに特化した言語で、特に印刷に使用される言語です。
さすがにこれを全部を理解するのは難しいです。読む分にはテキストに値するところさえわかれば良いので、その他の部分を丸無視します。

テキストを読む

さて、この中でテキストに当たる部分はBT〜ETの部分です。
Begin Text、End Textの略だと思ってます。
さらにこの中でテキストの内容「Hello World」に当たる部分を見つけたいと思います。

テキスト構文(BT〜ET)の中でテキストの内容を表すのはTjという文字が書かれた部分です。

<00290046004D004D00500001003800500053004D0045>Tj

これですね。
Tjの前に書かれているのがテキストの内容です。
文字列なので、()<>で囲われているのが通常です。今回は<>なのでバイナリリテラルですね。

00290046004D004D00500001003800500053004D0045

2バイト文字扱いっぽいですね。2バイトずつ区切って最初の1バイトは00なので消してしまいましょう。

29 46 4D 4D 50 01 38 50 53 4D 45

きっとASCIIコード表と照らし合わせたら「Hello World」になるは・・・あれ?0x01が混ざってる。0x1f(31)までは制御文字のはずなのに。

あ、なるほどね。制御文字を飛ばして0x20(SPC)0x01としているようですね!

ということで、29 46 4D 4D 50 01 38 50 53 4D 45の全てに1fを足して48 65 6c 6c 6f 20 57 6f 72 6c 64。ASCIIコード表と照合して

「Hello World」

無事読めた!

ついに読めましたねー。
テキスト構文の中にはフォント情報だったりフォントサイズ、位置などなど。
もっとたくさんのことが書かれています。興味があれば追いかけてみてくださいね!
ということでPDFはクロスリファレンステーブルの参照を元にオブジェクトの参照を繰り返し、データ型を解析。必要があれば展開・デコード。BT〜ETのような構文を解析することで内容を取得できます。

どうです?PDFを読み込むプログラムが書けそうではないですか!?
画像とか抜き出したりできたら楽しいだろうなー。
バイナリ操作とかの勉強にはもってこいだと思いますよ!

出典・参考