حذف صفحة من ملف PDF لا يحذف خطوطها ولا صورها ولا تدفقات محتواها. تستعيد losLab PDF Library هذه الكائنات عبر جامع من نوع mark-sweep يمشي في رسم الكائنات بدءًا من جذور trailer ويزيل كل كائن غير مباشر لا يصل إليه شيء. يعمل هذا الجامع عند الحفظ الكامل، وهو معطّل افتراضيًا، ويُعيد عدد الكائنات التي أزالها
لماذا لا يقلّص حذف صفحات PDF حجم الملف؟
لأن حذف الصفحة تعديل مرجعي وليس عملية تخزين. DeletePages(StartPage, PageCount) يفصل كائنات الصفحات عن شجرة الصفحات ويصلح مدخلات المخطط التفصيلي التي كانت تشير إليها. ما لا يستطيع فعله هو تقرير أن برنامج الخط وتدفق المحتوى وكائن الصورة XObject التي استخدمتها تلك الصفحات أصبحت الآن ميتة، لأن الملف لا يسجّل عند لحظة الحذف من قد يظل يشير إليها. تبقى هذه الكائنات في قائمة كائنات المستند، ويكتبها الحفظ الكامل جميعًا من جديد. والنتيجة هي الشكوى التي تبدأ بها معظم محادثات الدعم هذه: يحذف العميل تسعين بالمئة من الصفحات، يحفظ الملف، فيتقلّص حجمه باثنين بالمئة فقط. والأسوأ أن التسرب يتراكم؛ فمع كل تحميل وحذف وحفظ، ثم تحميل وحذف وحفظ من جديد، يكبر حجم الملف باطراد بينما يتناقص عدد الصفحات. هذه مشكلة مختلفة عن تلك التي يعالجها تقليص الخطوط وتقليل دقة الصور، والتي تصغّر الكائنات الحية. هنا لا تكون الكائنات كبيرة جدًا، بل ببساطة لم تعد جزءًا من المستند
مجموعة الجذور هي trailer، لا شجرة الصفحات
رسم كائنات PDF لا يملك حقل مرجع عكسي. لا تُعرّف الصيغة عدّاد مراجع ولا قائمة مؤشرات خلفية، ومفاتيح /Parent الموجودة فعلًا تخص بنى محددة مثل شجرة الصفحات لا رسم الكائنات ككل. لا شيء في كائن غير مباشر يخبرك من يشير إليه، لذا فإن السؤال "هل ما زال أحد يستخدم الكائن رقم 47" له إجابة واحدة فقط: اجتز الرسم انطلاقًا من جذر معروف وانظر هل تصل إليه. لهذا السبب فإن الجامع في losLab PDF Library هو جامع من نوع mark-sweep وليس مخططًا بعدّاد مراجع
تأتي الجذور من trailer الملف (ISO 32000-1 §7.5.5). ثلاثة مفاتيح تحملها: /Root، وهو فهرس المستند catalog في §7.7.2 الذي تتفرّع منه شجرة الصفحات والأسماء والمخطط التفصيلي و AcroForm والبيانات الوصفية جميعًا؛ و/Info، قاموس معلومات المستند؛ و/Encrypt، قاموس التشفير. أما مفتاحا trailer المتبقيان فهما طُعم مضلِّل. /ID مصفوفة من سلسلتي بايت، و/Prev إزاحة بايت صحيحة تشير إلى قسم الإحالة المرجعية السابق. لا يُعدّ أي منهما مرجعًا غير مباشر، فلا يسهم أي منهما في مجموعة الجذور. تضع losLab PDF Library قاموس trailer كاملًا في قائمة الانتظار بدل ثلاثة مفاتيح مسمّاة فقط، وهو أمر لا يكلّف شيئًا ويبقي أي امتداد خاص لـ trailer حيًا
الاجتياز نفسه تكراري وليس عودي recursive. عندما يصادف الاجتياز مرجعًا غير مباشر فإنه يسجّل فقط رقم الكائن وجيله، ويعلّم الخانة المقابلة، ويدفعها إلى طابور FIFO بدل حل المرجع فورًا، وهذا يبقي أشجار الصفحات العميقة وسلاسل المخطط التفصيلي الطويلة بعيدة عن مكدس الاستدعاء ويمنع فك ترميز الكائن نفسه مرتين. أما القواميس والمصفوفات وقواميس التدفقات المباشرة فتوضع في طابور ثانٍ محروس بمجموعة زيارة، لأن المستندات الحقيقية تحتوي على دورات حقيقية: مفتاح /Parent لصفحة يشير عائدًا إلى عقدة شجرة الصفحات الخاصة به، وعناصر المخطط التفصيلي تتسلسل عبر /Prev و/Next في الاتجاهين. أرقام الأجيال جزء من عملية المطابقة لا زخرفة؛ فالمرجع لا يُحلّ إلا حين يتطابق رقم الكائن والجيل معًا، ويُعامل المرجع إلى رقم موجود بجيل مختلف على أنه الكائن الفارغ null الذي تفرضه المواصفة، لا كحافة حية أبدًا
كيف تُفعّل جمع المهملات عند الحفظ؟
جمع المهملات اختياري opt-in وينتمي إلى سجل خيارات الحفظ. قيمته الافتراضية False لأن الجامع يمرّ مرورًا هدّامًا على رسم الكائنات، ولا ينبغي لأي مكتبة أن تحذف كائنات بصمت لم يطلب المستدعي فحصها قط
var
Pdf: TPDFlib;
Opt: TPDFlibSaveOptions;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report-500pages.pdf', '') <> 1 then
Exit;
Pdf.DeletePages(11, 490); // keep the first ten pages
FillChar(Opt, SizeOf(Opt), 0);
Opt.CompressContent := True;
Opt.CompressFonts := True;
Opt.OptimizeContentStreams := True;
Opt.PackObjectStreams := True;
Opt.GarbageCollect := True; // drop everything the pages left behind
Pdf.SaveToFileOptions('report-10pages.pdf', Opt);
finally
Pdf.Free;
end;
end;
ثمة نقطتا دخول أخريان تصلان إلى الجامع نفسه. SetGarbageCollect(1) يضبط العلامة على المستند المحدد بحيث يحترمها استدعاء SaveToFile عادي، بينما GarbageCollectObjects يشغّل التمريرة فورًا ويُعيد عدد الكائنات غير المباشرة اليتيمة التي أُزيلت. الصيغة الفورية هي ما تُستخدم حين تريد رقمًا تسجّله أو تتحقق منه، ويستحق التحقق منه فعلًا، لأن القيمة السالبة ليست عددًا
var
Removed: Integer;
begin
Pdf.DeletePages(11, 490);
Removed := Pdf.GarbageCollectObjects;
if Removed < 0 then
// The graph could not be fully decoded. Nothing was swept and the
// document is unchanged; save it without GC or reject the input.
LogWarning('object graph incomplete, GC skipped')
else
LogInfo(Format('reclaimed %d orphaned objects', [Removed]));
end;
مسار الفشل هذا أهم مما يبدو. تُفكّ ترميز الكائنات بتكاسل lazily، والكائن الذي لم يُفكّ ترميزه قط لا يُظهر أي مراجع على الإطلاق. فلو عامل الجامع كائنًا يتعذّر فك ترميزه معاملة عقدة فارغة لكسح كل ما لا يصل إليه إلا عبره. لذلك يفرض الاجتياز فك الترميز عند لمس كل كائن، وأي خطأ واحد في فك الترميز يوقف التمريرة كلها بنتيجة سالبة ويترك المستند مطابقًا بايتًا ببايت لحالته الأصلية. فكسح رسم لا تفهمه إلا جزئيًا هو بالضبط ما يحوّل ملفًا تالفًا إلى ملف مدمَّر
ما الذي يكسر جامعًا ساذجًا لملفات PDF؟
تفصيلان، وكلاهما يفشل بصمت لا بصخب. الأول هو تدفقات الكائنات object streams. منذ PDF 1.5 يمكن لكائن غير تدفقي أن يعيش مضغوطًا داخل حاوية /ObjStm (§7.5.7)، ويكون مدخل الإحالة المرجعية الخاص به من النوع 2 الذي يسمّي الحاوية إضافة إلى فهرس داخلها. فالكائن المضغوط إذن لا يُصل إليه إلا عبر حاويته. فإذا علّمت العضو وكسحت الحاوية لأن لا شيء أشار إليها كائنًا مستنديًا، فقد كتبت ملفًا يشير جدول xref فيه إلى كائن لم يعد موجودًا. الحاوية تخزين بنيوي لا بيانات مستند، فلا تظهر أبدًا كحافة في رسم الكائنات الذي تجتازه. تعالج losLab PDF Library هذا بفصل كل عضو مضغوط ناجٍ عن حاويته المصدر قبل زوال الحاويات، ثم يعيد الحفظ تعبئة الناجين في تدفقات كائنات جديدة. أما التفصيل الثاني فهو ما يشير إليه كائن التدفق فعلًا. البايتات ليست جزءًا من الرسم. فتدفق محتوى يرسم نصًا بـ/F1 12 Tf يسمّي خطًا باسم مورد resource name، ويُحلّ هذا الاسم عبر قاموس /Resources الخاص بالصفحة، فحافة الوصول تمر صفحة ← /Resources ← /Font ← كائن الخط، لا عبر حمولة التدفق أبدًا. المراجع الوحيدة التي يسهم بها التدفق تأتي من قاموسه، حيث يُسمح لـ/Length و/Filter و/DecodeParms جميعًا أن تكون غير مباشرة. الجامع الذي يحلّل بايتات التدفق بحثًا عن مراجع يقوم بعمل مكلف بلا طائل؛ والجامع الذي يتخطى قواميس التدفقات يفقد كائن الطول ويُفسد الملف
ماذا يحدث لأرقام الكائنات التي تُحرّرها؟
تصبح مدخلات حرّة، ولا تُعاد استخدامها في الحفظ نفسه. يجتاز الكسح قائمة الكائنات بترتيب تنازلي كي يظل استقرار الفهارس ثابتًا أثناء الحذف، ويعيد بناء فهرس البحث مرة واحدة في النهاية بدل إعادة بنائه بعد كل إزالة، ولكل كائن يُزال يسجّل رقمه في القائمة الحرة مع زيادة جيله بمقدار واحد، تمامًا كما يفرض §7.5.4 على مدخل قد يُعاد استخدامه لاحقًا. أما الجيل الذي بلغ 65535 بالفعل فيبقى عندها، وهو ما يعلّم ذلك الرقم متقاعدًا نهائيًا. أرقام الكائنات لا تُضغط عن قصد؛ فبعد الجمع يحتفظ الملف بفجوات: قد يكون الكائن 12 حرًا بينما 13 و14 قيد الاستخدام، ويظل /Size في trailer يُبلغ عن أعلى رقم زائد واحد لا عن عدد الكائنات الناجية. إعادة الترقيم كانت ستوفر بضع بايتات في جدول الإحالة المرجعية وتستلزم إعادة كتابة كل مرجع في المستند، وهو نوع التغيير الذي يُبطل بصمت أي شيء يحتفظ بأرقام كائنات من خارج الملف. الحجم الذي تستعيده يأتي من أجسام الكائنات لا من جدول xref
متى يجب ألا تُشغّل الجامع
أبدًا في تحديث تزايدي incremental update. الجامع مقيّد بالحفظ الكامل فقط، ولا تُقرأ العلامة أصلًا حين يُضاف إلى المستند، وهذا القيد ليس عائقًا يجب الالتفاف عليه. التحديث التزايدي (§7.5.6) يترك البايتات الأصلية دون مساس ويلحق قسم إحالة مرجعية جديدًا مرتبطًا بالسابق عبر /Prev. كل مراجعة سابقة ما زالت تشير إلى الكائنات التي كانت تشير إليها دومًا، فالكائن الذي يتعذّر الوصول إليه في المراجعة الحالية قد يكون في متناول اليد تمامًا في مراجعة أقدم. حذفه سيكسر كل مراجعة عدا الأخيرة، وتفاصيل ذلك مشروحة في المقال عن التحديثات التزايدية والحفظ بوضع الإلحاق. المنطق نفسه يستبعد جمع المهملات على مستند موقّع، لأن إعادة الكتابة الكاملة التي تجعل الجمع ممكنًا هي بالضبط ما يُبطل التوقيع
يستحق الأمر توضيحًا أيضًا: الجمع ليس أداة تعقيم sanitizer. الجامع يزيل الكائنات التي لا يشير إليها شيء؛ وليس له رأي في ما إذا كانت محتوياتها حساسة، والكائن الذي ما زال يُشار إليه يبقى كما كان. فإن كان الهدف جعل المعلومات غير قابلة للاسترجاع لا جعل الملف أصغر، فرسم الكائنات ليس الطبقة الصحيحة، بل التنقيح على مستوى التعليمات وتعقيم المستند هو الصحيح. والاثنان يتآلفان جيدًا بهذا الترتيب: نقّح وعقّم أولًا، ثم اجمع المهملات، كي تغادر الكائنات التي فصلها التنقيح الملف فعلًا. التناغم نفسه موجود في واجهة تطهير الموارد resource purge، حيث يجعل تمرير خيار جمع المهملات عملية التطهير تُشغّل جمعًا بعدها وتُبلغ عن الكائنات اليتيمة التي أزالتها في OrphanObjectsRemoved
عادة أخيرة تستحق التبني: سجّل القيمة المُعادة من GarbageCollectObjects في أي مهمة دفعية batch تقوم بحذف صفحاتك، وراقبها على مدى أسابيع من مستندات حقيقية. صفر على ملف قللت حجمه للتو إلى النصف يعني أن جهة ما أعلى في السلسلة ما زالت تحتفظ بمرجع لم تتوقعه، وعادة ما يكون مدخل شجرة أسماء name tree أو وجهة في المخطط التفصيلي أو حقل AcroForm نجا من الصفحة التي كان مرتبطًا بها. الجامع هو أرخص مصحّح إمكانية وصول reachability ستملكه على الإطلاق، لأنه يجيب عن السؤال الذي ترفض صيغة PDF نفسها الإجابة عنه
جامع المهملات وسجل خيارات الحفظ وواجهة تطهير الموارد الموصوفة هنا جزء من losLab PDF Library لـ Delphi وC++Builder، وتحمل صفحة المنتج المرجع الكامل لخط أنابيب الحفظ متضمنًا التفاعل بين الجمع وتعبئة تدفقات الكائنات والتخطية linearization