افتح ملف PDF عاش بضع سنوات في استخدام إنتاجي، وغالبًا ستجد فيه سجلات إدارية أكثر من المحتوى نفسه: آلاف الكائنات القاموسية الصغيرة، يسبق كل واحد منها رأس obj خاص به، مع جدول إحالة متقاطعة يكلف 20 بايت ASCII لكل كائن. في حالة دعم حللناها، كان أرشيف سياسات حجمه 58 MB ينفق أقل من نصف بايتاته على محتوى الصفحات الفعلي؛ أما الباقي فكان كلفة بنيوية ومراجعات قديمة. يتيح HotPDF، وهو مكتبة PDF أصلية بنمط VCL لـ Delphi وC++Builder، آليتي تنسيق الملف اللتين تعالجان نصفي هذه المشكلة: تدفقات الكائنات للتخزين المضغوط، والتحديثات التزايدية للتعديل بالإلحاق فقط من دون إتلاف ما سبق
كيف تعيد تدفقات الكائنات وتدفقات xref تشكيل الملف
حتى PDF 1.4، كان كل كائن غير مباشر يجلس غير مضغوط في جسم الملف، وكان جدول الإحالة المتقاطعة في النهاية بنية نصية ثابتة العرض: 20 بايت بالضبط لكل إدخال، ومن دون أي ضغط. لذلك يحمل مستند يضم 200,000 كائن نحو 4 MB من بيانات xref وحدها، قبل رسم أي محرف. أدخل PDF 1.5 بديلين، موصوفين في ISO 32000-1 §7.5.7 و§7.5.8: تدفقات الكائنات (/Type /ObjStm)، التي تجمع الكائنات غير التدفقية داخل حاوية واحدة مضغوطة بـ Flate، وتدفقات الإحالة المتقاطعة، التي تخزن جدول البحث نفسه كبيانات ثنائية مضغوطة ذات حقول متغيرة العرض
تظهر الوفورات الأكبر في الموضع نفسه الذي تؤذي فيه المولدات الساذجة أكثر من غيره: مستندات النماذج الكثيفة التي تضم آلاف قواميس الحقول، وأشجار المخطط التفصيلي العميقة، وعناصر بنية PDF الموسوم. هذه الكائنات صغيرة جدًا ومنفردة وكثيرة التكرار، وهذا يجعلها مدخلًا مثاليًا لـ Flate بعد جمعها معًا. كانت تدفقات محتوى الصفحات قابلة للضغط أصلًا قبل PDF 1.5، لذلك لا تقلص تدفقات الكائنات الملفات التي تهيمن عليها الصور كثيرًا؛ لكنها تقلص الملفات التي تهيمن عليها البنية بدرجة كبيرة
في HotPDF تُفعّل الميزتان بزوج من الخصائص، كما أن اعتماد الترتيب مهم:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.UseXRefStream := True; // binary xref, prerequisite for ObjStm
Pdf.UseObjectStreams := True; // pack objects into /Type /ObjStm
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
Pdf.EndDoc; // emits XRefStm + ObjStm containers
finally
Pdf.Free;
end;
end;
تتطلب UseObjectStreams أن تكون UseXRefStream مساوية لـ True، لأن الكائن المضغوط يُعنون بإدخال xref من النوع 2، أي رقم تدفق الكائنات مع الفهرس، ولا يمكن ببساطة تمثيل إدخالات النوع 2 في جدول نصي كلاسيكي عرضه 20 بايت. ضبط UseObjectStreams وحدها لا يحقق شيئًا؛ ضبطهما معًا قبل BeginDoc هو التهيئة العاملة
حد التوافق عند PDF 1.4
تبدأ الخاصيتان بالقيمة False لسبب يلسع الفرق التي لديها تكاملات قديمة. قارئ محدود بدلالات PDF 1.4 لا يبلغ عادة عن "compressed objects unsupported" عندما يواجه تدفق xref؛ بل يبلغ غالبًا عن جدول إحالة متقاطعة تالف أو يرفض الملف مباشرة، لأن تخطيط كلمة trailer المفتاحية الذي يتوقعه غير موجود. إذا كان مخرجك يغذي بوابة فاكس قديمة، أو طابعة عتادية بمفسر مدمج، أو محللًا لاحقًا كُتب على أساس PDF 1.4، فاترك العلمين متوقفين لذلك المسار واقبل الملف الأكبر. أما لقنوات الأرشفة والتوزيع عبر الويب، حيث تتعامل كل العارضات الرئيسية مع PDF 1.5 منذ عقدين، فإن تفعيلهما يكاد يكون ضغطًا مجانيًا
هناك أثر تشغيلي جانبي يستحق تنبيه فريق الدعم إليه: عندما تُحزم القواميس داخل تدفقات كائنات، يصبح الفرق الثنائي على مستوى البايت بين ملفين مولدين بلا معنى، لأن تغيير حقل واحد قد يعيد ضغط حاوية كاملة. ينبغي أن تقارن تحقيقات الدعم المستندات منطقيًا، حسب محتوى الكائنات، لا عبر diff ثنائي
لماذا توجد التحديثات التزايدية: الإزاحات والتوقيعات وسجلات التدقيق
يغطي التوقيع الرقمي في PDF نطاق /ByteRange صريحًا: مقطعين من الملف الفيزيائي، مقاسين بإزاحات بايت مطلقة، حُسبت فوقهما بصمة CMS. أعد كتابة الملف، حتى مع محتوى مرئي مطابق، وستتحرك كل إزاحة؛ عندها لا تعود البصمة مطابقة ويظهر التوقيع مكسورًا. هذا هو السبب العملي الذي جعل ISO 32000-1 §7.5.6 يعرّف التحديثات التزايدية: تُلحق الكائنات المتغيرة والمضافة بعد %%EOF الموجود، ثم يتبعها قسم إحالة متقاطعة جديد تشير خانة /Prev فيه إلى القسم السابق. لا تُلمس البايتات الأصلية أبدًا، لذلك تبقى المراجعة الموقعة سابقًا قابلة للتحقق، ويمكن لـ Acrobat عرض كل مراجعة موقعة على حدة في لوحة التواقيع
يلف HotPDF هذا الأمر كنقطة دخول مخصصة:
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf'); // appends the delta only
هناك تفصيلان يسهل الخطأ فيهما. أولًا، يجب تمرير اسم الملف الأصلي إلى BeginIncrementalUpdate؛ فالقسم الملحق من xref يخزن إزاحات لا تكون صحيحة إلا بالنسبة إلى تلك البايتات الأصلية نفسها. ثانيًا، الحفظ بالإلحاق فقط بحكم التعريف، لذلك يكون المخرج دائمًا أكبر من المدخل. هذا ليس عيبًا ينبغي تحسينه بعيدًا؛ بل هو الخاصية التي تبقي المراجعات الموقعة السابقة سليمة
تحرير المستندات المحملة: LoadFromFile وليس BeginDoc
هناك فخ منفصل يقع فيه المطورون الذين تعلموا HotPDF من واجهة التوليد. BeginDoc يبدأ مستندًا جديدًا؛ وهو الاستدعاء الخطأ عندما يكون الهدف تعديل مستند قائم. جراحة المستندات تمر عبر مسار المستند المحمل:
PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5); // pages 1-3 after page 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');
عرض الخلط بين النموذجين هو ملف يحتوي فقط على محتواك الجديد ولا يحتوي على شيء من الأصل، لأن BeginDoc أنشأ بكل بساطة مستندًا جديدًا إلى جانب المستند الذي ظننت أنك تعدله. عند مراجعة الشيفرة، تعامل مع LoadFromFile + SaveLoadedDocument كمفردة زوجية واحدة، ومع BeginDoc + EndDoc كمفردة أخرى؛ والإجراء الذي يستخدم الزوجين للمستند نفسه يكون في الغالب خطأ
احتواء النمو: متى تضغط ملفًا أُلحقت به مراجعات
للحفظ بالإلحاق فقط كلفة طويلة الأمد. مهمة ليلية تختم سطر حالة على ملف PDF نفسه تنتج 365 مراجعة في السنة، وكل مراجعة تسحب معها قسم xref جديدًا. بعد أن تؤدي سجلات المراجعة غرضها، وبشرط ألا يكون هناك توقيع يجب أن يبقى صالحًا، يمكن ضغط الملف بإعادة تسلسله عبر مسار المستند المحمل:
Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');
إعادة الحفظ هي إعادة كتابة كاملة، أي إنها تتعمد إسقاط المراجعات السابقة وستبطل أي توقيع في الملف، لذلك ضعها خلف فحص السياسة نفسه الذي تستخدمه قبل أي عملية مدمرة. قاعدة إنتاجية معقولة رأيناها تعمل: اضغط عندما يتجاوز عدد المراجعات عتبة معينة أو عندما تتجاوز كلفة الإلحاق نسبة من الملف الأساسي، ولا تضغط أبدًا الملفات التي تكون لوحة التواقيع فيها غير فارغة
فحص النتيجة قبل الشحن
التحقق من هذا الزوج من الميزات ميكانيكي على نحو مريح. افتح المخرج في Adobe Acrobat وافحص ثلاثة أشياء: خصائص المستند تبلغ عن PDF 1.5 أو أحدث عندما تكون تدفقات الكائنات مفعلة؛ ولوحة التواقيع لا تزال تتحقق من كل مراجعة موقعة سابقًا بعد تحديث تزايدي؛ وعدد الصفحات والإشارات المرجعية نجيا من دورة تحميل وتعديل وحفظ. لقنوات الأرشفة، مرر الملف عبر veraPDF أيضًا، لأن بنى xref المضغوطة من النوع الذي يفحصه محلل صارم بعناية أكبر من عارض متسامح. إذا كنت تعالج أيضًا مدخلات كبيرة جدًا، فإن تقنيات التفتيش في شرحنا العملي لـ Direct File API في سير عمل ملفات PDF الكبيرة تمتزج جيدًا مع الحفظ التزايدي، كما أن آليات التوقيع المشار إليها أعلاه مشروحة بتفصيل في مقال تواقيع HotPDF الرقمية وPAdES
الأسئلة الشائعة
هل يؤدي تفعيل تدفقات الكائنات إلى كسر قارئات PDF القديمة؟
القارئات التي لا تنفذ إلا PDF 1.4 لا تستطيع تحليل تدفقات xref وتبلغ عادة عن الملف كتالف. أبقِ UseXRefStream وUseObjectStreams على قيمتهما الافتراضية False للقنوات التي تغذي مفسرات قديمة، وفعلهما لقنوات العارضات الحديثة والأرشفة
هل يحافظ التحديث التزايدي على صلاحية توقيعي الرقمي؟
نعم، هذا هو الغرض منه: تُلحق الكائنات الجديدة بعد البايتات الموقعة، لذلك لا يزال /ByteRange الموقّع ينتج البصمة الصحيحة. إعادة الكتابة الكاملة، بما في ذلك الضغط عبر التحميل وإعادة الحفظ، تكسر كل توقيع قائم حتى عندما لا يتغير المحتوى المرئي
لماذا يستمر ملفي في النمو بعد عمليات حفظ متكررة؟
الحفظ التزايدي يلحق delta في كل عملية حفظ ولا يستعيد المساحة أبدًا. اضغط من حين إلى آخر باستخدام LoadFromFile مع SaveLoadedDocument بعد أن لا تعود سجلات المراجعة والتواقيع بحاجة إلى الحفظ
إلى أين تذهب بعد ذلك
كلتا الميزتين جزء من مجموعة الميزات القياسية في HotPDF Component، إلى جانب واجهات التوليد والنماذج والتشفير والتوقيع المعروضة في مواضع أخرى من هذه المدونة. ترتبط صفحة المنتج بالمرجع الكامل للـ API إذا أردت ربط الاستدعاءات أعلاه بخط أنابيب المستندات لديك