مقال تقني

ترتيب صفحات PDF: كيف تتحكم شجرة الصفحات في تسلسل الصفحات

رقم الكائن 1 ليس الصفحة 1. هذه الحقيقة الوحيدة تُعثر تعليمات معالجة PDF البرمجية أكثر من أي جانب آخر للتنسيق، وفهم السبب يتطلب النظر إلى ما وراء ما يعرضه العارض وإلى الرسم البياني للكائن الذي يقرأه العارض فعليًا

ملف PDF عبارة عن مجموعة من الكائنات غير المباشرة المرقمة. الصفحات من بين تلك الكائنات، لكن تسلسل عرضها لا علاقة له بمكان وجودها في الملف أو الأرقام التي تحملها. يتم تحديد ترتيب العرض بالكامل بواسطة شجرة /Pages، وهي بنية متصلة متجذرة في كتالوج المستند. إذا تجاهلت الشجرة وقمت بمسح الكائنات رقميًا، فسوف تقوم بتجميع الصفحات بالترتيب الخاطئ لجزء كبير من ملفات العالم الحقيقي

شجرة الصفحات: ما يحدد الترتيب فعليًا

يبدأ كل ملف PDF بكتالوج مستند (ISO 32000-2 §7.7.2). يحمل الكتالوج إدخال /Pages يشير إلى العقدة الجذرية لشجرة الصفحات. هذه العقدة الجذرية عبارة عن قاموس يحتوي على /Type /Pages، ومصفوفة /Kids من المراجع غير المباشرة، و /Count يعطي إجمالي عدد صفحات الأوراق (leaf-page) أسفلها. ترتيب العرض هو اجتياز العمق أولاً (depth-first traversal) من اليسار إلى اليمين لتلك الشجرة، نقطة انتهى

ملف صغير من ثلاث صفحات يجعل هذا ملموسًا:

%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]، إذن الكائن 20 هو الصفحة 1، والكائن 4 هو الصفحة 2، والكائن 9 هو الصفحة 3. ترقيم الكائن غير ذي صلة. أي تعليمات برمجية تكرر الكائنات بترتيب رقمي وتجمع تلك التي تحتوي على /Type /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 فقط كتلميح أداء للتخصيص المسبق للقدرة أو البحث الثنائي، واستخلاص العدد الفعلي من الاجتياز هو النمط الأكثر أمانًا للمستندات المهمة

 المقال التالي