استبدال الصفحة 3 من عقد مُعتمد يجب ألا يحرّك فهرس المحتويات. احذف الصفحة القديمة، أدرج الجديدة، وكل إشارة مرجعية كانت تشير إلى هناك تهبط الآن في مكان آخر. تتجنب مكتبة PDFlibPas Delphi PDF ذلك بالإبقاء على كائن الصفحة الهدف نفسه ونقل المدخلات التي تحمل المحتوى المرئي فقط
لماذا تنكسر الإشارات المرجعية بعد استبدال صفحة PDF؟
تنكسر الإشارات المرجعية لأن وجهة PDF تسمّي الصفحة بمرجع كائن غير مباشر، لا برقم صفحة. يعرّف ISO 32000-1 §12.3.2.2 الوجهة الصريحة كمصفوفة عنصرها الأول مرجع غير مباشر إلى كائن الصفحة. احذف ذلك الكائن وألحق بديلًا، ويصبح المرجع معلّقًا: تستجيب معظم العارضات بإنزال القارئ على الصفحة 1، وهو بالضبط العرَض الذي يبلّغ عنه الناس بعد استبدال بالحذف ثم الإدراج. شجرة الصفحات تبدو مثالية، وعدد الصفحات صحيح، والعرض صحيح، بينما طبقة التنقل بأكملها خاطئة بصمت
الوجهات المسمّاة لا تنقذك أيضًا. يوجّه §12.3.2.3 الاسم عبر شجرة أسماء /Dests في فهرس المستند catalogue، لكن الورقة leaf التي يُحلّ إليها ذلك الاسم ما تزال مصفوفة وجهة صريحة تحمل مرجع الصفحة نفسه. التسمية تضيف طبقة تحويل فوق مرجع الصفحة، لا حولها. المنطق نفسه يغطي بقية الطبقة التفاعلية الموصوفة في §12.5: تعليق رابط annotation يحمل /Dest أو إجراء GoTo عبر /A يكون /D فيه تلك المصفوفة نفسها، وكل تعليق توضيحي annotation قد يحمل مدخل /P وهو مرجع غير مباشر إلى صفحته، وعنصر واجهة حقل نموذج form field widget هو تعليق توضيحي بالوضع نفسه تمامًا. استبدال ساذج واحد للصفحة يفصل أربعة أنظمة فرعية دفعة واحدة، وإن أردت رؤيتها معدودة على ملف حقيقي، فرسم الكائنات نفسه هو ما يجتازه استقصاء المخطط التفصيلي والتعليقات التوضيحية
أي مدخلات صفحة تحمل الهوية وأيها يحمل المظهر
قاموس الصفحة يمزج نوعين من المدخلات، والاستبدال في المكان ينجح تحديدًا حين تفصل بينهما. جانب المظهر محدود وقابل للعدّ: /Contents، و/Resources، وصناديق الصفحة الخمسة /MediaBox و/CropBox و/BleedBox و/TrimBox و/ArtBox، إضافة إلى /Rotate و/Group و/UserUnit و/BoxColorInfo. هذه المدخلات الأحد عشر تقرّر كل ما ينتجه أي مُرسِّم rasteriser للصفحة، ولا شيء آخر في الملف يشير إليها بالاسم
جانب الهوية هو ما ربط بقية المستند نفسه به: رقم كائن الصفحة وجيله، والوصلة الخلفية /Parent إلى شجرة الصفحات، و/Annots. تُبقي PDFlibPas كل واحد منها دون مساس. ReplacePageRanges يطهّر المدخلات المرئية الأحد عشر من قاموس الصفحة الهدف ويعيد إضافتها من صفحة المصدر المستوردة، بحيث يُعدَّل كائن الصفحة الهدف في مكانه بدل استبداله. بنية شجرة الصفحات التي يفرضها §7.7.3 تبقى مطابقة بايتًا ببايت في الشكل أيضًا: ترتيب /Kids، و/Count، وكل /Parent ناجٍ هي نفسها قبل وبعد، لأنه لم يُفصَل أي عقدة قط
كيف تستبدل PDFlibPas صفحة دون إعادة ترقيم الكائنات؟
يأخذ الاستدعاء مستند مصدر، وصفحة بدء هدف مرقّمة من 1، وتعبير نطاق مصدر، وعلامة خيارات. يجب أن يكون كلا المستندين مفتوحين في المثيل نفسه، والمستند الهدف هو المحدد. ولأن عدد صفحات الهدف لا يتغير أبدًا، فإن النطاق الذي تطلبه يجب أن يتسع داخل المستند بدءًا من TargetStartPage، ويُتحقَّق من ذلك قبل إنشاء أي شيء
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
داخليًا لا يمكن قراءة صفحات المصدر عبر حدود المستندات ببساطة، لأن كل مرجع غير مباشر داخلها ينتمي إلى ترقيم كائنات المصدر. فيُستورَد نطاق المصدر أولًا بالطريقة العادية، كصفحات مؤقتة مُلحقة بعد آخر صفحة حقيقية، وهو ما يُشغّل إعادة تخطيط رسم الكائنات بالكامل: تدفقات المحتوى والخطوط وكائنات XObject والتظليلات ومساحات الألوان كلها يُعاد ترقيمها إلى المستند الهدف. بعد ذلك فقط تُنسَخ المدخلات المرئية الأحد عشر من كل صفحة مؤقتة إلى صفحتها الهدف، وبعد ذلك فقط تُفصَل الصفحات المؤقتة عن شجرة الصفحات. عمل إعادة التخطيط يحدث حيث يكون رخيصًا وآمنًا، ويُختزَل التعديل الهدّام إلى تبديل على مستوى القاموس في صفحات موجودة أصلًا
مسار الحذف الذي كان سيدمّر ما نقلته للتو
إزالة تلك الصفحات المؤقتة هي الخطوة التي تبدو تافهة وليست كذلك. مسار حذف الصفحة العادي في المكتبة يفعل أكثر من مجرد فصل عقدة: فهو يدمج طبقات كل صفحة تُحذف، ويُفرغ تدفق المحتوى الأول، ويستعيد الموارد التي لا تشاركها أي صفحة أخرى. هذا سلوك صحيح لحذف حقيقي، وكارثي هنا، لأنه بحلول لحظة إزالة الصفحات المؤقتة تكون صفحات الهدف تشير بالفعل إلى تدفقات المحتوى وكائنات الموارد نفسها تمامًا. إفراغها كان سيفرِغ الصفحة التي استبدلتها للتو، وكسح الموارد كان سيجمع خطوطًا وصورًا صار لها الآن مالك حي
الحل هو نمط الحفاظ على الكائنات المُشار إليها preserve-referenced-objects على مسار الحذف الداخلي. حين يُضبط، يتخطى الحذف كلًا من كسح الموارد غير المشتركة وإفراغ تدفق المحتوى، ولا يفعل شيئًا سوى فصل الصفحات عن شجرة الصفحات وإصلاح محاسبة الشجرة. الكائنات المنقولة تنجو بمالك جديد، وملكية الكائنات بعد العملية هي بالضبط ما كنت سترسمه على لوح أبيض: تدفق محتوى واحد، وصفحة مالكة واحدة، ورقم كائن واحد لم يتحرك قط. قواعد دورة الحياة ذات الصلة بإنشاء الصفحات وحذفها وإعادة ترتيبها مشروحة بشكل منفصل في ملاحظات عمليات دورة حياة المستند والصفحة
الترتيب، والتكرارات، والفشل الكلي أو العدم
علامة الخيارات تحدد كيفية تفسير نطاق المصدر. 0 يفرز أرقام الصفحات المحلَّلة ويزيل التكرارات، وهو الافتراضي المنطقي حين يمرّر المستدعي شيئًا مثل '4-6,2' ويعني ببساطة تلك الصفحات الأربع. 1 يحافظ على الترتيب الذي كتبته ويسمح بتكرار صفحة، فـ'2,1,2' تعني فعليًا ثلاثة استبدالات مأخوذة من صفحتي مصدر. يعمل التحقق أولًا ويعمل كاملًا: صياغة النطاق، وكل رقم صفحة مقابل عدد صفحات المصدر، وقيمة الخيار نفسها، وسعة الهدف، كلها تُفحص قبل إنشاء كائن واحد. الاستدعاء المرفوض يضبط LastErrorCode إلى 412، ويستعيد الصفحة المحددة سابقًا، ويترك المستند تمامًا كما كان
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
الذرّية Atomicity تمتد إلى ما بعد التحقق داخل النقل نفسه. قبل استيراد أول صفحة مصدر، تُلتقط المدخلات المرئية الأحد عشر لكل صفحة هدف في النطاق كقيم مُرمَّزة. فإن فشل الاستيراد، أو لم يطابق عدد الصفحات المستورَدة ما طُلب، تُفَك تلك اللقطات مجددًا على صفحات الهدف وتُزال الصفحات المؤقتة، بحيث يترك فشل منتصف الطريق المظاهر الأصلية في مكانها على كائناتها الأصلية. هذا أهم مما يبدو: نطاق صفحات مستبدَل جزئيًا في عقد أسوأ من استدعاء فاشل، لأنه لا شيء في الملف يعلّمه بأنه منجَز جزئيًا
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
ما الذي لا يزال الاستبدال في المكان لا يقدّمه لك
التعليقات التوضيحية وحقول النموذج والإشارات المرجعية الخاصة بالمصدر لا تُستورَد عمدًا. جلب عنصر واجهة widget عبر الحدود دون مدخل حقل /AcroForm الخاص به، أو تعليق توضيحي يحمل محتوى موسومًا marked-content دون ملكية شجرة البنية structure tree الخاصة به، ينتج كائنًا تفاعليًا نصف مستورَد لا تستطيع أي عارضة أن تتعامل معه، لذا فإن العملية تنقل المظهر فقط. النتيجة العملية هي أنه إن كان المفترض بصفحة الاستبدال أن تحمل حقول نماذج جديدة أو روابط جديدة، فأنت تضيفها إلى صفحة الهدف بعد ذلك، على كائن صفحة الهدف الذي ما يزال جالسًا هناك بانتظارها
حدّان آخران يستحقان الفحص على ملفاتك الخاصة. أولًا، يُحفَظ /Annots لكن هندسة الصفحة لا تُحفَظ، فاستبدال صفحة 220 ملم بصفحة 320 ملم يُبقي مستطيلات التعليقات التوضيحية عند إحداثياتها القديمة داخل /MediaBox بحجم مختلف؛ فإن تغيرت الهندسة، أعِد وضع التعليقات التوضيحية التي أبقيت عليها. ثانيًا، المدخلات خارج المفاتيح المرئية الأحد عشر تبقى مع صفحة الهدف بالتصميم، وهذا صحيح لـ/Trans أو /AA وقديم لـ/Thumb، فأعد توليد الصور المصغّرة thumbnails بعد الاستبدال. المستندات الموسومة tagged تحتاج فكرة إضافية واحدة: عناصر البنية ما تزال تشير إلى كائن الصفحة الصحيح عبر /Pg، لكن معرّفات المحتوى الموسوم الخاصة بها تصف محتوى لم يعد موجودًا، فاستبدال صفحة داخل سير عمل PDF/UA هو تعديل لشجرة البنية بقدر ما هو تعديل للمحتوى. إن كانت مهمتك في الواقع تركيبًا لا استبدالًا، أي طبقة عمل فني فوق صفحات تحتفظ بها، فإن أسلوب خياطة الصفحات والقوالب هو الأداة الأرخص
كل ما شُرح هنا، بما في ذلك صياغة تعبير النطاق وقيم الخيارات وواجهة معالجة الصفحات المحيطة، يُشحن ضمن PDFlibPas Delphi PDF Library القياسية لـ Delphi وC++Builder، وتحمل وثائقها المرجعية المدخل الكامل لاستدعاء استبدال الصفحات ورموز أخطائه