تكتب 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، فتكون كل صفحة مغطاة بمدى ما
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 متوازنة بعد ذلك. وللتسميات تلك الكلفة صفر، لأن حتى دليلاً مرجعياً ضخماً نادراً ما يتجاوز بضعة عشرات من المديات، والورقة الواحدة هي ما يكتبه معظم المنتجين أصلاً
// إعادة تسمية الملحق في ملف جذر /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 نفسها، لكن مساعدة شجرة أعداد تقرأ الخطوة الخطأ تكون فاسدة لحظة تكون فيها أي قيمة عدداً، وأُصلحت في الممر نفسه
قراءة التسميات مجدداً وتداولها ذهاباً وإياباً
يعيد 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 يوثق قيم النمط وأكواد الإعادة