يطبق HotPDF Component تدفق FDF /Differences من Delphi عبر THPDFHeadlessDocument.SaveFDFDifferencesToFile: تتحقق الطريقة أن PDF المحمّل ما يزال يطابق مراجعة أساسية تحددها بطول بايتات تام وبصمة SHA-256، وتلحق بايتات المراجعة التراكمية مرة واحدة تماماً، وتمرر النتيجة عبر أذونات التشفير وقبول DocMDP/FieldMDP، وتنشرها ذرياً. وتكرار الإرسال نفسه عملية لا فعل فيها بايتاً ببايت
سير العمل الذي وُجد لأجله غير بهيج وشائع. ترسل عقداً أو استمارة فحص بوصفها PDF، فيملؤها أحدهم ويوقعها في عارضته الخاصة، ويعود إليك ليس الـ PDF بل ملف FDF. تسافر قيم الحقول في /Fields، لكن المراجعة الموقعة نفسها، البايتات التي يغطيها توقيع CMS فعلاً، تسافر في /Differences. أخطئ في الدمج فتكسر التوقيع أو، والأسوأ، تقبل قيم حقول لم يغطها أي توقيع قط
ما يحمله تدفق FDF /Differences فعلاً؟
يحمل تدفق FDF /Differences بايتات تحديث تدريجي خام لملف PDF الهدف، لا تاريخاً على مستوى FDF. يعرف ISO 32000-1 §12.7.7.3 (الجدول 243) مدخل Differences في قاموس FDF بوصفه التغييرات التدريجية على PDF الأساسي منذ فتحه، بالصيغة نفسها لتحديث PDF تدريجي بموجب §7.5.6: كائنات جديدة ومستبدلة، وقسم مراجع متقاطعة، ومقطع ختامي يشير /Prev فيه إلى الملف الأساسي
وحاوي FDF نفسه على النقيض. يحظر §12.7.7.1 التحديثات التدريجية لملفات FDF، فللـ FDF مقطع ختامي واحد، وكائنات جيل 0 فقط، وأرقام كائنات فريدة. يفرض HotPDF Component ذلك عينه في محلل FDF ولا يخففه أبداً لتسهيل /Differences. تعيش البيانات التدريجية داخل تدفق واحد في حاوي صارم اللا تدريجية، وإبقاء الطبقتين منفصلتين نصف العمل
خاصيتان للتدفق تشكلان كل قرار تصميم لاحق:
- التدفق تراكمي: يحمل كل بايت أُلحق منذ فتح ملف PDF الهدف، فالإرسال الثاني من الجلسة نفسها يكرر المراجعة التي أُجريت بالفعل ويضيف الجديدة خلفها
- لا ضمان اتفاق مدخلي
/Fieldsو/Annotsالخارجيين في ملف FDF نفسه مع/Differences، فقيم الحقول التي تقرؤها بسهولة ليست هي البيانات التي يغطيها التوقيع
تستحق تلك النقطة الثانية الصراحة. إن قرأ كود الاستيراد عندك /V من /Fields وعامل النتيجة بوصفها موقعة لمجرد أن توقيعاً وصل في الملف نفسه، فقد بنيت قناة تزوير. يفصل HotPDF Component المسارين عمداً: مستورد تعليقات FDF يرفض /Differences جملة، ولا يمسها سوى ناقل Differences المخصص
ربط الإلحاق بمراجعة أساسية معلنة
يرفض SaveFDFDifferencesToFile التخمين أي مراجعة ينتمي إليها ملف FDF. يمرر المستدعي BaseBytes، الطول التام لمراجعة PDF الكاملة التي فُتحت منها الاستمارة، و BaseSHA256، بصمة SHA-256 السداسية عشرية ذات 64 خانة له. تعيد الطريقة تجزئة تلك البادئة من المستند المحمّل، وترفض عدم التطابق برسالة "The loaded PDF does not match the explicitly selected base"، وتتحقق أن البادئة تنتهي عند نقطة نهاية مراجعة حقيقية يشير startxref فيها إلى بيانات مراجعها المتقاطعة الفعلية
القاعدة العملية هي تسجيل الرقمين عند إرسال الاستمارة لا حين يعود ملف FDF. مساعد صغير فوق روتينات SHA256Init / SHA256Update / SHA256Final في HPDFCrypt يكفي:
uses
SysUtils, Classes, HPDFCrypt;
function PrefixSHA256Hex(const FileName: string; PrefixBytes: Int64): AnsiString;
var
Stream: TFileStream;
Ctx: TSHA256Context;
Digest: TSHA256Digest;
Buffer: array[0..65535] of Byte;
Remaining: Int64;
Count, I: Integer;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if (PrefixBytes <= 0) or (PrefixBytes > Stream.Size) then
raise EArgumentException.Create('Base length is outside the file');
SHA256Init(Ctx);
Remaining := PrefixBytes;
while Remaining > 0 do
begin
Count := SizeOf(Buffer);
if Remaining < Count then
Count := Integer(Remaining);
Stream.ReadBuffer(Buffer, Count);
SHA256Update(Ctx, Buffer, Count);
Dec(Remaining, Count);
end;
SHA256Final(Ctx, Digest);
finally
Stream.Free;
end;
Result := '';
for I := 0 to High(Digest) do
Result := Result + AnsiString(LowerCase(IntToHex(Digest[I], 2)));
end;
// وقت الإرسال: الملف الصادر كله هو المراجعة الأساسية
// BaseLength := حجم 'out\contract-0042.pdf' بالبايت
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
لا يلزم أن يكون المستند المحمّل هو الأساس بعينه. قد يكون الأساس مضافاً إليه مراجعات أجريتها من إرسالات سابقة، وهكذا يظل التدفق التراكمي قابلاً للإدارة: يواصل BaseBytes تسمية البادئة الأصلية، ويعامل كل ما بعدها بايتات إلحاق سابقة
تطبيق Differences بواسطة SaveFDFDifferencesToFile
تأخذ THPDFHeadlessDocument.SaveFDFDifferencesToFile اسم ملف المخرجات، وبايتات FDF، وطول الأساس وبصمته، واختيار FDF مضمّن اختيارياً، وسجل حدود THPDFFDFPackageOptions، وتعيد THPDFHeadlessFDFDifferencesResult. في الداخل تفتح ملف FDF بوصفه THPDFFDFPackage وتستدعي DecodeDifferences بنفسها، فتمرر ملف FDF لا تدفقاً مفكوك الترميز سلفاً. الاختيار Selection الفارغ يعني الـ FDF العليا؛ وغير الفارغ يدخل أولاً إلى FDF ابنة مضمّنة
uses
SysUtils, IOUtils, HPDFHeadlessDocument, HPDFHeadlessCMS, HPDFFDFPackage;
procedure CommitReturnedForm(const CurrentPdf, NextPdf, FdfFile: string;
BaseLength: Int64; const BaseHash: AnsiString);
var
Doc: THPDFHeadlessDocument;
Policy: THPDFHeadlessCMSOptions;
Selection: THPDFFDFChildSelections;
Applied: THPDFHeadlessFDFDifferencesResult;
begin
Doc := THPDFHeadlessDocument.Create;
try
Policy := THPDFHeadlessCMSOptions.Default;
Policy.TrustedCAFile := 'trust\issuing-ca.pem';
Doc.SetSignaturePolicyOptions(Policy);
Doc.LoadFromFile(CurrentPdf);
Selection := nil; // FDF عليا، بلا ابنة مضمّنة
Applied := Doc.SaveFDFDifferencesToFile(NextPdf,
TFile.ReadAllBytes(FdfFile), BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
Writeln(Format('base %d, prior %d, total %d, added %d, no-op %s',
[Applied.BaseBytes, Applied.PriorAppendBytes, Applied.TotalAppendBytes,
Applied.AddedBytes, BoolToStr(Applied.NoOp, True)]));
finally
Doc.Free;
end;
end;
ترتيب الفحوصات أهم من الاستدعاء نفسه. بعد نجاح بصمة الأساس، تقارن الطريقة بايتات الإلحاق المرتبطة سلفاً، كل ما بين BaseBytes ونهاية الملف المحمّل، ببداية الحمولة المفكوكة. الإرسال الأقصر مما أجريت سلفاً يفشل برسالة "A cumulative submission cannot remove committed append bytes"، وغير المطابق يفشل برسالة "The cumulative Differences conflict with the committed revision". اللاحقة المتبقية وحدها جديدة، وتلك اللاحقة وحدها تهبط في AddedBytes
يُكتب المرشح المجمَّع إلى ملف مؤقت حصري في مجلد الهدف، ويُصرَّف، ويُعاد فتحه مصدر مراجعة مستقلاً، ويُتحقق منه قبل أن يستبدل المخرجات. فشل التحليل أو الإلغاء أو السياسة أو الميزانية أو النشر يترك المستند المحمّل وأي ملف مخرجات قائم لم يمسّهما شيء. وشرطان مسبقان يلقطان الناس مبكراً: يجب ألا تكون المخرجات اسماً بديلاً للمصدر المحمّل، وأن يكون المستند بلا تحريرات غير محفوظة، لأن إلحاقاً معتماً سيتخلص منها بصمت ("Publish pending document edits before applying Differences")
لماذا يكون التطبيق المتكرر عملية بلا فعل بايتاً ببايت؟
التطبيق المتكرر لملف FDF تراكمي نفسه عملية بلا فعل لأن كل بايت من حمولته موجود في الملف أصلاً. عندما تساوي الحمولة بايتات الإلحاق السابقة تماماً، يبلّغ SaveFDFDifferencesToFile بـ NoOp = True و AddedBytes = 0، والمخرجات المنشورة مطابقة لملف PDF الحالي. هذا ضمان أقوى من "إلحاق فقط"، وهو ما يجعل إعادة المحاولة بعد مهلة شبكة أو انهيار عامل آمنة
// المراجعة 1 أُجريت في contract-0042-r1.pdf سلفاً؛ ويصل ملف FDF نفسه مرتين
// (Doc و Again و Fdf و Selection و BaseLength و BaseHash معرفة كأعلاه)
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile('store\contract-0042-r1.pdf');
Again := Doc.SaveFDFDifferencesToFile('store\contract-0042-r1-retry.pdf',
Fdf, BaseLength, BaseHash, Selection, THPDFFDFPackageOptions.Default);
// Again.NoOp = True, Again.AddedBytes = 0,
// وملف إعادة المحاولة مطابق بايتياً لـ contract-0042-r1.pdf
finally
Doc.Free;
end;
اكتب اختبارات قبولك لتطابق ذلك. أثبت المساواة البايتية التامة بين مخرجات إعادة المحاولة والمراجعة الحالية، لا مجرد أن الملف لم يتقلص. اختبار يفحص سلوك الإلحاق فقط سيمرّ بسرور تنفيذاً يلحق المراجعة نفسها مرة ثانية، فينتج PDF يبدو سليماً بقسم تحديث مكرر يضطر تدقيق DocMDP لاحق إلى تفسيره
ما فحوصات القبول التي تجري قبل نشر المراجعة؟
قبل النشر على مرشح PDF أن يجتاز فحوصات الهوية والتوقيع والأذونات، ولا يوثق أي منها حقول FDF الخارجية. يجب أن ينجو العنصر الأول من المقطع الختامي /ID دون تغيير، وأن يظل كل حقل توقيع موجود في المستند المحمّل قائماً بقاموس توقيع مطابق وبنية قيمة توقيع نفسها. حمولة Differences تُسقط توقيعاً قائماً أو تستبدله أو تعيد كتابته تفشل بـ hdSignatureModificationUnsupported
ثم تأتي طبقات التخويل. في إلحاق غير فارغ على مستند مشفر تُصنَّف الكائنات المتغيرة بحسب الدور (الكتالوج والصفحات، التعليقات، حقول AcroForm، تدفقات المظهر) وتُفحص مقابل بتات أذونات المستخدم المصادق عليها، فمراجعة تحرر التعليقات تحتاج إذن التعليقات وهكذا. ثم يمر المرشح عبر فرض الاعتماد وأقفال الحقول نفسه الذي يستخدمه HotPDF Component في مواضع أخرى: توقيع اعتماد DocMDP (ISO 32000-1 §12.8.2.2) يحد أي أنواع تغيير يجوز للمراجعات اللاحقة إجراؤها، وأقفال FieldMDP (§12.8.2.4) تحمي الحقول المسماة. وتغطي تحليل مراجعات DocMDP و FieldMDP في HotPDF كيف تُصنَّف تلك الأذونات لكل مراجعة
ينبغي لمعالجة الأخطاء أن تفصل "ملف FDF هذا مشوه" عن "هذه المراجعة غير مسموحة"، لأن الحالة الثانية تحتاج عادة إنساناً:
try
Applied := Doc.SaveFDFDifferencesToFile(NextPdf, Fdf, BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
except
on E: EHPDFFDFPackageError do
QuarantineFdf(FdfFile, 'No usable Differences stream: ' + E.Message);
on E: EHPDFHeadlessDocumentError do
case E.Code of
hdPermissionDenied, hdSignatureModificationUnsupported:
EscalateForReview(FdfFile, E.Message);
hdLimitExceeded:
QuarantineFdf(FdfFile, 'Over budget: ' + E.Message);
else
QuarantineFdf(FdfFile, E.Message); // عدم تطابق أساس، تعارض، نقطة نهاية سيئة
end;
end;
ما لا تفعله العملية أبداً بنفس أهمية ما تفحصه. لا يجلب SaveFDFDifferencesToFile قط مواصفة الملف /F في FDF، فلا يستطيع ملف PDF المسمى داخل FDF تحويل الدمج إلى ملف آخر على القرص أو الشبكة، ولا ينفذ JavaScript أو أفعالاً قط. كل مدخل يصل معلناً: الأساس المحمّل، وبايتات FDF، ومواد الثقة التي أعددتها
معاملات Windows وواجهة C fdf.differences.apply
الناقل نفسه متاح دون كتابة كود headless مباشرة. تأخذ THPDFWindowsFormDataTransaction.ApplyFDFDifferences بايتات FDF وطول الأساس وبصمة SHA-256 والاختيار وخيارات الحزمة وكلمة مرور، وتفوض إلى SaveFDFDifferencesToFile، وتضيف محاسبة بايتات المعاملة والتراجع والإلغاء وسلوك إعادة المحاولة على المقبض نفسه. وللمستدعين من غير ديلفي تكشف واجهة C العملية JSON fdf.differences.apply، حيث يحمل base الطول والبصمة:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
يبلّغ الرد JSON baseBytes و priorAppendBytes و totalAppendBytes و addedBytes و noop، الحقول نفسها لـ THPDFHeadlessFDFDifferencesResult. تجري بوابات القبول المنشورة لهذا الناقل على Linux FPC 3.3.1 و Windows Delphi بيتي 32 و64 و Windows FPC بيتي 32 و64؛ ويغطي كل هدف أمني 21 حالة بـ 1244 فحصاً أصلياً و382 أمر استدعاء عكسي
حدود وفخاخ تجهيزات تستحق المعرفة
ناقل Differences ضيق عن قصد، وبعض حوافه قضمت مجموعات اختبار حقيقية:
- يجب أن يكون الأساس مراجعة PDF تامة. الطول الذي يهبط وسط الملف أو الذي لا يشير
startxrefفيه إلى بيانات مراجعه المتقاطعة الفعلية يُرفض قبل نسخ أي بايت - تجهيزات التوقيع تحتاج
/Lockغير مباشراً. يرفض قارئ أذونات المستند المحمّل في Windows حقل توقيع يكون/Lockفيه قاموساً مضمّناً برسالة "Signature field /Lock must be an indirect reference"، وإن كانت بعض مسارات التوقيع تتسامح مع الصيغة المضمّنة، فاكتب قاموس القفل كائناً خاصاً به - CA اختبار منشأة بـ
openssl req -x509مجردة ينقصهاCA:TRUEوkeyCertSign، فيفشل التحقق من السلسلة بخطأ تحقق شهادة يبدو عيباً في المكتبة وليس كذلك - لا تدمج هذه الدعوة قيم
/Fieldsالخارجية. إن أردتها في المستند أيضاً فقرر صراحة ما إذا كانت إعلامية، ولا تعرضها موقعة أبداً - يبقى ملف FDF نفسه غير تدريجي. ملف FDF بمقاطع ختامية متعددة أو أجيال غير صفرية أو أرقام كائنات مكررة هو FDF مشوه لا FDF أغنى
أما الجانب القيمي الصرف من FDF و XFDF، دون مراجعات موقعة في المشهد، فنقطة البداية الأفضل هي رحلة ذهاب وعودة FDF و XFDF متعدد الاختيار في HotPDF و استيراد وتصدير تعليقات XFDF داخل Delphi
FAQ
هل يمكن تمرير تدفق Differences المفكوك بدل ملف FDF؟
لا. تحلل SaveFDFDifferencesToFile ملف FDF الذي تمرره بوصفه THPDFFDFPackage، وتطبق اختيار التضمين الاختياري وتستدعي DecodeDifferences داخلياً. استخدم THPDFFDFPackage.DecodeDifferences بمفرده عندما تريد فحص بايتات المراجعة الخام أو أرشفتها، لكن سلّم ملف FDF إلى دعوة التطبيق
ماذا يحدث عندما يرسل العميل ملف FDF تراكمياً ثانياً أكبر؟
حمّل ملف PDF الذي يحوي المراجعة الأولى المرتبطة سلفاً واحتفظ بـ BaseBytes و BaseSHA256 نفسيهما. تتحقق الطريقة أن الحمولة الجديدة تبدأ بالبايتات المرتبطة سلفاً تماماً، وتلحق اللاحقة الجديدة وحدها وتبلّغ عنها في AddedBytes، ثم تجري القبول الكامل للأذونات و DocMDP/FieldMDP على النتيجة
هل يعني التطبيق الناجح أن قيم الحقول في /Fields موقعة؟
لا. لا تشترط ISO 32000-1 اتفاق /Fields و /Annots و /Differences في ملف FDF واحد، ولا يصالحها HotPDF Component. الموقع هو ما تغطيه تواقيع CMS في المراجعة الملحقة، فاقرأ القيم الموقعة من ملف PDF المدمج لا من غلاف FDF
ناقل FDF Differences ومحرك سياسة التوقيع اللا رأسي ومعاملات بيانات النماذج في Windows كلها متوفرة في HotPDF Component لـ Delphi و C++Builder و Free Pascal