مقالة تقنية

الكشف عن نماذج XFA واستخراجها في Delphi باستخدام PDFium

نعم — يدعم PDFium نماذج XFA، مع وجود انقسام واحد مهم. يمكن لمكون PDFium الكشف عن نموذج XFA واستخراج حزم template و datasets و config كـ XML خام باستخدام مكتبة DLL القياسية، مع صفر من التبعيات لوقت التشغيل. أما تشغيل نموذج XFA ديناميكي — وتخطيط وصف XML الخاص به في صفحات حية وتفاعلية — فهي مشكلة منفصلة تتطلب بالإضافة إلى ذلك البناء المُمكّن لـ V8. يستمر المجتمع في التساؤل عما إذا كان PDFium "يدعم XFA" كما لو كانت قدرة واحدة بنعم أو لا، والإجابة الصادقة هي أن قراءة بيانات XFA ورسم نموذج XFA هما شيئان مختلفان تماماً ولهما بصمتا تبعية مختلفتان تماماً

يمشي هذا المقال عبر كلا النصفين باستخدام مكون PDFium، وهو غلاف VCL الأصلي حول PDFium لـ Delphi و C++Builder. أولاً مسار الكشف والاستخراج الثابت — FormType وخاصية XFA وقراء GetXfaPacket* — ثم مسار وقت التشغيل الديناميكي مع قيود V8 الخاصة به، وأخيراً فحوصات القدرة التي تتيح لك الفشل بلباقة عندما لا يتمكن ملف DLL المنشور من تشغيل المحرك

هل يدعم PDFium نماذج XFA؟

اقرأها كقدرتين، وليس قدرة واحدة. استخراج XFA الثابت متاح دائماً: صادرات FPDF_GetXFAPacket* منخفضة المستوى التي تدعم GetXfaPacketCount و GetXfaPacketName و GetXfaPacketContent تم تجميعها في كل ملف pdfium.dll حديث معقول، حتى لو تم إيقاف تشغيل XFA في البناء. هذا هو السبب في أن سحب حزم template و datasets و config لـ XML الخام من النموذج لا يحمل أي تبعية لوقت التشغيل على الإطلاق — فهو قراءة هيكلية بحتة، ولا يتدخل فيها أي محرك. XFA الديناميكي هو القدرة الأخرى: تشغيل النموذج فعلياً، وتقييم حساباته، والسماح لمحرك تخطيط XFA بترقيم الصفحات. هذا المسار موجود فقط في بناء PDFium الذي تم تجميعه مع دعم XFA فوق محرك V8 JavaScript، وهو الجزء الذي تدور حوله معظم نقاشات "هل يدعم PDFium نماذج XFA" حقاً

تحدد المعايير هذا الانقسام بوضوح. يتم تعريف XFA خارج قواعد لغة PDF الأساسية في معيار Adobe XFA 3.3 (2012)، ويصف معيار PDF 1.7 الفقرة 8.6.7 كيفية حمل نموذج XFA داخل PDF: تعيش حزم النموذج تحت إدخال /XFA لقاموس AcroForm، والمستند الذي يجب تخطيطه بواسطة معالج XFA قبل أن يعني أي شيء يعين /NeedsRendering true في الكتالوج. هاتان العلامتان هما بالضبط ما يبحث عنه مكون PDFium، وما يمكنك الاعتماد عليه تماماً عندما تبحث في ملف لم تره من قبل

كيف يختلف XFA عن AcroForm؟

نموذج AcroForm هو نموذج PDF الكلاسيكي: تعليقات توضيحية للأدوات (widgets) — حقول النص، خانات الاختيار، مجموعات الراديو — مثبتة عند إحداثيات ثابتة على صفحات PDF العادية. الصفحة التي تراها هي الصفحة الموجودة في الملف، وتجلس الحقول فوقها. بينما يتبع XFA النهج المعاكس. حيث يتم وصف النموذج التفاعلي بالكامل في XML، وفي مستند XFA كامل (ديناميكي) تكون صفحات PDF بمثابة عنصر نائب فعلي — يتم إنشاء المحتوى الحقيقي في وقت الفتح بواسطة محرك XFA الذي يقرأ قالب XML ويخططه، وينمو أو يتقلص ليناسب البيانات. وهذا ما يعلنه /NeedsRendering true: بدون معالج XFA، قد تكون الصفحات الثابتة فارغة أو تحمل فقط إشعاراً "يرجى الفتح في عارض متوافق"

حمولة XFA نفسها هي مجموعة من الحزم المسماة، وثلاثة منها تحمل كل ما تحتاجه معظم عمليات التكامل. حزمة template هي تعريف النموذج — الحقول والتخطيط والحسابات والنصوص البرمجية. حزمة datasets تحمل بيانات المثيل الفعلية، وقيم الحقول المعبأة كشجرة XML. بينما حزمة config تحمل تعليمات المعالجة لمحرك XFA. توجد حزم أخرى — localeSet و connectionSet وغلاف xdp — ولكن للتدقيق في نموذج أو ترحيل البيانات منه، فإن القالب ومجموعات البيانات هما العمل بأكمله عادةً. ولأن هذه البيانات هي مجرد XML يجلس في تدفق، فإن قراءتها لا تتطلب أبداً تنفيذ أي شيء، وهو السبب الجذري في أن استخراج البيانات الثابتة خالٍ من التبعيات

الكشف عن نموذج XFA في دلفي

يبدأ الكشف بالطريقة التي تبدأ بها كل مهمة في مكون PDFium: قم بتعيين FileName، وقم بتبديل Active := True، وتحقق من فتحه بالفعل. لا يثير Active := True أي استثناء أبداً — فوجود ملف مفقود، أو كلمة مرور خاطئة، أو غياب DLL يترك Active عند القيمة False — لذا فإن الحارس إلزامي. وبمجرد فتح المستند، يخبرك FormType بنوع نموذج النموذج الذي يستخدمه. اثنتان من قيم TPdfFormType هما XFA: وهما ftXfaFull لنموذج XFA كامل وديناميكي، وهو النوع الذي يحتاج إلى تحويل، و ftXfaForeground وهو XFAF، المجموعة الفرعية الأمامية حيث يتم رسم محتوى XFA فوق صفحات AcroForm العادية بحيث تظل الصفحات الثابتة ذات مغزى. القيمة البولية XFA هي اختصار لـ "هذا مستند XFA من أي من النوعين". وإذا كنت تعمل أيضاً مع حقول مستوى الأدوات (widget-level) في مستند XFAF أو AcroForm، فإن الآليات مغطاة في دليل التنقل في حقول نماذج PDFium

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // damaged, encrypted, or DLL missing
    case Pdf.FormType of
      ftXfaFull:
        Log('Full (dynamic) XFA form');
      ftXfaForeground:
        Log('XFAF: XFA layered over static AcroForm pages');
      ftAcroForm:
        Log('Classic AcroForm');
    else
      Log('No interactive form');
    end;
    if Pdf.XFA then
      Log('Document carries an XFA packet payload');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

استخراج حزم XFA كـ XML خام

الاستخراج هو النصف عالي القيمة والخالي من التبعيات. احرسه بـ XfaStaticReadable — فهو يؤكد أن صادرات قارئ الحزم قد حُلت في مكتبة DLL المحملة — ثم امشِ عبر الحزم بالفهرس. يعيد GetXfaPacketCount عدد الحزم التي يحملها النموذج، ويعطي GetXfaPacketName اسماً لكل حزمة، ويعيد GetXfaPacketContent البايتات الخام، والتي تكون لحزم معروفة عبارة عن UTF-8 XML. عندما تعرف بالفعل الحزمة التي تريدها، فإن المساعدات المسماة GetXfaTemplate و GetXfaDatasets و GetXfaConfig تحل الاسم لك وتسلم TBytes مباشرة. بالنسبة لمهمة ترحيل البيانات، فإن الحزمة المهمة هي GetXfaDatasets: فهي تمنحك قيم الحقول المقدمة كمستند XML يمكنك تحليله وتعيينه في مخططك الخاص، وهو نفس نوع العمل اللاحق الذي ستقوم به بعد استخراج النص من ملف PDF

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // packet reader exports not present
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // raw UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Or jump straight to the filled-in field values by name:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

لماذا يحتاج تحويل XFA الديناميكي إلى محرك V8؟

الخلاصة أولاً: لأن نموذج XFA الديناميكي هو برنامج صغير، وليس رسماً ثابتاً. تحمل حزمة القالب حسابات FormCalc و JavaScript، وعمليات التحقق من الصحة، والنصوص البرمجية للأحداث، ويتعين على محرك تخطيط XFA تنفيذها لتحديد شكل الصفحات. لا يقوم PDFium بتنفيذ هذا المحرك إلا في بناء تم تجميعه مع دعم XFA فوق محرك V8، وهو محرك JavaScript من جوجل — وهو نفس السبب في أن مكتبة DLL المُمكّنة لـ V8 (وهي pdfium32v8.dll أو pdfium64v8.dll) أكبر بشكل ملحوظ من البناء القياسي. وبدون V8، لا يوجد من يقوم بتشغيل النصوص البرمجية، وبالتالي لا توجد صفحات محولة لإظهارها، بل يوجد فقط XML خام يمكنك قراءته بشكل ثابت بالفعل

اختيار هذا البناء له قيد صارم لا يمكنك تجنبه في التصميم: فهو قرار يتخذ مرة واحدة لكل عملية. لا يمكن لعملية واحدة تحميل كل من ملف pdfium.dll القياسي والبناء المُمكّن لـ V8 — فهما يصدران نفس الرموز ويحتفظ V8 بحالة عزل عالمية (isolate state) — لذا يجب على مكون PDFium اختيار بناء V8 قبل تحميل المكتبة لأول مرة على الإطلاق. ولجعل ذلك تلقائياً، تقوم LoadDocument بمسح الملف مسبقاً بحثاً عن علامات /XFA و /NeedsRendering true (وهو نفس الفحص المعروض كدالة مستقلة PdfFileLooksXfa) وتعيين EnableV8Engine := True قبل التحميل عندما تجدها. وبمجرد أن يصبح ملف pdfium.dll العادي مقيماً بالفعل، لا يمكن للمفتاح أن يدخل حيز التنفيذ بعد الآن، وتلك الحالة هي ما يبلغ عنه مسار فقدان وقت التشغيل

begin
  // Peek for XFA before PDFium is loaded for the first time, so the
  // V8-enabled build can still be selected. PdfFileLooksXfa scans for
  // /XFA and /NeedsRendering true without parsing the whole file.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // engine is live: dynamic layout
      else
        ExtractXfaData(Pdf, 'C:\out'); // fall back to static packets
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

التعامل مع وقت تشغيل XFA المفقود بلباقة

تخبرك ثلاثة فحوصات بدقة بمدى قدرة XFA التي تمتلكها، ويقوم الكود الصادق بفحصها بدلاً من الافتراض. XfaFeaturesAvailable هو فحص عالمي يؤكد وجود صادرات مساعد XFA/V8 في مكتبة DLL المحملة. ويؤكد XfaStaticReadable، لكل مستند، أن قراء الحزم قد حُلوا — وهو الحارس للاستخراج. و XfaRuntimeAvailable هو الفحص القوي: فهو صحيح فقط عندما يقوم هذا المستند المحدد بتنشيط محرك XFA بالفعل، مما يعني أنه XFA، وتم تحميل بناء V8، ونجحت دالة FPDF_LoadXFA. يمكن للمستند الإبلاغ عن XFA = True ومع ذلك تكون XfaRuntimeAvailable = False كلما كان ملف DLL المنشور يفتقر إلى XFA أو كان البناء القياسي محملاً بالفعل

عندما يفتح مستند XFA ولكن المحرك لا يمكنه العمل، فإن مكون PDFium لا يغامر. فهو يثبت النموذج على الوضع الثابت الآمن بدلاً من طلب وقت تشغيل ديناميكي لا يمكنه الوفاء به، ويثير OnXfaRuntimeMissing مرة واحدة حتى يتمكن المضيف من التصرف — عادةً عن طريق إخبار المستخدم بإعادة التشغيل مع البناء المُمكّن لـ V8، مع الاستمرار في تقديم البيانات المقروءة بشكل ثابت في هذه الأثتاء. ربط هذا المعالج هو الفرق بين التراجع النظيف ونموذج لا يظهر شيئاً بصمت

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires once when an XFA document opens but the engine cannot run:
  // a plain pdfium.dll was already loaded, or the build lacks XFA/V8.
  ShowMessage('This is a dynamic XFA form. Restart with the ' +
    'V8-enabled build to render it; its packet data is still readable.');
end;

كن صادقاً بشأن الحدود في منتجك الخاص أيضاً. فإذا لم تقم أبداً بشحن مكتبة DLL المُمكّنة لـ V8، فلن يتم تحويل نماذج XFA الديناميكية أبداً لمستخدميك — ولكن لا يزال بإمكانك الكشف عنها، واستخراج كل حزمة، وسحب البيانات المقدمة، وهو ما يكفي لفئة كبيرة من المهام التي تتعلق حقاً بإخراج البيانات من النماذج الحكومية والمصرفية القديمة بدلاً من تقديمها بشكل تفاعلي. احجز بناء V8 الأثقل للحالات التي تحتاج فعلياً إلى نموذج حي وقابل للتعبئة، ودع فحوصات القدرة توجه كل مستند إلى المسار الذي يمكنه اتخاذه بالفعل. وإذا كان خط أنابيب الاستخراج الخاص بك يتعامل أيضاً مع الملفات المضمنة، فإن نفس الفلسفة المخصصة للقراءة فقط تنطبق على مرفقات PDF في دلفي

إن واجهات برمجة التطبيقات FormType و XFA و GetXfaPacketCount و GetXfaPacketName و GetXfaPacketContent و GetXfaTemplate و GetXfaDatasets و GetXfaConfig وفحوصات القدرة المعروضة هنا هي جزء من مكون PDFium لـ Delphi و C++Builder