الصفحة 1 من PDF ليست الكائن 1. هذا التمييز هو المصدر الأكثر شيوعًا لخلل استخراج الصفحات الخاطئة في محللات PDF، والإصلاح يبدأ بقراءة المواصفة لا بايتات الملف.
الكائنات والمراجع والفهرس
ملف PDF هو مجموعة من الكائنات المرقمة. يحمل كل كائن رقمًا فريدًا ورقم جيل، ويكتب على الصورة N G obj حيث يكون G عادةً صفرًا في الملفات التي لم تُحدَّث تحديثًا تراكميًا. تشير الكائنات إلى بعضها بالصيغة N G R، لذا فإن 3 0 R تعني "الإصدار الحالي من الكائن 3". يشير التذييل إلى كائن فهرس جذري يقود إدخال /Pages فيه إلى شجرة الصفحات. كل شيء قابل للتنقل داخل PDF يبدأ من ذلك الجذر، لا من أول بايت في جسم الملف.
يقوم جدول الإسناد المرجعي، أو تيار الإسناد المرجعي في PDF 1.5 وما بعده، بربط أرقام الكائنات بإزاحات الملف. مهمته هي الوصول العشوائي، لا الترتيب. يمكن للكاتب الذي يبني مستندًا بشكل تراكمي أن يلحق كائنات جديدة في النهاية بأرقام أعلى بينما تسبق هذه الكائنات منطقيًا كائنات موجودة في تسلسل الصفحات. هذا ليس خللًا، بل تصميم مقصود.
شجرة الصفحات (ISO 32000-1 §7.7.3)
تعيش سلسلة الصفحات في شجرة الصفحات. يحتوي الفهرس الجذري على مرجع /Pages يشير إلى عقدة من النوع /Pages. وتحتوي تلك العقدة على مصفوفة /Kids تسرد أبناءها بترتيب القراءة. كل ابن إما عقدة ورقية من النوع /Page أو عقدة /Pages وسيطة أخرى تحتوي على مصفوفة /Kids خاصة بها. الصفحة 1 هي أول ورقة يتم الوصول إليها عبر اجتياز عمق-أول من اليسار إلى اليمين لمصفوفات Kids. ويخزن الإدخال /Count في كل عقدة وسيطة العدد الكلي لصفحات الأوراق التابعة لها، بحيث يمكن للعارض القفز إلى الصفحة 500 من دون اجتياز الشجرة كلها.
هذا ما يبدو عليه هيكل ثلاث صفحات بسيط في صياغة PDF الخام:
16 0 obj
<<
/Type /Pages
/Count 3
/Kids [20 0 R 1 0 R 4 0 R]
/MediaBox [0 0 612 792]
>>
endobj
20 0 obj
<< /Type /Page /Parent 16 0 R /Contents 21 0 R /Resources 22 0 R >>
endobj
1 0 obj
<< /Type /Page /Parent 16 0 R /Contents 2 0 R /Resources 3 0 R >>
endobj
4 0 obj
<< /Type /Page /Parent 16 0 R /Contents 5 0 R /Resources 6 0 R >>
endobj
تقرأ مصفوفة Kids على الشكل [20 0 R, 1 0 R, 4 0 R]. إذن الصفحة المنطقية 1 هي الكائن 20، والصفحة المنطقية 2 هي الكائن 1، والصفحة المنطقية 3 هي الكائن 4. أي شفرة تتكرر على أرقام الكائنات من 1 صعودًا ستجدها بالترتيب 1، ثم 4، ثم 20، وتنتج التسلسل الصفحة 2، ثم الصفحة 3، ثم الصفحة 1. سيعرض المستند الناتج بترتيب مختلط قد يبدو طبيعيًا تمامًا في عارض يتبع الشجرة، ومشوهًا بشكل كارثي في عارض لا يفعل ذلك.
الوراثة
يمكن للعقد الوسيطة أن تحمل خصائص يرثها أبناؤها. أكثر الإدخالات الموروثة شيوعًا هي /MediaBox (أبعاد الصفحة) و /CropBox و /Resources (الخطوط والصور) و /Rotate. الصفحة الورقية التي تُغفل /MediaBox ليست مكسورة؛ بل تأخذ القيمة من أقرب عقدة أصلية تعرفها. أما الصفحة التي تعرف /MediaBox بنفسها فتتجاوز ما يقوله الأصل، وذلك لتلك الصفحة فقط.
هذا مهم عند التحليل. قراءة كائن /Page بمعزل واعتبار خصائصه كاملة ستعطيك أبعادًا خاطئة في أي صفحة تعتمد على الوراثة. القارئ الصحيح يجب أن يتتبع سلسلة /Parent، ويجمع الخصائص التي لم يرها بعد، ويتوقف عند الجذر.
الأشجار المتداخلة
لا شيء في المواصفة يحد الشجرة بمستوى واحد. قد يجمع مستند كبير الصفحات تحت عقد وسيطة تقابل الفصول بشكل تقريبي:
2 0 obj % root Pages node, Count = 8
<< /Type /Pages /Count 8 /Kids [3 0 R 4 0 R] >>
endobj
3 0 obj % first chapter, 5 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 % second chapter, 3 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
الخوارزمية نفسها لا تتغير: زرع Kids بالترتيب، وارجع إلى أي عقدة /Pages، واجمع العقد الورقية /Page. تتيح قيم /Count للعارض تجاوز شجرة فرعية كاملة عندما يقفز إلى صفحة تقع بعدها، ولهذا يجب أن تكون هذه العدادات دقيقة. بعض محررات PDF من أواخر التسعينيات وبدايات الألفية لم تكن تعيد حسابها بعد التعديلات الموضعية، لذلك يتحقق المحلل المتحفظ من /Count مقابل العدد الفعلي للأوراق بدل أن يثق به للحجز المسبق للمصفوفة.
أين يظهر هذا عمليًا
تظهر مشكلة ترتيب الصفحات غالبًا في سيناريوهين. الأول هو محلل مخصص يمسح الكائنات من النوع /Page بدلًا من اتباع الشجرة. سيعثر على كل صفحة، لكن بترتيب أرقام الكائنات لا بترتيب القراءة. الإصلاح دائمًا هو نفسه: ابدأ من التذييل، وحل الفهرس الجذري، واتبع /Pages، واجتز مصفوفات Kids.
السيناريو الثاني هو ملف تحديث تراكمي. عندما يلحق محرر PDF تغييرات من دون إعادة كتابة الملف كله، تحصل كائنات الصفحات الجديدة على أرقام عالية بينما تظل مصفوفة Kids في الشجرة الأصلية هي التي تتحكم في موضعها المنطقي. قد يُستبدل الكائن 5 الأصلي بكائن جديد 143، لكن مصفوفة Kids تشير الآن إلى 143 حيث كانت تشير إلى 5، فتظل الترتيب المنطقي محفوظًا. الاجتياز حسب رقم الكائن سيضع صفحة الاستبدال في الموضع الخاطئ من التسلسل.
تضيف ملفات PDF الخطية (المحسّنة للويب) تنويعًا ثالثًا: يُعاد ترتيب الملف ماديًا بحيث يظهر محتوى الصفحة الأولى قرب بداية الملف لعرض أسرع عبر اتصال بطيء. تظل بنية شجرة الصفحات هي المرجع النهائي للترتيب، لكن جدول الإسناد المرجعي يطابق الإزاحات المعاد ترتيبها. أي محلل يعتمد على موضع الملف بدلًا من جدول xref سيقرأ حتى الصفحة الأولى من ملف خطي قراءة خاطئة.
يتولى مكوّن HotPDF اجتياز شجرة الصفحات، وحل الوراثة، ودمج xref للتحديثات التراكمية داخليًا. العمل مع كائنات الصفحة فيه يعني أن ترتيب Kids مطبق مسبقًا؛ ففهارس الصفحات تقابل الصفحات المنطقية، لا أرقام الكائنات.