0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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

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

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

image.png

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

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

image.png

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

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

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

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

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

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

image.png

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

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

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

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

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

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

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

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

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

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

image.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バイト目を読み取ってみましょう。

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

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

image.png

ここで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

image.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 
dd skip=320 bs=1 count=100 if=sample.pdf

image.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

Contents
image.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でやります

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

終わりに

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

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?