Техническая статья

Понимание деревьев страниц PDF: почему важен порядок страниц

PDF-документы могут казаться простыми на первый взгляд, но их внутренняя структура может быть удивительно сложной. Одна из областей, которая часто вызывает затруднения у разработчиков, - это понимание того, как на самом деле работает порядок страниц в PDF. При исправлении и улучшении примера программы для работы с PDF-страницами нашего продукта HotPDF Delphi PDF Component, мы столкнулись с такими сложными проблемами. Это подробное руководство объяснит ключевые концепции, которые должен знать каждый разработчик PDF, от базовой структуры объектов до продвинутых методов навигации по дереву.

Архитектура 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. The R суффикс в ссылках, например 3 0 R означает "ссылка на объект 3, поколение 0".

Понимание номеров поколений.

Номер генерации (обычно 0 в современных PDF-файлах) выполняет важную функцию:

  • 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-файл состоит из четырех основных частей:

  1. Заголовок: Информация о версии (%PDF-1.7)
  2. Основной текст: Определения объектов и данные
  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), он получит неправильную последовательность страниц (2, 3, 1) вместо правильного логического порядка (1, 2, 3).

Сложный пример: Вложенное дерево страниц

Большие документы часто используют вложенные деревья страниц для лучшей организации:

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 могут перенумеровать объекты во время оптимизации.
  • Инкрементные обновления добавляют новые объекты с более высокими номерами.
  • Потоки объектов могут изменять схемы нумерации.

Реальность.Номера объектов - это просто идентификаторы. Фактический порядок страниц определяется массивом Kids в дереве Pages.

Пример из реальной жизни:

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: Обработка страниц в физическом порядке.

Последовательное чтение объектов из PDF-файла не дает страницы в правильном порядке.

Пример проблемы.:

  • Файл содержит объекты в физическом порядке: 1, 4, 16, 20.
  • Массив Kids в дереве Pages: [20 0 R, 1 0 R, 4 0 R].
  • Правильный логический порядок страниц: Объект 20 (страница 1), Объект 1 (страница 2), Объект 4 (страница 3).
  • Неправильный физический порядок файлов: Объект 1 (страница 2), Объект 4 (страница 3), Объект 16 (не является страницей), Объект 20 (страница 1).

Почему это происходит:

  • Генераторы PDF оптимизируют размер файла, а не порядок страниц.
  • Потоки объектов могут переупорядочивать содержимое.
  • Линеаризация изменяет порядок объектов для просмотра в веб-браузере.
  • Несколько инструментов редактирования могут накладывать изменения.

Ошибка №3: Игнорирование каталога документа.

Некоторые участки кода пытаются найти страницы напрямую, не следуя правильной цепочке: Root → Pages → Kids.

Проблемный подход:

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 являются точными.

Иногда... /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: Обработать массив "kids" в порядке.

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 Debugger (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-файлов.

Для проверки концепций, представленных в этой статье, мы провели фактический анализ с использованием qpdf на проблемном PDF-файле. Результаты четко продемонстрировали проблему с порядком страниц:

Анализ фактического вывода 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 (middle number)

Этот реальный пример доказывает, почему разбор объектов по порядку не работает: обработка объектов в числовом порядке (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"

Реальное влияние.

Этот анализ подтвердил подход отладки, описанный в нашей сопутствующей статье. Исправление заключалось в реализации 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 окупаются сокращением времени отладки и повышением удовлетворенности пользователей.