مقالنا المرافق حول ترتيب صفحات PDF يغطي القاعدة الأساسية: ترتيب العرض يأتي من اجتياز عمق-أول من اليسار إلى اليمين لمصفوفات /Kids في شجرة /Pages، لا من أرقام الكائنات مطلقًا. هذا المقال ينظر إلى الشجرة من زاوية مختلفة — شكلها. لماذا تُصدر كاتبات PDF الناضجة تسلسلات هرمية من عقد وسيطة بينما مصفوفة مسطحة واحدة قانونية تمامًا؟ ما الذي يتغيّر فعليًا عندما تُسطّح أداة ما الشجرة أو تعيد بناءها؟ وماذا يحدث عندما تتوقف محاسبة /Count التي تجعل البنية كاملة سريعة عن قول الحقيقة
التفرّع قرار أداء
لا شيء يُجبر الكاتب على التعشيش. مستند من 10,000 صفحة بعقدة /Pages جذرية واحدة و10,000 مرجع ورقي في مصفوفة /Kids واحدة يتوافق مع المواصفة. ومع ذلك يوصي مرجع PDF بشجرة متوازنة للمستندات الكبيرة، وتتبع المولّدات السائدة تلك النصيحة بتفرّع معتدل، عادة بضع عشرات من الأبناء لكل عقدة وسيطة
السبب هو ما يجب على العارض قراءته قبل أن يتمكن من عرض أي شيء. تخيّل قفزة مباشرة إلى الصفحة 8,214 من ذلك الملف ذي 10,000 صفحة. مع شجرة مسطحة، يجب على العارض أولًا تحليل العقدة الجذرية، وتلك العقدة الجذرية مصفوفة ضخمة واحدة: بمعدل ثماني بايتات تقريبًا لكل مرجع غير مباشر، كائن بحجم 80 كيلوبايت يجب ترميزه بالكامل من البداية إلى النهاية قبل أن يمكن حلّ الإدخال 8,213. مع شجرة متوازنة بتفرّع 32، تقرأ القفزة نفسها الجذر، وتقارن مجاميع /Count الجارية لاختيار الابن الصحيح، ثم تنزل — ثلاثة أو أربعة قواميس صغيرة في المجموع، كل منها بضع مئات من البايتات. هذا هو الوصول العشوائي O(log n) الذي صُممت الشجرة لتوفيره، وهو السبب الكامل وراء وجود /Count في العقد الوسيطة: فهو يسمح للقارئ بتخطي شجرة فرعية كاملة من دون فتح كائن واحد بداخلها
شكل الشجرة يحدد أيضًا تكلفة التحرير. التحديث التراكمي الذي يُدرج صفحة واحدة يجب أن يعيد كتابة كل عقدة تغيّر فيها /Kids أو /Count، أي المسار من والد الورقة الجديدة صعودًا إلى الجذر. في شجرة متوازنة يكون ذلك المسار حفنة من القواميس الصغيرة المُلحقة بالملف. أما في الشجرة المسطحة فإن "المسار" هو المصفوفة الجذرية العملاقة الوحيدة، المكررة بالكامل في كل مراجعة. عقد يمر بثلاثين دورة مراجعة وتعليق يمكن أن ينتهي به الأمر حاملًا ثلاثين نسخة متجاوزة من المصفوفة ذاتها البالغة 80 كيلوبايت في تدفق البايتات الخاص به
العقد الداخلية تحمل خصائص موروثة
العقد الوسيطة ليست مجرد توجيه. الخصائص الأربع القابلة للتوريث للصفحة — /Resources و/MediaBox و/CropBox و/Rotate — يمكن رفعها إلى أي عقدة /Pages، حيث تُطبَّق على كل ورقة تحتها ما لم يتجاوزها نسل. كاتب ينتج تقريرًا بملحق أفقي يمكنه التعبير عن ذلك التخطيط في الشجرة نفسها:
5 0 obj % جذر المستند
<< /Type /Pages /Count 6 /Kids [6 0 R 7 0 R] >>
endobj
6 0 obj % متن التقرير: A4 عمودي، خط المتن
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [30 0 R 31 0 R 32 0 R]
/MediaBox [0 0 595 842]
/Resources << /Font << /F1 8 0 R >> >> >>
endobj
7 0 obj % الملحق: A4 أفقي، مُدار، بخط خاص به
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [40 0 R 41 0 R 42 0 R]
/MediaBox [0 0 842 595] /Rotate 90
/Resources << /Font << /F2 9 0 R >> >> >>
endobj
40 0 obj % صفحة الملحق: ترث الحجم والدوران والخطوط
<< /Type /Page /Parent 7 0 R /Contents 43 0 R >>
endobj
الكائنات من 40 إلى 42 شبه فارغة. حجم صفحتها ودورانها وموارد الخطوط فيها تصل جميعًا بالوراثة من العقدة 7، ما يبقي الملف مضغوطًا وذاتي الصيانة: أضِف صفحة رابعة تحت عقدة الملحق وستخرج أفقية تلقائيًا
الآلية نفسها تُنشئ خطر نقل الصفحة الكلاسيكي. لنفترض أن أداة تنقل الكائن 40 إلى متن التقرير بتحرير مصفوفتي /Kids وإعادة توجيه /Parent إلى العقدة 6. النقل صالح بنيويًا، غير أن الكائن 40 يرث الآن /MediaBox العمودي، بلا دوران، والخط /F1 — بينما لا يزال تدفق محتواه يختار /F2، الذي لم يعد يُحل. تتقلص الصفحة، ويُلغى دورانها، وتفقد نصّها في تحرير واحد. لذلك يُجسّد كود إعادة الترتيب القوي القيم المحلولة للخصائص الأربع القابلة للتوريث كلها على قاموس الصفحة قبل إعادة تعيين والدها. إن كنت قد سحبت صفحة يومًا في محرر ورأيتها تُغيّر حجمها أو اتجاهها، فهذه هي الآلية التي شهدتها
التسطيح: قانوني وشائع ومكلف أحيانًا
أدوات كثيرة تسلك الاتجاه المعاكس. الكاتبات الدنيا تُصدر شجرة أحادية المستوى لأن ذلك بسيط، والعديد من أدوات الدمج والتقسيم تعيد بناء أي شجرة تقرأها في مصفوفة /Kids مسطحة واحدة، لأن توليد بنية متوازنة عمل إضافي والمخرجات المسطحة متوافقة دائمًا. يجب على إعادة البناء الصحيحة حل الوراثة في الوقت نفسه: كل خاصية كانت الورقة ترثها يجب نسخها إلى الورقة، أو رفعها إلى الجذر الجديد إن كانت موحدة عبر المستند بأكمله — وإلا غيّرت المخرجات الهندسة تمامًا كما تفعل حالة نقل الصفحة
بالنسبة للمستندات النموذجية، التسطيح غير ضار. يضر عند الحجم الكبير، بالطريقتين الموصوفتين سابقًا: تصبح المصفوفة الجذرية كائنًا كبيرًا واحدًا يجب على كل فتح وكل قفزة صفحة تحليله بالكامل، وكل تحرير بنيوي يعيد كتابته كاملًا. ما لا يدمره التسطيح هو المشاركة عبر المراجع غير المباشرة — شجرة مسطحة تشير فيها كل الصفحات العشرة آلاف إلى كائن قاموس /Resources نفسه تبقى مُزالة التكرار. ما يُفقد فقط هو خيار ترك الإدخال خارج الصفحة والسماح لسلف بتوفيره
عندما يكذب /Count
/Count محاسبة بحتة: يجب أن يساوي عدد صفحات الأوراق في الشجرة الفرعية للعقدة، ولا شيء في تنسيق الملف يفرض ذلك. نمطان من الفساد يفسران معظم العدادات الكاذبة المشاهَدة في الواقع
الأول هو العداد القديم المتروك من تحديث تراكمي. يُدرج محرر صفحة، ويعيد كتابة الوالد المباشر بـ/Kids جديد و/Count محدَّث، ويُلحق الاثنين بالملف — ولا يمس الأسلاف أبدًا:
% المراجعة الأصلية
12 0 obj
<< /Type /Pages /Count 9 /Kids [13 0 R 14 0 R 15 0 R] >>
endobj
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 3
/Kids [50 0 R 51 0 R 52 0 R] >>
endobj
% مراجعة مُلحقة: صفحة واحدة أُدرجت في الفرع الأوسط.
% الكائن 14 استُبدل؛ الكائن 12 لم يُعَد كتابته أبدًا
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 4
/Kids [50 0 R 51 0 R 90 0 R 52 0 R] >>
endobj
تحمل الشجرة الآن عشر أوراق، لكن الجذر لا يزال يقول تسعة. عارض يثق بالجذر يبلغ عن تسع صفحات في عدّاده. آخر يستخدم العدادات الداخلية لبحث ثنائي عند القفز لصفحة يحسب فهرسًا خاطئًا لكل صفحة بعد نقطة الإدراج. اجتياز كامل يجد عشرة. ثلاث إجابات مختلفة، ملف واحد
النمط الثاني هو العداد الذي لم يكن ليصح أبدًا: سالب، أو صفر في عقدة مأهولة، أو ضخم بشكل سخيف. هذه تأتي من الفَزّ، ومن تلف النقل، وأحيانًا من أخطاء حسابية في المحررات. وهي خطيرة تحديدًا على الكود الذي يثق بـ/Count للتخصيص — تحديد حجم مصفوفة من /Count يساوي -3 يثير خطأ نطاق في أفضل الأحوال، وفعل ذلك من /Count يساوي ملياري يمثل تخصيصًا لحرمان الخدمة. القيمة مدخل غير موثوق، مثل أي رقم آخر في الملف
تنقسم المحللات إلى معسكرين حيال كل هذا. المستهلكون الصارمون — أدوات الفحص المسبق، مدققات PDF/A، خطوط أنابيب الأرشفة — يقارنون /Count بنتيجة الاجتياز ويرفضون الملف أو يُعلّمونه. العارضات التفاعلية متساهلة على نحو شبه عام: تجتاز، وتشتق العداد الحقيقي، وتتجاهل بصمت المخزَّن، وهذا بالضبط سبب إمكانية تداول ملف بعداد قديم لسنوات من دون شكوى إلى أن يلتقي بمحلل أكثر صرامة داخل سير عمل آلي. الحل الوسط الدفاعي لكود المكتبات هو معاملة /Count كتلميح — مفيد للتخصيص المسبق، ولتخطي الشجرة الفرعية بعد التحقق — بينما يبقى الاجتياز مصدر الحقيقة
لخوارزمية الاجتياز نفسها، وقواعد البحث عن الوراثة، والمسار من الفهرس إلى الورقة، ابدأ بـشارح ترتيب الصفحات. ولمعرفة كيف تبدو أنماط الفشل هذه حين يصل مستند عميل حقيقي إلى كود الإنتاج، اقرأ دراسة الحالة في تصحيح ترتيب الصفحات، التي تتتبع حادثة صفحات مبعثرة من العرض إلى السبب الجذري
يتعامل مكوّن HotPDF مع كل هذا داخليًا: فهو يجتاز الأشجار المتداخلة بأي عمق، ويحل الخصائص الموروثة عند نسخ الصفحات أو نقلها، ويتحقق من /Count مقابل عدد الأوراق الفعلي بدل الثقة به، بحيث تعني فهارس الصفحات في واجهته البرمجية دائمًا الصفحات المنطقية