기술 문서

PDF 페이지 순서 이해 – PDF 페이지가 없는 이유

· PDF 프로그래밍

PDF 구조의 숨겨진 복잡성

PDF 문서는 최종 사용자에게 보이는 것보다 훨씬 더 복잡합니다. 사용자는 페이지를 논리적이고 순차적인 순서(1, 2, 3...)로 보지만, PDF 파일의 내부 구조는 완전히 다른 이야기를 보여줍니다. 이러한 복잡성은 PDF 처리의 가장 오해받는 측면 중 하나이며, 수많은 버그, 잘못된 구현 및 좌절한 개발자를 야기합니다. 이 포괄적인 기사는 PDF 페이지 구성의 복잡한 세계를 탐구하고, 개발자가 종종 예상치 못한 페이지 순서 문제를 겪는 이유를 설명하며, 강력한 PDF 조작을 위한 실용적인 솔루션을 제공합니다.

PDF 객체 모델: 순차적 문서에서 완전히 다른 패러다임

PDF 페이지 순서 문제에 대한 이해를 높이기 위해서는 PDF가 더 간단한 문서 형식과 얼마나 근본적으로 다른지 이해해야 합니다. 일반 텍스트 파일, HTML 문서 또는 RTF와 같은 이전 형식과 달리 PDF는 콘텐츠 구성과 물리적 저장소가 완전히 분리된 정교한 객체 기반 아키텍처를 사용합니다.

이러한 아키텍처 결정은 여러 가지 중요한 이유로 이루어졌습니다.

  • 유연성: 객체는 중복 없이 여러 위치에서 참조될 수 있습니다.
  • 효율성: 공통 리소스(글꼴, 이미지, 그래픽 상태)는 페이지 전체에서 공유될 수 있습니다.
  • 점진적 업데이트: 문서를 전체 파일을 다시 쓰지 않고 수정할 수 있습니다.
  • 임의 접근: 사용자는 전체 문서를 파싱하지 않고도 원하는 페이지로 바로 이동할 수 있습니다.

그러나 이러한 유연성은 복잡성을 수반하며, 특히 객체 저장 순서와 논리적 페이지 순서 간의 관계를 이해하는 데 어려움을 겪을 수 있습니다.

객체 참조 vs. 표시 순서: 구체적인 예시

다음은 저장과 표시 간의 불일치를 보여주는 일반적인 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 페이지 트리는 계층적일 수 있으며, 중간 페이지 객체는 자체 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 애플리케이션을 구축할 수 있습니다.

 다음 글