Đúng vậy — PDFium hỗ trợ các biểu mẫu XFA, với một sự phân chia quan trọng. PDFium Component có thể phát hiện một biểu mẫu XFA và trích xuất các gói template, datasets và config của nó dưới dạng XML thô bằng cách sử dụng DLL tiêu chuẩn, với không phần phụ thuộc nào lúc chạy. Việc chạy một biểu mẫu XFA động — trình bày mô tả XML của nó thành các trang trực quan, tương tác thực tế — là một vấn đề riêng biệt cần thêm bản dựng hỗ trợ V8. Cộng đồng liên tục đặt câu hỏi liệu PDFium "có chạy XFA" hay không như thể đó là một khả năng chỉ trả lời có hoặc không, và câu trả lời thực tế là việc đọc dữ liệu XFA và kết xuất một biểu mẫu XFA là hai việc rất khác nhau với hai dấu chân phụ thuộc (dependency footprints) hoàn toàn khác nhau
Bài viết này hướng dẫn chi tiết cả hai khía cạnh đó với PDFium Component, lớp bao VCL tự nhiên xung quanh PDFium dành cho Delphi và C++Builder. Trước hết là lộ trình phát hiện và trích xuất tĩnh — FormType, thuộc tính XFA và các trình đọc GetXfaPacket* — sau đó là lộ trình runtime động với ràng buộc V8 của nó, và cuối cùng là các phép thử khả năng (capability probes) giúp bạn xử lý lỗi một cách mềm mại khi một DLL được triển khai không thể chạy công cụ này
PDFium có hỗ trợ các biểu mẫu XFA không?
Hãy xem đó là hai khả năng riêng biệt chứ không phải một. Trích xuất XFA tĩnh luôn khả dụng: các xuất bản (export) cấp thấp FPDF_GetXFAPacket* hỗ trợ GetXfaPacketCount, GetXfaPacketName và GetXfaPacketContent được biên dịch vào mọi tệp pdfium.dll hiện đại hợp lý, ngay cả bản dựng tắt tính năng XFA. Đó là lý do tại sao việc kéo dữ liệu XML thô của template, datasets và config ra khỏi một biểu mẫu hoàn toàn không mang theo phụ thuộc thời gian chạy nào — đó thuần túy là việc đọc cấu trúc dữ liệu, không liên quan đến công cụ nào. XFA động là khả năng còn lại: thực sự chạy biểu mẫu, đánh giá các phép tính của nó và để công cụ bố cục XFA phân trang nó. Đường dẫn đó chỉ tồn tại trong một bản dựng PDFium được biên dịch với sự hỗ trợ XFA trên nền công cụ JavaScript V8, và đó là phần mà hầu hết các luồng thảo luận "PDFium có hỗ trợ XFA không" thực sự đang tranh luận
Các tiêu chuẩn phân chia ranh giới này một cách rõ ràng. XFA được định nghĩa bên ngoài ngữ pháp PDF cốt lõi trong Adobe XFA 3.3 (2012), và PDF 1.7 §8.6.7 mô tả cách một biểu mẫu XFA được truyền tải bên trong một tài liệu PDF: các gói biểu mẫu nằm dưới khóa /XFA của từ điển AcroForm, và một tài liệu bắt buộc phải được bố cục bởi bộ xử lý XFA trước khi hiển thị bất kỳ nội dung nào sẽ thiết lập /NeedsRendering true trong mục lục (catalog). Hai dấu hiệu đó chính xác là những gì PDFium Component tìm kiếm, và là thứ bạn có thể dựa vào khi xử lý một tệp tin bạn chưa từng thấy trước đây
XFA khác biệt thế nào so với một AcroForm?
Một AcroForm là biểu mẫu PDF cổ điển: các chú thích tiện ích — các trường văn bản, hộp kiểm, nhóm nút chọn — được neo tại các tọa độ cố định trên các trang PDF thông thường. Trang bạn nhìn thấy chính là trang có trong tệp, và các trường nằm trên đỉnh của trang đó. XFA chọn cách tiếp cận ngược lại. Biểu mẫu tương tác được mô tả hoàn toàn bằng XML, và trong một tài liệu XFA đầy đủ (động), các trang PDF thực chất là một trình giữ chỗ — nội dung thực tế được tạo ra tại thời điểm mở bởi một công cụ XFA đọc mẫu XML và bố trí nó, tự động mở rộng hoặc thu hẹp để vừa vặn với dữ liệu. Đó là những gì /NeedsRendering true công bố: không có bộ xử lý XFA, các trang tĩnh có thể trống trơn hoặc chỉ mang thông báo "vui lòng mở bằng trình xem tương thích"
Bản thân dữ liệu tải (payload) XFA là một tập hợp các gói được đặt tên, và ba trong số chúng mang theo mọi thứ mà hầu hết các tích hợp cần. Gói template là định nghĩa biểu mẫu — các trường, bố cục, các phép tính toán và các kịch bản. Gói datasets giữ dữ liệu phiên bản thực tế, các giá trị trường đã điền dưới dạng một cây XML. Gói config mang các chỉ dẫn xử lý cho công cụ XFA. Các gói khác cũng tồn tại — localeSet, connectionSet, lớp bao xdp — nhưng để kiểm toán một biểu mẫu hoặc di chuyển dữ liệu ra khỏi nó, gói template cộng với datasets thường là toàn bộ công việc. Bởi vì dữ liệu đó chỉ là mã XML nằm trong một luồng (stream), việc đọc nó không bao giờ yêu cầu thực thi bất kỳ thứ gì, đây là lý do cốt lõi giúp việc trích xuất tĩnh hoàn toàn không cần phụ thuộc
Phát hiện một biểu mẫu XFA trong Delphi
Việc phát hiện bắt đầu theo cách mà mọi tác vụ PDFium Component thực hiện: đặt FileName, bật Active := True, và kiểm tra xem nó có thực sự mở được hay không. Lệnh Active := True không bao giờ nâng ngoại lệ — một tệp tin bị thiếu, sai mật khẩu hoặc DLL vắng mặt chỉ khiến thuộc tính Active ở giá trị False — do đó việc kiểm tra bảo vệ là bắt buộc. Một khi tài liệu được mở, FormType cho biết mô hình biểu mẫu nào nó sử dụng. Hai trong số các giá trị TPdfFormType thuộc loại XFA: ftXfaFull là biểu mẫu XFA đầy đủ, động, loại cần được kết xuất, trong khi ftXfaForeground là XFAF, tập hợp con tiền cảnh nơi nội dung XFA được vẽ đè lên các trang AcroForm thông thường để các trang tĩnh vẫn có ý nghĩa. Giá trị boolean XFA là một phím tắt cho "đây là một tài liệu XFA thuộc một trong hai dạng". Nếu bạn cũng làm việc với các trường cấp độ tiện ích (widget-level fields) trên một tài liệu XFAF hoặc AcroForm document, các cơ chế hoạt động được đề cập trong hướng dẫn điều hướng trường biểu mẫu 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;
Trích xuất các gói XFA dưới dạng XML thô
Trích xuất là phần có giá trị cao và hoàn toàn không phụ thuộc. Hãy bảo vệ bằng XfaStaticReadable — thuộc tính này xác nhận các export của trình đọc gói được phân giải thành công trong DLL đã tải — sau đó duyệt qua các gói theo chỉ mục. GetXfaPacketCount trả về số lượng gói mà biểu mẫu mang theo, GetXfaPacketName cung cấp tên của từng gói và GetXfaPacketContent trả về các byte thô, đối với các gói quen thuộc thì đó là mã XML UTF-8. Khi bạn đã biết mình muốn gói nào, các hàm hỗ trợ được đặt tên sẵn GetXfaTemplate, GetXfaDatasets và GetXfaConfig sẽ phân giải tên giúp bạn và trả lại trực tiếp kiểu dữ liệu TBytes. Đối với tác vụ di chuyển dữ liệu, hàm quan trọng là GetXfaDatasets: nó cung cấp cho bạn các giá trị trường đã gửi dưới dạng một tài liệu XML mà bạn có thể phân tích cú pháp và ánh xạ vào sơ đồ dữ liệu (schema) riêng của mình, giống như công việc hạ nguồn bạn sẽ làm sau khi trích xuất văn bản từ một tài liệu 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;
Tại sao kết xuất XFA động lại cần công cụ V8?
Kết luận trước tiên: bởi vì một biểu mẫu XFA động là một chương trình nhỏ, chứ không phải một bản vẽ tĩnh. Gói template chứa các phép tính toán FormCalc và JavaScript, các xác thực dữ liệu và các kịch bản sự kiện, và công cụ bố cục XFA phải thực thi chúng để quyết định diện mạo của các trang. PDFium chỉ triển khai công cụ đó trong một bản dựng được biên dịch tích hợp hỗ trợ XFA trên nền V8, công cụ JavaScript của Google — đó là lý do tại sao DLL hỗ trợ V8 (pdfium32v8.dll hoặc pdfium64v8.dll) lớn hơn rõ rệt so với bản dựng tiêu chuẩn. Không có V8, không có thực thể nào chạy các kịch bản đó, do đó không có các trang đã kết xuất để hiển thị, chỉ có mã XML thô mà bạn đã có thể đọc tĩnh
Lựa chọn bản dựng đó có một ràng buộc cứng nhắc mà bạn không thể thiết kế lách qua: đó là quyết định một lần cho mỗi tiến trình. Một tiến trình duy nhất không thể tải cả tệp pdfium.dll tiêu chuẩn và bản dựng hỗ trợ V8 — chúng xuất các biểu tượng (symbols) giống nhau và V8 giữ trạng thái cô lập toàn cục — do đó PDFium Component phải chọn bản dựng V8 trước khi thư viện được tải lần đầu tiên. Để thực hiện điều đó một cách tự động, LoadDocument quét trước tệp tin để tìm các dấu hiệu /XFA và /NeedsRendering true (phép kiểm tra tương tự được xuất bản dưới dạng hàm độc lập PdfFileLooksXfa) và đặt EnableV8Engine := True trước khi tải khi tìm thấy them. Một khi tệp pdfium.dll thông thường đã nằm trong bộ nhớ, việc chuyển đổi không còn có hiệu lực, và trường hợp đó là những gì đường dẫn thiếu runtime báo cáo
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;
Xử lý mềm mại khi thiếu môi trường chạy XFA
Ba phép thử cho bạn biết chính xác bạn có bao nhiêu khả năng xử lý XFA, và mã nguồn trung thực sẽ kiểm tra chúng thay vì giả định. XfaFeaturesAvailable là phép kiểm tra toàn cục xem các export hỗ trợ XFA/V8 có hiện diện trong DLL đã tải hay không. XfaStaticReadable xác nhận, trên mỗi tài liệu, rằng các trình đọc gói đã phân giải thành công — chốt bảo vệ cho việc trích xuất. XfaRuntimeAvailable là thuộc tính mạnh mẽ nhất: nó chỉ có giá trị True khi tài liệu cụ thể này thực sự kích hoạt công cụ XFA, nghĩa là tài liệu đó là XFA, bản dựng V8 đã được tải, và lệnh FPDF_LoadXFA thành công. Một tài liệu có thể báo cáo XFA = True nhưng XfaRuntimeAvailable = False bất cứ khi nào DLL được triển khai thiếu XFA hoặc bản dựng tiêu chuẩn đã được tải trước đó
Khi một tài liệu XFA mở ra nhưng công cụ không thể chạy, PDFium Component không cá cược. Nó ghim biểu mẫu vào chế độ tĩnh an toàn thay vì yêu cầu một runtime động mà nó không thể đáp ứng, và nó kích hoạt sự kiện OnXfaRuntimeMissing một lần để máy chủ có thể phản ứng — điển hình là bằng cách yêu cầu người dùng khởi động lại với bản dựng hỗ trợ V8, trong khi vẫn cung cấp dữ liệu đọc tĩnh trong thời gian đó. Thiết lập trình xử lý đó là sự khác biệt giữa một phương án dự phòng sạch sẽ và một biểu mẫu âm thầm không hiển thị nội dung nào
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;
Cũng hãy trung thực về ranh giới đó trong sản phẩm của riêng bạn. Nếu bạn không bao giờ phân phối DLL hỗ trợ V8, các biểu mẫu XFA động sẽ không bao giờ kết xuất được cho người dùng của bạn — nhưng bạn vẫn có thể phát hiện chúng, trích xuất mọi gói và rút dữ liệu đã gửi ra ngoài, điều này đủ cho một số lượng lớn các công việc thực sự xoay quanh việc lấy dữ liệu ra khỏi các biểu mẫu của ngân hàng hoặc chính phủ cũ thay vì hiển thị chúng một cách tương tác. Hãy dành riêng bản dựng V8 nặng nề hơn cho các trường hợp thực sự cần một biểu mẫu hoạt động, có thể điền thông tin trực tiếp, và để các phép thử khả năng định tuyến từng tài liệu đến con đường mà nó thực sự có thể đi. Nếu đường ống trích xuất của bạn cũng xử lý các tệp nhúng, triết lý chỉ đọc tương tự cũng áp dụng cho các tệp đính kèm PDF trong Delphi
Các FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig và API phép thử khả năng hiển thị ở đây là một phần của PDFium Component dành cho Delphi và C++Builder