兩份表單可以帶著相同的欄位,行為卻南轅北轍。AcroForm 把欄位存成普通的 PDF 物件,坐落在真正的頁面內容之上,所以任何相容的閱讀器都畫得出來。動態 XFA 表單幾乎什麼都不存成 PDF:欄位、版面配置,甚至頁面幾何都活在一個 XML 封包裡,而可見的頁面是在開檔時由一套只有 Adobe 廣泛出貨過的版面引擎產生的。把這種檔案餵給網頁檢視器、封存呈現器或文字擷取器,您拿到的不是表單,而是一張灰色頁面,上頭寫著「Please wait... If this message is not eventually replaced by the proper contents of the document, your PDF viewer may not be able to display this type of document.」。凡是接收過政府或保險文書的人,一眼就認得那一頁
那張佔位頁不是檔案損毀。當系統中沒有 XFA 處理器時,格式規範要求的正是這個行為,而到了 2026 年,這幾乎描述了桌面版 Acrobat 以外的每一個檢視器。所以務實的作法,是在動態表單流到下游任何環節之前,先把它轉換成單純的 AcroForm。HotPDF 這套 losLab 為 Delphi 與 C++Builder 提供的 PDF 程式庫,能以程式碼完成這項轉換,把 XML 表單重建成原生頁面上的原生欄位
為什麼這兩種模型無法共存
AcroForm 定義於 ISO 32000-1 §12.7。每個欄位都是一個帶著小工具註記與外觀串流的 PDF 物件,頁面是貨真價實的 PDF 內容,資料則乘載在它之上。XFA 把這件事反過來:表單是一份 XML 文件,也就是存放在 AcroForm 字典 /XFA 條目中的 XDP 封包,而動態表單的 PDF 頁面裡只有那張「Please wait」佔位頁,別無他物,因為真正的內容從未被序列化成 PDF。閱讀器處理一份檔案時,只會採用其中一種模型。忽略 /XFA 條目,您看到的是空殼;尊重它卻沒有 XFA 引擎,您看到的是那則警告。ISO 32000-2 直接把 XFA 從 PDF 2.0 移除,結束了這場爭論,這也是「趁還能轉的時候就轉」從邊緣案例變成例行收件政策的主因
動手轉換之前先分類,因為不是每個 XFA 檔案都會出現佔位頁。靜態 XFA 表單會在 XML 旁邊附上預先呈現好的 PDF 頁面,所以到哪裡都顯示得出來,只有在填寫時才會出狀況。動態表單只附佔位頁,不轉換就無法使用。該相信的是文件本身,絕不是副檔名或寄件者。一份在非 Adobe 檢視器中呈現出真實內容、卻仍帶著 /XFA 條目的檔案,屬於靜態或混合式;一份顯示警告頁的檔案則是動態的。請記下每個收件檔案落在哪一類。這兩種檔案日後壞掉的方式並不相同,而當收件紀錄上已經寫著「動態 XFA、已轉換、對映 47 個欄位、2 則警告」時,一張抱怨封存表單空白的工單,幾秒鐘就能結案
把已載入的 XFA 文件轉成原生欄位
轉換是針對已經在記憶體中的文件執行的。FlattenLoadedXFA 會剖析 XFA 範本與它的資料封包,把表單排好版,再重建為真正的 PDF 頁面上的 AcroForm 欄位:
var
Pdf: THotPDF;
MappedCount, I: Integer;
Warnings: TStrings;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('dynamic_xfa.pdf');
MappedCount := Pdf.FlattenLoadedXFA(True); // True = 欄位保持可編輯
Warnings := Pdf.XFAFlattenWarnings;
for I := 0 to Warnings.Count - 1 do
Log('XFA flatten warning: ' + Warnings[I]); // 未對映的元素
Pdf.SaveLoadedDocument('native_acroform.pdf');
Log(Format('Mapped %d fields', [MappedCount]));
finally
Pdf.Free;
end;
end;
回傳值與警告清單都是輸出,不是除錯雜訊,兩者都請留著。轉換在本質上就會遺失資訊:XFA 指令碼、計算欄位與動態子表單的行為,在 AcroForm 中都找不到對應物,而 XFAFlattenWarnings 會列出每一個沒有對映成功的範本元素。把轉換後的檔案封存卻不留警告清單,總有一天您會盯著封存副本裡一個空白的合計欄位,卻查不到任何原因紀錄。Editable 旗標決定新欄位是否維持可填寫。若之後還有人要繼續使用這份表單,就傳 True;若目標是一份凍結的紀錄,就把值鎖死
檢查一次轉換,一半靠肉眼,一半靠結構,兩半都不能少。結構那一半很簡單:確認欄位數與 MappedCount 相符。肉眼那一半才抓得到真正的損傷。請在桌面版 Acrobat(至今仍是唯一跑得動 XFA 引擎的檢視器)中開啟來源表單,旁邊用普通閱讀器開啟轉換後的檔案,每份範本至少挑一個已填寫的樣本比對值與版面。XFA 引擎顯示為 2026-06-11 的日期,到了 AcroForm 副本裡可能變成未經格式化的原始值,而那只有您的眼睛抓得到
當輸入是一份 XDP 封包時
不是每項工作都從一份已填好的 PDF 開始。有時候您收到的就是一份獨立的 XDP 封包,由表單設計工具匯出,或由合作夥伴系統交付。ApplyXFAAsAcroForm 省去載入步驟,直接把封包套用到目前的文件上:
XDPBytes := TFile.ReadAllBytes('benefit-claim.xdp');
MappedCount := Pdf.ApplyXFAAsAcroForm(XDPBytes, True);
同一組呼叫也能往反方向跑,用在您必須產出而非消費 XFA 的少數情況。AddXFAPacket 會掛上個別具名封包,例如 'xdp' 或 'config'。SetXFADocument 一次呼叫就安裝一份完整的單一串流酬載。ClearXFAPackets 會清掉註冊內容,讓您重頭來過,而 AddXFASignaturePacket 會嵌入 XAdES 材料,供直接簽署 XML 表單資料的流程使用。在 2026 年產出 XFA 屬於冷門需求,幾乎總是被某個死不接受其他格式的老舊消費端逼出來的,但當合約點名要它時,這幾個呼叫能讓它停留在設定選項的層次,不必再多一套工具
「平面化」的另一種意思
「平面化」這個詞害不少對話走偏,因為它還指涉另一項完全不同的操作:把 AcroForm 欄位的外觀燒進頁面內容串流,直到不剩任何互動物件為止。HotPDF 今天沒有這項 API,而這件事您現在就該知道,而不是等專案做到一半才發現。程式庫給您的替代品,是在欄位建立時就在欄位層級鎖定,再以文件權限作為後盾:
// 在建立欄位時就鎖定值:唯讀文字欄位
Pdf.CurrentPage.AddTextField('CaseNumber', 'BC-2026-0117',
Rect(50, 700, 220, 720), 0, [ffReadOnly]);
// 雙保險:在整份文件層級限制表單填寫
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aes256;
Pdf.OwnerPassword := 'records-owner';
Pdf.ProtectOptions := [prPrint, prInformationCopy, prExtractContent];
// 保留不給填寫權限:集合中沒有 prFillAnnotations
請清楚認知這樣做買到了什麼、又沒買到什麼。唯讀欄位仍然是表單物件。它會出現在檢視器的欄位面板中,它的值透過表單 API 讀得到,而任何能改寫檔案的工具都能把唯讀旗標再清掉。權限旗標提高了門檻,卻取決於檢視器願不願意尊重它,這一點 ISO 32000-1 講得很直白。當主管機關堅持一份封存紀錄裡完全不得含有表單物件時,以 HotPDF 今天的能力,誠實的答案是重建文件:把值讀出來,再用普通的 TextOut 內容畫到新頁面上,而不是把唯讀旗標粉飾成平面化。走權限這條路還有一件事要記得:CryptKeyLength 必須在 BeginDoc 之前設定;其餘細節都在我們的 AES-256 加密與權限一文中
XFA 對封存合規的意義
PDF/A 與 PDF/X 都直接拒絕 XFA。因此一條要餵進 ISO 19005 封存庫的流水線必須先轉換,而且順序沒得商量:載入、FlattenLoadedXFA、儲存,接著才對 AcroForm 結果執行封存產生或驗證。別把轉換當成合規的證明。它修正的是表單模型,字型、色彩與中繼資料則原封不動,所以在信任輸出之前,請先用 veraPDF 驗證。表單一旦站到 AcroForm 這一邊,它的行為就有另一整組控制項可用。JavaScript 觸發器、送出動作與驗證指令碼,都涵蓋在 HotPDF AcroForm 欄位與動作一文中
本文示範的 XFA 註冊、轉換與表單 API,隨適用於 Delphi 與 C++Builder 的 HotPDF Delphi 元件出貨,其文件會跟著近期版本中不斷成長的 XFA 功能集同步更新