PDFを読む その1

# はじめに

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を読んでみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/1906f0ed-0d29-2bbd-494f-b526ab0d3688.png)


`This is a test`とだけ書かれた1ページのPDFです。

このPDFファイルをテキストエディターで無理やり開くと次のようなものが表示されます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/4a8428c1-9f96-dde1-540c-1609dfc1ccd2.png)


ちらほら読み取れる部分があったり読み取れない部分があったりします。

PDFを読むにはまずファイルの最後を見ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/769b6ecf-0e5c-c8f9-2f05-758991076354.png)


ファイルの最後には`%%EOF`がありファイルの終端を示しています。

その少し前に`startxref`という文字列があると思います。

startxrefと%%EOFに挟まれた数字

`9101`がクロスリファレンステーブルの定義が開始されているバイト位置です。

試しにddコマンドを使ってこのファイルの9101バイト目を読み取ります。

```terminal
dd skip=9101 bs=1 if=sample.pdf
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/b7283909-8cac-7799-7f4b-062891cbeacb.png)


するとちょうど`xref`という文字列からデータが始まることが見て取れます。

このxrefがクロスリファレンステーブルです。

ここからさらにオブジェクトにアクセスするためにクロスリファレンステーブルを読んでいきます。

まず1行目の`xref`はここからクロスリファレンステーブルの定義が始まりますという宣言です。


次に0 15ですがこれはオブジェクトの数を示しています。


その次にオブジェクトのバイトオフセット,世代番号,使用状況が定義されています。

ここで1行目は0バイトから始まり世代番号が65535の特殊なオブジェクトなので読み飛ばします。


次に2行目はファイルの開始位置から`320バイト`からデータ定義が始まることがわかります。

こちらも同様にddコマンドで見てみると次のようにデータが定義されています。

```terminal
dd skip=320 bs=1 count=100 if=sample.pdf
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/266c5679-8c60-393b-58f8-acef0e628449.png)


何やら`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バイト目を読み取ってみましょう。


```terminal
dd skip=3327 bs=1 count=100 if=sample.pdf
```

するとこのように10番目のデータが取れていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/5fc25425-3a9e-2468-17fc-b33fd4f0aeb1.png)

ここで10番目のデータに関してみると以下のように辞書オブジェクトが定義されています。

```
Type: Catalog
Pages: 2 0 R
```


PDFにはCatalog,PageTree,Page,Resource,Contentが存在しそれぞれ次のような内包関係を持ちます。

```mermaid
graph TD
    Catalog --> PageTree
    PageTree --> Page
    Page --> Resources
    Page --> Contents

```

さてここで察しのいい人はお気づきかもしれませんが`Pages`とはPageTreeを指し、その値の`2 0 R`というのはクロスリファレンステーブルの2番目ということを指します。


## PageTree
ではクロスリファレンステーブルの2番目をみてみましょう。

```
0000003244 00000 n 
```

3244バイト目から読んでいきます。


```terminal
dd skip=3244 bs=1 count=100 if=sample8.pdf
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/6d001a54-1231-0d59-841d-546e44cf0179.png)


さて先ほどと同様に辞書オブジェクトがあります

```
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 
```


```terminal
dd skip=320 bs=1 count=100 if=sample.pdf
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/c1813ef4-bdc5-d50b-3f52-fa4ceea5cd87.png)


こちらも同様に
```
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`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/766feb50-764d-3f5d-bffb-031b9525a5c5.png)


`Contents`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/b6348e2f-d54e-3959-505d-9650bf4dc3f4.png)

## 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でやります

```terminal
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になっています


ということで意味ありげな文字列を取得できました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3230849/ebfc53ce-1e71-f163-c6d7-f84f099209d3.png)



# 終わりに

ということでいいとこかもしれませんが今回はここで終わりです。


というのもある程度読めるようにするまでをここに記すには余白が狭すぎました。

~~というのと記事ネタが尽きないように~~

次回はテキストのデコードをします。