تدفقات كائنات PDF 1.5 تعبّئ كائنات غير مباشرة صغيرة كثيرة داخل حاوية واحدة مضغوطة بـFlate، وتُصدرها losLab PDF Library عند الحفظ الكامل عبر علامة PackObjectStreams. المكسب حقيقي: مئات قواميس الصفحات والخطوط والتعليقات التوضيحية التي تكلّف كل منها عشرات البايتات غير المضغوطة تنهار إلى حفنة من الكتل المضغوطة. أما الثمن فهو أن كل كائن معبّأ يحتاج الآن تدفق إحالة مرجعية يصفه
هذا النصف الثاني هو ما يكسر الكاتبين. بناء حاوية /ObjStm عملية حسابية بسيطة؛ أما تعليم آلية الإحالة المرجعية أن تشير إلى داخلها فهو إعادة تصميم. الكاتب الذي ينتج حاوية صحيحة تمامًا ثم يصف أعضاءها بإزاحات من النوع 1 العادية ينتج ملفًا سيفتحه Acrobat فقط ريثما يُعلن تلفه. الميزتان في الواقع ميزة واحدة، ويغطي هذا المقال جانب الكتابة لكليهما، كما هما معرّفتان في ISO 32000-1 §7.5.7 و§7.5.8
ما الذي تحتويه حاوية ObjStm فعلًا؟
تدفق الكائنات هو تدفق تُشكّل بايتاته المفكوكة منطقتين متتاليتين، ويمنح ISO 32000-1 §7.5.7 القاموس ثلاثة مفاتيح بالضبط تهم البناء. /Type /ObjStm يعرّفه، و/N يعطي عدد الأعضاء، و/First يعطي طول منطقة الترويسة بالبايت — أي بعبارة أخرى، الإزاحة التي يبدأ عندها الجسم. الترويسة أزواج مفصولة بمسافات بيضاء من رقم الكائن والإزاحة؛ والجسم هو الأعضاء مسلسلين تباعًا، وتُقاس كل إزاحة من بداية الجسم لا من بداية الحمولة المفكوكة. قراءة حاوية مفكوكة كاملًا توضح ذلك: أدناه، قيمة /First هي 14 لأن أسطر الترويسة الثلاثة تشغل أربعة عشر بايتًا، ويقع الكائن 7 على بُعد 55 بايتًا داخل الجسم لأن الكائن 4 تسلسل إلى 54 حرفًا زائد فاصل
// Decoded payload of: 12 0 obj << /Type /ObjStm /N 3 /First 14
// /Filter /FlateDecode /Length 118 >> stream
4 0
7 55
9 90
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
<< /Type /ExtGState /CA 1 /ca 1 >>
[ 0 0 595 842 ]
قاعدتا عضوية مطلقتان تأتيان مباشرة من §7.5.7. لا يمكن لكائن تدفق أن يكون عضوًا أبدًا، لأن التدفق يحمل بايتات خامًا كان سيتعين تعشيشها داخل تدفق آخر. ويجب أن يكون العضو قيمة كائن كاملة، لا مرجعًا غير مباشر مجردًا أبدًا — فكائن مضغوط ليس إلا 5 0 R يخلق إحالة لا يستطيع القارئ حلها دون معرفة مسبقة بوجهتها. تستبعد losLab PDF Library كلتا الحالتين أثناء جمع المرشحين، إلى جانب قاموس التشفير والكائن 0، ثم تعبّئ كل ما نجا في مجموعات من 200 لكل حاوية. هذا السقف قرار وصول عشوائي random-access لا حد تفرضه المواصفة: فالقارئ الذي يريد عضوًا واحدًا يضطر إلى فك ضغط الحاوية كاملة، فالحاويات الضخمة تجعل عمليات البحث الصغيرة مكلفة
لماذا يجب أن تستخدم أعضاء ObjStm مدخلات إحالة مرجعية من النوع 2؟
لأن الكائن المعبأ لا يملك إزاحة ملف يُسجّلها. يجيب ISO 32000-1 §7.5.8 عن هذا بثلاثة أنواع مدخلات في تدفق إحالة مرجعية ثنائي: النوع 0 للكائنات الحرة، والنوع 1 للكائنات العادية قيد الاستخدام المخزّنة عند إزاحة بايت، والنوع 2 للكائنات المضغوطة، اللذين يحمل حقلا بياناتها رقم كائن الحاوية وفهرس العضو داخلها. لا توجد طريقة للتعبير عن كائن معبأ في جدول xref النصي الصرف الكلاسيكي، وهذا بالضبط سبب تقديم PDF 1.5 للميزتين معًا
الترتيب اللاحق يوقع في الخطأ كل تطبيق أول تقريبًا، بما في ذلك تطبيقنا. تحصل الكائنات العادية على مدخلات من النوع 1. حاويات /ObjStm نفسها تحصل على مدخلات من النوع 1، لأن الحاوية كائن تدفق غير مباشر عادي تمامًا مكتوب عند إزاحة حقيقية. فقط الأعضاء تحصل على مدخلات من النوع 2. وتدفق الإحالة المرجعية نفسه كائن غير مباشر في الملف، فهو يحتاج مدخل النوع 1 الخاص به يشير إلى الإزاحة التي كُتب عندها للتو — الإزاحة نفسها التي يسجلها startxref. نسخة مبكرة من كاتبنا استبعدت أرقام كائنات الحاويات من حلقة الكتابة بدل استبعاد الأعضاء، وكانت النتيجة ملفًا فيه تدفق إحالة مرجعية بلا أي تدفقات كائنات على الإطلاق: متماسك بنيويًا، فارغ دلاليًا، مرفوض في المرحلة التالية. تُخفي قيمة /Size خطأ إزاحة واحد مماثلًا، لأنها أعلى رقم كائن زائد واحد، وتدفق الإحالة المرجعية يُخصَّص له أعلى رقم كائن، فيجب عدّه أيضًا
تحديد حجم مصفوفة /W: لماذا لا تكفي أربعة بايتات
تعلن مصفوفة /W عرض كل حقل من الحقول الثلاثة بالبايت، وتكتبها losLab PDF Library كـ/W [1 Field2 Field3] بحقل 1 ثابت عند بايت واحد لرمز النوع وحقل 3 ثابت عند بايتين، ما يغطي أرقام الأجيال حتى 65535 وفهارس الأعضاء على حد سواء. الحقل 2 هو الذي لا يمكن أن يكون ثابتًا، لأنه يحمل كميتين غير مرتبطتين: ففي مدخل من النوع 1 هو إزاحة بايت محدودة فقط بحجم الملف، بينما في مدخل من النوع 2 هو رقم كائن حاوية، وفي مدخل من النوع 0 هو الكائن الحر التالي في السلسلة. الحقل 2 الثابت عند أربعة بايتات يعمل جيدًا حتى يتجاوز الملف 4 جيجابايت، وعندها تُقتطع بصمت كل إزاحة تتجاوز الحد ويتحول الجدول بأكمله إلى بيانات فاسدة. لذا يفحص الكاتب الجدول المجمَّع بحثًا عن أكبر قيمة قد تحملها أي خانة حقل 2 على الإطلاق، بما في ذلك إزاحة تدفق الإحالة المرجعية نفسه، ويوسّع الحقل حتى ثمانية بايتات
// Field 2 must hold the largest byte offset AND the largest
// ObjStm container number AND the largest free-chain target.
MaxField2Value := XRefStart;
for X := 0 to MaxObj do
begin
if XRefTable[X].InUse and (XRefTable[X].ObjStrNum > 0) then
Field2Value := XRefTable[X].ObjStrNum // type-2: container number
else
Field2Value := XRefTable[X].ObjPos; // type-1 offset / type-0 next-free
if Field2Value > MaxField2Value then
MaxField2Value := Field2Value;
end;
Field2 := 4;
while (Field2 < 8) and
(MaxField2Value > ((Int64(1) shl (Field2 * 8)) - 1)) do
Inc(Field2);
Field3 := 2; // generation numbers and member indices both fit
حالما تُعرف العروض تُعرف حجم الحمولة بدقة، فيخصّص الكاتب المخزن المؤقت بأكمله سلفًا ويملؤه بالفهرس؛ أما إلحاق المدخلات بايتًا بايت إلى AnsiString فيجعل بناء الجدول تربيعيًا، وهو ما لا يلاحظه أحد في فاتورة من عشر صفحات ويلاحظه الجميع في مستند من مئتي ألف كائن. تفصيلان آخران يبقيان القراء الصارمين راضين. يعلن /Index نطاقات أرقام الكائنات التي يغطيها الجدول، وفي إعادة الكتابة الكاملة يكون ذلك ببساطة [0 N] بلا فجوات. وكل خانة لم يُصدرها الكاتب فعليًا يجب أن تُهيَّأ إلى حرة لا قيد الاستخدام افتراضيًا: الكائن 0 يتصدّر السلسلة الحرة، وكل خانة حرة ترتبط بالتالية، والخانة التي احتوت كائنًا محذوفًا سابقًا تُبقي رقم جيلها مزيدًا بواحد. الملاحظة المرافقة عن أمان الذاكرة عند تحليل ملفات PDF غير موثوقة تسوق حجة الحدود نفسها من جانب القراءة
لماذا يجب ألا يُشفَّر تدفق الإحالة المرجعية أبدًا؟
لأن على القارئ تحليله قبل أن يعرف كيف يفك تشفير أي شيء. تدفق الإحالة المرجعية هو ما يخبر القارئ أين يعيش قاموس /Encrypt؛ فلو كانت بايتاته نفسها مشفّرة لاحتاج القارئ مفتاح الملف ليجد الكائن الذي يصف مفتاح الملف. تفرض losLab PDF Library هذا في محمول واحد: يُعيد ShouldCryptStreamData القيمة False كلما حمل قاموس التدفق /Type /XRef، فيبقى الاستثناء قائمًا مهما كان المسار الذي يصل إلى المُسلسِل
تحاويةُ /ObjStm تلقى المعاملة المعاكسة، وعدم التماثل هذا مقصود. تُشفَّر الحاوية كاملة، بمفتاح مبني على رقم كائنها الخاص، تمامًا كأي تدفق آخر. لا تُشفَّر أعضاؤها فرادى — بل تُعبَّأ بصيغتها النصية الصريحة المفكوكة، وتغطيها المرَّة الواحدة على الحاوية المجمَّعة، بما فيها السلاسل النصية. تشفير الأعضاء مرتين ينتج ملفًا يُفك تشفيره إلى نص مشفَّر، ولأن الطبقة الخارجية تنجح يظهر الفشل كخطأ تحليل عميق في رسم الكائنات لا كفشل مصادقة. كائن واحد يبقى خارج المخطط تمامًا: في مستند مشفَّر يُبقى الفهرس Catalog كائنًا مباشرًا من النوع 1 ولا يُعبَّأ أبدًا، لأن تعبئته كانت ستجبر المحمّل على فك ضغط تدفق كائنات وفك تشفيره للوصول إلى جذر المستند، قبل أن يكتمل سياق فك التشفير الذي يساعد الجذر نفسه على تأسيسه
تفعيل التعبئة من Delphi
المفتاح العام هو PackObjectStreams، مكشوف كحقل في TPDFlibSaveOptions، وكدالة ضبط مستقلة SetPackObjectStreams، وكخاصية على كائن المستند. مفعّل افتراضيًا ومقيّد تلقائيًا بحسب الإصدار: يعبّئ الكاتب فقط حين يكون المستند بالفعل PDF 1.5 أو أحدث، ويستدعي حارس الحد الأدنى للإصدار الداخلي بحيث يُرفَع المستند المعبأ إلى 1.5 بدل أن يُوسَم خطأً. بعد الحفظ، يُبلغ GetLastSaveUsedObjectStreams عمّا إذا كان البوابة قد فُتحت فعلًا، وهذا هو التأكيد assertion الذي تريده في اختبار الانحدار لا مقارنة حجم البايتات
var
Doc: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('report.pdf', '') <= 0 then
Exit;
Doc.SetInformation(0, '1.5'); // packing is gated on PDF 1.5+
FillChar(Options, SizeOf(Options), 0);
Options.CompressContent := True;
Options.GarbageCollect := True; // drop orphans before packing
Options.PackObjectStreams := True;
if Doc.SaveToFileOptions('report-packed.pdf', Options) = 1 then
if Doc.GetLastSaveUsedObjectStreams = 1 then
Writeln('Saved with ObjStm containers and an xref stream');
finally
Doc.Free;
end;
end;
الترتيب مهم بين التعبئة وجمع المهملات. يجب أن يعمل تحليل إمكانية الوصول أولًا، لأن العضو الذي ينجو داخل حاوية يسحب الحاوية معه — فإن كان كائن حي معبأً، فرقم حاويته قابل للوصول بالتعريف، وكسح الحاوية بعيدًا يترك العضو محاصرًا بلا طريقة لتحديد موقعه. تشغيل الجامع أولًا يعني أيضًا أن الكائنات الميتة لا تدخل حاوية على الإطلاق أبدًا، وهذا مصدر مكسب الحجم المتراكم. التعبئة تكمّل روافع الحجم الأخرى بدل أن تحل محلها؛ يغطي شرح تحسين حجم ملف PDF وتقليص الخطوط الروافع التي تعمل على حمولات التدفقات، حيث تعمل تدفقات الكائنات على البنية
حدود تستحق المعرفة قبل تفعيلها
الحفظ التزايدي لا يعبّئ أبدًا. التحديث التزايدي يلحق كائنات جديدة وقسم إحالة مرجعية جديدًا بينما يترك المراجعات السابقة سليمة ماديًا، فإعادة تعبئة الكائنات القائمة في حاويات جديدة كانت ستيتّم مدخلات النوع 1 التي ما زالت المراجعة السابقة تشير إليها؛ تعطّل losLab PDF Library التعبئة كلما كان وضع الإلحاق نشطًا، ويغطي المقال عن التحديثات التزايدية وبث وضع الإلحاق هذا المسار بالكامل. المستندات دون PDF 1.5 تُبقي جدول الإحالة المرجعية النصي الصرف دون قيد وشرط: فقارئ 1.4 لا يعرف مطلقًا معنى /ObjStm، وترقية مستند بصمت لأن الكاتب فضّل ملفًا أصغر ستكون مقايضة خاطئة تُتخذ نيابة عن المستدعي. مفتاح اختياري واحد نمتنع عن إصداره عمدًا هو /Extends، الذي يعرّفه ISO 32000-1 §7.5.7 كي تسمّي حاوية سلفًا لها ويستطيع القراء معاملة سلسلة من الحاويات كمجموعة منطقية. إنه اختياري فعلًا، وكل حاوية نكتبها قائمة بذاتها وقابلة لفك الترميز باستقلالية، وتخطيه يزيل صنفًا كاملًا من أخطاء الدورات والمراجع المعلّقة من الكاتب — رغم أن على القراء بالطبع أن يحترموا /Extends حين يصادفونه في ملفات من منتجين آخرين
تعبئة تدفقات الكائنات وإصدار تدفقات الإحالة المرجعية يُشحنان كجزء من losLab PDF Library لـ Delphi وC++Builder، إلى جانب جامع المهملات ومحسّن تدفق المحتوى اللذين تتآلف معهما؛ وتحمل صفحة المنتج المرجع الكامل لخيارات الحفظ