مقالة تقنية

فهم أشجار صفحات PDF: لماذا يهم ترتيب الصفحات

· برمجة 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 الحديثة) له غرض مهم:

  • الجيل 0: الكائن الأصلي.
  • الجيل الأول+.: الإصدارات المحدثة (تستخدم في التحديثات التدريجية).
  • الجيل 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. الرأس (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)، فسيحصل على تسلسل الصفحات غير الصحيح (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 درجة).

مفاهيم خاطئة شائعة.

الخطأ الأول: افتراض أن أرقام الكائنات المتسلسلة تساوي ترتيب الصفحات.

يفترض العديد من المطورين أنه إذا كانت الصفحات في ملف PDF مخزنة ككائنات بأرقام 1 و 2 و 3، فإن الكائن رقم 1 هو الصفحة رقم 1. هذا افتراض خاطئ بشكل أساسي ويؤدي إلى أخطاء خفية.

سبب فشل هذا الافتراض:

  • يتم تعيين أرقام الكائنات أثناء إنشاء ملف PDF، وليس بناءً على ترتيب الصفحات.
  • قد تقوم برامج تحرير PDF بإعادة ترقيم الكائنات أثناء التحسين.
  • تؤدي التحديثات التدريجية إلى إضافة كائنات جديدة بأرقام أعلى.
  • يمكن لتدفقات الكائنات تغيير مخططات الترقيم.

الواقع.الأرقام التسلسلية هي مجرد معرفات. يتم تحديد ترتيب الصفحات الفعلي بواسطة مصفوفة "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: معالجة الصفحات بترتيب الملف الفعلي.

قراءة الكائنات بالتسلسل من ملف PDF لا يمنحك الصفحات بالترتيب الصحيح.

مثال على المشكلة::

  • يحتوي الملف على كائنات بترتيب فعلي: 1، 4، 16، 20.
  • مصفوفة "Kids" في شجرة الصفحات: [20 0 R, 1 0 R, 4 0 R].
  • الترتيب المنطقي الصحيح للصفحات: الكائن 20 (الصفحة 1)، الكائن 1 (الصفحة 2)، الكائن 4 (الصفحة 3).
  • ترتيب غير صحيح للملف الفعلي: الكائن 1 (الصفحة 2)، الكائن 4 (الصفحة 3)، الكائن 16 (ليس صفحة)، الكائن 20 (الصفحة 1).

سبب حدوث ذلك:

  • برامج إنشاء ملفات PDF تركز على حجم الملف، وليس ترتيب الصفحات.
  • يمكن لتدفقات الكائنات إعادة تنظيم المحتوى.
  • يؤدي التبسيط إلى تغيير ترتيب الكائنات للعرض عبر الويب.
  • قد تؤدي أدوات التحرير المتعددة إلى تراكم التغييرات.

الخطأ رقم 3: تجاهل فهرس المستند.

تحاول بعض أكواد التحليل العثور على الصفحات مباشرةً دون اتباع السلسلة الصحيحة: الجذر → الصفحات → العناصر الفرعية.

الطريقة غير الصحيحة:

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])

الخصائص الفعالة:

  • الصفحة 1.: MediaBox=[0,0,612,792] (موروثة)، Rotate=90 (موروثة)، Resources=10 0 R (موروثة)، Contents=20 0 R
  • الصفحة 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 (المساحة المرئية).
  • الموارد (الخطوط، الصور).
  • الدوران.

نصائح عملية للتنفيذ.

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 (إصدار تجاري).

  • عرض مرئي لهيكل المستند.
  • تحرير خصائص الكائنات في الوقت الفعلي.
  • التحقق من المراجع المتقاطعة.
  • فك ترميز وعرض البث.

مصحح أخطاء ملفات 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 الحقيقية.

للتحقق من المفاهيم الواردة في هذا المقال، قمنا بتحليل فعلي باستخدام 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 (أقل رقم).
  • الصفحة المنطقية 3 → الكائن 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"

التأثير الفعلي

هذا التحليل أثبت صحة طريقة التصحيح الموصوفة في مقالتنا المرافقة. الحل تضمن تنفيذ ReorderPageArrByPagesTree لمعالجة الصفحات بترتيب منطقي بدلاً من ترتيب الكائنات، مما يعالج المشكلة الموضحة بشكل مباشر.

الخلاصة.

فهم أشجار صفحات PDF أمر بالغ الأهمية لمعالجة PDF بشكل موثوق، ولكنه مجرد بداية إتقان هيكل مستندات PDF. لقد غطى هذا التحليل الشامل:

نقاط الإتقان الفني.

  1. هيكل المستند.: ملفات PDF هي قواعد بيانات كائنات معقدة مع أنظمة مرجعية معقدة.
  2. التنقل في شجرة الصفحات.: الترتيب المنطقي (مصفوفات الأطفال) مقابل الترتيب الفعلي يتطلب معالجة دقيقة.
  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 يؤتي ثماره في تقليل وقت تصحيح الأخطاء وتحسين رضا المستخدم.