Technical Article

การตรวจหาและแยกแบบฟอร์ม XFA ใน Delphi ด้วย PDFium

ใช่ — PDFium รองรับแบบฟอร์ม XFA โดยมีความแตกต่างที่สำคัญประการหนึ่ง PDFium Component สามารถตรวจหาแบบฟอร์ม XFA และแยกแพ็กเกจ template, datasets และ config ออกมาเป็น XML ดิบโดยใช้ไฟล์ DLL มาตรฐานได้โดยไม่มีการพึ่งพารันไทม์ใดๆ ทว่าการรันแบบฟอร์ม XFA แบบไดนามิก — ซึ่งเป็นการจัดวางโครงสร้างคำอธิบาย XML ออกมาเป็นหน้าเว็บที่โต้ตอบและทำงานได้จริง — เป็นอีกหัวข้อหนึ่งที่จำเป็นต้องใช้ไฟล์ไบนารีที่เปิดใช้งาน V8 เพิ่มเติม ชุมชนผู้พัฒนามักจะถามบ่อยครั้งว่า PDFium "รองรับ XFA หรือไม่" ราวกับว่าเป็นความสามารถแบบตอบใช่หรือไม่ข้อเดียว และคำตอบที่ถูกต้องคือการอ่านข้อมูล XFA และการเรนเดอร์แบบฟอร์ม XFA นั้นเป็นสองสิ่งที่แตกต่างกันอย่างสิ้นเชิงพร้อมการพึ่งพาเชิงโครงสร้างที่แตกต่างกันมาก

บทความนี้จะนำเสนอการทำงานทั้งสองส่วนร่วมกับ PDFium Component ซึ่งเป็นชุด VCL สำหรับเรียกใช้ PDFium ดั้งเดิมสำหรับ Delphi และ C++Builder เริ่มต้นด้วยการตรวจหาและพาธการแยกข้อมูลแบบคงที่ — ได้แก่ FormType, คุณสมบัติ XFA และตัวอ่านฟังก์ชัน GetXfaPacket* — ตามด้วยพาธการรันระบบแบบไดนามิกพร้อมข้อจำกัดของ V8 และสุดท้ายคือการตรวจสอบขีดความสามารถที่จะช่วยให้คุณจัดการระบบได้อย่างปลอดภัยเมื่อไฟล์ DLL ที่จัดส่งไม่สามารถรันเอนจินประมวลผลได้

PDFium รองรับแบบฟอร์ม XFA หรือไม่?

ขอให้ทำความเข้าใจว่าสิ่งนี้เป็นความสามารถสองประการที่ต่างกันไม่ใช่ประการเดียว การแยกข้อมูล XFA แบบคงที่นั้นพร้อมใช้งานเสมอ: ฟังก์ชันการส่งออกระดับล่าง FPDF_GetXFAPacket* ที่อยู่เบื้องหลัง GetXfaPacketCount, GetXfaPacketName และ GetXfaPacketContent จะได้รับการคอมไพล์ลงในไฟล์ pdfium.dll สมัยใหม่ทุกไฟล์ แม้กระทั่งในรุ่นที่ปิดการใช้งาน XFA ก็ตาม นี่คือสาเหตุที่การดึงข้อมูล XML ดิบของ template, datasets และ config ออกมาจากแบบฟอร์มไม่มีการพึ่งพารันไทม์เลย — เนื่องจากเป็นเพียงการอ่านเชิงโครงสร้างโดยไม่ต้องใช้เอนจินมาช่วย ส่วนความสามารถแบบไดนามิกของ XFA คืออีกเรื่องหนึ่ง: ซึ่งเป็นการรันแบบฟอร์มจริง ประมวลผลสูตรคำนวณ และเปิดโอกาสให้เอนจินเค้าโครง XFA จัดหน้าข้อมูล พาธนี้จะมีอยู่ในรุ่นคอมไพล์ PDFium ที่เปิดใช้งานการรองรับ XFA ควบคู่ไปกับเอนจิน JavaScript V8 เท่านั้น และนี่คือหัวข้อที่ฟอรัมส่วนใหญ่ถามว่า "PDFium รองรับ XFA หรือไม่" ถกเถียงกัน

ข้อกำหนดมาตรฐานได้แบ่งขอบเขตเรื่องนี้ไว้อย่างชัดเจน XFA ถูกกำหนดไว้นอกโครงสร้างหลักของ PDF ใน Adobe XFA 3.3 (2012) และ PDF 1.7 §8.6.7 อธิบายวิธีการจัดเก็บแบบฟอร์ม XFA ภายในไฟล์ PDF: โดนแพ็กเกจแบบฟอร์มจะจัดเก็บไว้ภายใต้ระเบียน AcroForm /XFA และเอกสารที่ต้องจัดวางเค้าโครงโดยเอนจิน XFA ก่อนจึงจะอ่านเนื้อหาได้ จะมีคุณสมบัติ /NeedsRendering true อยู่ในแค็ตตาล็อก เครื่องหมายระบุทั้งสองจุดนี้คือสิ่งที่ PDFium Component ค้นหา และคุณสามารถพึ่งพาเครื่องหมายเหล่านี้ในการประเมินไฟล์ที่ไม่เคยเห็นมาก่อนได้

XFA แตกต่างจาก AcroForm อย่างไร?

AcroForm คือแบบฟอร์ม PDF แบบคลาสสิก: ซึ่งมีคำอธิบายประกอบวิดเจ็ต — เช่น ฟิลด์ข้อความ, ช่องทำเครื่องหมาย, ปุ่มเลือกตัวเลือก — ยึดตำแหน่งด้วยพิกัดที่แน่นอนบนหน้ากระดาษ PDF ปกติ หน้าที่คุณเห็นคือหน้าในไฟล์ และฟิลด์ข้อมูลวางทับอยู่ด้านบน แต่ XFA จะใช้แนวทางตรงกันข้าม แบบฟอร์มโต้ตอบจะอธิบายโครงสร้างทั้งหมดในรูปแบบ XML และในเอกสาร XFA (แบบไดนามิก) ที่สมบูรณ์ หน้ากระดาษ PDF จะทำหน้าที่เป็นเพียงโหนดตัวแทน (placeholder) — เนื้อหาจริงจะถูกสร้างขึ้นในขณะที่เปิดไฟล์โดยเอนจิน XFA ที่ทำหน้าที่อ่านเทมเพลต XML และจัดวางเค้าโครง ขยายหรือลดขนาดพื้นที่ให้พอดีกับข้อมูล นี่คือสิ่งที่ค่า /NeedsRendering true บ่งบอก: หากไม่มีเอนจินประมวลผล XFA หน้าคงที่อาจจะว่างเปล่าหรือแสดงเพียงข้อความแจ้งเตือน "โปรดเปิดในโปรแกรมเปิดอ่านที่รองรับ" เท่านั้น

การตรวจหาแบบฟอร์ม XFA ใน Delphi

การตรวจหาเริ่มต้นในลักษณะเดียวกับงานอื่นๆ ของ PDFium Component: กำหนดค่า FileName, สลับ Active := True และตรวจสอบว่าเปิดไฟล์สำเร็จ คำสั่ง Active := True จะไม่มีการแสดงข้อผิดพลาด (exception) — หากไม่มีไฟล์, รหัสผ่านไม่ถูกต้อง หรือไม่มีไฟล์ DLL จะปล่อยค่า Active เป็น False ไว้ ดังนั้นการตรวจสอบนี้จึงจำเป็นอย่างยิ่ง เมื่อเอกสารเปิดขึ้นแล้ว FormType จะระบุโมเดลแบบฟอร์มที่ใช้ โดยมีค่า TPdfFormType สองค่าที่เป็น XFA: คือ ftXfaFull ซึ่งเป็นแบบฟอร์ม XFA แบบไดนามิกที่สมบูรณ์ประเภทที่ต้องเรนเดอร์ และ ftXfaForeground ซึ่งคือ XFAF เป็นโหมดส่วนหน้าที่เนื้อหา XFA ถูกวาดทับหน้า AcroForm ปกติ เพื่อให้หน้าคงที่ยังคงมีประโยชน์การแสดงผลอยู่ ส่วนคุณสมบัติบูลีน XFA คือคำสั่งสั้นๆ สำหรับระบุว่า "เอกสารนี้เป็นแบบฟอร์ม XFA ประเภทใดประเภทหนึ่ง" หากคุณต้องทำงานกับฟิลด์ระดับวิดเจ็ตบนเอกสาร XFAF หรือ AcroForm สามารถดูวิธีการทำงานได้ในหัวข้อ the guide to PDFium form field navigation

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 ที่คุณสามารถนำไปวิเคราะห์และทำแผนผังกับโครงสร้างข้อมูลของคุณเองได้ ซึ่งเป็นงานส่วนปลายทางแบบเดียวกับขั้นตอนหลังจากที่ extracting text from a 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 ของ Google — นี่คือสาเหตุที่ไฟล์ DLL รุ่นเปิดใช้งาน V8 (เช่น pdfium32v8.dll หรือ pdfium64v8.dll) มีขนาดใหญ่กว่ารุ่นมาตรฐานอย่างเห็นได้ชัด หากไม่มีเอนจิน V8 ก็จะไม่มีตัวประมวลผลสคริปต์ ทำให้ไม่มีการเรนเดอร์หน้ากระดาษแสดงผลออกมา มีเพียงข้อมูล XML ดิบที่คุณสามารถอ่านแบบคงที่ได้เท่านั้น

การเลือกรุ่นไบนารีดังกล่าวมีข้อจำกัดที่เข้มงวดซึ่งไม่สามารถเขียนโค้ดเลี่ยงได้: นั่นคือการตัดสินใจแบบครั้งเดียวต่อโปรเซส โปรเซสเดียวไม่สามารถโหลดทั้ง pdfium.dll มาตรฐานและรุ่นเปิดใช้งาน V8 พร้อมกันได้ — เนื่องจากมีการส่งออกสัญลักษณ์ชุดเดียวกันและเอนจิน V8 จะต้องจองสถานะการแยกแบบโกลบอล (global isolate state) ดังนั้น PDFium Component จึงต้องเลือกโหลดรุ่น V8 ตั้งแต่การเรียกใช้งานไลบรารีครั้งแรก เพื่อให้ขั้นตอนนี้เป็นไปโดยอัตโนมัติ เมธอด LoadDocument จะทำการสแกนไฟล์ล่วงหน้าเพื่อค้นหาตัวระบุ /XFA และ /NeedsRendering true (ซึ่งเป็นคำสั่งเดียวกับฟังก์ชันเดี่ยว PdfFileLooksXfa) และกำหนด EnableV8Engine := True ก่อนโหลดไฟล์เมื่อตรวจพบตัวระบุเหล่านี้ เมื่อใดที่มีการโหลดไฟล์ pdfium.dll ปกติเข้ามาอยู่ในหน่วยความจำแล้ว สวิตช์การทำงานนี้จะไม่สามารถส่งผลใดๆ ได้อีก และนี่คือสิ่งที่รายงานในส่วนพาธความล้มเหลวของรันไทม์

การจัดการกรณีขาดแคลน XFA รันไทม์อย่างปลอดภัย

มีเครื่องมือตรวจสอบสามประการที่จะแจ้งขีดความสามารถการใช้งาน XFA และโค้ดที่ดีควรตรวจสอบก่อนเสมอ คุณสมบัติ XfaFeaturesAvailable เป็นการตรวจสอบในระดับโกลบอลว่าฟังก์ชันช่วยเหลือ XFA/V8 มีอยู่จริงในไฟล์ DLL ที่โหลด ส่วน XfaStaticReadable เป็นการยืนยันต่อเอกสารว่าโปรแกรมอ่านแพ็กเกจสามารถทำงานได้สำเร็จ — ซึ่งเป็นตัวป้องกันสำหรับขั้นตอนการแยกข้อมูล คุณสมบัติ XfaRuntimeAvailable คือตัวตรวจสอบที่สำคัญ: ซึ่งจะมีค่าเป็นจริงเฉพาะเมื่อเอกสารเฉพาะตัวนี้สามารถเรียกเปิดเอนจิน XFA ได้สำเร็จจริงๆ เท่านั้น นั่นหมายความว่าเอกสารนี้เป็น XFA, ไฟล์รุ่น V8 ได้รับการโหลดเรียบร้อย และฟังก์ชัน FPDF_LoadXFA ทำงานสำเร็จ เอกสารสามารถรายงานค่า XFA = True แต่รายงาน XfaRuntimeAvailable = False ได้เมื่อไฟล์ DLL ที่จัดส่งไม่รองรับ XFA หรือระบบมีการโหลดไฟล์รุ่นมาตรฐานเข้ามาก่อนหน้าแล้ว

เมื่อเอกสาร XFA ถูกเปิดขึ้นแต่เอนจินไม่สามารถรันงานได้ PDFium Component จะไม่ยอมเสี่ยงระบบ แต่จะจำกัดขอบเขตแบบฟอร์มให้อยู่ในโหมดคงที่ (static mode) ที่ปลอดภัย แทนที่จะพยายามเรียกหารันไทม์แบบไดนามิกที่ไม่สามารถตอบสนองได้ และจะส่งคำสั่งเตือน 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 attachments in Delphi

ฟังก์ชัน API FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig และฟังก์ชันตรวจสอบขีดความสามารถการทำงานที่แสดง ณ ที่นี้เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder