是的——PDFium 支援 XFA 表單,但有一道重要的分野。PDFium Component 能偵測 XFA 表單,並用標準 DLL 把它的 template、datasets 與 config 封包擷取成原始 XML,完全不帶執行期相依。而執行一份動態 XFA 表單——把它的 XML 描述排版成活的、可互動的頁面——則是另一個問題,額外需要啟用 V8 的建置版本。社群一直在問 PDFium 到底「支不支援 XFA」,彷彿那是一個是非題般的單一能力,而誠實的答案是:讀取 XFA 資料與呈現 XFA 表單是兩件很不一樣的事,相依足跡也天差地遠
本文以 PDFium Component 走過這兩半,它是 PDFium 為 Delphi 與 C++Builder 提供的原生 VCL 包裝。先講偵測與靜態擷取這條路——FormType、XFA 屬性與 GetXfaPacket* 這幾個讀取器——接著是帶著 V8 限制的動態執行期路徑,最後是能力探測,讓您在部署的 DLL 跑不動引擎時能優雅地失敗
PDFium 支援 XFA 表單嗎?
請把它讀成兩種能力,而不是一種。靜態 XFA 擷取隨時都在:支撐 GetXfaPacketCount、GetXfaPacketName 與 GetXfaPacketContent 的低階 FPDF_GetXFAPacket* 匯出項,被編譯進每一份夠新的 pdfium.dll 裡,就連關掉 XFA 的建置版本也有。這正是為什麼把表單裡原始的 template、datasets 與 config XML 拉出來,完全不帶任何執行期相依——那是純粹的結構性讀取,沒有引擎介入。動態 XFA 則是另一種能力:真的把表單跑起來、對它的計算求值,並讓 XFA 版面引擎替它分頁。那條路只存在於在 V8 JavaScript 引擎之上、且編譯時開啟 XFA 支援的 PDFium 建置版本中,而那也正是多數「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 頁面實際上只是佔位——真正的內容是開檔時由 XFA 引擎讀取 XML 範本並排版產生的,會隨資料多寡而長高或縮短。這就是 /NeedsRendering true 所宣告的事:沒有 XFA 處理器,靜態頁面可能一片空白,或者只帶著一句「請以相容的檢視器開啟」的告示
XFA 酬載本身是一組具名封包,其中三個就裝下了多數整合工作所需的一切。template 封包是表單定義——欄位、版面配置、計算與指令碼。datasets 封包裝著實際的執行個體資料,也就是以 XML 樹表示的已填寫欄位值。config 封包則帶著給 XFA 引擎的處理指示。還有別的封包——localeSet、connectionSet、xdp 外框——但若目的是稽核一份表單或把資料遷移出來,template 加 datasets 通常就是全部工作了。由於那些資料只是躺在串流裡的 XML,讀它從來不需要執行任何東西,這正是靜態擷取不帶相依的根本原因
在 Delphi 中偵測 XFA 表單
偵測的起手式和每一項 PDFium Component 工作一樣:設定 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; // 檔案損毀、已加密,或 DLL 缺席
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; // 封包讀取器的匯出項不存在
Count := Pdf.GetXfaPacketCount;
for I := 0 to Count - 1 do
begin
PacketName := Pdf.GetXfaPacketName(I);
Content := Pdf.GetXfaPacketContent(I); // 原始的 UTF-8 XML
TFile.WriteAllBytes(
TPath.Combine(Dir, PacketName + '.xml'), Content);
end;
// 或者按名稱直接跳到已填寫的欄位值:
Datasets := Pdf.GetXfaDatasets;
if Length(Datasets) > 0 then
ParseAndMap(Datasets);
end;
為什麼動態 XFA 呈現需要 V8 引擎?
先講結論:因為一份動態 XFA 表單是一支小程式,不是一張靜態圖畫。template 封包帶著 FormCalc 與 JavaScript 的計算、驗證與事件指令碼,而 XFA 版面引擎必須執行它們,才能決定頁面究竟長什麼樣。PDFium 只在以 Google 的 JavaScript 引擎 V8 為基礎、且編譯時開啟 XFA 支援的建置版本中實作那套引擎——這也正是啟用 V8 的 DLL(pdfium32v8.dll 或 pdfium64v8.dll)明顯比標準版大上一截的原因。沒有 V8,就沒有人跑那些指令碼,所以也就沒有呈現出來的頁面可看,只剩您本來就靜態讀得到的原始 XML
選用那個建置版本有一項您繞不過去的硬性限制:那是每個行程一次性的決定。單一行程無法同時載入標準的 pdfium.dll 與啟用 V8 的建置版本——它們匯出相同的符號,而 V8 會保有全域的 isolate 狀態——所以 PDFium Component 必須在程式庫第一次被載入之前,就選定 V8 建置版本。為了讓這件事自動發生,LoadDocument 會預先掃描檔案裡的 /XFA 與 /NeedsRendering true 標記(也就是以獨立函式 PdfFileLooksXfa 暴露出來的同一項檢查),找到時就在載入前設定 EnableV8Engine := True。一旦普通的 pdfium.dll 已經常駐,這個切換就再也生效不了,而那種情況正是執行期缺席那條路徑所回報的
begin
// 在 PDFium 第一次被載入之前先偷看有沒有 XFA,這樣才來得及
// 選用啟用 V8 的建置版本。PdfFileLooksXfa 會掃描
// /XFA 與 /NeedsRendering true,而不剖析整個檔案。
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) // 引擎活著:動態版面配置
else
ExtractXfaData(Pdf, 'C:\out'); // 退回靜態封包
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
優雅地處理缺席的 XFA 執行環境
三項探測會準確告訴您手上有多少 XFA 能力,誠實的程式碼會去檢查它們,而不是憑空假設。XfaFeaturesAvailable 是一項全域檢查,確認載入的 DLL 中存在 XFA/V8 輔助匯出項。XfaStaticReadable 則逐份文件確認封包讀取器已解析成功——那是擷取時的防護。XfaRuntimeAvailable 是最強的一項:唯有這份特定文件真的啟動了 XFA 引擎時它才為真,也就是說它是 XFA、V8 建置版本已載入,而且 FPDF_LoadXFA 成功了。只要部署的 DLL 缺少 XFA,或標準建置版本早已載入,一份文件就可能回報 XFA = True,而 XfaRuntimeAvailable = False
當一份 XFA 文件打得開、引擎卻跑不動時,PDFium Component 不會賭一把。它會把表單釘在安全的靜態模式,而不是去要求一個它兌現不了的動態執行環境,並觸發一次 OnXfaRuntimeMissing,讓主應用程式有機會反應——通常是請使用者改用啟用 V8 的建置版本重新啟動,同時仍然把靜態讀得到的資料提供出來。有沒有接上那個處理常式,決定了您得到的是乾淨的退路,還是一份默默什麼都不顯示的表單
procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
// 當 XFA 文件打得開、引擎卻跑不動時觸發一次:
// 可能是普通的 pdfium.dll 早已載入,或建置版本缺少 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;
在您自己的產品裡,也請對這道界線誠實。如果您從不出貨啟用 V8 的 DLL,動態 XFA 表單對您的使用者而言就永遠呈現不出來——但您仍然偵測得到它們、擷取得出每一個封包,並把送出的資料拉出來,而這對那一大類工作已經足夠:那些工作真正要的是把資料從老舊的政府與銀行表單裡弄出來,而不是把它們互動地呈現出來。請把較笨重的 V8 建置版本,留給真正需要一份活的、可填寫表單的情況,並讓能力探測把每份文件導向它真正走得通的那條路。若您的擷取流水線也要處理內嵌檔案,同樣的唯讀哲學也適用於 Delphi 中的 PDF 附件
本文示範的 FormType、XFA、GetXfaPacketCount、GetXfaPacketName、GetXfaPacketContent、GetXfaTemplate、GetXfaDatasets、GetXfaConfig 與各項能力探測 API,都是適用於 Delphi 與 C++Builder 的 PDFium Component 的一部分