مقال تقني

تسميات صفحات PDF في Delphi: إصلاح أشجار الأعداد /Kids

تكتب PDF Library for Delphi مديات تسميات الصفحات عبر AddPageLabels، ومنذ v3.539.10 يعمل ذلك الاستدعاء أيضاً على الملفات المحملة التي شجرة أعداد /PageLabels فيها مقسمة إلى عقد /Kids: يُسطّح الجذر إلى ورقة /Nums واحدة قبل دخول المدى الجديد، فتظهر التسمية في العارض فعلاً بدل تجاهلها بصمت. الضحية النموذجية ملف PDF على نمط كتاب من أداة تنضيد، بأرقام رومانية في المقدمات وترقيم عشري في المتن وملحق موسوم A-1 و A-2، حيث أردت إعادة تسمية الملحق وحده ولم يتغير شيء

ما هي تسميات صفحات PDF وكيف تُخزن؟

تسميات الصفحات هي السلاسل التي يعرضها العارض في صندوق صفحته بدل فهرس الصفحة الفيزيائي، ويخزنها §12.4.2 من ISO 32000-1 شجرة أعداد تحت مفتاح الكتالوج /PageLabels. كل مفتاح فهرس صفحة صفري الأساس يبدأ مدى تسمية، وكل قيمة قاموس تسمية صفحة بثلاثة مدخلات على الأكثر: /S لنمط الترقيم (D أو R أو r أو A أو a)، و /P لسلسلة بادئة، و /St للقيمة العددية لأول صفحة في المدى، وهي افتراضياً 1. ويمتد المدى حتى المفتاح التالي، والمواصفة تشترط أن تحوي الشجرة قيمة لفهرس الصفحة 0، فتكون كل صفحة مغطاة بمدى ما

تخزين تسميات الصفحات بمصطلحات PDFlibPas: شجرة الأعداد /PageLabels تمثل كل مدى بمفتاح هو صفحته البادئة صفرية الأساس، وكل قيمة قاموس تسمية بنمط /S وبادئة /P ورقم أول /St، ومثال الكتاب يطابق مقدمات رومانية وصفحات متن عشرية وملحق A- بثلاثة مديات
يمتد المدى حتى المفتاح التالي، وتشترط المواصفة قيمة لفهرس الصفحة 0، ويطبق GetPageLabel آخر مدى مفتاحه عند الصفحة أو دونها، فتُحل كل صفحة إلى شيء
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
      Exit;
    // الصفحات 1-4: i, ii, iii, iv (رومانية صغيرة)
    Lib.AddPageLabels(1, 3, 1, '');
    // الصفحات 5-120: 1, 2, 3 ... (عشرية)
    Lib.AddPageLabels(5, 1, 1, '');
    // الصفحات 121 فما بعد: A-1, A-2 ... (عشرية ببادئة)
    Lib.AddPageLabels(121, 1, 1, 'A-');
    WriteLn(Lib.GetPageLabel(5));    // 1
    WriteLn(Lib.GetPageLabel(122));  // A-2
    Lib.SaveToFile('handbook-labeled.pdf');
  finally
    Lib.Free;
  end;
end;

يطابق TPDFlib.AddPageLabels(Start, Style, Offset, Prefix) وسائطه ذلك القاموس بلا مفاجآت متى عرفت ثلاث قواعد. ‏Start أحادي الأساس ككل وسيط صفحة آخر في المكتبة، ويُكتب في الشجرة بصيغة Start - 1. و Style يمتد من 0 إلى 5، حيث 0 يعني البادئة فقط وتصير 1 إلى 5 قيم /S هي D و R و r و A و a؛ وما كان خارج ذلك المدى يعيد 0 ولا يلمس شيئاً. و Offset يصير /St فقط حين يكون أكبر من الصفر، فتمرير 0 يحذف المفتاح ببساطة ويعود العارض إلى الافتراضي 1. ولأن تسميات الصفحات وصلت في PDF 1.3، يجري الاستدعاء أيضاً EnsureMinVersion('1.3', '/PageLabels')، التي ترفع نسخة الإخراج لملف أقدم ما لم تكن قد قفلت نسخة الحفظ صراحة

لماذا تختفي تسميات الصفحات الجديدة حين تحوي الشجرة /Kids؟

تختفي التسميات الجديدة لأن §7.9.7 من ISO 32000-1 (الجدول 37) يشترط أن يحمل جذر شجرة الأعداد /Kids أو /Nums، أبداً كليهما معاً، وكانت المساعدة NumTreeSet الأقدم تعرف البحث عن /Nums وحدها. المنتجون الذين يطلقون مستندات طويلة يقسمون الشجرة كثيراً إلى عقد وسيطة، كل منها بزوج /Limits، ويعلقونها بجذر لا يحمل /Kids فقط. وجد الكود القديم لا /Nums على ذلك الجذر، فأنشأ واحدة جديدة إلى جانب /Kids القائمة وأدخل المدى الجديد هناك. النتيجة جذر بمدخلين متنافيين. ينزل العارضون عبر /Kids ولا ينظرون أبداً إلى المصفوفة الشاردة، و EnumNumTree التابعة للمكتبة نفسها تفحص /Kids أولاً أيضاً، و NumTreeLookup ترفض عقدة يكون فيها HasKids xor HasNums خاطئاً. وما زال AddPageLabels يعيد 1 والملف المحفوظ ما زال يفتح بنظافة، وذلك أسوأ أنواع الفشل: لا أحد يشكو، والتسميات تبقى كما هي فحسب

الإصلاح في NumTreeSet يحول الجذر إلى ورقة قبل إدخال أي شيء. حين يحمل الجذر /Kids تتجول EnumNumTree في كل ورقة بالترتيب وتجمع كل زوج مفتاح وقيمة، وتُبنى من تلك القائمة مصفوفة /Nums مسطحة جديدة، وتُطهى /Kids و /Limits وأي /Nums قديمة من الجذر قبل إلحاق المصفوفة المسطحة. إسقاط /Limits ليس تجسيلاً، إذ يجيز الجدول 37 ذلك المدخل على العقد الوسيطة والأوراق فقط، أبداً على الجذر. من تلك اللحظة يكون الإدخال إدخالاً مرتباً عادياً في مصفوفة واحدة، وتنجو المديات القائمة بقواميس تسمياتها الأصلية. والمقايضة مقصودة: لا تُعاد بنية الشجرة إلى عقد /Kids متوازنة بعد ذلك. وللتسميات تلك الكلفة صفر، لأن حتى دليلاً مرجعياً ضخماً نادراً ما يتجاوز بضعة عشرات من المديات، والورقة الواحدة هي ما يكتبه معظم المنتجين أصلاً

إصلاح شجرة الأعداد في PDFlibPas: جذر يحمل /Kids ومصفوفة /Nums شاردة لا يراه العارضون لأن ISO 32000-1 يجيز واحدة من الاثنتين فقط، فيسطح NumTreeSet كل ورقة إلى مصفوفة /Nums واحدة ويطهر /Kids و /Limits، ما لا يجيزه الجدول 37 على جذر أبداً
لم يشكُ أحد لأن كل فحص نجح: أعاد AddPageLabels قيمة 1، وافتُح الملف المحفوظ بنظافة، والقارئ الذي ينزل عبر /Kids أولاً، كما يفعل العارضون والمكتبة نفسها معاً، لا يجد المدى الجديد أبداً
// إعادة تسمية الملحق في ملف جذر /PageLabels فيه يستخدم /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
  WriteLn('Before: ', Lib.GetPageLabel(121));  // مثلاً A-1
  // استبدال المدى الذي يبدأ عند الصفحة 121: App-a, App-b ...
  if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
    Lib.SaveToFile('vendor-manual-relabeled.pdf');
  // مديات الروماني والعشري القائمة ما زالت في الورقة المسطحة
  WriteLn('After: ', Lib.GetPageLabel(121));   // App-a
  WriteLn('Front: ', Lib.GetPageLabel(2));     // ii، بلا تغيير
end;

كيف تُقرأ مصفوفة /Nums قراءة خاطئة بوصفها مفاتيح؟

تُقرأ مصفوفة /Nums قراءة خاطئة حين يمشي فيها الكود عنصراً واحداً في كل خطوة، لأن المصفوفة سلسلة مسطحة من الأزواج المتناوبة، [key0 value0 key1 value1 ...]، والمواضع الزوجية وحدها مفاتيح. كانت حلقة NumTreeSet القديمة تفحص كل عنصر هل هو من نوع عددي، فالقيمة التي صادف أنها رقم كانت تُقارن وكأنها مفتاح؛ وضربة أصغر-من يمكن أن تضع نقطة الإدخال عند فهرس فردي وتسقط الزوج الجديد في منتصف زوج قائم، فتزيح كل الأزواج اللاحقة عن إيقاعها. وكانت EnumNumTree تمشي بالخطوة الواحدة نفسها. الاثنتان الآن تدوران على الأزواج بخطوة اثنين، تقرآن المفتاح عند X * 2 والقيمة عند X * 2 + 1، والتطابق التام للمفتاح يستبدل القيمة ويخرج بـ Break. وبإنصاف، قيم تسميات الصفحات قواميس، فهذا العيب الثاني نادراً ما انطلق على /PageLabels نفسها، لكن مساعدة شجرة أعداد تقرأ الخطوة الخطأ تكون فاسدة لحظة تكون فيها أي قيمة عدداً، وأُصلحت في الممر نفسه

إصلاح خطوة الأزواج في أشجار أعداد PDFlibPas: مصفوفة /Nums سلسلة مسطحة من مدخلات المفاتيح والقيم المتناوبة، فمشية تفحص كل عنصر قد تدخل زوجاً جديداً عند فهرس فردي وتزيح الأزواج اللاحقة عن إيقاعها، بينما تقرأ المشية المصلحة المفتاح عند X*2 والقيمة عند X*2+1
نادراً ما انطلق العيب على /PageLabels لأن قيم التسميات قواميس، لكن مساعدة شجرة أعداد تقرأ الخطوة الخطأ تتلف لحظة تكون فيها أي قيمة عدداً، فالمشيتان الآن تخطوان في أزواج

قراءة التسميات مجدداً وتداولها ذهاباً وإياباً

يعيد TPDFlib.GetPageLabel(Page) تسمية صفحة أحادية الأساس وله احتياطان يستحقان المعرفة. بلا مدخل /PageLabels أصلاً يعيد رقم الصفحة العشري، فيمكن للمستدعي استخدامه دون شروط. وبشجرة موجودة دون مدى يغطي الصفحة يعيد سلسلة فارغة، وهو بالضبط ما يحدث حين يترك ملف المدخل الإلزامي للفهرس 0؛ توثيق المرجع يقول إن مدى يبدأ عند الصفحة 1 لا بد أن يوجد حتى تعرض التسميات صحيحة، والكود يجعل ذلك الشرط مرئياً. وأنماط الحروف تتبع المواصفة لا أعمدة جداول البيانات: بعد Z تأتي AA ثم BB، بتكرار الحرف بدل الحمل

var
  P: Integer;
  Data: WideString;
begin
  // تدقيق سريع لما سيعرضه العارض في صندوق صفحته
  for P := 1 to Lib.PageCount do
    WriteLn(P, ' -> ', Lib.GetPageLabel(P));

  // قيمة الخيار 4 تصدر مديات التسميات فقط كسجلات PageLabelBegin
  Data := Lib.ExportDocumentData(4);
  // الاستيراد يعيد تشغيلها عبر ClearPageLabels + AddPageLabels
  Lib.ImportDocumentData(Data, 0);
end;

للتحرير بالجملة، تكتب ExportDocumentData بقيمة الخيار 4 كل مدى ككتلة PageLabelBegin بأسطر PageLabelNewIndex و PageLabelStart و PageLabelPrefix و PageLabelNumStyle، ويعامل ImportDocumentData أول سجل تسمية يراه بديلاً كاملاً: يستدعي ClearPageLabels مرة واحدة ثم يمرر كل سجل إلى AddPageLabels. ذلك يجعل جولة النص ذهاباً وإياباً حتمية حتى حين استخدم الملف الأصلي شجرة /Kids، لأن الإزالة تحذف مدخل الكتالوج كله والشجرة المعاد بناؤها ورقة واحدة من البداية

ماذا لا يضمنه الإصلاح بعد؟

التسطيح باتجاه واحد ويثق بالترتيب الذي يجده. تجمع EnumNumTree الأزواج بترتيب الملف، وطبّق GetPageLabel آخر مدى مفتاحه أصغر من فهرس الصفحة أو مساوياً له، فملف أجنبي أوراقه خارج الترتيب، وهو ما يحرمه §7.9.7 لكنه يدور في التداول فعلاً، ما زال قد يعطي تسميات خاطئة حتى تعيد بناء المديات بـ ClearPageLabels واستدعاءات AddPageLabels جديدة. والتسميات أيضاً مربوطة بفهارس الصفحات لا بكائنات الصفحات، فأي عملية تغير عدد الصفحات أو ترتيبها تترك المديات في مواضعها. وتبديل في الموقع مثل استبدال صفحات مع حفظ أرقام الكائنات يبقي العد وبالتالي التسميات متوافقة، بينما دمج مثل ترتيب مسحات الوجهين المتشابكة ينتج تتابع صفحات جديداً يستحق مجموعة مديات مكتوبة حديثاً

استدعاءات تسميات الصفحات ومعالجة شجرة الأعداد وتصدير واستيراد بيانات المستند الموصوفة هنا كلها تصدر في PDF Library for Delphi لـ Delphi و C++Builder و Lazarus، مع مدخل مرجعي لـ AddPageLabels يوثق قيم النمط وأكواد الإعادة