مقال تقني

تدفقات الكائنات والتحديثات التدريجية في Delphi باستخدام HotPDF

قدم PDF 1.5 بنيتين للتخزين لم تكن هناك طريقة للتعبير عنهما في تنسيق الملفات السابق: تدفق الكائنات (object stream) وتدفق المراجع التبادلية (cross-reference stream). تدفق الكائن هو حاوية واحدة مضغوطة بتنسيق Flate، موسومة بـ /Type /ObjStm، والتي تحمل العديد من الكائنات الصغيرة غير المباشرة المعبأة جنبًا إلى جنب بدلاً من بعثرتها عبر نص الملف. تدفق المراجع التبادلية هو جدول البحث الخاص بالملف المُعاد كتابته كثنائي مضغوط مع حقول متغيرة العرض، بدلاً من جدول ASCII ذي العرض الثابت الذي كان يغلق كل ملف PDF حتى الإصدار 1.4. هما ينتقلان معًا. بمجرد طي الكائنات في تدفق، لا يمكن لجدول النص القديم معالجتها بعد الآن، لذلك يجب أن يأتي المرجع التبادلي الثنائي معها

قارن ذلك بالتخطيط الكلاسيكي ومن السهل رؤية التكلفة التي يزيلها. في ملف PDF 1.4، يوجد كل كائن غير مباشر غير مضغوط خلف ترويسة obj الخاصة به، ويستهلك الجدول الموجود في الذيل بالضبط 20 بايت من ASCII لكل إدخال، والضغط ممنوع. يحمل المستند الذي يحتوي على 200,000 كائن ما يقرب من 4 ميغابايت من بيانات المراجع التبادلية قبل رسم حرف رسومي واحد، مع تكديس جميع أجسام القاموس غير المضغوطة في الأعلى. يهاجم PDF 1.5 كلا الرقمين في وقت واحد: تُطوى القواميس في حاويات Flate، ويتقلص جدول الـ 4 ميغابايت إلى بضع مئات من الكيلوبايت من البيانات الثنائية. يحدد ISO 32000-1 البنيتين في §7.5.7 و §7.5.8

أين يتحقق التوفير فعليًا

تؤثر تدفقات الكائنات فقط على الكائنات التي ليست تدفقات (non-stream objects)، لذا فهي تضغط البنية، وليس وحدات البكسل. كان محتوى الصفحة مضغوطًا بالفعل باستخدام Flate قبل الإصدار 1.5، وتحمل بيانات الصور برامج الترميز الخاصة بها، وهذا هو السبب في أن كتيبًا مليئًا بالصور بالكاد يتغير حجمه. الملفات التي يتقلص حجمها بشكل كبير هي تلك التي تعتمد بكثافة على البنية: نماذج AcroForms التي تحتوي على آلاف من قواميس الحقول، أشجار المخططات التفصيلية العميقة، عناصر البنية في tagged-PDF. تلك الكائنات صغيرة جدًا، وعددها كبير، ومتطابقة تقريبًا مع بعضها البعض، وهذا التكرار هو بالضبط ما يستغله Flate بمجرد وضعها في مخزن مؤقت (buffer) واحد بدلاً من انتشارها عبر نص الملف مع ترويسات محشورة بينها

من السهل التقليل من مقدار ما يمثله العبء الإضافي (overhead) في ملف قديم. يمكن لأرشيف نموذج استوعب سنوات من التعديلات أن ينفق أكثر من نصف بايتاته على ترويسات القاموس، وحشوة المرجع التبادلي (xref padding)، والمراجعات التي لن ينظر إليها أي قارئ أبدًا. الميزتان هنا تستعيدان أول اثنتين من هذه. الثالثة، المراجعات المتراكمة، لا تستسلم إلا للضغط (compaction)، بمجرد ألا يضطر الملف لتذكر تاريخه الخاص

في 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. يتم الوصول إلى الكائن المضغوط من خلال إدخال مرجع تبادلي من النوع 2 (type-2 xref)، والذي يسجل رقم تدفق كائن بالإضافة إلى فهرس، ولا يوجد مكان في صف النص الكلاسيكي المكون من 20 بايت لتخزين هذا الزوج. لذلك فإن UseObjectStreams بمفردها لا تفعل شيئًا مرئيًا؛ كلا العلامتين، المعينتين قبل BeginDoc، هي التكوين الذي يعمل. قم بتعيينهما بعد BeginDoc وسيكون HotPDF قد التزم بالفعل بالتخطيط الأقدم

لماذا يكون الوضع الافتراضي لكليهما متوقفًا

يترك HotPDF كلا الخاصيتين False افتراضيًا، ويظهر السبب في عمليات الدمج مع الأكواد القديمة في المراحل اللاحقة. القارئ الذي يفهم فقط PDF 1.4 لا يعلن أنه لا يمكنه التعامل مع الكائنات المضغوطة. يواجه تدفق مرجع تبادلي (xref stream)، ولا يجد أيًا من الكلمات الرئيسية للذيل (trailer) التي يتوقعها، ويبلغ عن جدول مرجع تبادلي تالف أو ببساطة يرفض فتح الملف. إذا كانت مخرجاتك تتدفق إلى بوابة فاكس قديمة، أو طابعة أجهزة تعمل بمترجم مضمن، أو محلل لغوي كتبه شخص ما مقابل مواصفات 1.4 منذ عقد من الزمن، فاترك كلا العلامتين متوقفتين لتلك القناة وتعايش مع الملف الأكبر حجمًا. أما بالنسبة للتخزين الأرشيفي وتوصيل الويب، حيث يقرأ كل عارض رئيسي PDF 1.5 منذ عشرين عامًا، فإن تشغيلهما هو ضغط تحصل عليه مقابل لا شيء تقريبًا

هناك تأثير من الدرجة الثانية يستحق إخبار فريق الدعم الخاص بك عنه. بمجرد تجميع القواميس في تدفقات كائنات، فإن مقارنة ملفين تم إنشاؤهما بايت مقابل بايت تتوقف عن إعطاء أي معنى، لأن تغيير حقل واحد يمكن أن يعيد ضغط حاوية بأكملها ويخلط كل شيء بعدها. قارن اختلافات مثل هذه الملفات حسب محتوى الكائن، وليس باستخدام مقارنة ثنائية

التحديثات التدريجية (Incremental updates) وإزاحات البايت (byte offsets) التي تحميها

يغطي التوقيع الرقمي /ByteRange صريح: فترتان (spans) من الملف المادي، يُعطى كإزاحات بايت مطلقة، والتي تم أخذ ملخص CMS عليها. إذا أعدت كتابة الملف، حتى إلى شيء يبدو متطابقًا على الشاشة، فستتحرك كل تلك الإزاحات. يتوقف الملخص عن المطابقة ويُقرأ التوقيع على أنه معطل. هذه هي المشكلة الدقيقة التي يحلها ISO 32000-1 §7.5.6 مع التحديثات التدريجية (incremental updates). يتم إلحاق الكائنات الجديدة والمعدلة بعد %%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 اسم الملف الأصلي، لأن قسم المرجع التبادلي المُلحق يسجل الإزاحات التي لا تكون ذات معنى إلا مقابل تلك البايتات الأصلية الدقيقة؛ إذا قمت بتوجيهه إلى نسخة تمت إعادة تسميتها أو إعادة حفظها، فإن الإزاحات تصف ملفًا لم يعد موجودًا. والحفظ يكون بإلحاق فقط (append-only) من حيث التصميم، لذلك يكون الإخراج دائمًا أكبر من الإدخال. هذا النمو ليس إهدارًا يمكن التخلص منه. إنها نفس الخاصية التي تترك المراجعات الموقعة السابقة سليمة

تعديل ملف مُحمّل يتم من خلال LoadFromFile

يميل المطورون الذين واجهوا HotPDF لأول مرة من خلال واجهة برمجة تطبيقات الإنشاء (generation API) الخاصة به إلى الاصطدام بجدار معين. يفتح 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 كمفردات أخرى. الروتين الذي يصل إلى كليهما مقابل نفس الملف يكون خاطئًا دائمًا تقريبًا

متى تضغط ملفًا مُلحقًا (compact an appended file)

يحمل الحفظ بإلحاق فقط تكلفة بطيئة. وظيفة ليلية تختم سطر حالة واحدًا على نفس الـ PDF تنتج 365 مراجعة عبر السنة، وكل مراجعة تسحب قسم مرجع تبادلي جديد وراءها. عندما يتجاوز هذا السجل فائدته، ولا يحتاج أي توقيع في الملف إلى البقاء، يمكنك تسطيح (flatten) كل شيء عن طريق إعادة إجراء التسلسل (re-serializing) من خلال مسار المستند المحمل:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

إعادة الحفظ هذه هي إعادة كتابة كاملة. إنها تتخلص من المراجعات السابقة عن قصد وتكسر أي توقيع لا يزال في الملف، لذلك ضعها خلف نفس بوابة السياسة التي تطبقها على أي خطوة مدمرة أخرى. إحدى قواعد الإنتاج التي تصمد: اضغط (compact) عندما يتجاوز عدد المراجعات حدًا معينًا، أو عندما يتجاوز العبء الإضافي المُلحق حصة ما من الملف الأساسي، ولا تضغط أبدًا مستندًا تحتوي لوحة توقيعه على أي شيء

فحص المخرجات قبل شحنها

إن التحقق من هذا الزوج من الميزات ملموس بشكل منعش. افتح النتيجة في Adobe Acrobat وتأكد من ثلاث نقاط: تبلغ خصائص المستند عن PDF 1.5 أو أحدث بمجرد تشغيل تدفقات الكائنات؛ لا تزال لوحة التوقيع تتحقق من صحة كل مراجعة سابقة تم توقيعها بعد تحديث تدريجي؛ وأن عدد الصفحات والإشارات المرجعية قد مرت عبر دورة تحميل وتعديل وحفظ دون أن تصاب بأذى. بالنسبة للإخراج الأرشيفي، ادفع الملف عبر veraPDF أيضًا، نظرًا لأن المرجع التبادلي المضغوط هو بالضبط نوع البنية التي يدقق فيها المدقق الصارم عن كثب أكثر مما قد يفعله عارض متسامح. إذا كان عملك يتضمن أيضًا مدخلات كبيرة جدًا، فإن طرق الفحص في دليلنا التفصيلي حول Direct File API لسير عمل PDF الكبيرة تقترن بشكل طبيعي مع الحفظ التدريجي، ويتم تناول آليات التوقيع وراء نطاقات البايت (byte ranges) المذكورة أعلاه بعمق في مقال التواقيع الرقمية لـ HotPDF و PAdES

يتم شحن كلتا الميزتين كجزء من مكون HotPDF لـ Delphi و C++Builder، بجوار واجهات برمجة تطبيقات الإنشاء والنماذج والتشفير والتوقيع التي تم تناولها في مكان آخر في هذه المدونة. تربط صفحة المنتج المرجع الكامل لواجهة برمجة التطبيقات إذا كنت ترغب في محاذاة الاستدعاءات المذكورة أعلاه مع خط أنابيب المستندات (document pipeline) الخاص بك