مقال تقني

تشغيل نماذج XFA الديناميكية في Delphi: معاملات HotPDF

يملأ HotPDF نماذج XFA الديناميكية في Delphi عبر TXFAWidgetRuntime، طبقة عناصر واجهة محايدة تجاه المضيف تتعامل مع كل تعديل حقل على أنه معاملة واحدة: لقطة، ثم تحقّق، ثم حساب، ثم إعادة تدفّق، ثم نشر كامل أو تراجع كامل. يعمل أحادي الخيط داخل مضيف VCL أو FMX الخاص بك، ولا يحتاج إلى تثبيت Acrobat، ويفرض كل الحدود قبل أن يخصّص أي شيء

المشهد مألوف لكل من شحن برمجيات مستندات إلى قطاع حكومي أو تأميني. تصل نموذج مطالبة أو إقرار ضريبي في ملف PDF يكون محتوى صفحاته عبارة عن إشارة «Please wait... if this message is not eventually replaced» وحيدة، بينما تعيش كل الحقول الحقيقية في حزمة XFA لا يعرضها سوى Adobe Acrobat. ويريد مستخدموك تعبئته داخل تطبيقك. ولا مخرج بالتنقيط أيضًا، لأن النموذج يضيف صفوفًا كلما أُدخلت البيانات، والتخطيط بعد الصف الثالث ليس هو التخطيط الذي وصل في الملف

لماذا تظل XFA الديناميكية مشكلة تستحق الحل

تبقى XFA الديناميكية لأن النماذج الموزَّعة تعمّر أطول من الصيغة التي حملتها. تصف ISO 32000-1 §12.7.8 XFA بأنه مدخل /XFA في قاموس AcroForm يحمل تدفّق حزمة XDP، بينما تُهمل ISO 32000-2 الآلية كاملة؛ الإهمال أزالها من خارطة الطريق لا من الميدان، فالنماذج المكتوبة وفق مواصفة XFA 3.3 ما زالت تُصدَر وما زالت ملزمة قانونًا. يمكن اختزال XFA الثابتة إلى تعليقات عناصر واجهة عادية، وهذا ما يفعله HotPDF حين تستدعي ApplyXFAAsAcroForm، مع المقايضات التي يغطيها مقال تسطيح نماذج XFA إلى حقول AcroForm. أما XFA الديناميكية فحيوان آخر: نطاقات occur فيها ونصوصها القابلة للتمدد وسكربتات calculate تجعل مجموعة الحقول دالةً في البيانات، فلا توجد قائمة تعليقات ثابتة يُسطَّح إليها حتى ينتهي المستخدم من الكتابة. هذه هي الفجوة التي يملؤها TXFAWidgetRuntime، بإبقاء شجرة XFA DOM حيّة، وإعادة حساب التخطيط بعد كل تعديل مقبول، وتسليم مضيفك مصفوفة مسطّحة من عناصر الواجهة المُموضعة لرسمها واختبار النقر عليها

ماذا يسلّم وقت التشغيل إلى التطبيق المضيف؟

يسلّمك الهندسة والحالة، ولا شيء يفترض إطار عمل واجهة بعينه. يكشف TXFAWidgetRuntime كلاً من WidgetCount وWidgets[I] كسجلات TXFAWidgetState تحمل ID وName وKind وPageIndex وBounds بنقاط PDF، وValue وEditValue وأعلام Focused وEditing وReadOnly وValid، بينما يبقى الطلاء ورسم مؤشر الإدخال وتوجيه لوحة المفاتيح في كودك أنت. هوية عنصر الواجهة مستقرة وترتيبية: كل عنصر يأخذ ID بالصيغة name[n] حيث يعدّ n المواضع السابقة لاسم الحقل ذاك بترتيب التخطيط، فيكون الصف الثاني لنموذج فرعي متكرر هو amount[1]. هذه الهوية هي ما يصمد بعد إعادة البناء، وهي ما تتخاطب به FocusWidget وBeginEdit وDispatchEvent وHitTest. وبالنسبة لمستند مفتوح أصلًا في مثيل THotPDF، تستخرج CreateLoadedXFAWidgetRuntime حزم XDP، وتأخذ مربع الصفحة الأولى مقاسًا لصفحة التخطيط، وتُعيد nil حين لا يحمل الملف أي XFA إطلاقًا

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // قيمة nil عند غياب /XFA
    if Runtime = nil then
      Exit;
    try
      for I := 0 to Runtime.WidgetCount - 1 do
        Memo1.Lines.Add(Format('%s p%d [%.1f %.1f %.1f %.1f] = %s',
          [string(Runtime.Widgets[I].ID), Runtime.Widgets[I].PageIndex,
           Runtime.Widgets[I].Bounds.Left, Runtime.Widgets[I].Bounds.Top,
           Runtime.Widgets[I].Bounds.Right, Runtime.Widgets[I].Bounds.Bottom,
           string(Runtime.Widgets[I].Value)]));
      // اختبار نقر في مساحة الصفحة؛ العنصر الأعلى هو الفائز
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

ما الذي يجب أن يكون ذرّيًا عند اعتماد الحقل؟

كل ما يمكن أن يلمسه التعديل، وهو أكثر بكثير من قيمة الحقل. تستدعي CommitEdit الدالة CaptureSnapshot قبل أن تكتب أي شيء، وتغطي اللقطة أربعة أشياء: شجرة XFA DOM المُسلسلة من TXFADocument.SaveToBytes، والمصفوفة الكاملة لسجلات تفاعل TXFAWidgetState، وعدّادَي LastCalculationPasses وLastReflowPasses، والقيمة الحالية لـ Warnings.Count. حفظ قيم العقد وحدها هو الاختصار المغري وهو خطأ، لأن سكربت calculate أو ربطًا غير محلول قد يستدعي EnsureValueNode فيُنشئ عقد بيانات لم تكن موجودة لحظة بدء التعديل؛ ولا تملك استعادة القيم فقط وسيلة لإزالتها، فيترك التعديل المرفوض بقايا بنيوية دائمة في حزمة البيانات. تسلسل الاعتماد نفسه صارم — اكتب القيمة المرشّحة، وشغّل validate للحقل المعدَّل، وشغّل calculate حتى نقطة الثبات، ثم أعد التدفّق حتى يستقر التخطيط — وأي فشل في أي مرحلة يمرّ عبر FailAndRestore، التي تعيد تحميل بايتات اللقطة في TXFADocument جديدة، وتعيد بناء قائمة عناصر الواجهة، وتعيد تطبيق حالات التفاعل المسجَّلة، وتصفّر العدّادات وتقتطع Warnings إلى طولها في اللقطة. تحمل LastDiagnostic السبب عند الفشل، وتحمل النص الحرفي XFA transaction rollback failed في الحالة الشاذة حين يفشل التراجع نفسه

يعامل HotPDF اعتماد حقل XFA كمعاملة واحدة، فيلتقط شجرة DOM المُسلسلة وكل حالات عناصر الواجهة وعدّادات المرات وعدد التحذيرات قبل التحقق والحساب وإعادة التدفق، ثم ينشر الحالات الأربع كلها أو يستعيدها معًا
تلتقط CommitEdit أربعة أنواع من الحالة قبل أن تكتب أي شيء، حتى لا يترك فشل التحقق أو الحساب أو إعادة التدفق أي بقايا بنيوية خلفه
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // للقراءة فقط، أو لا يوجد عنصر بهذا المعرّف
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // نطاق غير صالح، أو فصم لزوج بديل UTF-16
    Exit;
  end;
  Result := Runtime.CommitEdit;             // الكل أو لا شيء
  if not Result then
    // المستند والعناصر والعدّادات والتحذيرات عادت بالفعل إلى
    // حالة ما قبل التعديل؛ والعنصر المركَّز يُعلَّم غير صالح فقط
    ShowMessage(Runtime.LastDiagnostic);
end;

تستحق ReplaceSelection ملاحظة خاصة، لأنها أرخص موضع يُرفض فيه الإدخال المشوّه. ترفض تحديدًا يفصم زوج بديل UTF-16، وترفض نصًا بديلًا يحوي بديلًا عاليًا أو منخفضًا بلا قرين، وترفض أي نتيجة أطول من MaxValueChars. التقاط ذلك في طبقة ضغطات المفاتيح يعني أن آلية المعاملات لا تضطر أبدًا إلى التراجع عن حرف من المستوى النجمي كُتب نصفه

إعادة البناء في قائمة خاصة، والنشر بتبديل واحد

يجب ألا يُلاحَظ إعادة بناء عناصر الواجهة وهي نصف منجزة، لذلك تبني RebuildWidgets قائمة TObjectList مالكة منفصلة تمامًا وتبدّلها في مكانها بإسناد واحد في النهاية. السبب ليس تجميليًا: TXFALayoutEngine.ComputeLayout يعمل أثناء إعادة البناء ويستدعي كود المضيف عبر دالة MeasureText التي زوّدتها بها، وقد يطلق EXFAWidgetRuntimeError حين يُبلغ حد العناصر. لو عدّل وقت التشغيل قائمته الحيّة في مكانها، لانتهى أي من المسارين بيد المضيف على قائمة جزء منها التخطيط القديم وجزءها الجديد، وفيها مؤشرات DataNode إلى مستند على وشك التراجع عنه. تقارب إعادة التدفّق يُحسم بعدها عبر LayoutSignature، سلسلة تُبنى من عدد عناصر الواجهة مع كل ID ورقم صف ومربع محيط مقرّب إلى أربع خانات عشرية: CommitEdit تعيد البناء وتقارن البصمات وتكرر حتى تتطابق بصمتان متتاليتان أو يُستنفد حد المرات. حين لا تتغير البصمة إطلاقًا، يبقى LastReflowPasses صفرًا، وهكذا تُميّز تعديل القيمة فقط عن تعديل أنمى النموذج فعلًا، وتُنقل حالة التفاعل عبر كل إعادة بناء بمعرّف العنصر ID، فيصمد التركيز والتحرير الجاري عبر إدراج صف

يعيد وقت تشغيل XFA في HotPDF بناء قائمة عناصر الواجهة في قائمة مالكة منفصلة أثناء عمل التخطيط واستدعاء كود القياس في المضيف، ثم ينشر القائمة المنجزة بإسناد واحد لا يستطيع المضيف ملاحظته نصف مكتمل
تجري إعادة البناء في قائمة خاصة لأن ComputeLayout قد يطلق استثناءً في منتصف العمل، وتبتّ LayoutSignature في لحظة تقارب إعادتي تدفّق متتاليتين

لماذا قد يقرأ حقل مربوط السجل الخطأ؟

لأن السكربت عُمل بلا سياق بيانات. الحقل الذي يحمل <bind match="dataRef" ref="$record.actual"/> صريحةً والحقل المسمّى باسم عقدة البيانات ذاتها هما عنصرا واجهة مختلفان يشيران إلى قيمة واحدة، والنموذج الفرعي المتكرر ذو <occur max="2"/> ينتج عدة عناصر تتشارك اسمًا ولا تختلف إلا في صف البيانات الذي تنتمي إليه؛ قيّم التحقق والحساب مقابل جذر المستند فيُحلّ كل منها this إلى أول عقدة مطابقة في حزمة البيانات كلها، فيتحقق صف ثانٍ من صف أول بصمت. يتفادى HotPDF ذلك بتخزين DataNode المحلول على كل مدخل عنصر حين ينتجه التخطيط، ثم تمرير تلك العقدة عبر استدعاءَي HPDFXFAEvaluateFieldScript كلاهما، لـ xfskValidate وxfskCalculate على السواء. السياق نفسه يحدد العقدة التي يُنشئ مقابلها EnsureValueNode حين يستهدف حسابٌ ربطًا غير موجود بعد، وحين يتعذر حل أي ربط يفشل الاعتماد بوضوح برسالة XFA calculation target is not bound بدل الكتابة في الصف الخطأ. دلالات FormCalc خلف تلك السكربتات تعكس ما تناله مستندات AcroForm من الإجراءات الموضحة في سكربتات التنسيق والحساب في AcroForm، لكن قواعد الحل هنا ذات نطاق XFA لا نطاق أسماء الحقول

الحدود تُفحص قبل الآثار الجانبية لا بعدها

كل حد في وقت التشغيل شرط مسبق، لأن الحد الذي يُفرض بعد وقوع التخصيص ليس حدًا. تأتي TXFAWidgetRuntimeOptions.Default بـ MaxWidgets عند 10000 وMaxValueChars عند 1048576 وMaxCalculationPasses عند 16 وMaxReflowPasses عند 4، وتحمل TXFAFormScriptOptions الافتراضية MaxOperations عند 100000 مع MaxElapsedMilliseconds عند 500. وتحت ذلك، تطبق شجرة XFA DOM حدودها TXFADOMLimits الخاصة: سقف 128 ميغابايت للدخل والخرج بعد فك الضغط، و1024 حزمة على الأكثر مجمّعة معًا، ومليون عقدة، وعمق تداخل 256. ثمتان تفصيلتان أهم من الأرقام نفسها. أولًا، حدود السكربتات على مستوى المعاملة لا كل سكربت على حدة: CommitEdit تهيّئ عدّاد عمليات متبقية واحدًا وموعدًا نهائيًا رتيبًا واحدًا، وكل استدعاء تحقق أو حساب يستنفد من العدّاد ذاته ولا يتلقى إلا المللي ثانية المتبقية فعلًا، فلا يستطيع نموذج فيه مئتا حقل حاسبة أن ينفق 500 مللي ثانية كاملة مئتي مرة. ثانيًا، الموعد النهائي يأتي من دالة MonotonicMilliseconds قابلة للحقن، وهو ما يجعل سلوك الزمن المنقضي قابلًا للتكرار في مجموعة الاختبارات بدل أن يكون رمية حظ على وكيل بناء مشغول

طبقات الحدود في وقت تشغيل XFA بـ HotPDF، من حدود العناصر والقيم إلى حدود عمليات السكربت وزمنه وحتى سقوف شجرة XFA DOM، بعدّاد عمليات واحد وموعد نهائي واحد يتشاركهما كل استدعاء في المعاملة
حدود السكربتات على مستوى المعاملة لا كل سكربت، فلا يستطيع مئتا حقل حاسبة أن يطالب كل منها بـ 500 مللي ثانية جديدة
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // الافتراضي 10000
  Options.MaxCalculationPasses := 8;                       // الافتراضي 16
  Options.MaxReflowPasses := 2;                            // الافتراضي 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // للمعاملة بأكملها
  Options.ScriptOptions.Limits.MaxElapsedMilliseconds := 200;
  Options.MeasureText :=
    function(const AText: UnicodeString; const AFont: TXFAFontSpec;
      AMaxWidth: Double): TXFATextExtent
    begin
      Result := MeasureWithHostCanvas(AText, AFont, AMaxWidth);
    end;
  Runtime := TXFAWidgetRuntime.Create(XDPBytes, 612, 792, Options);
  try
    Runtime.OnLayoutChanged :=
      procedure
      begin
        RepaintAllPages;   // يُطلق فقط حين تحرّك إعادة التدفق العناصر فعلًا
      end;
    // ... قُد النموذج ...
  finally
    Runtime.Free;
  end;
end;

حين يتوقف وقت التشغيل، ولماذا يعلن ذلك بصوت عالٍ

وقت التشغيل ليس محرك سكربتات XFA عامًا عمدًا. تتعامل DispatchEvent مع نشاطَي enter وexit أصيلًا بنقل التركيز، وأي نشاط آخر يحمل سكربتًا ترفضه بتشخيص محدد وثابت بدل التظاهر: السكربتات التي تذكر addInstance أو removeInstance أو instanceManager تُرجع XFA runtime does not support event-driven instance mutation، والسكربتات التي تمس .presence تُرجع ما يقابلها للخاصية presence، وأي شيء سواها يُرجع XFA runtime does not support this event script. الرفض المتوقع الذي يتفرّع عليه كودك خير من محاكاة جزئية تنجح على ملفك التجريبي وتحاد على ملف العميل

ونموذج الخيوط صريح بالقدر نفسه: مثيل تشغيل واحد يخص خيطًا واحدًا بلا أقفال داخلية، لأن محرك التخطيط يعود إلى ندوات القياس في المضيف، والقفل حول ذلك حالة توقف تام تنتظر إعادة رسم. المحتوى الغني داخل الحقول يسلك النهج المتحفظ ذاته المتبع في مواضع أخرى من المكتبة، حيث تُعالَج حمولات exData كما هو موضح في النص الغني والارتباطات التشعبية في XFA exData، وتعود عناصر التوقيع والأزرار بصفة ReadOnly بينما تظهر أنواع الواجهة غير المدعومة بصفة xwkUnsupported بدل أن تظهر كمربع نص قابل للتحرير يفقد البيانات بصمت

مجتمعةً، هذه إجابة عملية لـ XFA الديناميكية في Delphi: أبقِ شجرة DOM حيّة، واجعل كل تعديل معاملة إما أن تطبَّق كاملة أو لا تترك أثرًا، وحدّد كل مرّة، وكن صريحًا بشأن ما هو خارج النطاق. إن كنت تقيّم هذا لسير عمل مطالبات أو ضرائب أو إعانات، فإن وقت تشغيل XFA يأتي ضمن مكوّن HotPDF لـ PDF في Delphi، إلى جانب مسارات AcroForm والتسطيح والتصيير التي تنتهي تلك المشاريع عادةً بالحاجة إليها معًا