PDFファイル解析にあたって

PDFファイル解析にあたって

RustでPDF解析ツールを作成するにあたって、PDFの仕様をまとめる。 (作成プログラムはPDF.jsを参考に実装予定)

PDFとは

PDF32000_2008 1. Introduction より抜粋して意訳

PDFは、ISO 32000によって規定されている電子ドキュメントを実現するためのファイルフォーマットである。

1993年から2007年のISO標準になるまでAdobeによって開発されていたが、Adobe System Version PDF 1.7がISO 32000版の基となっている。 PDFの仕様は後方包括的であるため、PDF1.7はPDF1.0から1.6の仕様をそのまま含んでいる。

PDFのCoreは、PostScriptページ記述言語 から切り離された高い水準の描画モデルであり、テキストやグラフィックの記述をデバイスや解像度から独立して表されている。 それによって紙媒体から電子フォーム、デジタル紙までをカバーすることができる。

PDFは以下の電子ドキュメントに求められる要求を満たしている。

  • バイス、プラットフォーム、ソフトウェアから独立したドキュメントの保全

  • 以下の多様なソースから1つのPDFドキュメントに結合する

  • 正式なものであることを証明する電子署名

  • 作成者に対するセキュリティとパーミッションの許可

  • 障害のある方へのコンテンツの表示性

PDF構文

参照: PDF32000_2008 7. Syntax

PDF構文は、大まかに4つのパートに分けて理解することができる。

  • Objects: 文字列、数字、辞書、配列、ストリームといった基本データ型の定義
  • File構造: オブジェクトの配置やインクリメンタル更新、圧縮、暗号化を実現
  • Document構造: ページやリソースを一つのオブジェクト(indirect object)で表し、オブジェクト階層を実現
  • Content Stream: ページやグラフィックを表すための命令セットを定義

Document構造

参照: PDF32000_2008 7.7 Document Structure

PDFの解析には、まずそれによって得られる成果物であるドキュメント構造がどのようになっているのかを確認する。

PDFのドキュメント構造は、以下のようにドキュメントカタログ辞書をRootとしたオブジェクト階層を表している。 各オブジェクトはindirect objectであり、辞書によって表されている。 (※ indirect object: 相互参照テーブルから直接アクセスすることができる名前付きオブジェクト。)

カタログ辞書

ドキュメントオブジェクト階層のRoot。 トレーラー辞書の\Rootに、このオブジェクトにアクセスするためのオフセット値が記載されている。

辞書には、ページツリーのオブジェクト番号が記載されている。

1 0 obj
    << /Type /Catalog     % カタログ辞書であることを表す
       /Pages 2 0 R       % ページツリーノードのオブジェクト番号
       /Outlins 3 0 R     % アウトラインノードのオブジェクト番号
    >>
endobj

ページツリーノード

各ページのオブジェクト番号が記載されている。

2 0 obj
    << /Type /Pages      % ページツリーノードであることを表す
       /Kids [ 4 0 R     % 各ページのオブジェクト番号が記載
               10 0 R
               24 0 R
             ]
       /Count 3           % ページ数
    >>
endobj
  • /Kids: 各ページのオブジェクト番号が記載されている

ページオブジェクト

リソースが定義されているオブジェクト番号やその中身を表すコンテンツストリームのオブジェクト番号が定義されている。

4 0 obj
    << /Type /Page       % ページオブジェクトであることを表す
       /Parent 2 0 R     % ページツリーのオブジェクト番号
       /MediaBox [0 0 612 792]  % ページの領域を表す
       /Resources << /Font << /F3 7 0 R
                              /F5 9 0 R
                              /F7 11 0 R
                           >>
                      /ProcSet [/PDF]
                  >>
       /Contents 12 0 R   % コンテンツストリームのオブジェクト番号
       /Thumb 14 0 R      % サムネイル
    ...
    >>
endobj

コンテンツストリームオブジェクト

ページを描画するための命令が記述されている。

12 0 obj
<< /Length 65
>>
stream
1. 0. 0. 1. 50. 700. cm    % 現在位置を(50, 700)に移動
BT
    /F0 36. Tf             % 36ポイントの/F0フォントを選択
    (Hello World!) Tj      % 文字列 Hello World! を描画する
ET
ET
endstream
endobj

PDFファイル構造

参照: PDF32000_2008 7.5 File Structure

PDFを解析するにあたっては、PDFのファイル構造を理解する必要がある。 PDFファイル構造は互換性を伴いながら大きく3つ存在するが、それぞれ成果物としてオブジェクト階層を得ることができる。

  1. オブジェクト参照テーブルとトレーラー辞書によるPDF (version 1.0以降) - ランダムにオブジェクトを配置することができ、インクリメンタル更新ができる
  2. 単一オブジェクトストリームによるPDF (version 1.5以降) - PDFの暗号化
  3. Web最適化されたPDF (version 1.2以降) - ページ番号順に関連するオブジェクトが配置される

オブジェクト参照テーブルとトレーラー辞書によるPDF

5つのパートで構成されている

  • ヘッダー
  • バディ
  • 相互参照テーブル
  • トレーラー
  • startxref

例: 相互参照テーブルとトレーラー辞書によるPDF

%PDF-1.0              % ヘッダー
〓〓〓〓〓            % ASCII制御コードが埋め込まれる (バイナリファイルであることをFTPツールなどに判定させる)
1 0 obj               % バディ
    << /Type /Catalog
       /StructTreeRoot 3 0 R
       ...
    >>
endobj

3 0 obj
...
endobj
...

xref                  % 相互参照テーブル
0 7                   
0000000003 65535 f    
0000000017 00000 n    
0000000081 00000 n    
0000000000 00007 f    
0000000331 00000 n    
0000000409 00000 n    
trailler             % トレーラー辞書
    << /Size 100
       /Root 1 0 R 
       /ID ...
    >>
startxref
    2664            % 相互参照テーブルのオフセット位置
%EOF

ヘッダー

PDFバージョンを記載

バディ

カタログ辞書、ページツリーといったオブジェクトを配置

相互参照テーブル

各indirect objectへアクセスするための表が定義されている

例: 相互参照テーブル

xref                  % 相互参照テーブル
0 7                   
0000000003 65535 f    
0000000017 00000 n    
0000000081 00000 n    
0000000000 00007 f    
0000000331 00000 n    
0000000409 00000 n    
xref キーワード

相互参照テーブルの開始を表す

先頭行
0 7

オブジェクト番号0から6のObjectsが定義されていることを表す

2行目以降
nnnnnnnnnn ggggg *n* eol
  • nnnnnnnnnn オブジェクトのオフセット(位置)を先頭からのbytes単位で表す
  • ggggg 5桁の世代番号
  • n in-use項目であることを表す識別子
  • f free項目であることを表す識別子
  • eol 行末を表す2文字。以下の制御文字が使用可
    • SP CR
    • SP LF
    • CR LF
  • 10桁のオフセット(nnnnnnnnnn)と世代番号(gggg)は一文字のSPACEで区切る
  • fee項目になる方法は2つある
    • テーブルの最初の項目 (オブジェクト番号が0で、世代番号が必ず65,535)
    • indirect objectを削除したとき、相互参照テーブルの項目にfreeマークを付ける
      • 世代番号は、次に生成されるオブジェクト番号(現状のオブジェクト番号の最大値をインクリメントする)が与えられる
Examples

EXAMPLE オブジェクト番号3は削除され、その世代番号は次に生成されるオブジェクト番号を表す

xref
0 6                % 0から6つのobjectsを持つ
0000000003 65535 f % object number 0, free
0000000017 00000 n % object number 1, in-use
0000000081 00000 n % object number 2, in-use
0000000000 00007 f % object number 3, free, 最後なのでobject 0にリンクされる
0000000331 00000 n % object number 4, in-use
0000000409 00000 n % object number 5, in-use

EXAMPLE 4つのsubsectionをもつ相互参照テーブル

xref
0 1                  % オブジェクト番号0の一つのエントリーを持つサブセクション
0000000000 65535 f
3 1                  % オブジェクト番号3の一つのエントリーを持つサブセクション
0000025325 00000 n
23 2                 % オブジェクト番号23,24の二つのエントリーを持つサブセクション
0000025518 00002 n
0000025635 00000 n
30 1
0000025777 00000 n
  • 4つのsubsectionから構成され、トータルで5つの項目をもつ
  • オブジェクト番号23は、再使用されている。世代番号が2であるため。

トレーラー辞書

参照: 7.5.5 File Trailer

カタログ辞書の在りかや圧縮、暗号化情報が記載されている

startxref

startxref行は、 トレーラー辞書のブラケット(<<...>>)の次に必ず配置され、%%EOFの間に相互参照テーブルのオフセット位置がバイト値で記載されている。

PDFの解析にあたっては、ファイルの最後から startxref キーワードの値を読み取って、相互参照テーブルにアクセスする。

PDF.jsによる該当コード

src/core/document.js

get startXRef() {


  // Find `startxref` by checking backwards from the end of the file.
  const step = 1024;
  const startXRefLength = STARTXREF_SIGNATURE.length;
  let found = false,
    pos = stream.end; // ポジションを最終行に合わせる

  while (!found && pos > 0) {  // 最終行から1024バイトずつさかのぼってstartxrefを探している
    pos -= step - startXRefLength;
    if (pos < 0) {
      pos = 0;
    }
    stream.pos = pos;
    found = find(stream, STARTXREF_SIGNATURE, step, true);
  }

  if (found) {
    stream.skip(9);
    let ch;
    do {
      ch = stream.getByte();
    } while (isWhiteSpace(ch));
    let str = "";
    while (ch >= /* Space = */ 0x20 && ch <= /* '9' = */ 0x39) {
      str += String.fromCharCode(ch);
      ch = stream.getByte();
    }
    startXRef = parseInt(str, 10);
    if (isNaN(startXRef)) {
      startXRef = 0;
    }
  }
}
return shadow(this, "startXRef", startXRef);

単一オブジェクトストリームによるPDF

参照: 7.5.7 Object Stream

PDF 1.5からstreamオブジェクト内に複数のindirect objectを格納することができるようになった。

これによってファイルそのものを暗号化を実現することができる。

その際、各indirect objectへのアクセスは相互参照テーブルやトレーラー辞書の代わりに、相互参照ストリームが使用される。

2 0 obj             % オブジェクト番号2 (オフセット 3722)
<< /Length ...
   /N 8             % このストリームは8つのObjectsを含む
   /First 47        % ストリームの先頭行までのオフセット
...
>>
stream
 3 0 4 50 5 72      % オブジェクト番号とオフセットが交互に並んでいる
                    % オブジェクト番号3のオフセットは0、4のオフセットは50…
<< ... >>           % オブジェクト番号3
<< ... >>           % オブジェクト番号4
...
endstream

11 0 obj          % Cross-reference stream
<< /Type /XRef    % Cross-reference stream dictionary
   /Index [2 10]  % オブジェうと番号2から11のObjectを含むストリーム
   /Size 100      % トレーラー辞書の**Size**に該当
   /W [1 2 1]     % 相互参照テーブルの各フィールドのサイズをByteで表す。フィールド1が1バイト、フィールド2が2バイト、フィールド3が1バイト
   /Filter /ASCIIHexDecode % 読み取り専用
>>
stream
   01 0E8A 0      % オブジェクト番号2、Type1(非圧縮,オフセット値,世代番号)
   02 0002 00     % オブジェクト番号3、Type2(圧縮, 含まれているオブジェクトストリームのオブジェクト番号, ストリーム内のインデックス)
   02 0002 01     % オブジェクト番号4、Type2(圧縮, 含まれているオブジェクトストリームのオブジェクト番号, ストリーム内のインデックス)
....
   01 1323 0      % オブジェクト番号11、Type1(非圧縮, オフセット値, 世代番号)
endstream
endobj

startxref
byte offset_of_相互参照ストリーム (object 12への)
%%EOF
endobj

相互参照ストリーム

参照: 7.5.8 Cross-Refarence Stream

相互参照テーブルとトレーラー辞書に代わって、オブジェクトストリーム内のオブジェクトにアクセスするための情報を提供する。 streamオブジェクトで定義されている。

相互参照ストリーム辞書の中身
11 0 obj          % Cross-reference stream
<< /Type /XRef    % Cross-reference stream dictionary
   /Index [2 10]  % オブジェうと番号2から11のObjectを含むストリーム
   /Size 100      % トレーラー辞書の**Size**に該当
   /W [1 2 1]     % 相互参照テーブルの各フィールドのサイズをByteで表す。フィールド1が1バイト、フィールド2が2バイト、フィールド3が1バイト
   /Filter /ASCIIHexDecode % 読み取り専用
>>
  • /XRef: このストリームがオブジェクト参照ストリームであることを表す
  • トレーラー辞書の代わりとなる
相互参照ストリームデータの中身

相互参照テーブルが定義されている。

stream
   01 0E8A 0      % オブジェクト番号2、Type1(非圧縮,オフセット値,世代番号)
   02 0002 00     % オブジェクト番号3、Type2(圧縮, 含まれているオブジェクトストリームのオブジェクト番号, ストリーム内のインデックス)
   02 0002 01     % オブジェクト番号4、Type2(圧縮, 含まれているオブジェクトストリームのオブジェクト番号, ストリーム内のインデックス)
....
   01 1323 0      % オブジェクト番号11、Type1(非圧縮, オフセット値, 世代番号)
endstream

相互参照テーブルの1項目は、3つのフィールドで構成されている。

フィールド1 フィールド2 フィールド3
  • フィールド1: 項目のTypeを指定する。Typeは3つ定義されている。(Type0, Type1, Typ2)
  • フィールド2、3は、Typeによってその値が何を示すのか異なる
Type 0
Field Description
1 タイプを指定。00。この項目は相互参照テーブルのfに相当する
2 開放されたオブジェクト番号
3 このオブジェクト番号が再利用される際の世代番号
Type 1
Field Description
1 タイプを指定。01 。この項目は相互参照テーブルのnに相当する
2 オブジェクトへのオフセット値
3 世代番号。値は必ず0
Type 2
Field Description
1 タイプ指定。 02 。このオブジェクトはstream内に定義されたオブジェクトであることを表す
2 このオブジェクトが含まれているストリームオブジェクトのオブジェクト番号
3 このオブジェクトのストリーム内のインデックス

Web最適化されたPDF (直線化PDF)

参照: Annex F Liniearized PDF

Web上に置かれたPDFを表示するビューアーのために、ページ順にページ単位でオブジェクトを配置したPDF。 またヒントテーブルが導入され、各Objectへのアクセスが高速に行えるようになっている。

大きく2つのグループで構成され、1つめは、1ページ目と関連リソースおジェクトまたドキュメントカタログなどドキュメントの構造に関わるObjectがまとめられる。 2つめのグループには、ヒントテーブルと1ページ目以外のObjectsがページ順に配置される。(1ページ目に関連しない、共通リソースはそのあとに配置される)

これによって先頭からファイルを解析できるようになっている。

Web最適化されたPDFかどうかは、ヘッター行のあとに配置されるObjectを読み取ることで判定することができる。

%PDF-1.1        
〓〓〓〓〓
43 0 obj                 % 直線化PDFオブジェクトを表す辞書がヘッダーのあとに配置される
    << /Linearized 1.0   % Version
       /L 54567          % ファイルの大きさ
       /H [475 598]      % ヒントストリームへのオフセット値
       /O 45             % 先頭ページのオブジェクト番号
       /E 5437           % 先頭ページへのオフセット値
       /N 11             % ページ数
       /T 52785          % 相互参照テーブルまでのオフセット値
      ...
    >>
endobj

注意として、Web最適化されたPDFをインクリメンタル更新した場合、再度Web最適化する必要がある。 (ヒントテーブルなどの仕様に関しては、別途まとめる予定)

まとめ

  • PDFは、オブジェクト階層を表す
  • PDFファイル構造は、PDFファイルからオブジェクト階層を生成するための戦略が定義されている
  • オブジェクト階層のノード(indirect object)は、更にプリミティブなオブジェクト(文字列、数字、辞書、配列、ストリーム)によって構成されている
  • PDFをWeb専用に最適化することが出来る