技术文章

了解 PDF 页面顺序 – 为什么您的 PDF 页面不存在

· PDF 编程

PDF 结构背后的隐藏复杂性

PDF 文档比用户看到的要复杂得多。虽然查看器以逻辑、顺序的排列方式显示页面(1、2、3…),但 PDF 文件的内部架构却讲述着完全不同的故事。这种复杂性是 PDF 处理中最常被误解的方面之一,导致了无数的错误、不正确的实现以及沮丧的开发人员。本文深入探讨了 PDF 页面组织错综复杂的世界,解释了为什么开发人员经常遇到意外的页面排序问题,并提供了用于可靠 PDF 处理的实用解决方案。

PDF 对象模型:从顺序文档到一种新的范式

为了理解 PDF 页面排序的挑战,我们首先必须认识到 PDF 与更简单的文档格式有何根本不同。与纯文本文件、HTML 文档,甚至像 RTF 这样较旧的格式不同,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
% PDF file structure example - storage order vs. display order
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
 
2 0 obj
<< /Type /Pages /Kids [20 0 R 1 0 R 4 0 R] /Count 3 >>
endobj
 
% Object 4 appears third in file but represents page 3 in display
4 0 obj
<< /Type /Page
   /Contents 5 0 R
   /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 6 0 R >> >> >>
endobj
 
% Object 20 appears last in file but represents page 1 in display
20 0 obj
<< /Type /Page
   /Contents 21 0 R
   /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 6 0 R >> >> >>
endobj

在这个例子中,页面对象存储为对象 4 和 20,但显示顺序由 Kids 数组定义:[20, 1, 4]。这创建了以下映射:

  • 页面 1 (显示顺序) = 对象 20 (存储顺序:最后)。
  • 页面 2 (显示顺序) = 对象 1 (存储顺序:第一)。
  • 第 3 页 (显示顺序) = 第 4 个对象 (存储顺序:第三个)。

这种不一致并非偶然,而是 PDF 的一个基本特性,它支持高级文档处理和优化。

PDF 生成器创建非顺序对象顺序的原因。

了解 PDF 生成器为什么创建非顺序对象顺序,可以帮助开发人员认识到他们所面临的复杂性,并避免对文档结构做出错误的假设。

PDF 创建工作流程。

不同的 PDF 创建工作流程会导致不同的对象排序模式:

1. 顺序文档创建。

1
2
3
4
5
6
% Typical output from simple PDF generators
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >> endobj
3 0 obj << /Type /Page /Contents 6 0 R /Parent 2 0 R >> endobj
4 0 obj << /Type /Page /Contents 7 0 R /Parent 2 0 R >> endobj
5 0 obj << /Type /Page /Contents 8 0 R /Parent 2 0 R >> endobj

2. 优化资源共享。

1
2
3
4
5
6
7
8
9
% PDF with shared resources created first
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [10 0 R 11 0 R 12 0 R] /Count 3 >> endobj
3 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
4 0 obj << /Type /XObject /Subtype /Image /Width 100 /Height 100 >> endobj
% ... more shared resources ...
10 0 obj << /Type /Page /Resources << /Font << /F1 3 0 R >> >> >> endobj
11 0 obj << /Type /Page /Resources << /XObject << /Im1 4 0 R >> >> >> endobj
12 0 obj << /Type /Page /Resources << /Font << /F1 3 0 R >> >> >> endobj

3. 增量文档组装

1
2
3
4
5
6
7
8
9
% Document created by combining existing PDFs
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [100 0 R 25 0 R 75 0 R] /Count 3 >> endobj
% Objects from first source document
25 0 obj << /Type /Page /Contents 26 0 R /Parent 2 0 R >> endobj
% Objects from second source document  
75 0 obj << /Type /Page /Contents 76 0 R /Parent 2 0 R >> endobj
% Objects from third source document
100 0 obj << /Type /Page /Contents 101 0 R /Parent 2 0 R >> endobj

常见的开发错误及其后果

PDF 结构的复杂性会导致一些常见的错误,这些错误可能对应用程序的可靠性和用户体验产生严重后果。

错误 1:假设对象 ID 顺序等于显示顺序

这可能是 PDF 处理新手开发人员最常犯的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// WRONG: Processing pages by object ID order
function GetPagesInWrongOrder(Doc: TPDFDocument): TPageList;
var
  i: Integer;
  Obj: TPDFObject;
begin
  Result := TPageList.Create;
  
  // This approach processes pages in storage order, not display order
  for i := 0 to Doc.Objects.Count - 1 do
  begin
    Obj := Doc.Objects[i];
    if (Obj <> nil) and (Obj.GetValue('/Type') = '/Page') then
    begin
      Result.Add(Obj);  // Wrong order!
    end;
  end;
  
  // Result will be in object ID order: [1, 4, 20]
  // But display order should be: [20, 1, 4]
end;

此错误的后果包括:

  • 输出文档中的页面顺序不正确
  • 页面编号不一致
  • 用户困惑和支持请求。
  • 文档处理流程中潜在的数据损坏。

错误 2:基于观察结果的硬编码页面映射。

当开发人员遇到页面排序问题时,他们有时会根据观察到的模式实施硬编码的修复。

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
// WRONG: Hard-coded page reordering based on heuristics
function ApplyPageReorderingHeuristics(Pages: TPageArray): TPageArray;
var
  i: Integer;
begin
  SetLength(Result, Length(Pages));
  
  // Dangerous heuristic based on limited observations
  if Length(Pages) = 3 then
  begin
    // "Fix" for specific 3-page documents observed during testing
    Result[0] := Pages[1]; // Put second page first
    Result[1] := Pages[2]; // Put third page second
    Result[2] := Pages[0]; // Put first page last
  end
  else if Length(Pages) > 3 then
  begin
    // Generic "fix" that swaps first and last pages
    Result[0] := Pages[Length(Pages) - 1];
    Result[Length(Pages) - 1] := Pages[0];
    
    // Keep middle pages in original order
    for i := 1 to Length(Pages) - 2 do
      Result[i] := Pages[i];
  end
  else
  begin
    // For other cases, just copy as-is
    for i := 0 to High(Pages) do
      Result[i] := Pages[i];
  end;
end;

这种方法从根本上存在缺陷,因为:

  • 它仅适用于开发过程中观察到的特定 PDF 文件。
  • 对于具有不同结构的 PDF 文件,它会彻底失败。
  • 它会产生用户无法理解的不可预测行为。
  • 随着添加的特殊情况增多,它会积累技术债务。

错误 3:忽略分层页面树。

许多开发人员认为 PDF 页面树始终是扁平数组,但 PDF 规范允许使用分层结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// WRONG: Assuming flat page tree structure
function GetPagesFromFlatTree(PagesObj: TPDFObject): TPageArray;
var
  KidsArray: TPDFArray;
  i: Integer;
begin
  KidsArray := PagesObj.GetArray('/Kids');
  if KidsArray = nil then Exit;
  
  SetLength(Result, KidsArray.Count);
  for i := 0 to KidsArray.Count - 1 do
  begin
    // This assumes all Kids entries are Page objects
    // But they might be intermediate Pages objects!
    Result[i] := KidsArray.GetIndirectObject(i);
  end;
end;

正确的方法:遵循页面树结构。

处理 PDF 页面顺序的正确方法是实现完整的页面树遍历,该遍历完全遵循 PDF 规范。

理解页面树层级结构。

PDF 页面树可以是分层的,其中包含中间 Pages 对象,这些对象包含自己的 Kids 数组:

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
% Hierarchical page tree example
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
 
% Root Pages object
2 0 obj
<< /Type /Pages
   /Kids [3 0 R 8 0 R 15 0 R]
   /Count 7 >>
endobj
 
% First intermediate Pages object (contains 3 pages)
3 0 obj
<< /Type /Pages
   /Kids [4 0 R 5 0 R 6 0 R]
   /Count 3
   /Parent 2 0 R >>
endobj
 
% Second intermediate Pages object (contains 2 pages)
8 0 obj
<< /Type /Pages
   /Kids [9 0 R 10 0 R]
   /Count 2
   /Parent 2 0 R >>
endobj
 
% Third intermediate Pages object (contains 2 pages)
15 0 obj
<< /Type /Pages
   /Kids [16 0 R 17 0 R]
   /Count 2
   /Parent 2 0 R >>
endobj
 
% Actual page objects
4 0 obj << /Type /Page /Contents 40 0 R /Parent 3 0 R >> endobj
5 0 obj << /Type /Page /Contents 41 0 R /Parent 3 0 R >> endobj
% ... and so on

实现递归页面树遍历。

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
// CORRECT: Recursive page tree traversal
function GetPagesInCorrectOrder(Doc: TPDFDocument): TPageArray;
var
  CatalogObj, RootPagesObj: TPDFObject;
  PageList: TList;
begin
  PageList := TList.Create;
  try
    // Step 1: Find the document catalog
    CatalogObj := Doc.FindObject('/Type', '/Catalog');
    if CatalogObj = nil then
      raise Exception.Create('Document catalog not found');
    
    // Step 2: Get the root Pages object
    RootPagesObj := CatalogObj.GetIndirectObject('/Pages');
    if RootPagesObj = nil then
      raise Exception.Create('Root Pages object not found');
    
    // Step 3: Recursively traverse the page tree
    TraversePagesTree(RootPagesObj, PageList);
    
    // Step 4: Convert list to array
    SetLength(Result, PageList.Count);
    for i := 0 to PageList.Count - 1 do
      Result[i] := TPDFObject(PageList[i]);
      
  finally
    PageList.Free;
  end;
end;
 
procedure TraversePagesTree(PagesObj: TPDFObject; PageList: TList);
var
  KidsArray: TPDFArray;
  i: Integer;
  ChildObj: TPDFObject;
  ChildType: string;
begin
  if PagesObj = nil then Exit;
  
  // Get the Kids array from this Pages object
  KidsArray := PagesObj.GetArray('/Kids');
  if KidsArray = nil then Exit;
  
  // Process each child in the Kids array
  for i := 0 to KidsArray.Count - 1 do
  begin
    ChildObj := KidsArray.GetIndirectObject(i);
    if ChildObj = nil then Continue;
    
    ChildType := ChildObj.GetValue('/Type');
    
    if ChildType = '/Page' then
    begin
      // This is a leaf page object - add it to our list
      PageList.Add(ChildObj);
    end
    else if ChildType = '/Pages' then
    begin
      // This is an intermediate Pages object - recurse into it
      TraversePagesTree(ChildObj, PageList);
    end
    else
    begin
      // Unexpected object type in Kids array
      raise Exception.CreateFmt('Unexpected object type in Kids array: %s', [ChildType]);
    end;
  end;
end;

处理实际 PDF 文件的各种变体和边缘情况。

实际 PDF 文件通常会偏离规范中描述的理想结构。一个健壮的 PDF 处理库必须能够优雅地处理这些变体。

常见的结构异常。

1. 缺失或损坏的目录。

1
2
3
4
5
6
% PDF with missing catalog reference
%PDF-1.4
% Object 1 should be catalog but is missing or corrupted
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj

2. 循环引用。

1
2
3
4
5
6
7
8
% PDF with circular page tree references (corrupted)
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 /Parent 3 0 R >>
endobj
 
3 0 obj
<< /Type /Pages /Kids [2 0 R] /Count 1 /Parent 2 0 R >>
endobj

3. 不一致的计数值。

1
2
3
4
5
% PDF with incorrect Count value
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 5 >>
% Count says 5 but Kids array has only 3 elements
endobj

实现强大的错误处理。

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Robust page tree traversal with comprehensive error handling
function GetPagesWithFallbacks(Doc: TPDFDocument): TPageArray;
var
  AttemptCount: Integer;
  ErrorMessages: TStringList;
begin
  ErrorMessages := TStringList.Create;
  try
    AttemptCount := 0;
    
    // Attempt 1: Standard PDF specification approach
    Inc(AttemptCount);
    try
      Result := GetPagesViaStandardTraversal(Doc);
      if Length(Result) > 0 then
      begin
        LogMessage(Format('Success with standard traversal (attempt %d)', [AttemptCount]));
        Exit;
      end;
    except
      on E: Exception do
        ErrorMessages.Add(Format('Attempt %d failed: %s', [AttemptCount, E.Message]));
    end;
    
    // Attempt 2: Search for Pages objects and try each one
    Inc(AttemptCount);
    try
      Result := GetPagesViaObjectSearch(Doc);
      if Length(Result) > 0 then
      begin
        LogMessage(Format('Success with object search (attempt %d)', [AttemptCount]));
        Exit;
      end;
    except
      on E: Exception do
        ErrorMessages.Add(Format('Attempt %d failed: %s', [AttemptCount, E.Message]));
    end;
    
    // Attempt 3: Brute force search for Page objects
    Inc(AttemptCount);
    try
      Result := GetPagesViaBruteForce(Doc);
      if Length(Result) > 0 then
      begin
        LogMessage(Format('Success with brute force search (attempt %d)', [AttemptCount]));
        LogMessage('Warning: Document structure is non-standard');
        Exit;
      end;
    except
      on E: Exception do
        ErrorMessages.Add(Format('Attempt %d failed: %s', [AttemptCount, E.Message]));
    end;
    
    // All attempts failed
    raise Exception.Create('Failed to extract pages from PDF. Errors: ' +
                          ErrorMessages.Text);
                          
  finally
    ErrorMessages.Free;
  end;
end;
 
function GetPagesViaObjectSearch(Doc: TPDFDocument): TPageArray;
var
  i: Integer;
  Obj: TPDFObject;
  KidsArray: TPDFArray;
  PageList: TList;
  CandidateObjects: TList;
begin
  CandidateObjects := TList.Create;
  PageList := TList.Create;
  try
    // Find all objects that could be Pages objects
    for i := 0 to Doc.Objects.Count - 1 do
    begin
      Obj := Doc.Objects[i];
      if (Obj <> nil) and
         (Obj.GetValue('/Type') = '/Pages') and
         Obj.HasKey('/Kids') then
      begin
        CandidateObjects.Add(Obj);
      end;
    end;
    
    // Try each candidate Pages object
    for i := 0 to CandidateObjects.Count - 1 do
    begin
      Obj := TPDFObject(CandidateObjects[i]);
      KidsArray := Obj.GetArray('/Kids');
      
      if (KidsArray <> nil) and (KidsArray.Count > 0) then
      begin
        // Validate that this Kids array contains actual pages
        if ValidateKidsArray(KidsArray) then
        begin
          PageList.Clear;
          TraversePagesTree(Obj, PageList);
          
          if PageList.Count > 0 then
          begin
            // Found valid pages - convert to array and return
            SetLength(Result, PageList.Count);
            for j := 0 to PageList.Count - 1 do
              Result[j] := TPDFObject(PageList[j]);
            Exit;
          end;
        end;
      end;
    end;
    
    // No valid Pages object found
    SetLength(Result, 0);
    
  finally
    CandidateObjects.Free;
    PageList.Free;
  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
47
48
49
// Performance-optimized page access with caching
type
  TPDFPageCache = class
  private
    FPages: array of TPDFPage;
    FPageObjects: array of TPDFObject;
    FCacheHits: Integer;
    FCacheMisses: Integer;
    FMaxCacheSize: Integer;
    
  public
    constructor Create(MaxCacheSize: Integer = 100);
    destructor Destroy; override;
    
    function GetPage(Index: Integer): TPDFPage;
    procedure ClearCache;
    procedure GetCacheStatistics(out Hits, Misses: Integer);
  end;
 
function TPDFPageCache.GetPage(Index: Integer): TPDFPage;
begin
  // Check if page is already cached
  if (Index >= 0) and (Index < Length(FPages)) and
     (FPages[Index] <> nil) then
  begin
    Inc(FCacheHits);
    Result := FPages[Index];
    Exit;
  end;
  
  Inc(FCacheMisses);
  
  // Load page from object if not cached
  if (Index >= 0) and (Index < Length(FPageObjects)) and
     (FPageObjects[Index] <> nil) then
  begin
    Result := TPDFPage.CreateFromObject(FPageObjects[Index]);
    
    // Cache the page if we have room
    if Length(FPages) < FMaxCacheSize then begin if Index >= Length(FPages) then
        SetLength(FPages, Index + 1);
      FPages[Index] := Result;
    end;
  end
  else
  begin
    Result := nil;
  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
// Streaming approach for processing large PDF documents
procedure ProcessLargePDFInChunks(const FileName: string; ChunkSize: Integer = 50);
var
  Doc: TPDFDocument;
  TotalPages: Integer;
  ChunkStart, ChunkEnd: Integer;
  i: Integer;
begin
  Doc := TPDFDocument.Create;
  try
    Doc.LoadFromFile(FileName);
    TotalPages := Doc.GetPageCount;
    
    LogMessage(Format('Processing %d pages in chunks of %d', [TotalPages, ChunkSize]));
    
    ChunkStart := 0;
    while ChunkStart < TotalPages do
    begin
      ChunkEnd := Min(ChunkStart + ChunkSize - 1, TotalPages - 1);
      
      LogMessage(Format('Processing chunk: pages %d-%d', [ChunkStart + 1, ChunkEnd + 1]));
      
      // Process this chunk of pages
      for i := ChunkStart to ChunkEnd do
      begin
        ProcessSinglePage(Doc, i);
      end;
      
      // Optional: Force garbage collection between chunks
      if (ChunkStart mod (ChunkSize * 4)) = 0 then
      begin
        ForceGarbageCollection;
      end;
      
      ChunkStart := ChunkEnd + 1;
    end;
    
  finally
    Doc.Free;
  end;
end;

高级PDF结构分析.

对于需要处理复杂PDF要求的开发人员,理解高级结构元素至关重要.

页面继承和资源管理.

PDF页面可以从其父级Pages对象继承属性,从而创建一个分层资源管理系统.

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
% Example of page inheritance in PDF structure
2 0 obj
<< /Type /Pages
   /Kids [3 0 R 4 0 R]
   /Count 2
   /MediaBox [0 0 612 792]
   /Resources <<
     /Font << /F1 10 0 R >>
     /ProcSet [/PDF /Text]
   >> >>
endobj
 
% Child page inherits MediaBox and Resources from parent
3 0 obj
<< /Type /Page
   /Parent 2 0 R
   /Contents 5 0 R >>
% This page inherits MediaBox [0 0 612 792] and Resources from parent
endobj
 
% Child page overrides inherited MediaBox
4 0 obj
<< /Type /Page
   /Parent 2 0 R
   /Contents 6 0 R
   /MediaBox [0 0 792 612] >>
% This page overrides MediaBox but still inherits Resources
endobj

在代码中处理页面继承.

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
// Proper handling of page inheritance
function GetEffectivePageProperties(PageObj: TPDFObject): TPDFPageProperties;
var
  CurrentObj: TPDFObject;
  MediaBox: TPDFArray;
  Resources: TPDFObject;
begin
  // Initialize result
  Result := TPDFPageProperties.Create;
  
  // Walk up the parent chain to collect inherited properties
  CurrentObj := PageObj;
  while CurrentObj <> nil do
  begin
    // Check for MediaBox at this level
    if Result.MediaBox.IsEmpty then
    begin
      MediaBox := CurrentObj.GetArray('/MediaBox');
      if MediaBox <> nil then
        Result.MediaBox := MediaBox;
    end;
    
    // Check for Resources at this level
    if Result.Resources = nil then
    begin
      Resources := CurrentObj.GetDictionary('/Resources');
      if Resources <> nil then
        Result.Resources := Resources;
    end;
    
    // Check for other inheritable properties
    CheckForInheritableProperty(CurrentObj, '/Rotate', Result.Rotate);
    CheckForInheritableProperty(CurrentObj, '/CropBox', Result.CropBox);
    
    // Move to parent object
    CurrentObj := CurrentObj.GetIndirectObject('/Parent');
    
    // Prevent infinite loops in corrupted PDFs
    if CurrentObj = PageObj then
      break;
  end;
  
  // Validate that we found required properties
  if Result.MediaBox.IsEmpty then
    raise Exception.Create('No MediaBox found in page inheritance chain');
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
# Comprehensive PDF test case generation script
 
# Test Case 1: Sequential pages (baseline)
echo "Creating sequential page test..."
pdftk A=template.pdf cat A A A output test-sequential.pdf
 
# Test Case 2: Non-sequential object IDs
echo "Creating non-sequential object ID test..."
pdftk A=page3.pdf B=page1.pdf C=page2.pdf cat A B C output test-nonsequential.pdf
 
# Test Case 3: Hierarchical page tree
echo "Creating hierarchical page tree test..."
# This requires custom PDF generation tool
generate-hierarchical-pdf --depth 3 --pages-per-node 2 output test-hierarchical.pdf
 
# Test Case 4: Large document with mixed structures
echo "Creating large document test..."
pdftk A=large-doc.pdf cat 1-100 50-149 200-299 output test-large-mixed.pdf
 
# Test Case 5: Corrupted page tree
echo "Creating corrupted page tree test..."
# This requires custom corruption tool
corrupt-pdf-structure --target pages-tree test-sequential.pdf test-corrupted.pdf
 
# Test Case 6: Minimal single-page document
echo "Creating minimal single-page test..."
pdftk A=template.pdf cat 1 output test-single-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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Comprehensive PDF page ordering validation framework
type
  TPDFTestCase = record
    FileName: string;
    ExpectedPageCount: Integer;
    ExpectedPageOrder: array of Integer;
    Description: string;
  end;
 
function RunPDFPageOrderingTests: Boolean;
var
  TestCases: array of TPDFTestCase;
  i: Integer;
  PassCount, FailCount: Integer;
begin
  // Define test cases
  SetLength(TestCases, 6);
  
  TestCases[0].FileName := 'test-sequential.pdf';
  TestCases[0].ExpectedPageCount := 3;
  TestCases[0].ExpectedPageOrder := [0, 1, 2];
  TestCases[0].Description := 'Sequential page ordering';
  
  TestCases[1].FileName := 'test-nonsequential.pdf';
  TestCases[1].ExpectedPageCount := 3;
  TestCases[1].ExpectedPageOrder := [2, 0, 1]; // Based on how pdftk reorders
  TestCases[1].Description := 'Non-sequential object IDs';
  
  // ... define other test cases ...
  
  PassCount := 0;
  FailCount := 0;
  
  WriteLn('Running PDF page ordering tests...');
  WriteLn('=' * 50);
  
  for i := 0 to High(TestCases) do
  begin
    Write(Format('Test %d: %s... ', [i + 1, TestCases[i].Description]));
    
    if ValidateTestCase(TestCases[i]) then
    begin
      WriteLn('PASS');
      Inc(PassCount);
    end
    else
    begin
      WriteLn('FAIL');
      Inc(FailCount);
    end;
  end;
  
  WriteLn('=' * 50);
  WriteLn(Format('Results: %d passed, %d failed', [PassCount, FailCount]));
  
  Result := FailCount = 0;
end;
 
function ValidateTestCase(const TestCase: TPDFTestCase): Boolean;
var
  Doc: TPDFDocument;
  ActualPages: TPageArray;
  i: Integer;
begin
  Result := False;
  Doc := TPDFDocument.Create;
  try
    if not Doc.LoadFromFile(TestCase.FileName) then
    begin
      WriteLn(Format('Failed to load %s', [TestCase.FileName]));
      Exit;
    end;
    
    ActualPages := GetPagesInCorrectOrder(Doc);
    
    // Validate page count
    if Length(ActualPages) <> TestCase.ExpectedPageCount then
    begin
      WriteLn(Format('Page count mismatch: expected %d, got %d',
                    [TestCase.ExpectedPageCount, Length(ActualPages)]));
      Exit;
    end;
    
    // Validate page order (simplified - in real implementation,
    // you'd compare actual page content or identifiers)
    for i := 0 to High(ActualPages) do
    begin
      if not ValidatePageAtPosition(ActualPages[i], TestCase.ExpectedPageOrder[i]) then
      begin
        WriteLn(Format('Page order mismatch at position %d', [i]));
        Exit;
      end;
    end;
    
    Result := True;
    
  finally
    Doc.Free;
  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
// Extensible PDF page processing architecture
type
  IPDFPageProcessor = interface
    ['{12345678-1234-1234-1234-123456789012}']
    function ProcessPage(Page: TPDFPage; Context: TPDFProcessingContext): Boolean;
    function GetProcessorName: string;
    function GetSupportedPDFVersions: TStringArray;
  end;
 
  TPDFProcessingPipeline = class
  private
    FProcessors: TList;
    FContext: TPDFProcessingContext;
    
  public
    constructor Create;
    destructor Destroy; override;
    
    procedure RegisterProcessor(Processor: IPDFPageProcessor);
    procedure UnregisterProcessor(Processor: IPDFPageProcessor);
    function ProcessDocument(Doc: TPDFDocument): Boolean;
  end;
 
function TPDFProcessingPipeline.ProcessDocument(Doc: TPDFDocument): Boolean;
var
  Pages: TPageArray;
  i, j: Integer;
  Page: TPDFPage;
  Processor: IPDFPageProcessor;
  Success: Boolean;
begin
  Result := True;
  
  // Get pages in correct order using our robust method
  Pages := GetPagesInCorrectOrder(Doc);
  
  // Process each page through all registered processors
  for i := 0 to High(Pages) do
  begin
    Page := TPDFPage.CreateFromObject(Pages[i]);
    try
      FContext.CurrentPageIndex := i;
      FContext.TotalPages := Length(Pages);
      
      for j := 0 to FProcessors.Count - 1 do
      begin
        Processor := FProcessors[j];
        Success := Processor.ProcessPage(Page, FContext);
        
        if not Success then
        begin
          LogError(Format('Processor %s failed on page %d',
                         [Processor.GetProcessorName, i + 1]));
          Result := False;
          // Continue with other processors/pages or break based on policy
        end;
      end;
      
    finally
      Page.Free;
    end;
  end;
end;

投入精力理解正确的PDF结构,可以减少支持负担,提高用户满意度,并在应用程序的整个生命周期内简化维护。PDF页面排序不仅仅是一个技术细节,而是文档完整性的一个基本方面,它直接影响用户体验。掌握这种复杂性,您将构建用户可以放心地用于存储其重要文档的PDF应用程序。

 下一篇文章。