مقاله فنی

شناسایی و استخراج فرم‌های XFA در دلفی با استفاده از PDFium

بله — پی‌دی‌اف‌یوم (PDFium) از فرم‌های XFA پشتیبانی می‌کند، اما با یک تفکیک مهم. کامپوننت PDFium می‌تواند یک فرم XFA را شناسایی کرده و بسته‌های template، datasets و config آن را به عنوان XML خام با استفاده از DLL استاندارد و بدون هیچ وابستگی زمان اجرا استخراج کند. اجرای یک فرم XFA پویا (dynamic) — یعنی چیدمان توضیحات XML آن در صفحات زنده و تعاملی — مسئله مجزایی است که علاوه بر آن به نسخه فعال‌شده با V8 (V8-enabled build) نیاز دارد. توسعه‌دهندگان همواره می‌پرسند که آیا پی‌دی‌اف‌یوم «از XFA پشتیبانی می‌کند» گویی این یک قابلیت بله یا خیر ساده است، در حالی که پاسخ واقعی این است که خواندن داده‌های XFA و رندر کردن یک فرم XFA دو کار بسیار متفاوت با پیش‌نیازهای وابستگی کاملاً متفاوت هستند

این مقاله هر دو بخش را با استفاده از کامپوننت PDFium که پوشش‌دهنده بومی VCL برای پی‌دی‌اف‌یوم در دلفی و سی‌پلاس‌پلاس‌بیلدر است بررسی می‌کند. ابتدا مسیر شناسایی و استخراج ایستا (static extraction) — شامل ویژگی‌های FormType، XFA و متدهای خواندن GetXfaPacket* — سپس مسیر زمان اجرای پویا با محدودیت V8 آن، و در نهایت ابزارهای سنجش قابلیت که به شما اجازه می‌دهند در زمان عدم اجرای موتور توسط DLL مستقرشده، به صورت مناسبی خطا را مدیریت کنید

آیا PDFium از فرم‌های XFA پشتیبانی می‌کند؟

به آن به عنوان دو قابلیت نگاه کنید، نه یک قابلیت. استخراج ایستای XFA همیشه در دسترس است: توابع سطح پایین FPDF_GetXFAPacket* که پشتیبان متدهای GetXfaPacketCount، GetXfaPacketName و GetXfaPacketContent هستند، در هر نسخه نسبتاً جدید از pdfium.dll کامپایل شده‌اند، حتی در نسخه‌ای که XFA در آن غیرفعال شده باشد. به همین دلیل است که استخراج XML خام بسته‌های template، datasets و config از یک فرم، اصلاً وابستگی زمان اجرا ندارد — این یک خواندن ساختاری محض است و هیچ موتوری در آن درگیر نیست. قابلیت دیگر XFA پویا است: یعنی اجرای واقعی فرم، ارزیابی محاسبات آن، و اجازه دادن به موتور چیدمان XFA برای صفحه‌بندی آن. این مسیر تنها در نسخه‌ای از پی‌دی‌اف‌یوم وجود دارد که با پشتیبانی XFA بر پایه موتور جاوااسکریپت V8 کامپایل شده باشد، و این همان بخشی است که بیشتر بحث‌های مربوط به پشتیبانی پی‌دی‌اف‌یوم از 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 است: یادداشت‌های کنترل‌های تعاملی (widget annotations) — شامل فیلدهای متنی، چک‌باکس‌ها، دکمه‌های رادیویی — که در مختصات ثابت در صفحات معمولی PDF قرار گرفته‌اند. صفحه‌ای که می‌بینید همان صفحه موجود در فایل است و فیلدها در بالای آن قرار دارند. XFA رویکرد معکوسی دارد. فرم تعاملی به طور کامل در XML توصیف شده است، و در یک سند XFA کامل (پویا)، صفحات PDF در واقع یک فیلد نگهدارنده (placeholder) هستند — محتوای واقعی در زمان باز کردن فایل توسط موتور XFA تولید می‌شود که قالب XML را می‌خواند و آن را چیدمان می‌کند، و برای هماهنگی با داده‌ها بزرگ یا کوچک می‌شود. این همان چیزی است که عبارت /NeedsRendering true اعلام می‌کند: بدون یک پردازنده XFA، صفحات ایستا ممکن است خالی باشند یا فقط حاوی پیام «لطفاً در یک برنامه سازگار باز کنید» باشند

خود بدنه XFA مجموعه‌ای از بسته‌های نام‌گذاری‌شده است و سه مورد از آنها حاوی تمام چیزهایی هستند که بیشتر پیاده‌سازی‌ها نیاز دارند. بسته template تعریف فرم است — شامل فیلدها، چیدمان، محاسبات و اسکریپت‌ها. بسته datasets داده‌های واقعی را نگه می‌دارد، یعنی مقادیر فیلدهای پرشده را به صورت یک درخت XML. بسته config حاوی دستورالعمل‌های پردازش برای موتور XFA است. موارد دیگری نیز وجود دارند — مانند localeSet، connectionSet و بسته xdp — اما برای ممیزی یک فرم یا انتقال داده‌های آن به بیرون، ترکیب template به همراه datasets معمولاً تمام کار است. از آنجا که آن داده‌ها فقط یک XML قرار گرفته در یک جریان (stream) هستند، خواندن آنها هرگز نیازی به اجرای هیچ کدی ندارد، که دلیل اصلی عدم نیاز به وابستگی در استخراج ایستا است

شناسایی یک فرم XFA در دلفی

فرآیند شناسایی مانند تمام کارهای کامپوننت PDFium شروع می‌شود: تنظیم FileName، تغییر وضعیت Active := True، و بررسی اینکه فایل واقعاً باز شده است. تغییر وضعیت Active := True هرگز خطایی ایجاد نمی‌کند — مفقود بودن فایل، رمز عبور اشتباه، یا نبود DLL فقط وضعیت Active را روی False باقی می‌گذارد — بنابراین این بررسی الزامی است. پس از باز شدن سند، ویژگی FormType مشخص می‌کند که از کدام مدل فرم استفاده شده است. دو مورد از مقادیر TPdfFormType مربوط به XFA هستند: مقدار ftXfaFull یک فرم XFA کامل و پویا است، از نوعی که نیاز به رندر شدن دارد، در حالی که ftXfaForeground همان XFAF است، زیرمجموعه پیش‌زمینه که محتوای XFA روی صفحات معمولی AcroForm رسم می‌شود تا صفحات ایستا همچنان معنادار باشند. مقدار بولی XFA یک میانبر برای عبارت «این یک سند XFA از هر دو نوع است» می‌باشد. اگر با فیلدهای سطح کنترل‌های تعاملی در یک سند 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 بایت‌های خام را برمی‌گرداند، که برای بسته‌های معروف به صورت XML با انکودینگ UTF-8 هستند. وقتی از قبل می‌دانید کدام بسته را می‌خواهید، متدهای کمکی نام‌گذاری‌شده 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 یک برنامه کوچک است، نه یک نقشه ایستا. بسته template حاوی محاسبات، اعتبارسنجی‌ها و اسکریپت‌های رویداد FormCalc و جاوااسکریپت است، و موتور چیدمان XFA باید آنها را اجرا کند تا تصمیم بگیرد صفحات چگونه به نظر برسند. پی‌دی‌اف‌یوم آن موتور را تنها در نسخه‌ای پیاده‌سازی می‌کند که با پشتیبانی XFA بر پایه V8 (موتور جاوااسکریپت گوگل) کامپایل شده باشد — به همین دلیل است که DLL فعال‌شده با V8 (مانند pdfium32v8.dll یا pdfium64v8.dll) به طور قابل توجهی بزرگ‌تر از نسخه استاندارد است. بدون V8 هیچ چیزی برای اجرای اسکریپت‌ها وجود ندارد، بنابراین هیچ صفحه رندرشده‌ای برای نمایش وجود نخواهد داشت، بلکه فقط همان XML خامی است که می‌توانید به صورت ایستا بخوانید

انتخاب آن نسخه یک محدودیت جدی دارد که نمی‌توانید آن را دور بزنید: این تصمیم یک‌باره برای کل پروسه (process) است. یک پروسه واحد نمی‌تواند هر دو فایل pdfium.dll استاندارد و نسخه فعال‌شده با V8 را بارگذاری کند — آنها علائم یکسانی را صادر می‌کنند و V8 وضعیت ایزوله سراسری را حفظ می‌کند — بنابراین کامپوننت 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 بررسی اصلی است: تنها زمانی true است که این سند خاص واقعاً موتور 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;

آنچه قفل بارگذاری (load lock) از آن محافظت نمی‌کند

در محصول خود نیز درباره این مرز واقع‌بین باشید. اگر هرگز نسخه DLL فعال‌شده با V8 را ارسال نکنید، فرم‌های پویای XFA هرگز برای کاربران شما رندر نخواهند شد — اما همچنان می‌توانید آنها را شناسایی کنید، هر بسته را استخراج نمایید و داده‌های ثبت‌شده را خارج کنید، که این کار برای طیف وسیعی از تسک‌ها که صرفاً مربوط به دریافت داده از فرم‌های بانکی و دولتی قدیمی هستند کافی است، به جای اینکه آنها را به صورت تعاملی ارائه دهید. نسخه سنگین‌تر V8 را برای مواردی رزرو کنید که واقعاً به یک فرم زنده و قابل پر کردن نیاز دارند، و اجازه دهید بررسی‌های قابلیت، هر سند را به مسیری هدایت کنند که می‌تواند طی کند. اگر خط لوله استخراج شما فایل‌های تعبیه‌شده را نیز مدیریت می‌کند، همین رویکرد فقط‌خواندنی برای پیوست‌های PDF در دلفی نیز صدق می‌کند

ویژگی‌های FormType، XFA، متدهای GetXfaPacketCount، GetXfaPacketName، GetXfaPacketContent، GetXfaTemplate، GetXfaDatasets، GetXfaConfig و APIهای بررسی قابلیت نشان داده شده در اینجا بخشی از PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر هستند