مقال تقني

حفظ XFA في PDFium Component: LF وإيموجي و restoreState

يحفظ PDFium Component قيم نماذج XFA المحررة بدقة تامة، عبر الحفظ وإعادة الفتح، حين يشغّل بيئة تشغيل Windows ‏V8 ‏pdfium.v8.dll المشحونة في v3.125.2 أو أحدث. بيئات التشغيل الأقدم أضافت أسطر تغذية إلى قيم الحقول، وقصّت الإيموجي إلى محرف BMP غير ذي صلة، وتخطت بصمت حفظ XFA أحادي التدفق، وربما ابتلعت كتابةً نهائية فاشلة. وعَرَض واحد من إعادة الفتح ليس عيباً في المكتبة أصلاً: النموذج الديناميكي الذي يفتقر subform جذوره إلى restoreState="auto" يعيد بناء تخطيطه من القالب

بلاغات العيوب لهذا كلها بدت متشابهة. يملأ عميلٌ نموذج مطالبة XFA في عارض Delphi، ويحفظ، ويعيد الفتح، فيكون شيء ما منحرفاً قليلاً. صندوق تعليقات فارغ يحمل الآن سطراً فارغاً، وبعد حفظٍ ثانٍ يحمل سطرين. واسمٌ كُتب بإيموجي يعود بمحرف منطقة استخدام خاص. لا أحد يحصل على خطأ، وهذا ما يجعل تلك العيوب مكلفة: يظهر الانحراف أسابيع بعد ذلك في تصدير شخص آخر

ما الذي يخطئ حين يُحفظ نموذج XFA ويُعاد فتحه؟

أربعة عيوب منفصلة في مسار حفظ XFA الأصلي سببت انحراف القيم، واختبى كل واحد خلف حفظٍ يبدو ناجحاً. جاء اثنان من التسلسل، وواحد من تخطيط التخزين أحادي التدفق، وواحد من كاتب PDF نفسه. يُسقط الجدول كل عَرَضٍ إلى سببه وإلى الإصدار الذي أصلحه PDFium Component

العَرَض بعد إعادة الفتحالسببأُصلح في
حقل فارغ يحمل تغذية سطر؛ القيم تكبر سطراً جديداً مع كل حفظأدخل كاتب XFA كلاهما أسطر تخطيط بعد وسوم البداية‏v3.125.2، ‏pdfium.v8.dll
تعود U+1F642 على هيئة U+F642، أو يختفي الإيموجي من حزمة النموذجبتر wchar_t ‏16-بت في الفك؛ ترشيح البدائل في مُسلسل النموذج‏v3.125.2، ‏pdfium.v8.dll
تحريرات مستند XFA أحادي التدفق زالت ببساطةرفض الحفظ الأصلي تخطيط التدفق، لكن قيمة العودة أُهملت‏v3.125.2؛ حُفظت التعليقات وتعليمات المعالجة منذ v3.126.0
ملف مبتور رغم أن الحفظ أبلغ النجاحفشلت الكتابة المخبأة الأخيرة بعد أن كان الكاتب قد أبلغ النجاح سلفاً‏v3.125.2 بيئة V8؛ ‏v3.125.3 ‏pdfium.dll العادية
نموذج ديناميكي من ثلاث صفحات يعيد الفتح صفحتينsubform الجذور لا يطلب restoreState="auto"تأليف النموذج، لا عيب في المكتبة

خلصت كتابات سابقة إلى أن تحريرات حقول XFA لا يمكن إبقاؤها مع PDFium أصلاً، وكان ذلك دقيقاً لبيئات التشغيل آنذاك. بيئة التشغيل V8 الأحدث تحفظ قيم XFA أصلياً، فيصل التحرير الذي أُجري في النموذج الحي إلى حزمة datasets المحفوظة من دون جراحة حزمٍ من جانبك

أي بيئة تشغيل PDFium تحفظ قيم XFA؟

أمانة حفظ XFA تعتمد على الـ DLL الأصلية لا على غلاف Delphi، فأول فحص هو أي بيئة تشغيل حملتها عمليتك فعلاً. يشحن PDFium Component بناءَي ويندوز لكل معمارية: ‏pdfium.dll العادية، المبنية بلا V8 ولا XFA، و pdfium.v8.dll التي تحمل محرك JavaScript وبيئة تشغيل نماذج XFA. ‏pdfium.v8.dll وحدها تستطيع تشغيل نموذج XFA، فكل إصلاح XFA الموصوف هنا يسكن هناك، بدءاً بمكتبتي Win32 و Win64 V8 المعاد بناؤهما في v3.125.2

إصلاح الكتابة النهائية كود كاتب PDF عام، فهو مهم للمستندات العادية أيضاً. أعادت v3.125.3 بناء مكتبات pdfium.dll العادية لتحمل ذلك الإصلاح نفسه. المصدر المشترك ليس دليلاً على سلوك مشترك: حتى يُعاد بناء الثنائي يبقي الـ DLL القديمة العيبَ القديم

وجلس فخ ثانٍ في المحمِّل. قبل v3.125.2 كان ضبط EnableV8Engine على True يجعل الربط ينتقي اسم pdfium.v8.dll الافتراضي ويتجاهل المسار الكامل في LibraryName. تطبيق يشير إلى بيئة تشغيل منشورة طازجة كان يستطيع أن يواصل تحميل نسخة أقدم من مجلد آخر. ومنذ v3.125.2 ينق LibraryName الذي يحوي دليلاً ذلك الملفَ بعينه في كلا وضعي المحرك، ويفشل المسار المفقود بدل التراجع إلى مكتبة مرفقة أخرى

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // دليل في LibraryName يثبت هذا الملف بعينه ‏(v3.125.2 وما بعدها)؛
  // إن غاب الملف يرفع التحميل بدل التراجع إلى أخرى
{$IFDEF WIN64}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
  PDFium.EnableV8Engine := True;
  PDFium.LoadLibrary;  // افشل عند الإقلاع لا عند أول حفظ
end;

بعد فتح مستندٍ يخبرك TPdf.XFA أن الملف يحوي XFA، ويخبرك TPdf.XfaRuntimeAvailable أن الـ DLL المحملة تستطيع تنفيذه فعلاً. وإن احتجت أيضاً تمييز النماذج الساكنة عن الديناميكية تعيد TPdf.FormType ‏ftXfaFull أو ftXfaForeground؛ تغطي مقالة كشف نماذج XFA واستخلاص حزمها في Delphi ذلك المسح بالتفصيل

لماذا تكسب حقول XFA المحفوظة أسطر تغذية إضافية؟

اكسبت حقول XFA المحفوظة أسطرَ تغذية لأن كاتبي XFA الأصليين كليهما، كاتب عناصر XML العام ومُسلسل حزمة النموذج، جمّلا ناتجهما بسطر جديد بعد وسوم البداية. في أغلب XML تلك المسافة تجميلية. وفي بيانات XFA ليست كذلك: حين تُفسَّر حزمة datasets مرة أخرى يكون النص بين <Comments> و </Comments> هو قيمة الحقل، سطر التغذية ضمنه. أعاد حقلٌ فارغ إذن الفتح حاملاً LF واحدة، وكل دورة حفظ وإعادة فتح إضافية قد تضيف أخرى

مخطط دورة حفظ XFA في PDFium Component حيث يضيف الكاتب سطراً جديداً بعد وسوم البداية، ويقرأ المحلل المعاد فتحه الـ LF بين وسمي Comments قيمةً للحقل، ويلحق كل حفظ إضافي تغذية سطر أخرى حتى يزيل v3.125.2 المسافة التي صنفها المُسلسل وحدها
تزرع دورة حفظ وإعادة فتح واحدة أول تغذية سطر وكل جولة إضافية تضيف أخرى، ولهذا أظهر الانحراف صورته الكاملة في الجيل الثاني فقط

الإصلاح البديهي، قص القيم عند التحميل، سيكون خطأ. يكتب المستخدمون مسافات ابتدائية ومسافات ختامية ونصاً متعدد الأسطر مقصوداً في حقول XFA، وكتلة عنوان أو شفرة بعرض ثابت يجب أن تنجو بايتةً بايتة. يزيل إصلاح v3.125.2 لذلك المسافةَ التي صنّفها المُسلسل نفسه حول الوسوم فقط. قيمُ المستخدم وعُقد النص القائمة وأقسام CDATA تعبر بلا مساس، فيبقى " indented" مسنداً ويبقى الحقل المقصود فراغه فراغاً

لماذا يعود الإيموجي محرفاً مختلفاً؟

عاد الإيموجي خطأً لأن wchar_t في ويندوز بعرض 16 بت، وخزن مسارا فكٍّ قيمة Unicode عددية كاملة في wchar_t واحدة. فعل ذلك فك تشفير تدفق UTF-8 والمحلل الخاص بمراجع المحارف العددية مثل &#x1F642;. لا يتسع U+1F642، الوجه المبتسم قليلاً، في 16 بت، فسقطت البتات العليا وظهر U+F642 بدلاً منها: نقطة شيفرة في منطقة الاستخدام الخاص تصيرها أغلب الخطوط صندوقاً أو لا شيء

وكان لمُسلسل النموذج المشكلة المعاكسة. رشّح المحارف wchar_t واحدة في كل مرة، فرأى وحدتَي شيفرة بدالتين غير صالحتين في انفرادهما وأسقطتاهما، فاختفى الإيموجي من حزمة النموذج كلياً. في v3.125.2 يستهلك الفك كل قيمة عددية كاملة ويبث زوج بدائل سليماً. وحين يبقى خانة ناتج واحدة يحبس البديل الدنيا معلقاً ولا يبلّغ نهاية التدفق بينما تلك الوحدة ما تزال مخبأة. وتسلسلة UTF-8 مقسومة عبر كتل قراءة تحمل إلى القراءة التالية بدل إهمالها. ويبقي مُصدِّر النموذج الآن أزواج البدائل الصالحة مجتمعة، وتنتج مراجع المحارف العددية أزواجاً سليمة هي الأخرى

مخطط معالجة البدائل في PDFium Component حيث تصل U+1F642 بوصفها زوج UTF-16 ‏D83D DE42 ويفسدها مسارا عيبين: فاكتا wchar_t ذوو 16 بت يقتربان العددية إلى U+F642 في منطقة الاستخدام الخاص، بينما يرشح مُسلسل النموذج البدائل المنفردة ويسقط الإيموجي كلياً
‏wchar_t في ويندوز بعرض 16 بت، فعددية تحتاج زوج بدائل إما فقدت نصفها العلوي أو اختفت من الحزمة حتى تعلم كلا المسارين إبقاء الأزواج مجتمعة

بيانات اختبار Latin-1 لا تعرض أي شيء من هذا، فكل اختبار دورة XFA يحتاج محرفاً واحداً على الأقل من المستوى التكميلي

‏XFA أحادي التدفق وإخفاقات حفظ لم يرها أحد

فقد مستند XFA أحادي التدفق تحريراته لأن مساعد الحفظ الأصلي رفض تخطيط التخزين ذاك وتجاهل مستدعيه الإخفاقَ. يجيز ISO 32000-1 §12.7.8 أن يكون مدخل /XFA لقاموس النموذج التفاعلي مصفوفة أسماء حزم وتدفقات أو تدفقاً واحداً يحمل مستند XDP كله. مصفوفات الحزم هي الحالة الشائعة، لكن التدفقات الفردية مشروعة تماماً، واكتمل حفظ PDF كأن لم يحدث شيء بينما بقيت بيانات النموذج على قيمها القديمة

منذ v3.125.2 تعالج بيئة التشغيل V8 المجموعة الجزئية المدعومة من التدفق الفردي. تصدّر أولاً الحقتين الحيتين، datasets و form، إلى منطقة تجهيز وتتحقق منهما، وبعدها فقط تستبدل الحزمتين المطابقتين في XDP الأصلي. وتُحفظ الحزم الأخرى وتصريحات نطاق الجذر. وإن فشل التجهيز لم تُمس البثق XFA الدائمة إطلاقاً ويحتفظ المستند بعلامة تعديله

احتاجت تعليقات XML وتعليمات المعالجة عناية إضافية لأن DOM الداخلي لـ XML يُسقطها. في v3.125.2 كان حضورهما يجعل الحفظ يفشل تماماً بدل فقد محتوى بصمت. يحفظها v3.126.0: قبل التحليل تُستبدل كل تعليقة أو تعليمة معالجة علامةً مبنية من بادئة لا تقع في أي مكان في النص الأصلي. وبعد استبدال الحزم الحية يجب أن تظهر كل علامة مرة واحدة بالضبط قبل استعادة الرمز الأصلي وكتابة التدفق. تحفظ الرموز خارج الحزم المستبدلة نصَّها وترتيبَها، بما في ذلك الرموز في المقدمة والقالب والحزم الأخرى

بعض المدخلات ما زالت مرفوضة عن قصد، وكل رفض إخفاق حفظ صريح:

  • تعليقات أو تعليمات معالجة داخل حقتي datasets أو form الحيتين، لأن مواضعهما الأصلية لا يمكن إسقاطها في محتوى مصدَّر طازج
  • تصريحات DTD وتواقيع XMLDSig، لأن إعادة كتابة XDP لا تستطيع إبقاء توقيع XML صالحاً
  • ترميز UTF-8 أو UTF-16 غير صالح، وسوم غير مكتملة، مراجع محارف غير صالحة، كيانات مجهولة وتعليمات معالجة مشوهة، ترفض بدل أن تُصلَّح بصمت
مخطط مسار حفظ XFA أحادي التدفق في PDFium Component حيث تصدَّر الحقتان الحيتان datasets و form إلى تجهيز وتتحققان ثم تستبدلان داخل XDP الأصلي مع حفظ التعليقات عبر علامات، بينما ترفض إخفاقات التجهيز ومدخلات مثل DTD أو XMLDSig الحفظَ صراحةً
يُتحقق من التصدير المجهز قبل استبدال أي شيء، فيترك الحفظ الفاشل بثق XFA الدائمة بلا مساس ويحتفظ المستند بعلامة تعديله

ناتج التدفق الفردي ‏UTF-8 ويحفظ نموذج محتوى XML، لا التخطيط البايتي الأصلي ولا تصريح الترميز

جلس العيب الأخير تحت XFA. يخبئ كاتب الملفات الأصلي الناتج كتلًا من 32 كيلوبايت ويفرغ الكتلة الجزئية الأخيرة في هادمه فقط، بعد أن كان كاتب المستند قد أبلغ النجاح سلفاً. فخطأ قرص ممتلئ أو خطأ إدخال/إخراج في تلك الكتلة الأخيرة كان غير مرئي للمستدعي. ومنذ v3.125.2 في بيئة V8 و v3.125.3 في البيئة العادية صار ذلك الإفراغ النهائي جزءاً من نتيجة الحفظ، ولا يُمسح علام تعديل XFA إلا بعد نجاح حقيقي. وعلى جانب Delphi تكتب TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean إلى ملف مؤقت بجوار الهدف وتنقله إلى مكانه فقط عندما يعيد الحفظ True، فيترك الحفظ الفاشل الملف السابق سليماً

لماذا يعاد فتح نموذج XFA ديناميكي بصفحات أقل؟

يُعاد فتح نموذج XFA الديناميكي بصفحات أقل حين لا يصرّح subform جذوره بـ restoreState="auto"، وذلك قرار تأليف نموذجٍ لا عيب في PDFium Component. في XFA 3.3 افتراض restoreState على subform الجذور هو manual. وتحت manual يعيد معالج XFA حالة محدودة فقط من حزمة النموذج المحفوظة ويترك البقية لسكربتات المؤلف. قيم الحقول المحفوظة وأعداد نسخ subforms المتكررة تعود مع ذلك، أما الخصائص الهندسية المضبوطة وقت التشغيل فلا

الحالة التي كشفت هذا نموذجٌ من ثلاث صفحات كبر سكربته subform إلى h="450pt". حملت حزمة النموذج المحفوظة الارتفاعَ الجديد والقيمَ وأعداد النسخ. لكن عند إعادة الفتح أُعيد بناء التخطيط من ارتفاعات القالب وأعيد تدفق النموذج على صفحتين. وكانت بيئة التشغيل محقة: القالب لم يطلب الاستعادة الآلية قط. تصريح ذلك على subform الجذور يصلح إعادة الفتح:

<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
  <subform name="form1" layout="tb" restoreState="auto">
    <pageSet>
      <pageArea name="Page1">
        <contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
        <medium stock="letter"/>
      </pageArea>
    </pageSet>
    <subform name="Details" layout="tb" w="7.5in">
      <!-- حقول؛ قد تغير السكربتات h أو تضيف نسخاً وقت التشغيل -->
    </subform>
  </subform>
</template>

إن لم تكن تملك القالب فلا ترقعه في العارض: نموذج يعتمد وضع manual يتوقع أن تعيد سكربتاته بناء الحالة. إعادة الترقيم الحية بينما يكتب المستخدم موضوع مستقل، مغطى في كيف يتتبع PDFium Component أعداد صفحات XFA الديناميكية والحقول المنقولة

كيف تتحقق من حفظ XFA في Delphi؟

الفحص الموثوق الوحيد لحفظ XFA هو إعادة فتح الملف المحفوظ في مثيل TPdf جديد وقراءة البيانات المخزنة عائدةً. تعيد TPdf.GetXfaDatasets حزمة datasets كما هي مخزنة في المستند، لا نموذج بيانات XFA الحي، فاستدعاؤها قبل الحفظ يعرض القيم القديمة. وبعد إعادة الفتح تعرض بالضبط ما كُتب. مستند التدفق الفردي لا يملك حزمًا مسماة منفصلة: يبلّغ PDFium عن XDP كله حقةً واحدة باسم فارغ، فتعيد GetXfaPacketByName('datasets') و GetXfaDatasets لا شيء، ويقرأ الاحتياط التدفقَ الكامل عبر GetXfaFormPackets

uses
  System.SysUtils, PDFium, FPdfXfa;

function ReadSavedXfaData(const FileName: string): string;
var
  Pdf: TPdf;
  Packets: TXfaPacketList;
  Bytes: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Bytes := Pdf.GetXfaDatasets;          // تخطيط مصفوفة حزم
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // تدفق واحد: حقة بلا اسم
      if Length(Packets) = 1 then
      begin
        SetLength(Bytes, Length(Packets[0].Content));
        if Length(Bytes) > 0 then
          Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
      end;
    end;
    Result := TEncoding.UTF8.GetString(Bytes);  // ناتج XDP المحفوظ ‏UTF-8
  finally
    Pdf.Free;
  end;
end;

يثبت روتين الحفظ بعد ذلك التحريرَ المعلق، ويفحص نتيجة SaveAs ويقارن القيمة المعادة الفتح. يقتل TPdf.ClearFormFieldFocus تركيزَ النموذج، وهي لحظة إثبات PDFium مخزنَ تحرير الحقل المركز. تملأ TPdf.SetFocusedFormFieldText(const Value: WString): Boolean الحقلَ المركز برمجياً، لكنها تعتمد على تركيزٍ يتتبعه الغلاف عبر FocusFormField التي تجوال تعليقات الودجات. صفحة XFA ديناميكية لا تملك عادة أياً منها، فهناك يصل النص عادة عبر إدخال لوحة المفاتيح في TPdfView، وتعيد الدالة False حين لا حقل متتبع له تركيز

function XmlText(const S: string): string;
begin
  Result := StringReplace(S, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
end;

procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
  Expected: string);
var
  Saved: string;
begin
  // ملء سكربتي اختياري؛ ‏False تعني لا حقل متتبع له تركيز
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // يثبت مخزن التحرير
  if not Pdf.SaveAs(FileName) then      // يشمل الإفراغ النهائي ‏(v3.125.2+)
    raise EPdfError.CreateFmt('Saving %s failed', [FileName]);

  Saved := ReadSavedXfaData(FileName);
  if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
    Saved) = 0 then
    raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;

عامل اختبار السلسلة الجزئية اختبار دخان. قد يُسلسل عنصر فارغ <Tag/>، وقد تظهر خصائص على عناصر البيانات، والتهريب أبعد من & و < قرارُ مُسلسل. لفحوص الإنتاج حمّل XML المعاد فتحه بمحلل XML حقيقي وقارن عقدة النص لعنصر البيانات المرتبط. وشغّل الفحص مرتين متتاليتين أيضاً، لأن عيب السطر الجديد لم يظهر صورته الكاملة إلا في الجيل الثاني

مرجع سريع: قائمة تحقق أمانة حفظ XFA

  • انشر pdfium.v8.dll من v3.125.2 أو أحدث لنماذج XFA، و v3.125.3 أو أحدث لـ pdfium.dll العادية، فيكون إصلاح الكتابة النهائية في كليهما
  • وجّه LibraryName إلى مسار كامل واضبط EnableV8Engine على True؛ يفشل المسار المفقود بدل تحميل نسخة أخرى
  • أكّد TPdf.XFA و TPdf.XfaRuntimeAvailable بعد فتح المستند
  • استدعِ ClearFormFieldFocus قبل SaveAs ليُثبَت الحقل المركز
  • لا تتجاهل النتيجة المنطقية لـ SaveAs قط؛ تترك النتيجة False الملفَ السابق في مكانه
  • تحقق بإعادة الفتح في TPdf جديد وقراءة GetXfaDatasets، مع التراجع إلى GetXfaFormPackets لـ XFA أحادي التدفق
  • اختبر بقيم فارغة ومسافات ابتدائية ونص متعدد الأسطر و & ومحرف من المستوى التكميلي، عبر جيلي حفظ
  • توقع إخفاقات حفظ صريحة لـ DTD و XMLDSig والتعليقات داخل الحزم الحية لـ XFA أحادي التدفق
  • إن فقد نموذج ديناميكي هندسته وقت التشغيل عند إعادة الفتح، افحص subform الجذور عن restoreState="auto" قبل أن تشك في المكتبة

لبنية الاستدعاءات التي تتوقعها بيئة تشغيل XFA من تطبيق مضيف راجع الإصدار 2 من FPDF_FORMFILLINFO وواجهة XFA ABI في Delphi. بيئة التشغيل V8 والغلاف لـ Delphi و C++Builder وعنصر العارض كلها أجزاء من PDFium Component for Delphi and C++Builder، التي تضم بيئتَي تشغيل ويندوز لـ Win32 و Win64