مقال تقني

لماذا تختفي تعديلات حقول XFA عند الحفظ في PDFium لـDelphi

تكتب TPdf.SetFocusedFormFieldText في مكوّن PDFium إلى مخزن التحرير الحي لحقل النموذج الذي يحمل التركيز حاليًا، وبالنسبة لنموذج XFA لا يصل ذلك المخزن أبدًا إلى حزمة datasets التي تُسلسل إلى القرص — بحيث تختفي قيمة يكتبها مستخدم، وتؤكد شيفرتك أنها قُبلت، بصمت في المرة التالية التي يُفتح فيها الملف. لا تعاني حقول AcroForm من هذه المشكلة: يُلزِم الاستدعاء نفسه إدخال /V الخاص بالحقل في اللحظة التي ينتقل فيها التركيز بعيدًا. مستخدم يملأ نموذج استلام XFA، ويحفظ، ويعيد الفتح ليجد حقل المبلغ فارغًا مرة أخرى لا يصطدم بعلة رسم — إنه يصطدم بحافة ما يعرضه محرك PDFium نفسه لكتابة بيانات النموذج

هذا سؤال أضيق من اكتشاف نموذج XFA أصلًا، أو تشغيل جافاسكريبت الخاص به: ليس "هل يدعم PDFium XFA" وليس "كيف أنفّذ سكربتات AcroForm" بل تحديدًا ما يحدث لقيمة بعد أن تُبلّغ SetFocusedFormFieldText عن النجاح. الملخص القصير أن AcroForm وXFA ليسا لهجتين لنموذج النموذج نفسه بقدر ما يتعلق الأمر بمسار كتابة PDFium — إنهما نموذجا نماذج بعلاقتين مختلفتين تمامًا بين ما يكتبه المستخدم وما يلتقطه حفظ فعليًا، وخلط الاثنين هو ما يحوّل استدعاء واجهة برمجية من سطر واحد إلى تذكرة دعم بعد ثلاثة أسابيع من إطلاق نشر تجريبي لعميل. تُظهر مقالة جافاسكريبت AcroForm الاستدعاء أحادي السطر وتنص على نتيجة AcroForm مقابل XFA في تعليق شيفرة؛ هذه تبقى على الواجهة البرمجية نفسها وتستعرض مسار الكتابة الداخلي، والدليل عبر حزمة datasets على أن كتابة XFA لا تصل أبدًا، ولماذا تقع الفجوة في PDFium نفسه بدلًا من ربط Delphi، وحلًا بديلًا يتمثل في ترقيع XML الخاص بك للمستندات التي تحتاج التعديل للنجاة من الحفظ

كيف تكتب SetFocusedFormFieldText قيمة حقل؟

تعمل TPdf.SetFocusedFormFieldText بمحاكاة تعديل على مستوى ضغطات المفاتيح، لا بدفع قيمة إلى نموذج المستند مباشرة. تستدعي داخليًا FORM_SelectAllText لتحديد محتوى الحقل المُركَّز الحالي، ثم FORM_ReplaceSelection للكتابة فوق التحديد بالسلسلة الجديدة — العمليتان نفسهما اللتان كانتا لتُفعِّلهما عملية تحديد-الكل-وكتابة مدفوعة بلوحة المفاتيح. ولأن الكتابة تمر عبر مسار تحرير النص التفاعلي في PDFium بدلًا من الالتفاف حوله، فإن أي سكربت ضغطة مفتاح، أو تنسيق، أو حساب مرتبط بالحقل يُطلَق تمامًا كما كان ليفعل لإنسان يكتب، وهذا ما يجعل الواجهة البرمجية مفيدة لملء النماذج البرمجي في عارض يبقي جافاسكريبت حية. نظير جانب القراءة هو FocusedFormFieldText، مدعومًا بـFORM_GetFocusedText، ويعكس المخزن الحي نفسه الذي كتبت فيه SetFocusedFormFieldText للتو

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

لماذا يحتفظ AcroForm بالقيمة بينما يفقدها XFA؟

تدوم نصوص AcroForm وحقول القوائم المنسدلة لأن بيئة ملء النماذج الخاصة بـPDFium نفسها تُلزِم مخزن التحرير عنك: في اللحظة التي يفقد فيها الحقل التركيز، يُكتب المخزن في إدخال /V الخاص بالحقل، المفتاح نفسه الذي ينظر إليه كل قارئ PDF متوافق لمعرفة قيمة حقل مخزَّنة. TPdf.ClearFormFieldFocus — التي تستدعي FORM_ForceToKillFocus تحت الغطاء — تفرض ذلك الإلزام عند الطلب، بحيث لا تحتاج شيفرة تضبط قيمة برمجيًا الانتظار لنقرة ماوس حقيقية في مكان آخر من الواجهة. احفظ فورًا بعد ذلك، ويصبح النص الجديد جزءًا من رسم كائنات المستند قبل أن تعمل TPdf.SaveAs على الإطلاق، لأن /V إدخال حقيقي في قاموس حقل حقيقي، لا شيء ملحق لاحقًا

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

أين يعيش تعديل حقل XFA فعليًا؟

لا تملك حقول XFA أي توصيل كهذا. النص الذي يكتبه المستخدم يقع في مخزن CPWL_Edit ينتمي إلى طبقة رسم وتفاعل XFA الخاصة بـPDFium، وتلك الطبقة لا تملك أي مسار شيفرة ينسخ المخزن مرة أخرى إلى حزمة datasets المخزَّنة في PDF. تجعل TPdf.GetXfaDatasets الفجوة مرئية: استدعِها قبل وبعد تعديل على حقل XFA وستكون البايتات التي تعيدها متطابقة، لأن الطريقة تقرأ الحزمة الأصلية التي فُتح بها المستند، لا الحالة الحية للأداة التي عدّلتها للتو. لا شيء في ذلك خلل ذاكرة تخزين مؤقت أو مشكلة توقيت تحديث — حزمة البيانات على القرص ومخزن التحرير في الذاكرة قطعتا حالة مختلفتان ببساطة لا تربطهما أبدًا الواجهة البرمجية العامة لـPDFium

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

هل هذا خلل في مكوّن PDFium أم قيد في PDFium؟

القطعة المفقودة تقع في PDFium نفسه، لا في ربط Delphi فوقه. لا تملك الواجهة البرمجية العامة لـPDFium FPDF_SetXFAPacket لحقن حزمة مُحدَّثة ولا FPDF_SaveAsXFA لطلب من محرك XFA تسلسل نموذج DOM الحالي الخاص به مرة أخرى إلى XML لـdatasets قبل الحفظ. تكتب FPDF_SaveAsCopy — التصدير الذي يدعم TPdf.SaveAs — رسم كائنات المستند الذي يمتلكه PDFium بالفعل؛ ولا يملك خطاف لطلب من محرك XFA تفريغ حالته الحية أولًا، لأن ذلك الخطاف غير موجود في المنبع. لا يمكن لمكوّن PDFium إضافة تسوية لم ينفّذها PDFium نفسه قط، وشحن مُسلسِل DOM-إلى-XML منزلي الصنع يخمّن حالة XFA الداخلية لـPDFium كان سيكون أسوأ من الفجوة الصادقة: كان سيبدو أنه يعمل حتى يغيّر إصدار PDFium التالي شيئًا لا يستطيع أحد خارج المشروع رؤيته

ظهر هذا الحد أثناء تدقيق v2.13.2 نفسه الذي بنى SetFocusedFormFieldText أصلًا. كانت FORM_ReplaceSelection مربوطة في جدول استيراد DLL لعدة إصدارات دون أن تُستدعى قط من شيفرة Pascal، وإضافة مسار الكتابة الذي استخدمها أخيرًا هو ما جعل فجوة الديمومة ملموسة بما يكفي لتوثيقها بدلًا من كونها نظرية. كشفت جولة التدقيق نفسها فجوة غير مرتبطة لكنها ذات صلة بالروح: كانت جافاسكريبت AcroForm مُعطَّلة بصمت منذ v2.13.0 لأن منصة JS كانت موصولة فقط داخل فرع تهيئة XFA، بحيث لم تحصل مستندات AcroForm العادية بـapp.alert أو حقول محسوبة على أي محرك سكربت على الإطلاق. كانت تلك قابلة للإصلاح — توسيع منصة JS إلى كل مستند بصرف النظر عن XFA — وشُحنت في الإصدار نفسه؛ لم تكن فجوة الديمومة المغطاة هنا قابلة للإصلاح، للأسباب أعلاه. إصلاح جافاسكريبت وأحداث حق النقض المضيفة حوله مشروحة في تشغيل جافاسكريبت AcroForm عبر مكوّن PDFium

ماذا يجب أن تفعل حيال ذلك في Delphi؟

بالنسبة لمستندات AcroForm، الإصلاح ليس أكثر من عادة جيدة: استدعِ ClearFormFieldFocus (أو انقل التركيز بعيدًا بطريقة أخرى) قبل SaveAs كلما ضُبطت قيمة برمجيًا، بدلًا من افتراض أن تفاعل واجهة لاحق سيُفعّل الإلزام عنك. بالنسبة لمستند قد يكون AcroForm أو XFA — وهي الحالة الشائعة في عارض عام الغرض — تحقق من FormType أو المنطقي XFA قبل أن تعد مستدعيًا بأن حفظًا سيلتصق، واقرأ اكتشاف نماذج XFA واستخراج حزم XFA للحصول على المجموعة الكاملة من الفحوصات، بما في ذلك حالة XFAF حيث يُوضَع محتوى XFA فوق أدوات AcroForm العادية من ناحية أخرى والتي تحترم /V بالفعل

بالنسبة لنموذج XFA ديناميكي حقيقي حيث يجب أن تنجو القيم المُعدَّلة من الحفظ، مخزن التحرير التفاعلي ليس الأداة الصحيحة على الإطلاق. المسار الدائم هو معاملة GetXfaDatasets كخط أساسك، لا كنتيجتك: اقرأها مرة واحدة عند فتح المستند، احتفظ بسجلك الخاص لما غيّره المستخدم حقلًا بحقل — القيم نفسها بالضبط التي تملكها واجهتك بالفعل، بما أن PDFium لن تعيدها إليك بعد وقوعها — رقّع تلك في XML الأساسي بنفسك، وشغّل مخرجاتك الخاصة. كتابة تمر عبر XML تتحكم فيه شيفرتك الخاصة تنجو من حفظ لم يستطع مخزن CPWL_Edit النجاة منه أبدًا

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

اكتشاف الفجوة قبل أن يكتشفها عميل

تُعيد TPdf.SaveAs القيمة True سواء نجت قيمة حقل XFA أم لا، لأنه من وجهة نظر PDFium نجح الحفظ حقًا — كتب كل بايت طُلب منه كتابته. هذا ما يجعل هذا بالضبط نوع العيب الذي يفلت من اختبار دخان ويصل إلى عميل: لا شيء يُطلق استثناءً، لا شيء يُسجَّل، يُفتح الملف بشكل جيد، فقط القيمة المحددة خاطئة. اختبار ذهاب وإياب يعيد فعليًا فتح الملف المحفوظ ويقارن قيمة الحقل — أو يقارن GetXfaDatasets قبل وبعد، وفق المثال السابق — ينتمي إلى مجموعة اختبارات الانحدار لأي عارض يسمح للمستخدمين بتحرير محتوى XFA، لا مسارات AcroForm فقط التي يصادف أنها تعمل افتراضيًا

لا شيء من هذا عيب يُبلَّغ عنه ضد مكوّن PDFium بقدر ما هو حد يجب التصميم حوله: تفعل SetFocusedFormFieldText بالضبط ما يقوله اسمها لكلا نموذجي النموذج، ويعود الفرق في النتيجة بنظافة إلى ما يربطه كل من AcroForm وXFA بذلك المخزن على جانب PDFium. الواجهة البرمجية، وأوليات التركيز والحفظ، وقارئات الحزم المُشار إليها هنا جزء من مكوّن PDFium لـDelphi وC++Builder