مقال تقني

‏XFA الديناميكية في PDFium Component: عدد الصفحات دلتا

حين يضيف نموذج XFA ديناميكي في عارض Delphi صفحاتٍ أو يزيلها، يبلّغ PDFium Component المجموع الجديد عبر TPdf.PageCount و TPdf.OnXfaPageCountChanged منذ v3.126.1، لأن حدث الصفحات الأصلي يحمل دلتا إضافة/إزالة لا مجموعاً. وتنقل مكتبات V8 في ويندوز في v3.126.1 مناطق نقر الإدخال مع الحقول المنقولة أيضاً، ويعيد v3.126.2 تحميل معالجات الصفحات اليتيمة بعد عودة استدعاء التخطيط. وبلاغ العيب الذي أشعل هذا كان نموذج مطالبة نفقات: انقر Add Row مرتين، فيكبر النموذج إلى صفحتين، ويكتب مؤشر الصفحات بفخر 1 من 1. واكتب في حقل انتقل إلى الصفحة 2 فتهبط ضربات المفاتيح في مكان غير مرئي. لم يظهر شيء من ذلك في النماذج ذات الطول الثابت التي يجرّبها الجميع أولاً، وأسباب ذلك جديرة بالمعرفة إن كنت تدمج عارض نماذج

ماذا يحدث حين يعيد نموذج XFA ديناميكي الترقيم؟

نموذج XFA الديناميكي لا يملك قائمة صفحات ثابتة، فيكون عدد صفحاته ناتجاً عن التخطيط ويمكن أن يتغير مع كل تحرير بيانات من المستخدم. يصف XFA 3.3 النموذج شجرة subforms؛ ويقود subform متكرر instanceManager، وسكربت مثل _Row.addInstance() يستنسخ صفاً إضافياً. ثم يعيد معالج التخطيط تدفق المحتوى إلى مناطق الصفحات من جديد، فيضيف صفحةً أو يسقط صفحةً أو يدفع حقولاً قائمة إلى صفحة أخرى. يعرف ISO 32000-1 §12.7.8 فقط كيف تركب حزم XFA داخل PDF؛ وكل ما يجري بعدها يعود إلى محرك XFA، وهو في PDFium Component تخطيط XFA الخاص بـ PDFium شغّال في العملية المضيفة. يتعامل عارض Delphi إذن مع مستند يكون عدد صفحاته وأحجام صفحاته ومواضع ودجاته كلها حالة حية. وتصيب ثلاثة أخطاء حين يفترض المضيف خلاف ذلك:

  • عدد الصفحات الذي يخبئه المضيف للملاحة ونطاقات التمرير ومؤشرات الصفحات يصير يتيم الأحدث، أو أسفل من ذلك، يحدث بالرقم الخطأ
  • الحقول المنقولة تعرض حدودها في الموضع الجديد بينما يبقى المحرر ومنطقة نقر الفأرة في الإحداثيات القديمة
  • يحتفظ العارض بمعالج صفحة استبدله التخطيط، فيذهب النقر والرسم إلى صفحة لم تعد موجودة في ذلك النموذج

إبقاء تحريرات الصفوف عبر الحفظ وإعادة الفتح مشكلة مستقلة لها قواعدها؛ تبقى هذه المقالة مع ما يجري وقت التشغيل داخل العارض

أي بيئة تشغيل PDFium تحتاج XFA الديناميكية؟

تتطلب XFA الديناميكية في PDFium Component بناء V8/XFA من المكتبة الأصلية، المنتقاة بالمتغير العام EnableV8Engine في وحدة PDFium قبل تحميل أول مستند. تلتزم العملية بـ DLL واحدة أول مرة يحمل فيها أي TPdf المكتبة، ولا يستطيع بناء PDFium عاري تشغيل محرك XFA أصلاً. وحين يُفتح مستند يلمح TPdf الملفَ بحثاً عن علامات XFA ويتحول إلى بناء V8 آلياً، لكن فقط إن لم تُحمَل مكتبة عارية بعد في تلك العملية. وحين مضى الالتزام في الاتجاه الخطأ يشتعل TPdf.OnXfaRuntimeMissing مرة واحدة ليخبر المضيف المستخدمَ بضرورة إعادة التشغيل. وضبط العلم صراحةً عند الإقلاع يزيل التخمين. كما يجب أن تطابق بنية الاستدعاءات FPDF_FORMFILLINFO الحاملة لأحداث XFA الـ DLL؛ والخلفية في الإصدار 2 من FPDF_FORMFILLINFO وواجهة ABI للاستدعاءات في XFA، وتغطي كشف نماذج XFA وقراءة حزمها التمييز بين أنواع النماذج قبل أن تفتح عارضاً

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // احسم قبل أن يحمل أول TPdf المكتبة الأصلية:
  // لا تستطيع العملية التبديل من pdfium.dll إلى pdfium.v8.dll لاحقاً
  EnableV8Engine := True;

  FPdf := TPdf.Create(nil);
  FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
  FPdf.FileName := 'C:\Forms\expense-claim.pdf';
  FPdf.Active := True;

  PdfView1.Pdf := FPdf;
  PdfView1.OnPageChange := PdfViewPageChange;
  PdfView1.Active := True;

  UpdatePageRange(FPdf.PageCount);
end;

procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  StatusBar1.SimpleText :=
    'This XFA form needs the V8 runtime; restart the application to enable it';
end;

لماذا بلّغ PageCount قيمة 1 لنموذج من صفحتين؟

قبل v3.126.1 خزن PDFium Component وسيط page_count لحدث الصفحات الأصلي مجموعاً للمستند، وذلك الوسيط هو في الحقيقة الفرق المطلق بين عددَي الصفحات الجديد والقديم. يرفع PDFium ‏FFI_PageEvent بعد انتهاء ممر تخطيطٍ بنوع حدث صفحة مضافة أو صفحة محذوفة؛ ويحدث داخلياً عدده المخزن أولاً ثم يمرر abs(new - old). في التخطيط الأولي يكون العدد القديم صفراً، فتساوي الدلتا المجموع، وتبلّغ عيّنة ساكنة من ثلاث صفحات ثلاث صفحات كما هو متوقع. وذلك بالضبط سبب عدم كشف نماذج الاختبار ذات الطول الثابت للعيب قط. أول مرة يكبر نموذج ديناميكي من صفحة إلى صفحتين تكون الدلتا 1، وضعت الغلاف TPdf.PageCount ووسيط NewCount لـ OnXfaPageCountChanged كليهما على 1. وإزالة صف من نموذج من ثلاث صفحات أنتجت نوع الهرم نفسه في الاتجاه الآخر

وتجميع الدلتا فوق القيمة السابقة ليس إصلاحاً آمناً هو الآخر. ترتيب استدعاءات الإقلاع والتخطيط يعني أن الغلاف لا يستطيع دوماً الوثوق بعدده السابق أساساً، فيمكن لمجموع جارٍ أن ينحرف. ومنذ v3.126.1 يتجاهل الاستدعاء الوسيطَ عدداً ويستدعي FPDF_GetPageCount على المستند، الذي يقرأ المجموع من التخطيط الذي اكتمل للتو. ثم يمسح مشاهد الصفحات المخبأة، ويخزن ذلك المجموع تجاوزَ عدد صفحات XFA الكامن خلف TPdf.PageCount، وبعدها فقط يرفع OnXfaPageCountChanged. فبحسب الوقت الذي يعمل فيه معالجك يتوافق NewCount و FPdf.PageCount

مخطط XFA الديناميكية في PDFium Component حيث تعيد إضافة صف ترقيم نموذج من صفحة واحدة إلى صفحتين ويمرر FFI_PageEvent ‏abs(new ناقص old) دلتاً، فبلّغ الغلاف القديم ‏TPdf.PageCount بقيمة 1 بينما يقرأ v3.126.1 ‏FPDF_GetPageCount ويبلّغ المجموع الصحيح
حدث الصفحات الأصلي يبلّغ دلتا إضافة أو إزالة لا مجموعاً، فيتجاهل v3.126.1 الوسيط ويقرأ التخطيط المكتمل قبل رفع OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // ‏v3.126.1+: ‏NewCount هو مجموع التخطيط المكتمل، لا دلتا قط.
  // يجري هذا داخل استدعاء تخطيط PDFium: حدّث حالة واجهة المضيف فقط،
  // ولا تغلق المستند ولا تعيد تحميل صفحات من هنا
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // يشتعل بعد كل إعادة تحميل صفحة، بما فيها تحديث XFA المؤجل
  PageSpin.Value := PdfView1.PageNumber;
end;

procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
  PageSpin.MinValue := 1;
  PageSpin.MaxValue := Count;
  PageLabel.Caption := Format('of %d', [Count]);
end;

الحدث لا يشتعل إلا لنماذج Full XFA التي يتغير تخطيطها وقت التشغيل. مستندات Static XFA و AcroForm لا ترفعه قط، فيستطيع عارض يعالج كليهما أن يبقي المعالج نفسه مسنداً. وتركه بلا إسناد آمن كذلك؛ يُطبَّق التجاوز الكامن خلف TPdf.PageCount على أي حال، والحدث موجود ليجدد المضيف ما خبأه

لماذا يبقى صندوق الإدخال في الصفحة القديمة حين ينتقل حقل؟

انتقل الحد ولم ينتقل المحرر لأن المُنبِّه الأصلي لـ XFA قارن مستطيلاً بنفسه. حين يغير التخطيط هندسة ودجة محملة أصلاً كان PDFium يفترض أن ينتبه للمستطيل الجديد ويستدعي PerformLayout على الودجت، التي تعيد تموضع محرر النص ومنطقة نقره. قارن الفحص GetWidgetRect() بـ RecacheWidgetRect(). وتعيد كلتا الدالتين مرجعاً ثابتاً إلى العضو نفسه، والـ recache يكتب فوق ذلك العضو في مكانه، فرأى المقارنة قيمتين متطابقتين دوماً وتخطت الودجات المحملة إعادة تخطيطها

ظهر العَرَض حين غيّر اختبار ارتفاع subform بحيث عبرت حقول قائمة إلى الصفحة التالية. على معماريتي V8 رُسمت حدود الحقل في موضعها الجديد بينما بقي النص المكتوب ومنطقة نقر الفأرة في الإحداثي Y السابق. لم تصلحها إعادة تخطيط صريحة، ولا إعادة تحميل الصفحة، لأن الودجت ما زالت تؤمن أن هندستها حالية. تنسخ مكتبات Windows V8 المشحونة مع v3.126.1 المستطيل القديم بالقيمة قبل الـ recache وتقارن تلك النسخة، فتعيد الودجات المنقولة تخطيطها ويظهر القيمة المكتوبة حيث الحد بالضبط. هذا إصلاح أصلي: يسافر مع الـ DLLs، فتحديث وحدات Pascal مع إبقاء pdfium.v8.dll أقدم يبقي مناطق النقر في مواضعها الخاطئة. وفحص الانحدار الذي قاده يحرر صفاً ناجياً إلى قيمة غير افتراضية أولاً ثم يطلب تلك القيمة في الموضع الجديد للحقل، لأن صفاً أعيد بناؤه بقيم افتراضية كان سيبدو ناجحاً وإلا

مخطط إعادة تخطيط الودجات في PDFium Component يقابل المقارنة الذاتية القديمة حيث أعادت GetWidgetRect و RecacheWidgetRect عضواً مشتركاً واحداً فتخطت الودجات المنقولة PerformLayout، بفحص النسخ بالقيمة في Windows V8 في v3.126.1 الذي يعيد تموضع المحرر ومنطقة نقر الفأرة على الحد المعاد رسمه
مقارنة مستطيل بنفسه لا تفشل قط، فانتقل الحد بينما بقي النص المكتوب والنقر في الخلف حتى حفظ الفحص نسخةً بالقيمة أولاً

كيف يعيد TPdfView تحميل الصفحات دون سحب معالج من تحت PDFium؟

منذ v3.126.2 يؤجل TPdfView إعادة تحميل الصفحة التي تلي تغيير تخطيط XFA حتى تفك حزمة الاستدعاء الأصلية مكدسَها. يشتعل حدث الصفحات عادة بينما ما يزال PDFium يعالج الإدخال: نقر المستخدم زر Add Row، فجرى النقر سكربتاً، فغيّر السكربت عدد النسخ، واكتمل التخطيط داخل الاستدعاء الأصلي نفسه. إغلاق معالج الصفحة وإعادة فتحه في تلك اللحظة كان سيحرر كائناً ما يزال المستدعي يستخدمه. قبل v3.126.2 لم يفعل العارض إلا إبطال نفسه، فبقي معالج الصفحة المعروض يشير إلى حالة ما قبل التخطيط، وإن كان المستخدم في الصفحة الأخيرة حين اختفت صار رقم الصفحة المنتقى خارج المدى

يعمل التحديث المؤجل بخطوات صغيرة، وهي تشرح السلوك الذي تراه من المضيف:

  1. يوسم استدعاء حدث الصفحات العرضَ بوجود تحديث XFA معلق ويرسل رسالة نافذة خاصة؛ وتندمج الأحداث المتكررة قبل وصول الرسالة في تحديث واحد
  2. عرض بلا معالج نافذة بعد يبقي العلم المعلق ويرسل الرسالة من CreateWnd، بينما تغيير المستندات أو تعطيل العرض أو إتلافه يمسح العلم
  3. عند وصول الرسالة يمسح العرض تحديدَ النص وتظليل البحث وفهرس الحقل المركز، لأن الثلاثة كلها أشارت إلى التخطيط القديم
  4. يُحاصر رقم الصفحة المنتقى في PageCount الجديد؛ ورقم الصفحة المتغير يمر عبر التبديل العادي، وإلا أعيد تحميل الصفحة الحالية، ويطبق وضع الملاءمة من جديد
  5. إن ترك التخطيط بلا صفحات على الإطلاق أفرغ العرض معالج صفحته القديم بدل رسم صفحة لم تعد موجودة
مخطط تحديث XFA المؤجل في TPdfView بـ PDFium Component حيث يوسم حدث صفحة داخل حزمة استدعاء التخطيط الأصلية تحديثاً معلقاً فقط ويرسل رسالة نافذة، تُمسح لاحقاً حالة التحديد اليتيمة وتحاصر الصفحة في PageCount الجديد وتعيد تحميل معالج الصفحة أو تفرغه
ينتظر إعادة التحميل حتى تفك حزمة الاستدعاء الأصلية مكدسها: رسالة مرسلة تدمج الأحداث المتكررة، ثم يحاصر العرض الصفحة ويعيد تحميلها ويرفع OnPageChange

يسري التقييد نفسه على كودك. تعمل OnXfaPageCountChanged داخل استدعاء التخطيط الأصلي ذاك، فعاملها إشعاراً: حدّث فيها العناوين ومدى المؤشر وحالة شريط الأدوات، وأجّل كل ما هو أثقل، كإغلاق المستند أو فتح مستند آخر، برسالة مرسلة تعمل بعد عودة الاستدعاء. ويخبرك TPdfView.OnPageChange حين يعيد العرض تحميل الصفحة فعلاً، وقراءة PdfView1.PageNumber عند تلك النقطة تعطيك القيمة المحاصرة. تجوال Tab و FormType التي يفحصها عارض نماذج عند الفتح مغطاةً في ملاحة حقول النماذج في PDF مع PDFium Component

لماذا يرفع النقر على حقل Full XFA استثناء «Cannot open text page»؟

صفحات Full XFA لا تملك صفحة نص PDF، وقبل v3.126.2 حاول التحديد الافتراضي للنص وكشف الوصلات في العارض تحميل واحدة على أي حال. مع TPdfView.AllowUserTextSelection على افتراضه True سأل التحويمُ طبقةَ النص عن محرف تحت الفأرة، ونقر رفع الفأرة جرى مسحاً تلقائياً للوصلات على نص الصفحة. على صفحة Full XFA لا يمكن فتح صفحة النص، فيمكن لنقرة عادية في حقل أن تنتهي استثناء Cannot open text page. ومنذ v3.126.2 تعيد كلا المسارين الداخليين لا نتيجة حين يكون TPdf.FormType هو ftXfaFull وبيئة تشغيل XFA متاحة، فتعمل الإعدادات الافتراضية ويبقى إدخال الحقول متاحاً

إيقاف AllowUserTextSelection لمستندات Full XFA لا يزال خيار واجهة معقولاً، لأن لا نص صفحة يُحدَّد ولا يجب لجرَسات السحب أن تبدأ وضع تحديد. لكنه ليس بديلاً عن الترقية: في الإصدارات الأقدم لم يعتمد مسح الوصلات عند النقر تلك الخاصية، فيمكن لعارض أن يصطدم الاستثناء نفسه والتحديد معطل

procedure TClaimForm.ConfigureViewerForForm;
begin
  // يقرأ FormType المستند المفتوح، فاستدعِ هذا بعد ‏FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // لا توجد طبقة نص PDF على صفحات Full XFA؛ وتبقى الحقول قابلة للتحرير
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

احتاج الكتابة إصلاحها الخاص في v3.126.2. محرر النص الأصلي لـ XFA لا يستبدل تحديداً حين يستلم محرفاً: يُدخل FORM_OnChar عند المؤشر، ويمحو Backspace محرفاً واحداً، فأنتقاء قيمة والكتابة فوقها أنتج نصاً قديماً وجديداً جنباً إلى جنب. يتذكر PDFium Component الآن أن النقر هبط على حقل نص XFA ويوجه المحارف المكتوبة و Backspace و Delete عبر FORM_ReplaceSelection كلما وُجد تحديد ومنح المستند إذن ملء أو تعديل. ويظل حكم هل يجوز تغيير حقل XFA للقراءة فقط لمحررِ الأصل: الحقل الموسوم للقراءة فقط في النموذج يبقي قيمته حتى في مستند يسمح خلاف ذلك بالملء. وضبط TPdfView.AllowFormEvents على False يوقف هذا التوجيه لوحة المفاتيح أيضاً، فيبقى عارض للقراءة فقط عارضاً للقراءة فقط

مرجع سريع: XFA الديناميكية في عارض Delphi

العَرَضالسببأُصلح في
عدد الصفحات يعرض 1 بعد أن يكبر النموذج إلى صفحتينحدث الصفحات الأصلي يمرر دلتا إضافة/إزالة لا مجموعاً‏v3.126.1 ‏(الغلاف)
حد الحقل ينتقل، ويبقى النص المكتوب ومنطقة النقر في الخلفتخطت ودجة محملة إعادة تخطيطها بعد مقارنة ذاتية‏v3.126.1 ‏(مكتبات Windows V8)
يرسم العارض أو يوجه الإدخال إلى حالة صفحة ما قبل التخطيطلم يُعَد تحميل معالج الصفحة بعد إعادة الترقيم‏v3.126.2 ‏(تحديث مؤجل)
نقر في حقل يرفع Cannot open text pageتحديد نص ومسح وصلات على صفحات بلا طبقة نص‏v3.126.2
الكتابة فوق قيمة منتقاة تلحق بدل أن تستبدلمحرر XFA الأصلي يُدخل عند المؤشر‏v3.126.2
  • اضبط EnableV8Engine على True قبل تحميل أي مستند، وعالج OnXfaRuntimeMissing للحالة التي حُملت فيها المكتبة العارية أولاً
  • اقرأ المجموع من TPdf.PageCount أو وسيط NewCount لـ OnXfaPageCountChanged؛ ولا تجمع ولا تطرح أعداد صفحات بنفسك قط
  • أبقِ معالج OnXfaPageCountChanged خفيفاً، لأنه يعمل داخل استدعاء التخطيط الأصلي
  • زامن مؤشر الصفحة الحالية في TPdfView.OnPageChange، الذي يشتعل بعد أن يحاصر إعادة التحميل المؤجلة رقم الصفحة
  • انشر DLLs ‏Windows V8 من v3.126.1 أو أحدث مع الوحدات؛ إصلاح إعادة تخطيط الودجات يسكن الكود الأصلي
  • اختبر بنموذج يغيّر عدد صفحاته فعلاً وينقل حقلاً محرراً عبر فاصل صفحات، لأن العينات ذات الطول الثابت تخفي كل عيب في هذه القائمة

تحوّل XFA الديناميكية عددَ الصفحات وهندسة الحقول قيماً حية، ولا يبقى العارض صائباً إلا إذا أخذها من التخطيط المكتمل وأعاد تحميل الصفحات في لحظة آمنة. يتولى PDFium Component الاثنين داخل TPdf و TPdfView، فلا يبقى على المضيف إلا الإصغاء. التفاصيل والتنزيلات في صفحة منتج PDFium Component for Delphi