رقم الكائن 1 ليس الصفحة 1. هذه الحقيقة الواحدة تربك من شيفرات معالجة PDF أكثر من أي جانب آخر في التنسيق، وفهم السبب يتطلب النظر إلى ما وراء ما يراه العارض، وإلى الرسم البياني للكائنات الذي يقرأه العارض فعليًا.
ملف PDF هو مجموعة من الكائنات غير المباشرة المرقمة. الصفحات من ضمن هذه الكائنات، لكن تسلسل عرضها لا علاقة له بموقعها في الملف أو بالأرقام التي تحملها. يُحدَّد ترتيب العرض بالكامل بواسطة شجرة /Pages، وهي بنية مترابطة جذورها في فهرس المستند. إذا تجاهلت الشجرة ومسحت الكائنات بحسب الرقم، فستجمع الصفحات بترتيب خاطئ في نسبة كبيرة من الملفات الواقعية.
شجرة الصفحات: ما الذي يحدد الترتيب فعليًا
يبدأ كل PDF بفهرس مستند (ISO 32000-2 §7.7.2). يحتوي الفهرس على إدخال /Pages يشير إلى العقدة الجذرية لشجرة الصفحات. تلك العقدة الجذرية هي قاموس يحتوي /Type /Pages، ومصفوفة /Kids من المراجع غير المباشرة، و/Count يحدد إجمالي عدد صفحات الأوراق تحته. ترتيب العرض هو اجتياز عمق-أول من اليسار إلى اليمين لهذه الشجرة، بكل وضوح.
يوضح ملف صغير من ثلاث صفحات ذلك عمليًا:
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [20 0 R 4 0 R 9 0 R] /Count 3 >>
endobj
% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
تقرأ مصفوفة /Kids على شكل [20 0 R 4 0 R 9 0 R]، لذا تكون الصفحة 1 هي الكائن 20، والصفحة 2 هي الكائن 4، والصفحة 3 هي الكائن 9. الترقيم غير ذي صلة. أي شفرة تتكرر على الكائنات بترتيبها الرقمي وتجمع الكائنات التي نوعها /Page ستنتج تسلسلًا خاطئًا في هذا الملف.
لماذا ينتج المولدون ترتيبات غير متسلسلة؟ هناك عدة أسباب. المكتبة التي تحجز أرقام كائنات لكل الصفحات قبل كتابة محتواها ستمنحها أرقامًا حسب ترتيب الإنشاء، ثم تكتب البايتات الفعلية بأي ترتيب يناسب المولّد. أداة الدمج التي تجمع المستندات تعيد ترقيم الكائنات من كل مستند مصدر لتجنب التعارضات؛ فتتوزع كائنات الصفحات المعاد ترقيمها عبر جدول الكائنات المركب بينما تحتفظ مصفوفة /Kids الجذرية الجديدة بتسلسل العرض الصحيح. التحديثات التراكمية تلحق كائنات جديدة في نهاية الملف بأرقام جديدة، لذا فإن الصفحة المضافة كمراجعة قد تعيش قرب نهاية تيار البايتات حتى لو كان مكانها المنطقي في موضع الصفحة 1.
الأشجار المسطحة والأشجار الفرعية المتداخلة
يسمح المعيار بشكلين لشجرة الصفحات. المولدات البسيطة تنتج بنية مسطحة: عقدة /Pages جذرية واحدة تحتوي مصفوفة /Kids فيها فقط كائنات أوراق من نوع /Page. هذا سهل الاجتياز: مستوى واحد، وتمرير واحد.
لكن المستندات الكبيرة تستخدم شجرة متوازنة في العادة. مصفوفة /Kids في عقدة /Pages الجذرية تحتوي عقد /Pages وسيطة، وكل واحدة منها بدورها تملك مصفوفة /Kids خاصة بها. قيمة /Count في كل عقدة وسيطة تبلغ عن العدد الكلي لصفحات الأوراق في الشجرة الفرعية الخاصة بها، وبذلك يمكن للعارض تجاوز شجرة فرعية كاملة عند القفز إلى صفحة برقم فهرس من دون تحليل كل كائن. مستند من 1000 صفحة مبني كشجرة متوازنة مع 10 صفحات في كل عقدة ورقية يمكنه الوصول إلى الصفحة 750 عبر بحث ثنائي في ثلاثة أو أربعة عمليات بحث داخل القواميس بدلًا من مسح 750 إدخالًا في /Kids.
العاقبة على شفرة المعالجة واضحة: لا يمكنك افتراض أن المستوى الأول من /Kids يحتوي كائنات /Page. يجب فحص كل ابن. إذا كان /Type هو /Pages فاستدعِ الاجتياز عليه. وإذا كان /Type هو /Page فهو ورقة نهائية. التوقف عند المستوى الأول فقط سيحذف شجرات فرعية كاملة بصمت في أي مستند يقرر فيه المولد أن يتداخل.
سمات الصفحة الموروثة
تحمل شجرة الصفحات أيضًا آلية لمشاركة الموارد. بعض سمات الصفحة: /MediaBox و /CropBox و /Resources و /Rotate قابلة للوراثة (ISO 32000-2 §7.7.3.4). إذا أغفل قاموس /Page إحدى هذه السمات، يصعد القارئ عبر سلسلة /Parent حتى يجد السمة أو يصل إلى الجذر. وضع قاموس خطوط مشترك في عقدة /Pages الجذرية بدلًا من نسخه في كل صفحة نهائية يمكن أن يقلل حجم الملف بشكل ملحوظ في المستندات التي تستخدم الخطوط نفسها طوال الوقت.
تخلق قاعدة الوراثة دقة مهمة بالنسبة إلى الشفرة التي تقرأ خصائص الصفحة. قراءة /MediaBox مباشرة من كائن /Page واعتبار غياب المفتاح خطأً أمر غير صحيح؛ فقد يكون المفتاح موروثًا ببساطة. الشفرة التي تحل هندسة الصفحة بشكل صحيح يجب أن تتبع سلسلة الأبناء. كما تحتاج أيضًا إلى حارس ضد الحلقات: قد يحتوي ملف تالف على مرجع /Parent يعود إلى عقدة سبق زيارتها، وهذا سيؤدي إلى حلقة لا تنتهي من دون فحص للكائنات التي تمت زيارتها.
جدول xref وتيارات الإسناد المرجعي
يتم البحث عن الكائنات غير المباشرة عبر جدول الإسناد المرجعي، أو عبر وريثه جدول الإسناد المرجعي المتدفق الذي ظهر في PDF 1.5. يربط xref كل رقم كائن بإزاحة بايت داخل الملف. القارئ المطابق للمواصفات يستخدم xref للقفز مباشرة إلى أي كائن؛ ولا يمسح الملف تسلسليًا. هذا التصميم الذي يعتمد على الوصول العشوائي هو ما يجعل القفز السريع بين الصفحات ممكنًا: يقرأ العارض الفهرس، ويحل مرجع /Pages عبر xref، ويقرأ عقدة /Pages الجذرية، ويحل إدخال /Kids، وهكذا، من دون لمس إلا الكائنات التي يحتاجها.
تضيف التحديثات التراكمية قسم xref جديدًا في نهاية الملف مع تذييل يربطه بالذي قبله. الكائن الذي يُحدَّث في مراجعة يحصل على إدخال جديد في قسم xref الملحق؛ وتبقى البايتات الأصلية في مكانها لكنها تُستبدل من الناحية المرجعية. بهذه الطريقة تبقى ملفات PDF الموقعة رقميًا قابلة للتحقق حتى بعد إضافة تعليقات أو مراجعات تعبئة نماذج: نطاق البايتات الموقعة لا يُمس أبدًا، والمحتوى الجديد يعيش في القسم الملحق. ويمكن تحديث شجرة الصفحات أيضًا، بحيث تنتج الإضافات أو الحذوفات في مراجعة ما جذر /Pages جديدًا مع مصفوفة /Kids معدلة، بينما يظل الكائن الجذري القديم في موضعه الأصلي داخل الملف.
ما الذي يفسد من دون اجتياز الشجرة
وضع الفشل في أساليب المسح حسب الكائنات صامت. يبدو المستند الناتج مقنعًا: عدد الصفحات صحيح، وكل صفحة تحتوي محتوى يمكن التعرف عليه. المشكلة أن الترتيب فقط هو الخطأ، وهو خطأ يعتمد على المولّد، وعدد المراجعات، وما إذا كانت أي صفحات قد دُمجت من مصادر خارجية. قد تمر مجموعة اختبار من الملفات المنتجة بأداة واحدة بالكامل؛ أما الملفات المنتجة بأداة مختلفة أو بسير دمج مختلف فسوف تفشل. هذا التباين هو السبب في أن الإصلاحات القائمة على التخمين لا تصمد أبدًا.
الملفات ذات التحديثات التراكمية معرضة بشكل خاص لهذا، لأن الصفحات التي أضيفت أو أُعيد ترتيبها في المراجعات اللاحقة تحمل أرقام كائنات عالية بينما يتحكم في ترتيب عرضها مصفوفة /Kids المحدّثة. أي مسح يعالج الكائنات بالترتيب الرقمي سيضع تلك الصفحات ذات الأرقام المتأخرة في النهاية مهما كانت الشجرة تقول إنها تنتمي إلى مكان آخر.
الإصلاح ليس معقدًا. ابدأ من الفهرس، وحل مرجع /Pages، وامشِ في مصفوفة /Kids بشكل递归، وأصدر الأوراق بالترتيب الذي تصادفها فيه. هذا هو ترتيب العرض بحكم التعريف، بغض النظر عن أرقام الكائنات أو إزاحات البايت أو بنية الملف. معظم مكتبات PDF الناضجة تعرض عداد صفحات ومحدد صفحة مفهرسًا يقوم بذلك بالفعل بشكل صحيح؛ والخطر يكمن في الشفرة التي تتجاوز نموذج الصفحة في المكتبة وتمس طبقة الكائنات مباشرة.
أحد الشذوذات البنيوية التي تستحق المعالجة الصريحة: قيمة /Count في عقدة /Pages الوسيطة قد تكون خاطئة في الملفات المشوهة. الوثوق بـ /Count للتحقق من الحدود ثم التوقف قبل اجتياز كامل سيؤدي إلى حذف صفحات بصمت عندما يكون العدد المذكور أقل من الحقيقي. استخدام /Count فقط كتلميح أداء للحجز المسبق للسعة أو للبحث الثنائي، ثم اشتقاق العدد الفعلي من الاجتياز، هو النمط الأكثر أمانًا.