技術記事

PDF ページツリーを理解する: ページ順序が重要な理由

· PDFプログラミング

PDFドキュメントは、表面上は単純に見えますが、内部構造は驚くほど複雑になることがあります。開発者が陥りやすい分野の一つは、PDFのページ順序が実際にどのように機能するかを理解することです。 HotPDF Delphi PDF Componentを修正および改善する際に、このような厄介な問題に遭遇しました。

PDFドキュメントのアーキテクチャ

基本概念

PDFドキュメントの核心は、オブジェクトのデータベースとして構築されています。各オブジェクトは一意の識別子を持ち、他のオブジェクトを参照できます。これにより、複雑なデータ構造が形成され、ドキュメントカタログ(ルート)がドキュメントのさまざまな部分へのエントリポイントとして機能します。

PDFを氷山だと考えてください。ドキュメントを表示したときに目に見えるのは表面だけであり、その下には、ドキュメントのあらゆる側面、つまり外観と動作を定義する、オブジェクト、参照、メタデータの洗練された構造が存在します。

オブジェクト参照システム

1
2
3
4
5
6
7
8
9
1 0 obj                <- Object 1
<<
  /Type /Page
  /Parent 3 0 R
  /Contents 4 0 R
  /MediaBox [0 0 612 792]
  /Resources 5 0 R
>>
endobj

すべてのPDFオブジェクトは、以下のパターンに従います。 ObjectNumber Generation obj。 R suffix in 参照先のように 3 0 R これは「オブジェクト3への参照、世代0」を意味します。

世代番号の理解

作成番号(通常、最新のPDFでは0)は、重要な役割を果たします。

  • Generation 0: 元のオブジェクト
  • Generation 1+: 更新バージョン(差分更新で利用)
  • Generation 65535: 削除済みオブジェクトのマーカー

1
2
3
4
5
6
7
8
9
% Original object
5 0 obj
<< /Type /Page /Contents 6 0 R >>
endobj
 
% Updated version (incremental update)
5 1 obj  
<< /Type /Page /Contents 6 0 R /Rotate 90 >>
endobj

PDFファイル構造の概要

PDFファイルは主に4つの部分で構成されます:

  1. ヘッダー (Header): バージョン情報(%PDF-1.7)
  2. 本文 (Body): オブジェクト定義とデータ
  3. 相互参照表: オブジェクトの場所インデックス
  4. トレイラー: ルート参照とファイルメタデータ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
%PDF-1.7                          <- Header
1 0 obj << /Type /Catalog ... >>  <- Body (objects)
2 0 obj << /Type /Pages ... >>
...
xref                              <- Cross-reference table
0 10
0000000000 65535 f
0000000009 00000 n
...
trailer                           <- Trailer
<< /Size 10 /Root 1 0 R >>
startxref
1234
%%EOF

ページツリー構造

ページツリーの概念

PDFは、ファイルシステムがディレクトリを整理するのと同様に、階層的なツリー構造を利用してページを整理します。この設計には複数の目的があります。

  1. 効率的なナビゲーション: ドキュメント全体を解析することなく、任意のページにすばやくアクセスできます。
  2. ページ継承親ノードから共通のプロパティを継承できます。
  3. 拡張性大量のページ数があるドキュメントも効率的に処理できます。
  4. 柔軟性複雑なドキュメント構造とネストされたセクションをサポートします。

1
2
3
4
5
6
7
Root Catalog
    
Pages Tree Root (/Type /Pages)
    
Kids Array [Page1, Page2, Page3, ...]
                          
         /Type /Page /Type /Page /Type /Page

実例:シンプルなページツリー

PDFファイルにおける典型的なページツリーの例を以下に示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
16 0 obj  (Pages Tree Root)
<<
  /Type /Pages
  /Count 3
  /Kids [
    20 0 R    <- Reference to first page
    1 0 R     <- Reference to second page  
    4 0 R     <- Reference to third page
  ]
  /MediaBox [0 0 612 792]  <- Inherited by all pages
>>
endobj
 
20 0 obj  (First Page)
<<
  /Type /Page
  /Parent 16 0 R
  /Contents 21 0 R
  /Resources 22 0 R
>>
endobj
 
1 0 obj  (Second Page)  
<<
  /Type /Page
  /Parent 16 0 R
  /Contents 2 0 R
  /Resources 3 0 R
  /Rotate 90
>>
endobj
 
4 0 obj  (Third Page)
<<
  /Type /Page
  /Parent 16 0 R
  /Contents 5 0 R
  /Resources 6 0 R
>>
endobj

重要な点: Kids配列は、以下のものを定義します。 論理的な ページの順序、ファイル内のオブジェクトの物理的な順序とは異なります。

qpdf出力からの実例

以下は実際の出力結果です。 qpdf --show-pages 問題のあるPDFファイルについて:

1
2
3
4
5
6
page 1: 20 0 R
  content: 192 0 R
page 2: 1 0 R  
  content: 190 0 R
page 3: 4 0 R
  content: 188 0 R

注意点:

  • 論理ページ 1 は格納されています。 オブジェクト 20 (最大のオブジェクト番号)
  • 論理ページ 2 は格納されています。 オブジェクト 1 (最小のオブジェクト番号)
  • 論理ページ 3 は格納されています。 オブジェクト 4 (中間オブジェクト番号)

もし解析コードがオブジェクトを数値順に処理する場合 (1, 4, 20)、正しい論理順序 (1, 2, 3) ではなく、誤ったページ順序 (2, 3, 1) になる。

複雑な例:ネストされたページツリー

大規模なドキュメントでは、より良い整理のために、ネストされたページツリーがよく利用されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
1 0 obj  (Document Catalog)
<<
  /Type /Catalog
  /Pages 2 0 R
>>
endobj
 
2 0 obj  (Root Pages Node)
<<
  /Type /Pages
  /Count 8
  /Kids [3 0 R 4 0 R]  <- Two intermediate nodes
>>
endobj
 
3 0 obj  (Chapter 1 Pages)
<<
  /Type /Pages
  /Parent 2 0 R
  /Count 5
  /Kids [10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
  /MediaBox [0 0 612 792]
>>
endobj
 
4 0 obj  (Chapter 2 Pages)
<<
  /Type /Pages
  /Parent 2 0 R
  /Count 3
  /Kids [20 0 R 21 0 R 22 0 R]
  /MediaBox [0 0 612 792]
>>
endobj
 
% Individual page objects follow...
10 0 obj << /Type /Page /Parent 3 0 R ... >>
11 0 obj << /Type /Page /Parent 3 0 R ... >>
...

これにより、ツリー構造が作成されます。

1
2
3
4
5
6
7
8
9
10
11
Root (8 pages)
├── Chapter 1 (5 pages)
   ├── Page 1 (10 0 R)
   ├── Page 2 (11 0 R)
   ├── Page 3 (12 0 R)
   ├── Page 4 (13 0 R)
   └── Page 5 (14 0 R)
└── Chapter 2 (3 pages)
    ├── Page 6 (20 0 R)
    ├── Page 7 (21 0 R)
    └── Page 8 (22 0 R)

ページツリーのプロパティ

必須プロパティ:

  • /Type必須: /Pages 中間ノードで利用、または /Page リーフノードで利用
  • /Kids子ページ参照配列(中間ノードのみ)
  • /Count子孫ページの総数
  • /Parent親ノード参照(ルートノードを除く)

任意の継承プロパティ:

  • /MediaBoxページ寸法
  • /CropBox表示領域
  • /BleedBox印刷出血領域
  • /TrimBox最終トリミング後のページサイズ
  • /ArtBox有効コンテンツ領域
  • /Resourcesフォント、画像、グラフィックの状態
  • /Rotateページ回転 (0, 90, 180, 270 度)

よくある誤解

誤り #1: 順序付きオブジェクト番号 = ページ順序と仮定すること

多くの開発者は、PDFファイル内のページがオブジェクト1、2、3で保存されている場合、オブジェクト1がページ1であると仮定します。これは根本的に誤りで、微妙なバグにつながります。

この仮定がなぜ間違っているのか:

  • オブジェクト番号は、ページ順序に基づいてではなく、PDF作成時に割り当てられます。
  • PDFエディタは、最適化中にオブジェクト番号を再割り当てする場合があります。
  • 段階的な更新により、より高い番号の新しいオブジェクトが追加されます。
  • オブジェクトストリームは、番号付けスキームを変更する可能性があります。

現実オブジェクト番号は単なる識別子です。実際のページ順序は、Pagesツリー内のKids配列によって決定されます。

実世界の例:

1
2
3
4
5
6
7
8
9
10
11
12
% These pages were created in order: Page 1, Page 2, Page 3
% But stored in PDF with these object numbers:
150 0 obj << /Type /Page ... >>  % Actually page 1  
23 0 obj << /Type /Page ... >>   % Actually page 2
8 0 obj << /Type /Page ... >>    % Actually page 3
 
% The Pages tree defines the correct order:
16 0 obj
<<
  /Type /Pages
  /Kids [150 0 R 23 0 R 8 0 R]  % Logical order
>>

間違いその2:物理的なファイル順序でPagesを処理する。

PDFファイルからオブジェクトを順番に読み取るだけでは、正しい順序でページが得られません。

例:問題点:

  • ファイルには、物理的な順序でオブジェクトが含まれています:1、4、16、20
  • PagesツリーのKids配列:[20 0 R, 1 0 R, 4 0 R]
  • 正しい論理的なページ順序:オブジェクト20(ページ1)、オブジェクト1(ページ2)、オブジェクト4(ページ3)
  • 物理ファイルの順序が正しくありません:オブジェクト1(ページ2)、オブジェクト4(ページ3)、オブジェクト16(ページではない)、オブジェクト20(ページ1)。

発生原因:

  • PDF作成ツールは通常、ページ順序ではなくファイルサイズを最適化します。
  • オブジェクトストリームによって内容が再編成される場合があります。
  • リニアライズにより、Web表示に適したオブジェクト順序へ変更される場合があります。
  • 複数の編集ツールによる変更が積み重なる場合があります。

誤り #3: ドキュメントカタログを無視すること

一部の解析コードは、ルート → Pages → 子ページという正しいチェーンをたどらず、ページを直接探そうとします。

問題のあるアプローチ:

1
2
3
4
5
6
// Wrong: Direct page search
for i := 0 to Objects.Count - 1 do
begin
  if Objects[i].GetValue('/Type') = '/Page' then
    AddToPageList(Objects[i]);  // Wrong order!
end;

正しいアプローチ:

1
2
3
4
5
6
7
8
9
10
// Right: Follow the document structure
CatalogObj := FindObjectByReference(TrailerRoot);
PagesObj := FindObjectByReference(CatalogObj.GetValue('/Pages'));
KidsArray := PagesObj.GetValue('/Kids');
for i := 0 to KidsArray.Count - 1 do
begin
  PageRef := KidsArray.GetReference(i);
  PageObj := FindObjectByReference(PageRef);
  AddToPageList(PageObj);  // Correct order!
end;

間違い #4: ネストされたページツリーの処理を怠る:

すべてのページツリーがフラット(単一レベル)であると仮定すると、複雑なドキュメント構造を見逃す可能性があります。

シンプルなツリー(一般的に想定されるもの):

1
2
3
4
Pages Root
├── Page 1
├── Page 2
└── Page 3

実際の複雑なツリー:

1
2
3
4
5
6
7
8
9
10
Pages Root
├── Part 1 Pages
   ├── Chapter 1 Pages
      ├── Page 1
      └── Page 2
   └── Chapter 2 Pages
       ├── Page 3
       └── Page 4
└── Part 2 Pages
    └── Page 5

再帰的な構造の処理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
procedure ProcessPageNode(Node: TPDFObject; var PageList: TPageList);
begin
  if Node.GetValue('/Type') = '/Pages' then
  begin
    // Intermediate node - process all kids
    KidsArray := Node.GetValue('/Kids');
    for i := 0 to KidsArray.Count - 1 do
    begin
      ChildRef := KidsArray.GetReference(i);
      ChildObj := FindObjectByReference(ChildRef);
      ProcessPageNode(ChildObj, PageList);  // Recursive call
    end;
  end
  else if Node.GetValue('/Type') = '/Page' then
  begin
    // Leaf node - actual page
    PageList.Add(Node);
  end;
end;

間違い #5: ページ継承を無視する:

継承されるプロパティを考慮しないと、ページ描画エラーの原因になります。

継承チェーンの例:

1
2
3
4
Root Pages (/MediaBox [0 0 612 792], /Resources 10 0 R)
├── Chapter Pages (/Rotate 90)
   └── Page 1 (/Contents 20 0 R)
└── Page 2 (/Contents 21 0 R, /MediaBox [0 0 595 842])

有效プロパティ:

  • Page 1: MediaBox=[0,0,612,792] (継承), Rotate=90 (継承), Resources=10 0 R (継承), Contents=20 0 R
  • Page 2: MediaBox=[0,0,595,842] (覆盖), Rotate=0 (未継承), Resources=10 0 R (継承), Contents=21 0 R

実装方法(HotPDF コンポーネント):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function GetEffectivePageProperties(PageObj: TPDFDictionary): TPDFDictionary;
var
  EffectiveProps: TPDFDictionary;
  CurrentNode: TPDFDictionary;
begin
  EffectiveProps := TPDFDictionary.Create;
  CurrentNode := PageObj;
  
  // Walk up the tree collecting inherited properties
  while CurrentNode <> nil do
  begin
    // Add properties not already set (inheritance chain)
    if not EffectiveProps.HasKey('/MediaBox') and CurrentNode.HasKey('/MediaBox') then
      EffectiveProps.SetValue('/MediaBox', CurrentNode.GetValue('/MediaBox'));
    if not EffectiveProps.HasKey('/Resources') and CurrentNode.HasKey('/Resources') then
      EffectiveProps.SetValue('/Resources', CurrentNode.GetValue('/Resources'));
    // ... other inheritable properties
    
    // Move to parent
    if CurrentNode.HasKey('/Parent') then
      CurrentNode := FindObjectByReference(CurrentNode.GetValue('/Parent'))
    else
      CurrentNode := nil;
  end;
  
  Result := EffectiveProps;
end;

エラー #6: カウント値が正確だと仮定する

場合によっては、 /Count ページツリーノード内の値が、実際のページ数と一致しないことがあります。

問題:

1
2
3
4
5
6
7
8
9
Pages Root
<<
  /Count 5      <- Claims 5 pages
  /Kids [A B C] <- But only 3 direct children
>>
 
Node A: /Count 2, /Kids [Page1, Page2]
Node B: /Count 1, /Kids [Page3]  
Node C: /Count 3, /Kids [Page4, Page5, Page6]  <- 3 pages, not matching parent count

防御性编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// HotPDF VCL Component code snippet
function CountActualPages(PagesNode: TPDFDictionary): Integer;
var
  ActualCount: Integer;
  KidsArray: TPDFArray;
  i: Integer;
  ChildObj: TPDFDictionary;
begin
  ActualCount := 0;
  KidsArray := PagesNode.GetValue('/Kids');
  
  for i := 0 to KidsArray.Count - 1 do
  begin
    ChildObj := FindObjectByReference(KidsArray.GetReference(i));
    if ChildObj.GetValue('/Type') = '/Page' then
      Inc(ActualCount)
    else if ChildObj.GetValue('/Type') = '/Pages' then
      Inc(ActualCount, CountActualPages(ChildObj));
  end;
  
  // Verify against claimed count
  ClaimedCount := PagesNode.GetValue('/Count');
  if ClaimedCount <> ActualCount then
    WriteLn('Warning: Count mismatch - claimed: ', ClaimedCount, ', actual: ', ActualCount);
    
  Result := ActualCount;
end;

方法正しい解析ページ

ステップ 1: ドキュメントルートを見つける

1
2
3
// Find trailer and get Root reference
RootRef := GetTrailerRootReference();
RootObject := FindObject(RootRef);

步骤 2:导航到ページ树

1
2
3
// Get Pages reference from Root catalog
PagesRef := RootObject.GetValue('/Pages');
PagesObject := FindObject(PagesRef);

步骤 3:按順序処理子配列

1
2
3
4
5
6
7
8
9
10
// Extract Kids array - this defines page order
KidsArray := PagesObject.GetValue('/Kids');
 
// Process each page in the order specified by Kids
for i := 0 to KidsArray.Count - 1 do
begin
  PageRef := KidsArray[i];
  PageObject := FindObject(PageRef);
  // Now you have the actual page i+1
end;

高度な概念

ネストされたページツリー

大きなドキュメントでは、より良い整理のためにネストされたページツリーを利用できます。

1
2
3
4
5
6
7
8
Root Pages
  ├── Chapter 1 Pages
     ├── Page 1
     ├── Page 2
     └── Page 3
  └── Chapter 2 Pages
      ├── Page 4
      └── Page 5

ページ継承

ページは、親ページツリーノードからプロパティを継承できます。

  • MediaBox (ページサイズ)
  • CropBox (表示領域)
  • Resources (フォント、画像)
  • Rotation (回転)

実用的な実装のヒント

1. 常にツリー構造に従ってください

1
2
3
4
5
// Wrong: Assumes sequential object order
PageObject := GetObject(PageNumber);
 
// Right: Follows Pages tree structure  
PageObject := GetPageFromKidsArray(PageNumber - 1);

2. 再帰的なページツリーを処理する

一部のPDFには、複数のレベルのページツリーノードがあります。コードは、再帰的にツリーを走査する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
procedure ProcessPageNode(Node: TPDFObject);
begin
  if Node.Type = 'Pages' then
  begin
    // Intermediate node - process Kids
    for each Kid in Node.Kids do
      ProcessPageNode(Kid);
  end
  else if Node.Type = 'Page' then
  begin
    // Leaf node - actual page
    AddPageToArray(Node);
  end;
end;

3. ページ数を検証する

常に、 /Count Pagesオブジェクト内の値が、実際に検出されたページ数と一致することを確認してください。

1
2
3
4
ExpectedCount := PagesObject.GetValue('/Count');
ActualCount := CountPagesInTree(PagesObject);
if ExpectedCount <> ActualCount then
  RaiseError('Page count mismatch');

PDFのページに関する問題のデバッグ

一般症状

  1. 誤ったページを抽出した: 通常の表示では無視されます Kids 配列の順序
  2. 缺少ページ: ネストしたページツリーを正しく処理していないことが原因でよく発生します
  3. 重複ページ: 中間ノードとリーフノードを同時に処理すると発生することがあります

デバッグのヒント

  1. ページツリー構造をログに記録します。:

1
2
WriteLn('Pages tree Kids: [', KidsArrayToString(Kids), ']');
WriteLn('Processing page object: ', PageObjectNumber);

  1. ページの内容を確認します。小さなサンプルを抽出し、期待される内容と一致することを確認します。

  2. 外部ツールを利用します。ツールとしては、 qpdf または pdftk などがあり、PDFの構造分析に役立ちます。

ベストプラクティス

1. 正しいデータ構造を構築する

内部ページ配列では、PDF の論理ページ順序と同じ順序で並べます。

1
2
3
4
5
6
7
// Build PageArray following Kids order
SetLength(PageArray, PageCount);
for i := 0 to KidsArray.Count - 1 do
begin
  PageRef := KidsArray[i];
  PageArray[i] := FindObject(PageRef);
end;

2. 解析と処理を分離する

まず完全なページ構造を解析し、その後で操作を実行します。ドキュメント構造を解析しながらページ処理を行わないでください。

3. 処理特殊情况。

  • 空ドキュメント(0页)。
  • 单页ドキュメント。
  • 縦向きと横向きのページが混在するドキュメント
  • 継承されたプロパティを持つドキュメント

高度なPDFオブジェクトの種類

PDFオブジェクトの階層構造の理解

基本的なページオブジェクトに加えて、PDFには多数の特殊なオブジェクトタイプが含まれており、これらが連携して完全なドキュメントを構成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Document Catalog (Root)
├── Pages Tree
├── Outlines (Bookmarks)
├── Names Dictionary
├── Dests (Named Destinations)
├── ViewerPreferences
├── PageLabels
├── Metadata
├── StructTreeRoot (Tagged PDF)
├── MarkInfo
├── Lang
├── SpiderInfo
├── OutputIntents
├── PieceInfo
├── AcroForm (Interactive Forms)
├── Encrypt (Security)
└── Extensions

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

ページの内容は、描画コマンドを含むストリームオブジェクトに格納されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
5 0 obj  (Content Stream)
<<
  /Length 1274
  /Filter /FlateDecode
>>
stream
BT                    % Begin text
/F1 12 Tf            % Set font (F1) and size (12)
100 700 Td           % Move to position (100, 700)
(Hello World) Tj     % Show text "Hello World"
ET                   % End text
Q                    % Save graphics state
q                    % Restore graphics state
endstream
endobj

リソースオブジェクト

リソースは、コンテンツストリームで利用されるフォント、画像、およびグラフィックスの状態を定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
6 0 obj  (Resources)
<<
  /Font <<
    /F1 7 0 R      % Font resource
    /F2 8 0 R
  >>
  /XObject <<
    /Im1 9 0 R     % Image resource
  >>
  /ExtGState <<
    /GS1 10 0 R    % Graphics state
  >>
  /ColorSpace <<
    /CS1 11 0 R    % Color space
  >>
>>
endobj

フォントオブジェクト

フォントは、複数のサブタイプを持つ複雑なオブジェクトです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
7 0 obj  (Type 1 Font)
<<
  /Type /Font
  /Subtype /Type1
  /BaseFont /Helvetica
  /Encoding /WinAnsiEncoding
>>
endobj
 
8 0 obj  (TrueType Font)
<<
  /Type /Font
  /Subtype /TrueType
  /BaseFont /ArialMT
  /FirstChar 32
  /LastChar 126
  /Widths [278 278 355 ...]
  /FontDescriptor 12 0 R
>>
endobj

プロフェッショナルなPDF解析ツール

コマンドラインツール

QPDF – PDFの万能ツール

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Show page tree structure and page order
qpdf --show-pages input.pdf
 
# Show detailed page information in JSON format
qpdf --json=latest --json-key=pages input.pdf
 
# Validate PDF structure
qpdf --check input.pdf
 
# Show cross-reference table
qpdf --show-xref input.pdf
 
# Show specific object (e.g., pages tree root)
qpdf --show-object="16 0 R" input.pdf
 
# Show encryption details
qpdf --show-encryption input.pdf
 
# Show filtered stream data
qpdf --filtered-stream-data input.pdf
 
# Show complete document structure in JSON
qpdf --json input.pdf

CPDF – コヒーレントなPDFコマンドラインツール

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Get comprehensive PDF information in JSON format
cpdf -info-json input.pdf
 
# Get detailed page information with boxes and rotation
cpdf -page-info-json input.pdf
 
# List all fonts with encoding and type information
cpdf -list-fonts-json input.pdf
 
# List images with dimensions, color space, and compression
cpdf -list-images-json input.pdf
 
# View specific PDF objects (great for debugging)
cpdf -obj 16 input.pdf
# Output: <</Count 3/Kids[20 0 R 1 0 R 4 0 R]/Type/Pages>>
 
# Analyze document composition and size breakdown
cpdf -composition-json input.pdf
# Shows percentage of images, fonts, content streams, etc.
 
# List bookmarks in JSON format
cpdf -list-bookmarks-json input.pdf
 
# Export complete PDF structure as JSON for detailed analysis
cpdf -output-json input.pdf -o structure.json

PDFtk – PDFツールキット

1
2
3
4
5
6
7
8
9
10
11
# Dump document metadata
pdftk input.pdf dump_data
 
# Show bookmarks
pdftk input.pdf dump_data | grep -A 5 "Bookmark"
 
# Extract specific pages
pdftk input.pdf cat 1-3 output pages_1_to_3.pdf
 
# Rotate pages
pdftk input.pdf cat 1-endright output rotated.pdf

MuPDFツール

1
2
3
4
5
6
7
8
9
10
11
# Show PDF structure
mutool show input.pdf
 
# Extract text with positioning
mutool draw -F txt input.pdf
 
# Convert to HTML (preserves structure)
mutool convert -F html input.pdf output.html
 
# Show object details
mutool show input.pdf 1 0 R

デスクトップ分析ツール

PDF Explorer (有料版):

  • ドキュメント構造の視覚的なツリー表示
  • オブジェクトのプロパティのリアルタイム編集
  • 相互参照の検証
  • ストリーミングデコードと表示

PDF デバッガー (Adobe):

  • PDF レンダリングのステップ実行
  • オブジェクトインスペクタ、構文ハイライト付き
  • コンテンツストリーム解析
  • エラー検出とレポート

解析のためのプログラミングライブラリ

Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import PyPDF2
import fitz  # PyMuPDF
 
# PyPDF2 analysis
with open('input.pdf', 'rb') as file:
    reader = PyPDF2.PdfFileReader(file)
    
    # Show page tree structure
    pages_obj = reader.trailer['/Root']['/Pages']
    print(f"Pages object: {pages_obj}")
    
    # Show each page's properties
    for i in range(reader.numPages):
        page = reader.getPage(i)
        print(f"Page {i+1}: {page}")
 
# PyMuPDF detailed analysis
doc = fitz.open('input.pdf')
for page_num in range(doc.page_count):
    page = doc[page_num]
    
    # Get page dictionary
    page_dict = page.get_contents()
    print(f"Page {page_num + 1} contents: {len(page_dict)} bytes")
    
    # Get text with positioning
    blocks = page.get_text("dict")
    for block in blocks["blocks"]:
        if "lines" in block:
            for line in block["lines"]:
                for span in line["spans"]:
                    print(f"Text: '{span['text']}' at {span['bbox']}")

JavaScript (PDF.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Load and analyze PDF
pdfjsLib.getDocument('input.pdf').promise.then(function(pdf) {
    // Get page count
    console.log('Page count:', pdf.numPages);
    
    // Analyze each page
    for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
        pdf.getPage(pageNum).then(function(page) {
            // Get page annotations
            page.getAnnotations().then(function(annotations) {
                console.log(`Page ${pageNum} annotations:`, annotations);
            });
            
            // Get text content
            page.getTextContent().then(function(textContent) {
                console.log(`Page ${pageNum} text items:`, textContent.items.length);
            });
        });
    }
});

性能考量

効率的なページツリーの走査

大規模なドキュメントを扱う場合、効率的な走査が重要になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// HotPDF Component code snippet
// Optimized page tree traversal with caching
type
  TPageCache = class
  private
    FPageObjects: TDictionary<Integer, TPDFPageObject>;
    FPageTree: TPDFPagesTree;
  public
    function GetPage(PageNumber: Integer): TPDFPageObject;
    procedure PreloadPageRange(StartPage, EndPage: Integer);
    procedure ClearCache;
  end;
 
function TPageCache.GetPage(PageNumber: Integer): TPDFPageObject;
begin
  // Check cache first
  if FPageObjects.ContainsKey(PageNumber) then
    Exit(FPageObjects[PageNumber]);
    
  // Load on demand
  Result := FPageTree.LoadPage(PageNumber);
  FPageObjects.Add(PageNumber, Result);
end;
 
procedure TPageCache.PreloadPageRange(StartPage, EndPage: Integer);
var
  I: Integer;
  PageObj: TPDFPageObject;
begin
  // Batch load for better performance
  for I := StartPage to EndPage do
  begin
    if not FPageObjects.ContainsKey(I) then
    begin
      PageObj := FPageTree.LoadPage(I);
      FPageObjects.Add(I, PageObj);
    end;
  end;
end;

メモリ管理

大きな PDF ファイルには慎重なメモリ管理が必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// losLab HotPDF Component code snippet
// Memory-efficient PDF processing
type
  TPDFProcessor = class
  private
    FMemoryLimit: Int64;
    FCurrentMemoryUsage: Int64;
    procedure CheckMemoryUsage;
    procedure FlushCaches;
  public
    procedure ProcessPagesInBatches(PDF: TPDFDocument; BatchSize: Integer);
  end;
 
procedure TPDFProcessor.ProcessPagesInBatches(PDF: TPDFDocument; BatchSize: Integer);
var
  I, StartPage, EndPage: Integer;
  PageCount: Integer;
  Batch: TList<TPDFPageObject>;
begin
  PageCount := PDF.GetPageCount;
  StartPage := 1;
  
  while StartPage <= PageCount do
  begin
    EndPage := Min(StartPage + BatchSize - 1, PageCount);
    Batch := TList<TPDFPageObject>.Create;
    try
      // Load batch of pages
      for I := StartPage to EndPage do
      begin
        Batch.Add(PDF.GetPage(I));
        CheckMemoryUsage;
      end;
      
      // Process batch
      ProcessPageBatch(Batch);
      
    finally
      // Clean up batch
      Batch.Free;
      FlushCaches;
    end;
    
    StartPage := EndPage + 1;
  end;
end;

遅延読み込み戦略

大きなドキュメント向けに遅延読み込みを実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Lazy-loaded page tree
type
  TLazyPDFPage = class
  private
    FPageReference: TPDFReference;
    FPageObject: TPDFPageObject;
    FLoaded: Boolean;
    function GetPageObject: TPDFPageObject;
  public
    constructor Create(PageRef: TPDFReference);
    property PageObject: TPDFPageObject read GetPageObject;
    property IsLoaded: Boolean read FLoaded;
    procedure Unload; // Free memory when not needed
  end;
 
function TLazyPDFPage.GetPageObject: TPDFPageObject;
begin
  if not FLoaded then
  begin
    WriteLn('[DEBUG] Loading page from reference ', FPageReference.ObjectNumber);
    FPageObject := LoadObjectFromReference(FPageReference);
    FLoaded := True;
  end;
  Result := FPageObject;
end;
 
procedure TLazyPDFPage.Unload;
begin
  if FLoaded then
  begin
    WriteLn('[DEBUG] Unloading page ', FPageReference.ObjectNumber);
    FPageObject.Free;
    FPageObject := nil;
    FLoaded := False;
  end;
end;

エラー処理と検証

堅牢な PDF 解析

形式エラーや破損した PDF ファイルを適切に処理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// losLab Software Development code snippet
// Defensive PDF parsing with error recovery
type
  TPDFParseResult = (prSuccess, prWarning, prError, prCriticalError);
  
function ParsePDFWithRecovery(FileName: string): TPDFParseResult;
var
  PDF: TPDFDocument;
  ErrorCount: Integer;
  WarningCount: Integer;
begin
  Result := prSuccess;
  ErrorCount := 0;
  WarningCount := 0;
  
  try
    PDF := TPDFDocument.Create;
    try
      // Basic file validation
      if not ValidatePDFHeader(FileName) then
      begin
        WriteLn('[ERROR] Invalid PDF header');
        Inc(ErrorCount);
      end;
      
      // Load with error recovery
      if not PDF.LoadFromFileWithRecovery(FileName) then
      begin
        WriteLn('[ERROR] Failed to load PDF structure');
        Inc(ErrorCount);
      end;
      
      // Validate page tree
      case ValidatePageTree(PDF) of
        vtValid:
          WriteLn('[INFO] Page tree is valid');
        vtWarning:
          begin
            WriteLn('[WARN] Page tree has minor issues');
            Inc(WarningCount);
          end;
        vtError:
          begin
            WriteLn('[ERROR] Page tree is corrupted');
            Inc(ErrorCount);
          end;
      end;
      
      // Validate cross-references
      if not ValidateXRefTable(PDF) then
      begin
        WriteLn('[WARN] Cross-reference table has issues, attempting repair');
        if RepairXRefTable(PDF) then
          Inc(WarningCount)
        else
          Inc(ErrorCount);
      end;
      
      // Determine result based on error counts
      if ErrorCount > 0 then
        Result := prError
      else if WarningCount > 0 then
        Result := prWarning
      else
        Result := prSuccess;
        
    finally
      PDF.Free;
    end;
    
  except
    on E: Exception do
    begin
      WriteLn('[CRITICAL] Exception during PDF parsing: ', E.Message);
      Result := prCriticalError;
    end;
  end;
end;

検証チェックリスト

包括的な検証を実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// losLab Software code snippet
// PDF validation checklist source codes
type
  TValidationCheck = record
    Name: string;
    Passed: Boolean;
    Message: string;
  end;
  
function ValidatePDFDocument(PDF: TPDFDocument): TArray<TValidationCheck>;
var
  Checks: TArray<TValidationCheck>;
begin
  SetLength(Checks, 10);
  
  // Check 1: File header
  Checks[0].Name := 'PDF Header';
  Checks[0].Passed := ValidatePDFVersion(PDF.Version);
  Checks[0].Message := 'PDF version: ' + PDF.Version;
  
  // Check 2: Document catalog
  Checks[1].Name := 'Document Catalog';
  Checks[1].Passed := PDF.Catalog <> nil;
  Checks[1].Message := 'Root catalog ' + IfThen(Checks[1].Passed, 'found', 'missing');
  
  // Check 3: Page tree structure
  Checks[2].Name := 'Page Tree';
  Checks[2].Passed := ValidatePageTreeStructure(PDF);
  Checks[2].Message := Format('Page tree contains %d pages', [PDF.PageCount]);
  
  // Check 4: Cross-reference table
  Checks[3].Name := 'Cross-Reference Table';
  Checks[3].Passed := ValidateXRefConsistency(PDF);
  Checks[3].Message := 'XRef table consistency check';
  
  // Check 5: Object integrity
  Checks[4].Name := 'Object Integrity';
  Checks[4].Passed := ValidateObjectIntegrity(PDF);
  Checks[4].Message := 'All referenced objects exist';
  
  // Check 6: Page content streams
  Checks[5].Name := 'Content Streams';
  Checks[5].Passed := ValidateContentStreams(PDF);
  Checks[5].Message := 'All pages have valid content';
  
  // Check 7: Font resources
  Checks[6].Name := 'Font Resources';
  Checks[6].Passed := ValidateFontResources(PDF);
  Checks[6].Message := 'Font resources are complete';
  
  // Check 8: Image resources
  Checks[7].Name := 'Image Resources';
  Checks[7].Passed := ValidateImageResources(PDF);
  Checks[7].Message := 'Image resources are accessible';
  
  // Check 9: Encryption
  Checks[8].Name := 'Encryption';
  Checks[8].Passed := ValidateEncryption(PDF);
  Checks[8].Message := 'Encryption settings are valid';
  
  // Check 10: Metadata
  Checks[9].Name := 'Metadata';
  Checks[9].Passed := ValidateMetadata(PDF);
  Checks[9].Message := 'Document metadata is well-formed';
  
  Result := Checks;
end;

実践的な検証:実際のPDF解析

本記事の概念を検証するために、問題のあるPDFファイルに対してqpdfを用いた実際の解析を実施しました。その結果は、ページの順序に関する問題を明確に示しました。

実際のqpdf出力解析

コマンド: qpdf --show-pages input-all.pdf

結果:

1
2
3
4
5
6
page 1: 20 0 R
  content: 192 0 R
page 2: 1 0 R  
  content: 190 0 R
page 3: 4 0 R
  content: 188 0 R

解析:

  • 論理ページ1 → オブジェクト20 (最大の番号)
  • 論理ページ2 → オブジェクト1 (最小の番号)
  • Logical Page 3 → Object 4 (中央の数値)

この現実世界の例は、オブジェクト順の解析がなぜ失敗するのかを証明しています。オブジェクトを数値順に処理すると (1, 4, 20)、ページは (2, 3, 1) の順になるため、正しい論理的な順序 (1, 2, 3) とはなりません。

検証コマンド。

これらの qpdf コマンドは、ドキュメントの構造を正常に検証しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Show page structure - WORKS
qpdf --show-pages input-all.pdf
 
# Show detailed page info in JSON - WORKS  
qpdf --json=latest --json-key=pages input-all.pdf
 
# Validate PDF structure - WORKS
qpdf --check input-all.pdf
# Output: "No syntax or stream encoding errors found"
 
# Show cross-reference table - WORKS
qpdf --show-xref input-all.pdf
 
# Show specific object (e.g., pages tree root)
qpdf --json=latest --json-key=qpdf input-all.pdf | findstr "Pages"
# Output: "/Pages": "16 0 R"

Real Impact

この分析は、弊社の関連記事で説明されているデバッグ手法を検証しました。修正には、以下の実装が含まれていました。 ReorderPageArrByPagesTree 論理的な順序でページを処理し、オブジェクトの順序で処理するのではなく、これにより、示された問題を直接解決します。

結論

PDFページのツリー構造を理解することは、信頼性の高いPDF操作には不可欠ですが、PDFドキュメントの構造を完全に理解するための第一歩に過ぎません。この包括的な分析では、以下の内容を取り上げました。

技術スキルポイント

  1. ドキュメントアーキテクチャ: PDFは複雑なオブジェクトデータベースであり、複雑な参照システムを持っています。
  2. ページツリーナビゲーション: 論理的な順序(Kids配列)と物理的な順序の違いには、注意が必要です。
  3. オブジェクト間の関係: オブジェクト同士がどのように参照し合っているかを理解することで、解析エラーを防ぐことができます。
  4. 継承パターンページプロパティは、ツリー階層における親ノードから継承されます。
  5. エラー回復強力な解析機能により、形式エラーを含むドキュメントにも適切に対応できます。

取り上げた高度な概念

  1. 嵌套構造実際の PDF ファイルは通常、複数階層のページツリーを備えています。
  2. オブジェクト类型PDF ファイルにはページ以外にも、フォント、画像、フォーム、メタデータが含まれます。
  3. 性能最適化。大きなドキュメントでは、遅延読み込みとメモリ管理が必要です。
  4. 検証戦略徹底したチェックにより、微妙なエラーを防止できます。
  5. ツール集成専門的なツールにより、デバッグと分析能力を高められます。

開発のベストプラクティス

  1. 仕様に従うISO 32000 は権威ある PDF 構造を定義しています。
  2. 厳密なプログラミングを実装する。ドキュメントの構造に関する仮定は常に検証する。
  3. 適切なツールを利用する。デバッグには、既存のPDF解析ツールを活用する。
  4. 包括的なテストを実施する。さまざまなPDF作成ツールは、異なる構造を作成する。
  5. 賢明にキャッシュを利用する。メモリ利用量とパフォーマンスのニーズのバランスを取る。

実際の応用

本ガイドの概念は、以下のものに適用されます:

  • PDFビューア: 正しいページ順序とレンダリング
  • ドキュメントプロセッサ: ページ抽出、結合、および操作
  • アクセシビリティツール: スクリーンリーダーのための構造理解
  • アーカイブシステム: 長期的な文書保存
  • セキュリティ分析: 法医学分析のための構造理解

主要なポイント

PDFのページ順序は、些細な技術的な詳細のように見えるかもしれませんが、間違えると、追跡が困難な微妙なバグを引き起こす可能性があります。基本的な原則は単純です。 常に、ファイル内のオブジェクトの物理的な配置ではなく、PDF仕様で定義された論理的な構造を尊重してください。.

これらの概念を理解し、正しく実装することで、現実世界のドキュメントの複雑さを処理できるPDF処理アプリケーションを構築できます。シンプルなページ抽出ツールを構築する場合でも、高度なドキュメント管理システムを構築する場合でも、この基盤が役立ちます。

覚えておいてください。PDFは、特定のルールを持つ構造化されたドキュメントです。コードでこれらのルールを尊重することで、より優れた互換性、ユーザーからの苦情の減少、およびより堅牢なアプリケーションを実現できます。PDFの構造を理解するための投資は、デバッグ時間の短縮とユーザー満足度の向上につながります。