يحذف HotPDF Delphi Component صفحة من PDF محمّل عبر THotPDF.DeletePage، ومنذ الإصدار 2.751.0 يقلّم ذلك الاستدعاء أيضاً كل مرجع على مستوى المستند ما زال يشير إلى الصفحة: الوجهات المسماة في شجرة /Names /Dests، وقاموس /Dests القديم في الكتالوج، وأفعال /GoTo للإشارات المرجعية، وعناصر البنية تحت /StructTreeRoot، و ParentTree، وإدخالات OBJR للتعليقات، وتعليقات الروابط على الصفحات الناجية. وتُعاد بناء شجرة الصفحات آخراً، بعدما لا يستطيع شيء آخر بلوغ الكائن المحذوف
الفشل الذي يمنعه سهل الاستنساخ صعب التشخيص. احذف صفحة غلاف تقرير موسوم، واحفظ، وافتح الناتج: يعرض Acrobat عدد الصفحات الصحيح، لكن إشارة "Contents" المرجعية تهبط الآن إلى لا مكان، ويفحص مدقق الوصولية عنصر بنية بلا صفحة، ويسرد محقق صرامة صارم مرجعاً إلى كائن حر. لا شيء في شجرة الصفحات خاطئ. المشكلة أن صفحة PDF ليست ورقة /Pages فحسب؛ إنها هدف يشير إليه نصف الكتالوج، وإزالة الورقة تترك كل واحدة من تلك الإشارات معلقة
لماذا لا تكفي إزالة صفحة من /Kids؟
لأن ISO 32000-1 يتيح لسبع بنيات مستقلة على الأقل أن تحمل مرجعاً إلى كائن صفحة، وواحدة منها وحدها هي شجرة الصفحات. إسقاط الصفحة من /Kids وإنقاص /Count يرضي §7.7.3، ويصير كل مرجع آخر إشارة إلى كائن إما حرر في xref أو غاب ببساطة عن الملف المعاد كتابته. والعارض الذي يتبع واحدة من تلك الإشارات يحصل على null، وما يفعله بهذا null يعود إليه
- شجرة الأسماء تحت
/Names/Dests(§7.7.4، §12.3.2.3) تربط الأسماء بمصفوفات وجهات عنصرها الأول هو الصفحة - قاموس
/Destsالسابق للإصدار 1.2 مباشرة في الكتالوج يحمي النوع نفسه من المصفوفات بمفاتيح أسماء - عناصر المخطط (§12.3.3) تبلغ صفحة إما عبر
/Destداخلي أو عبر فعل/Aبـ/S /GoToومصفوفة/D - عناصر البنية (§14.7.2) تحمل مفتاح
/Pgيسمي الصفحة التي يسكن عليها محتواها الموسوم، وقد تكون أبناؤها/Kمراجع محتوى موسوم ومراجع كائنات (§14.7.4.3) مربوطة بتلك الصفحة - تربط
ParentTree(§14.7.4.4) أعداد/StructParentsللصفحات والتعليقات بعناصر البنية، ويمكن لعنصر أن يسكن هناك دون أن يظهر على سلسلة/Kمن الجذر إطلاقاً - تعليقات الروابط على صفحات أخرى (§12.5.6.5) تحمل
/Destأو فعل/GoToيستهدف الصفحة، وقد يفعل الكتالوج/OpenActionالشيء نفسه
ماذا ينظف THotPDF.DeletePage قبل أن يلمس شجرة الصفحات؟
يشغّل THotPDF.DeletePage(PageIndex) على مستند محمّل المسح المرجعي كله أولاً، ثم يميز كائن الصفحة محذوفاً بـ DeleteObj، ويفصل أي تعليقات ودجات من شجرة حقول AcroForm، ويزحف مصفوفة الصفحات الداخلية، وأخيراً يستدعي RebuildLoadedPageTree لإعادة كتابة /Kids و /Count و /Parent لكل صفحة ناجية. ويزور المسح الكتالوج بترتيب ثابت: شجرة الأسماء /Names /Dests، وقاموس /Dests الطراز القديم، و /OpenAction، وشجرة المخطط، و /StructTreeRoot مع ParentTree، وأخيراً مصفوفات /Annots لكل صفحة تبقى. وكل خطوة تقرر هل يُزال المرجع أم يُعاد توجيهه أم يُترك بحسب ما تسمح المواصفة لذلك البنية فعله دون الصفحة. حارسان يسريان قبل أي ذلك: يرفع DeletePage الخطأ Invalid page number لفهرس خارج المدى ويرفض إزالة آخر صفحة، لأن عقدة /Pages بلا أبناء ليست PDF صالحاً، بينما تأخذ DeletePages الترميز الواحد الأساس "1,3-5,7-" نفسه الذي تستخدمه عمليات صفحات المستند المحمّل الأخرى، وتتكرر من أعلى فهرس مختار نزولاً حتى تبقى الفهارس التي كتبتها صالحة أثناء عملها
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// صفرية الأساس: أسقط صفحة الغلاف. الوجهات المسماة،
// والإشارات المرجعية، وشجرة البنية، و ParentTree، وتعليقات
// الروابط التي كانت تشير إليها تُقلَّم قبل إعادة
// بناء شجرة /Pages.
Pdf.DeletePage(0);
// صيغة مدى واحدة الأساس للدفعات، وأعلى فهرس أولاً
// داخلياً حتى تبقى الفهارس الأسبق صالحة.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
كيف تختلف معالجة الوجهات المسماة عن الإشارات المرجعية؟
الوجهات المسماة تُزال والإشارات المرجعية تُعاد توجيهها، لأن اسماً لم يعد موجوداً نتيجة مقبولة بينما إشارة مرجعية بلا وجهة عيب مرئي. في شجرة /Names /Dests تجول HotPDF في كل عقدة، وتفحص كل وجهة، بالشكلين المصفوفة العارية وقاموس بمفتاح /D معاً، مقابل الصفحة المحذوفة، وتزيل زوج الاسم/القيمة حين يكون العنصر الأول للمصفوفة هو تلك الصفحة. والعقدة التي ينتهي بها /Names و /Kids كلاهما فارغين تُميَّز محذوفة وتُفصل عن أمها، فلا يبقي الشجرة أبداً أوراقاً مجوّفة. ويجري الفحص نفسه على قاموس /Dests القديم في الكتالوج، ويُسقط الكتالوج /OpenAction ببساطة إن كان يفتح على الصفحة المحذوفة. حدّ واحد هنا: حين تفقد عقدة شجرة أسماء إدخالاتها تحذف HotPDF زوج /Limits لتلك العقدة بدل إعادة حساب المفتاحين الأدنى والأعلى الجديدين، ومع أن العارضين يحلون الأسماء حسناً بدونه، فقد يشير محقق مطابقة صارم يقرأ ISO 32000-1 §7.9.6 إلى عقدة غير جذر تنقصها /Limits
أما عناصر المخطط فاتجاهها الآخر. تعبر RetargetOutlineDestinations /First و /Next من جذر المخطط، بقائمة زيارات وحد عمق 128 حتى لا يعطّل شجرة حلقيّة تالفة الاستدعاء، ولكل مصفوفة /Dest أو مصفوفة /D لفعل /GoTo تستهدف الصفحة تستبدل العنصر الأول بـ NearestRetainedPage: الصفحة التي لحقت المحذوفة، أو الصفحة التي قبلها إن كانت المحذوفة آخرها. وتُترك وسائط العرض بعد مرجع الصفحة كما كانت. فإشارة مرجعية كانت تستهدف افتتاحية فصل محذوف تقبع إذن على أول صفحة مما بقي بدل أن تختفي من الشريط الجانبي، وهو السلوك الذي يتوقعه المراجعون من مستند مقصوص. لكن فحص الوجهة يطابق المصفوفات الصريحة وحدها: عنصر مخطط /Dest فيه سلسلة اسم كانت تحل سابقاً إلى الصفحة المحذوفة لا يُعاد توجيهه، لأن إدخال شجرة الأسماء ذهب والمرجع يحل الآن إلى لا شيء لا إلى كائن حر، فيعامله العارض كإشارة مرجعية ميتة. وميكانيكا شجرة المخطط ذاتها، /First و /Next ودلالات /Count غير البديهية، مغطاة في دليل إضافة الإشارات المرجعية والوجهات المسماة على PDF محمّل
// تحقق من المسح بدل أن تثق به.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// إشارة مرجعية كانت تستهدف الغلاف تعود الآن تحل إلى
// الصفحة التي لحقتها (الفهرس الصفري 0 بعد الحذف).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
ماذا يحدث لشجرة البنية و ParentTree؟
عناصر البنية التي لا وجود لها إلا بسبب الصفحة المحذوفة تُزال، وعناصر تمتد على صفحات عدة تفقد مفتاح /Pg لكنها تبقي أبناءها. تهبط PruneStructureElement سلسلة /K من /StructTreeRoot إلى عمق 128، متعاملة مع شكل المصفوفة وشكل القاموس الواحد لـ /K اللذين يجيزهما §14.7.2. ولكل عنصر تقلّم أبناءه أولاً، ثم تقيّم العنصر ذاته: إن أفرغ التقليم /K عنه مُيِّز العنصر محذوفاً وأسقطته أمه. وإن كان /Pg للعنصر نفسه يسمي الصفحة المحذوفة وما زال للعنصر أبناء وأم /P، أُزيل /Pg وحده، لأن /Pg على عنصر هي الصفحة الافتراضية لأبنائه من المحتوى الموسوم وأولئك الأبناء قد يشيرون إلى صفحات أخرى صراحة. ولا يُزال صراحةً إلا عنصر /Pg فيه هي الصفحة المحذوفة ولم يبق تحته شيء
وتحصل ParentTree على المعاملة نفسها، والسبب هو ما عضّ أثناء التطوير: يمكن لعنصر بنية أن يكون قابلاً للبلوغ من ParentTree ومن لا شيء غيرها. تربط شجرة الأعداد أعداد /StructParents الصحيحة إما بعنصر واحد وإما بمصفوفة عناصر، وتشغّل PruneParentTreeNode التابع PruneStructureElement على كل قيمة تجدها، وتزيل القيم التي قُلّمت، وتحذف زوج /Nums حين تكون مصفوفة قيمته فارغة، وتفصل عقدة ذهب /Nums و /Kids كلاهما. التقليم الذي يقتصر على سلال /K كان سيترك تلك العناصر اليتيمة تشير عبر /Pg إلى صفحة حُررت وعبر أبنائها /MCR إلى مراجع محتوى موسوم حرّرت. وإن كنت تستخرج النص بترتيب البنية فهذا يهمك مباشرة: استخراج النص بترتيب البنية يجول في هذه الأشجار بالضبط، والعنصر بـ /Pg معدومة فقرة تسقط بصمت من ترتيب القراءة
أي تعليقات روابط على الصفحات الناجية تُزال؟
أي تعليق رابط على صفحة باقية /Dest فيه أو فعله /GoTo يشير إلى الصفحة المحذوفة يُزال مع ملكيته في شجرة البنية. تجول RemoveRetainedPageDestinationAnnotations مصفوفة /Annots لكل صفحة غير الهدف، وتطبق فحص الوجهة نفسه المستخدم للمخططات، وتميّز التعليق المطابق محذوفاً، وتسقطه من المصفوفة، ثم تستدعي PruneAnnotationReferencesInStructureTree حتى يُزال قاموس OBJR الذي سمى /Obj ذلك التعليق من عنصر بنية التابع له، مع إزالة العنصر ذاته إن كان الـ OBJR ابنه الوحيد. ترك الـ OBJR في مكانه كان سينتهك §14.7.4.3، الذي يشترط أن يشير /Obj إلى كائن قائم، وكان سيظهر في فحص PDF/UA كرابط موسوم بلا تعليق خلفه. ولاحظ اللامتماثلية مع الإشارات المرجعية: الروابط تُزال لا تُعاد توجيهها. إحالة متقاطعة في متن نص قالت "انظر الصفحة 3" تصبح خاطئة بمجرد ذهاب الصفحة 3، وتوجيهها إلى الصفحة 4 كان سيكذب بطريقة لا يكذب بها تقبع الإشارة المرجعية على أقرب فصل، فإذا احتاج سير عملك حفظ تلك الروابط فأعد توجيهها بنفسك قبل استدعاء DeletePage
لماذا يجب ألا يُسجل /MCR أو /OBJR مُزال ككائن حر؟
لأن مراجع المحتوى الموسوم ومراجع الكائنات عادة قواميس مباشرة داخل مصفوفة /K لعنصرها الأم، وسجل التغيير التزايدي يحل الكائن المباشر إلى أقرب كائن غير مباشر يحويه. حين تسقط RemoveArrayItem ابنًا من مصفوفة /K تحرر الكائن في الذاكرة فقط إذا كان THPDFLink أو قيمة غير غير-مباشرة، و MarkRemovedObject يسجل كائناً لقائمة الأحرار فقط حين يكون رقم كائنه أكبر من صفر. النسخة الأولى من هذا المسح لم تُجرِ ذلك التمييز، وكان الأثر في حفظ تزايدي بالضبط ما صُمم السجل لفعله: تجولت RegisterIncrementalChange من /MCR المباشر صعوداً إلى جذر معاملة الرسم التابع لها، وهو عنصر البنية الباقي الذي يملكه، وكتبت ذلك العنصر كـ null. فجاء مستند فقد صفحة واحدة وقد أصبح المحتوى الموسوم على الصفحات الأخرى غير موسوم بصمت. التحرك الصحيح الوحيد لابن مباشر هو تمييز وعائه متسخاً عبر TouchContainer حتى يُعاد كتابة العائد، وترك قائمة الأحرار على حالها
// تحديث تزايدي: العوائد الملموسة فقط وكائن الصفحة
// المحرر يهبطان في القسم الملحق.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// عناصر البنية الباقية التي فقدت /K منها /MCR مباشراً
// يعاد كتابتها في مكانها، ولا تُكتب أبداً كـ null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
نفس الحذر يصوغ ما لا يحرره DeletePage عمداً على مستند محمّل. تدفقات المحتوى و XObjects والتعليقات غير الودجتية للصفحة المحذوفة تُترك كائنات، لأن ملفاً محمّلاً قد يتشارك أيها مع صفحة تبقى ولا توجد طريقة رخيصة لإثبات العكس وقت الحذف. إزالة مرجع شجرة الصفحات كافية للصواب؛ أما البايتات التي ما زلت تلك الكائنات تشغلها فسؤال منفصل، و رسم تبعية الكائنات وتحليل البايتات المحتجزة هو الأداة لقياس ما ما زال المستند المقصوص يحمله
DeletePage مقابل DeleteLoadedPage: أيهما تستدعي؟
استدعِ DeletePage لأي إزالة صفحة موجهة للمستخدم، واحفظ DeleteLoadedPage للحالة التي يُعاد فيها سكب المستند كله ولا يستحق أي مرجع على مستوى المستند الاحتفاظ به. THotPDF.DeleteLoadedPage(PageIndex)، المضافة في الإصدار 2.508.0، هي النسخة الخفيفة: تزحف مصفوفة الصفحات الداخلية، وتستدعي RebuildLoadedKidsArray لإعادة كتابة /Kids و /Count، وتبطل ذاكرة الصفحات المصوّرة، وتطلق OnLoadedDocumentModified. ولا تجول في شجرة الأسماء ولا المخططات ولا شجرة البنية ولا تعليقات الصفحات الأخرى، ولا تميز كائن الصفحة محذوفاً. إنها الأداة الصحيحة داخل فرض N-up، حيث تُلحق HotPDF صحائف مركّبة حديثاً ثم تسقط كل صفحة أصلية بـ DeleteLoadedPage(0): صفحات المصدر تُستبدل بالجملة، ومحتوى الصحيفة يشير إلى موارد لا إلى كائنات الصفحات. أما لمهمة "أزل الصفحة 7 من هذا العقد" العادية فـ DeletePage هي الاستدعاء الوحيد الذي يترك مستنداً موسوماً مضبوطاً بالإشارات المرجعية ومتقاطع الروابط متسقاً بما يكفي لياجتاز محققاً، في إعادة كتابة كاملة عبر SaveLoadedDocument وفي تحديث تزايدي عبر SaveIncrementalUpdate على السواء. وكلتا الطريقتين تشحنان في HotPDF Delphi Component لـ Delphi و C++Builder، دون أي زمن تشغيل عارض خارجي أو تبعية