تسمح تحديثات PDF التزايدية لتطبيق دلفي بتعديل مستند عن طريق إلحاق الكائنات المتغيرة فقط، مع ترك كل بايت أصلي دون مساس. وتنفذ مكتبة losLab PDF هذا من خلال AppendToStream، والتي تكتب فقط القسم التزايدي المحدد بواسطة ISO 32000-1 §7.5.6، بحيث يكلف تعديل إشارة مرجعية واحدة لملف بحجم 2 جيجابايت كيلوبايتات من المخرجات بدلاً من إعادة كتابة كاملة. وتعد هذه الآلية نفسها هي السبب في إمكانية تحديث المستندات الموقعة دون إبطال توقيعاتها
إن المشكلة التي يحلها هذا الأمر ملموسة. تؤدي عملية الحفظ الكاملة إلى إعادة كتابة الملف بأكمله: تتم إعادة تسلسل كل كائن، وإعادة حساب كل إزاحة مرجعية ترافقية، ولا تحمل المخرجات أي علاقة على مستوى البايت بالمدخلات. بالنسبة لفاتورة بحجم 40 كيلوبايت، فإن هذا أمر جيد. ولكن بالنسبة لأرشيف ممسوح ضوئيًا بحجم 2 جيجابايت حيث قمت فقط بتصحيح خطأ إملائي في عنوان المستند، فإن إعادة كتابة اثنين جيجابايت لتغيير عشرين بايت يعد أمرًا سخيفًا — وإذا كان الملف يحمل توقيعًا رقميًا، فإن إعادة الكتابة ستؤدي ببساطة إلى تدميره
لماذا يؤدي حفظ ملف PDF إلى كسر توقيعه الرقمي؟
لا يوقع توقيع PDF الرقمي على المحتوى المنطقي للمستند، بل يوقع على نطاقات البايت الخاصة بالملف الفعلي. يسجل إدخال /ByteRange في قاموس التوقيع بدقة الامتدادات من الملف التي يغطيها الملخص التشفيري. وأي عملية حفظ تعيد تسلسل تلك البايتات — حتى لو أنتجت مستندًا متطابقًا دلاليًا — تغير الملخص، وسيقوم كل مدقق بالإبلاغ عن التوقيع على أنه مكسور. هذا التصميم مقصود: فالتوقيع يشهد على البايتات التي رآها الموقع، وليس على نموذج مستند مجرد
تعد التحديثات التزايدية هي منفذ الهروب الذي توفره مواصفات PDF. ونظرًا لأن الحفظ التزايدي يلحق بيانات جديدة بعد %%EOF الأصلي ولا يلمس أبدًا نطاقات البايت الموقعة، فإن التوقيع الحالي يستمر في التحقق من صحته مقابل البايتات التي يغطيها. ثم يقوم المدققون بتصنيف التغييرات الملحقة بشكل منفصل — توقيع ثانٍ، أو ملء نموذج، أو تعليق توضيحي — ويقررون ما إذا كانت تعديلات مسموح بها. ويعتمد كل سير عمل متعدد التوقيعات على هذا: يضيف كل موقع قسمًا تزايدياً فوق الأخير. وإذا كنت تقوم ببناء خطوط أنابيب التوقيع، فإن المقال المصاحب حول توقيع PAdES والتحقق من صحته في دلفي يغطي بالتفصيل كيفية تفاعل نطاقات بايت التوقيع والأقسام التزايدية
كيف تعمل التحديثات التزايدية بموجب ISO 32000-1 §7.5.6
يحدد معيار ISO 32000-1 §7.5.6 النموذج في ثلاث قواعد. أولاً، يتم ترك محتوى الملف الأصلي كما هو تمامًا — ولا يتحرك بايت واحد. ثانياً، يتم إلحاق الكائنات التي تم تغييرها وحديثة الإنشاء بعد آخر %%EOF، ويحمل كل منها نفس رقم الكائن الذي كان لديه من قبل (تحصل الكائنات المتغيرة ببساطة على تعريف أحدث يظلل القديم). ثالثاً، يتم إلحاق قسم مرجعي ترافقي ومقطورة (trailer) جديدين؛ ويشير إدخال /Prev الخاص بالمقطورة إلى إزاحة البايت للقسم المرجعي الترافقي السابق، مما يشكل سلسلة يسير عليها القارئ من الأحدث إلى الأقدم لحل كل كائن إلى أحدث تعريف له
تنتج خاصيتان مفيدتان عن هذه البنية. التحديثات رخيصة بما يتناسب مع ما تغير، وليس مع حجم المستند — تكلفة الإلحاق هي حجم الكائنات المعدلة بالإضافة إلى نفقات xref/trailer الإضافية الصغيرة. ويصبح الملف هو سجل إصداراته الخاصة: فكل مراجعة سابقة لا تزال موجودة فعليًا، لذا يمكن للمدقق قطع الملف عند أي %%EOF سابق واستعادة المستند الذي كان موجودًا في تلك النقطة تمامًا. بالنسبة لخطوط عمل الامتثال التي يجب أن تثبت كيف كان يبدو المستند قبل كل تعديل، فإن مسار التدقيق المدمج هذا هو الحجة الحاسمة في الغالب لاختيار الحفظ التزايدي
كتابة تحديث تزايدي باستخدام AppendToStream
تعرض مكتبة losLab PDF المخرجات التزايدية من خلال AppendToStream(AppendMode: Integer; OutStream: TStream): Integer، والتي ترجع القيمة 1 عند النجاح و 0 عند الفشل. وتحدد المعلمة AppendMode ما يستقر في دفق المخرجات المستهدف. ويكتب الوضع 0 ملفًا كاملاً: يتم نسخ بايتات المصدر الأصلية إلى الدفق أولاً، ثم يتم إلحاق القسم التزايدي. ويكتب الوضع 1 القسم التزايدي نفسه فقط — الدلتا — ويتخطى بايتات المصدر تمامًا. ويكتب الوضع 2 أولاً بادئة يوفرها المستدعي والمسجلة عبر SetAppendInputFromString، ثم يلحق قسم التحديث فوقها
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Small edit: the kind of change that should not
// trigger a rewrite of the whole file
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: write only the incremental section.
// Original bytes + Delta = a complete, valid PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
الوضع 1 هو الوضع المثير للاهتمام لتصميم النظام. نظرًا لأن الدلتا قائمة بذاتها، يمكنك شحنها بشكل مستقل عن الملف الأصلي: مثل تخزين المراجعات ككتل منفصلة (blobs) في مخزن الكائنات، أو تكرار الدلتا فقط إلى موقع بعيد، أو إعادة بناء أي مراجعة عن طريق دمج الملف الأساسي مع سلسلة الزيادات الخاصة به. وقاعدة إعادة البناء هي مجرد دمج بسيط للبايتات — الملف الأصلي أولاً، ثم كل دلتا بالترتيب — لأن هذا هو بالضبط التخطيط الذي يفرضه القسم 7.5.6 للملف المحدث تزايديًا
كيف تحسب المكتبة إزاحات xref دون نسخ الملف الأصلي؟
يجب أن تحتوي إدخالات المراجع الترافقية داخل قسم تزايدي على إزاحات بايت مطلقة — مواضع تُقاس من بداية الملف الكامل، وليس من بداية الدلتا. وهذا يخلق لغزًا للوضع 1: لا يرسل الكاتب أبدًا البايتات الأصلية، ومع ذلك يجب أن يتظاهر كل إزاحة يسجلها بأنها موجودة. تحل مكتبة losLab PDF هذا باستخدام محول دفق داخلي، TPDFAppendSectionStream، الذي يقدم مساحة إحداثيات افتراضية للمسلسل (serializer). ويتم إنشاء المحول مع اعتبار طول بايت الملف الأصلي كإزاحة أساسية له، ويبلغ عن موضعه وحجمه كقيمة الأساس بالإضافة إلى كل ما تم إلحاقه حتى الآن، ويوجه فقط البايتات المكتوبة حديثًا إلى دفق الهدف الخاص بالمستدعي
والنتيجة هي أن الوضع 1 لا يجسد أبدًا نسخة من المستند مصدر — لا على القرص ولا في الذاكرة. التنفيذ البسيط (كتابة الملف بالكامل في مخزن مؤقت، ثم قطع الذيل) كان سيحمل نسخة مؤقتة من ملف PDF الأصلي بأكمله، وهو ما يمثل لمدخلات بحجم جيجابايت بالضبط التكلفة التي وجدت التحديثات التزايدية لتجنبها. تقنية افتراضية الإزاحة هذه هي قريبة لصيقة لنقل مرجع البايت المستخدم في أماكن أخرى من المكتبة؛ يوضح المقال الخاص بـ دمج PDF السريع مع نقل مرجع البايت في دلفي تطبيق الفكرة نفسها على دمج المستندات، ويغطي دليل دمج وتقسيم ملفات PDF الكبيرة مع الوصول المباشر للملف بنية الإدخال/الإخراج المحيطة بالملفات التي لا تتسع بشكل مريح في ذاكرة الوصول العشوائي (RAM)
دفق الحفظ الكامل باستخدام SaveToStream
المخرجات التزايدية هي نصف قصة الدفق؛ والنصف الآخر هو ما يحدث عند الحفظ الكامل. تقوم الوظيفة SaveToStream في مكتبة losLab PDF بتوجيه مسلسل المستند مباشرة ضد دفق الهدف، بدلاً من عرض المستند بأكمله أولاً في AnsiString وسيط ثم كتابة هذا المخزن المؤقت في استدعاء واحد. كان النهج الأقدم يعمل، ولكنه كان يعني أن كل حفظ كامل يحتفظ بشكل مؤقت بنسخة كاملة ثانية من المخرجات في الذاكرة — وهو أمر غير ضار عند 10 ميجابايت، ومؤلم عند 500 ميجابايت، ويمثل جدارًا صلبًا لمخرجات متعددة الجيجابايت على عمليات 32 بت. يجعل التسلسل المباشر ذاكرة الذروة تتعقب هياكل كائنات المستند بدلاً من طوله المتسلسل
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
درس في وضع المشاركة (share-mode): عندما ترجع AppendToFile القيمة 0
هناك تراجع واحد في هذا المجال يستحق إعادة سرده لأن نمط الفشل هذا يمكن تعميمه. تقوم AppendToFile(FileName) بإلحاق تحديث تزايدي مباشرة بملف PDF موجود على القرص — وهو الاستدعاء الطبيعي لسير عمل مسار التدقيق في المكان: تحميل ملف، وإجراء تغيير، والإلحاق بنفس المسار. في الإصدار v3.71.2، بدأ هذا التسلسل الدقيق في إرجاع القيمة 0. وكان السبب الجذري يكمن في برنامج التحميل، وليس الكاتب: لدعم القراءة عند الطلب للمستندات الكبيرة، تحتفظ LoadFromFile بمقبض ملف المصدر مفتوحًا طوال عمر كائن المستند، وتم فتح هذا المقبض باستخدام fmShareDenyWrite. وعندما حاولت AppendToFile بعد ذلك إعادة فتح نفس الملف للكتابة، رفض وضع المشاركة الخاص ببرنامج التحميل نفسه ذلك، وفشلت واجهة برمجة التطبيقات (API) قبل كتابة بايت واحد
لقد خفف الإصلاح وضع مشاركة برنامج التحميل إلى fmShareDenyNone، وهو أمر آمن تمامًا بسبب ماهية الإلحاق التزايدي: فهو يضيف بايتات بشكل صارم بعد نهاية الملف ولا يعيد كتابة المنطقة التي يخدمها مقبض القارئ طويل العمر. الدرس العام لأي شخص يقوم بتغليف هذه المكتبة — أو بناء برامج تحميل دفق مماثلة — هو أن القراء الكسالى الذين يحتفظون بالمقابض وكتاب نفس الملف في حالة توتر، ووضع المشاركة الذي تختاره في وقت الفتح هو عقد واجهة برمجة تطبيقات (API)، وليس تفاصيل تنفيذ. إذا أرجعت AppendToFile القيمة 0 في الكود الخاص بك، فتحقق أولاً مما إذا كان هناك شيء آخر في عمليتك لا يزال يحتفظ بالملف المستهدف بوضع مشاركة تقييدي
التكاليف الحقيقية: عندما تكون التحديثات التزايدية هي الأداة الخاطئة
تستبدل التحديثات التزايدية حجم الملف بكفاءة الكتابة، وهذا التبادل ليس مناسبًا دائمًا. تقوم كل مراجعة بإلحاق كائناتها المتغيرة بينما تظل التعريفات الملغاة في الملف، وبالتالي فإن المستند الذي تم تعديله مئات المرات يراكم الكائنات الميتة وسلسلة /Prev طويلة يجب على كل قارئ السير فيها. والأسوأ من ذلك أن المحتوى "المحذوف" لا يختفي: فالنص الذي تمت إزالته في المراجعة الخامسة لا يزال موجودًا فعليًا في بايتات المراجعة الرابعة، ويمكن لأي شخص يقطع الملف استعادته. لذلك يتطلب التحرير (redaction) أو التطهير أو أي إزالة لمحتوى حساس إعادة كتابة كاملة — الحفظ التزايدي للتحرير هو تسريب للبيانات بخطوات إضافية
عملية الحفظ الكاملة هي أيضًا القرار الصحيح عندما يكون الهدف هو الضغط (التخلص من الزيادات المتراكمة والكائنات غير المستخدمة)، أو عند تغيير خصائص على مستوى المستند مثل التشفير — تلمس إعادة التشفير كل سلسلة نصية ودفق، وبالتالي لا يتبقى أي شيء "تزايدي" حول التغيير — أو عند إنتاج نسخة نهائية نظيفة حيث لا ينبغي أن ينتقل تاريخ التحرير مع الملف. قاعدة معقولة: استخدم AppendToStream أو AppendToFile طوال حياة المستند وتغييره، خاصة بمجرد أن يحمل توقيعات؛ واستخدم إعادة كتابة كاملة SaveToStream عند حدود دورة الحياة، عندما يغادر المستند نظامك أو عندما يجب تسوية تاريخه
تعد التحديثات التزايدية ومخرجات الدلتا ذات الإزاحة الافتراضية والتسلسل المباشر إلى الدفق جزءًا من مكتبة losLab PDF القياسية لدلفي وC# وVB.NET؛ وتتسرد صفحة المنتج كامل واجهة برمجة تطبيقات الحفظ والإلحاق بالإضافة إلى ميزات التوقيع والملفات الكبيرة الموضحة أعلاه