يمنح PDFium Component محرر PDF تراجعًا وإعادة حقيقيين على مستوى المستند بأكمله، إضافة إلى حزمة تعافٍ من الأعطال، عبر سجل تحرير. تُغلِّف التغييرات بـ BeginEditTransaction و CommitEditTransaction، وتصبح كل معاملة مُلتزَم بها فرقًا ثنائي الاتجاه يمكن لـ UndoEdit و RedoEdit إعادة تشغيله في أي اتجاه. وتحفظ SaveRecoverySnapshot ذلك السجل بحيث يمكن لتطبيق أُنهي في منتصف الجلسة إعادة بناء عمل المستخدم
والفجوة التي يسدها هذا محددة. فقدرة التراجع الأصلية لا تنطبق إلا على حقل النموذج الذي يحمل التركيز حاليًا، بحيث يتذكر مربع نص تحريراته الخاصة ولا شيء غيره. أما إدراج الصفحات وحذفها وإعادة ترتيبها، وتغييرات كائنات الصفحة، والحواشي التوضيحية، والمرفقات، والبيانات الوصفية، فلا سجل لها إطلاقًا. ومحرر مبني على ذلك وحده يمكنه تقديم تراجع داخل حقل ولا مكان آخر، وهذا ليس ما يقصده المستخدمون بالتراجع
لماذا اللقطات لا سجل العمليات؟
التصميم الواضح هو تسجيل عمليات دلالية — "حُذفت الصفحة 4"، "نُقلت الحاشية التوضيحية 7" — وعكسها. وهذا ينجح إلى أن تنمو مجموعة العمليات، وعندئذ تحتاج كل ميزة جديدة إلى معكوس، وأي عملية معكوسها غير دقيق تُفسد المستند بصمت عبر سلسلة تراجع طويلة
ويلتقط السجل بدلاً من ذلك حالة المستند عند حدود المعاملات ويخزّن الفرق. ويحتفظ كل فرق بأطول بادئة ولاحقة مشتركتين ويحمل كلاً من البايتات المحذوفة والمُدرَجة، بحيث يمكن تطبيقه للأمام أو للخلف. ولا تحتاج ميزات التحرير الجديدة إلى أي دعم تراجع: فأي شيء تغيّره موجود داخل اللقطة
ويعتمد هذا النهج على شرط مسبق واحد. فالحفظ الكامل العادي ينتج معرّفات وحقول وقت غير حتمية، بحيث يمكن أن ينتج حفظ مستند غير مُعدَّل مرتين بايتات مختلفة. ولذلك تُطبَّع اللقطات عبر مسار المخرجات القابلة لإعادة الإنتاج أولاً. فبدون تلك الخطوة ستبدو معاملة فارغة كأنها تعديل، وسيفشل التراجع في التحقق الخاص به لأن الملخص الذي حسبه عند الالتزام لن يطابق المستند بعد ذلك
المعاملات في الممارسة العملية
تأخذ كل معاملة تسمية اختيارية، وهي ما يعرضه المحرر في قائمة التراجع الخاصة به:
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfEditJournalOptions;
Info: TPdfEditJournalInfo;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Options := TPdfEditJournalOptions.Default;
Options.MaxEntries := 50; // عمق التراجع
Options.MaxStoredBytes := 256 * 1024 * 1024;
Pdf.ConfigureEditJournal(Options);
Pdf.BeginEditTransaction('Delete cover page');
try
Pdf.DeletePage(1);
if not Pdf.CommitEditTransaction then
raise Exception.Create('commit failed - document was reloaded');
except
Pdf.RollbackEditTransaction;
raise;
end;
Info := Pdf.GetEditJournalInfo;
Writeln(Format('entries=%d position=%d stored=%d bytes',
[Info.EntryCount, Info.Position, Info.StoredBytes]));
if Pdf.EditCanUndo then
Pdf.UndoEdit; // الصفحة تعود
finally
Pdf.Free;
end;
end;
والالتزام الذي يفشل — لأن الالتقاط فشل، أو تجاوز ميزانية، أو تعذّر بناء الفرق — يستعيد الأساس المُلتقَط عند BeginEditTransaction ويعيد التحميل، بدلاً من ترك معاملة نشطة أو سجل مكتوب جزئيًا. وهذا نمط الفشل الأكثر أهمية، لأن سجلاً في حالة غير متسقة يُسمم كل تراجع لاحق
التحقق في كل خطوة
يخزّن كل فرق ملخص SHA-256 للمستند قبل وبعد، إلى جانب طول البادئة وكلتا الحمولتين. ويتحقق كل من التراجع، والإعادة، وإعادة التشغيل المُستورَدة، وإعادة البناء العكسية من الملخص الحالي والمنطقة التي تعتزم استبدالها قبل لمس أي شيء، ثم يتحقق من الملخص الناتج بعد ذلك
والتنقل عبر السجل حذر بالقدر نفسه. فلا يتقدم الموضع إلا بعد أن يتحقق كائن مستند مستقل من صحة ملف PDF الهدف وينجح في استبدال الملف الحالي. وإذا فشل ذلك، تُستعاد اللقطة الحالية كخطة احتياطية، بحيث يترك تراجع فاشل المستخدم حيث كان بدلاً من حالة معطوبة
والصيغة المُسلسَلة تُفحص كبيانات، لا تُصدَّق كمدخل: فالرقم السحري، والإصدار، وقيم الإدخال والموضع، وسلسلة الملخصات، وأطوال التسميات، وأطوال الرقع، وميزانية التخزين الإجمالية، كلها تُتحقق منها. فالسجل المُستورَد ملف ربما بُتر أو حُرِّر أو أُنتج بإصدار مختلف، ومعاملته كعدائي هو الموقف الآمن الوحيد
ما الذي يحدث عند امتلاء السجل
ثلاثة حدود تُقيِّد النمو: عدد الإدخالات، وبايتات الفروق التراكمية، وحجم لقطة واحدة. وعند بلوغ السعة، يُطرد أقدم إدخال ويتقدم بصمة الجذر
ولهذا الجزء الأخير نتيجة تستحق التصميم حولها. فلا يمكن تطبيق سجل مُصدَّر إلا على مستند يطابق بصمة الجذر التي يُبلّغ عنها، أو بصمة سجّلها. وبمجرد حدوث الطرد، لا يعود السجل يصل إلى الملف الأصلي. لذلك يجب أن يعامل الحفظ التلقائي لقطة الجذر المطابقة والسجل كوحدة تعافٍ ذرية واحدة — فالاحتفاظ بالسجل دون أساسه هو الاحتفاظ بشيء لا يمكن إعادة تشغيله
حزم التعافي من الأعطال
لا تُضمِّن حزمة التعافي ملف PDF المصدر عن قصد. بل تخزّن اسم عرض المصدر، وبصمات SHA-256 لحالتي المصدر والمُستعاد، وسجل التحرير. لذلك يتطلب التعافي الملف الأصلي، الذي يُحمِّله المستدعي أولاً؛ ثم تتحقق الحزمة من كلتا البصمتين بإعادة تشغيل السجل للأمام وللخلف:
// بعد كل التزام ناجح، حدّث ملف التعافي
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// في الجلسة التالية، بعد عطل: حمّل الأصلي، ثم استعِد
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
وعدم تضمين المصدر يُبقي ملفات التعافي صغيرة، وهذا ما يجعل الحفظ التلقائي بعد كل التزام ميسور التكلفة على مستند بأي حجم. ويغطي ملخص خارجي الترويسة واسم المصدر والسجل، بحيث تُرفض حزمة مبتورة أو مُتلاعب بها قبل تحليل أي فرق، ويُحدَّد إجمالي حجم القراءة
وتذهب كتابات الملفات إلى ملف مؤقت في الدليل الهدف ثم تُستبدل ذريًا، ويُرفض صراحة ضبط مسار التعافي إلى ملف PDF المصدر نفسه. ولا يعمل الحفظ التلقائي إلا بعد التزام مُغيِّر ناجح، أو تراجع، أو إعادة. وإذا فشلت كتابة القرص، تكون حالتا المستند والسجل في الذاكرة صالحتين بالفعل، بحيث يُبلّغ الاستثناء عن فشل في الاستمرارية لا عن فقدان بيانات، ويمكن للمستدعي إصلاح المسار وإعادة المحاولة
المستندات المُوقَّعة تُرفض افتراضيًا
لقطة المعاملة مراجعة مُطبَّعة جديدة، لا البايتات الموقَّعة الأصلية، بحيث يُبطل الالتزام بمعاملة على مستند موقَّع التوقيع. ويُرفض بدء معاملة على مستند كهذا ما لم يمرر المستدعي AllowSignedDocument، وتلجأ استعادة استيراد السجل والتعافي إلى الرفض نفسه افتراضيًا
وهذا مطب سرعة مقصود لا حظر. فبعض سير العمل يحرر بشكل مشروع مستندًا موقَّعًا ثم يعيد توقيعه بعد ذلك، والعلَم موجود من أجله. وما يمنعه هو الحالة العرضية، حيث يُجرد محرر بصمت مستندًا يعتقد المستخدم أنه لا يزال موقَّعًا من صحته. والجانب التحققي من تلك القصة موصوف في فحص التوقيعات الرقمية لملفات PDF ومستويات PAdES
وبالنسبة لمحرر، ثلاث عادات تجعل السجل يستحق العناء. غلّف كل إجراء مرئي للمستخدم في معاملة واحدة مُسمّاة، بحيث تقرأ قائمة التراجع كنموذج ذهني للمستخدم لا كمكدس استدعاءات لديك. وحدّد حد الإدخالات وفق المستند، لأن خمسين لقطة لمسح ضوئي بحجم 200 ميغابايت ليست إعدادًا متواضعًا. واكتب لقطة التعافي عند كل التزام لا على مؤقّت، لأن المؤقّت يضمن أن العمل المفقود هو بالضبط ما حدث بعد آخر نبضة. وعمليات التحرير على مستوى المستند التي تستفيد من هذا موصوفة في التحديثات التراكمية لملف PDF مشفَّر
تأتي التسجيل التحريري وحزم التعافي وأوليات التحرير التي تقف تحتها في المكوّن نفسه لـ Delphi و C++Builder و Lazarus؛ وتُوصف الواجهة البرمجية الكاملة على صفحة PDFium Component لـ Delphi