PDFium Component 透過 FPdfDocumentXfaFlatten 單元裡的 FlattenPdfXfaToAcroForm,從 Delphi 把靜態 XFA 表單轉成普通 AcroForm 欄位。這個函式讀取 XFA 的 template 與 datasets 封包,把每個絕對定位的欄位映射成帶自己外觀流的 AcroForm 終端欄位,並把結果以增量修訂版追加,轉換後的 PDF 因此在從未支援 XFA 的檢視器裡以普通表單打開
動機通常是支援工單而不是路線圖條目。某份政府或保險表單當年用存成 XFA 的設計器製作,在某個桌面閱讀器裡正常,其他地方打開卻是空白頁,提示文件需要更新的檢視器。XFA 在 PDF 2.0(ISO 32000-2)中已被棄用,瀏覽器與行動檢視器從未實作它,PDFium 本身也只在啟用 V8 的建置下渲染 XFA。靜態 XFA 表單——版面在設計期固定的那種——在 template 裡帶著足夠的幾何,可以重建為 AcroForm 欄位,這正是這個轉換器處理的那一片
FlattenPdfXfaToAcroForm 實際產出什麼?
FlattenPdfXfaToAcroForm 產出的是追加在未改動來源副本上的位元組層級 AcroForm 修訂版,絕不改寫原始檔案。PDFium 沒有建立表單欄位的公開 API,所以元件在這件事上根本不經過 PDFium:它用自己的物件讀取器讀取作用中修訂版,以原始 PDF 物件的形式建出欄位字典、介面元件註記、外觀 XObjects 與 /Helv 字型資源,再按 ISO 32000-1 §7.5.6 的意義透過稀疏增量更新序列化
新的 /AcroForm 字典帶 /Fields、/DA 與 /DR,沒有 /XFA 條目。來源把 AcroForm 存成間接物件時,那個物件被原地改寫;內嵌字典則在 catalog 內被替換。無論哪種,作用中修訂版不再指向 XFA 封包,同時理解兩種模型的檢視器因此打開 AcroForm 欄位,而不是去排 XFA 表單的版。原始 XFA 位元組仍在更早的修訂版裡——增量更新本來就是這個意思——但目前的修訂版裡沒有任何東西參照它們
uses
System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;
procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
Source: TFileStream;
Options: TPdfXfaFlattenOptions;
OutputPdf: TBytes;
Report: TPdfXfaFlattenReport;
Fields: TPdfXfaFlattenFields;
begin
Options := TPdfXfaFlattenOptions.Default;
Options.MaxFields := 1024; // 預設 512,允許範圍 1..4096
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
try
if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
finally
Source.Free;
end;
TFile.WriteAllBytes(TargetFile, OutputPdf);
Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
[Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;
函式對任何被拒或畸形的輸入回傳 False 並填好 Report.ErrorText,離開時還原來源流位置。取消是唯一會向外傳播的例外路徑:Options.CancellationToken 觸發時,報告填好之後重新丟出 EPdfOperationCancelled。TPdfXfaFlattenReport 還帶 SourceSha256 與 OutputSha256,批次作業要證明哪份輸入產出哪份轉換檔時很方便。TPdfXfaFlattenOptions.Default 設 8 MiB 封包預算、512 個欄位與 512 MiB 輸出上限;來源不得加密
讀取 template 與 datasets 封包
PDFium Component 兩種 ISO 32000-1 §12.7.8 允許的 /XFA 條目布局都收:交替封包名稱與流參照的陣列形式,以及單一流裝整份 <xdp:xdp> 文件的單流 XDP 形式。封包流可以未壓縮,或是不帶預測器的 /FlateDecode,解壓受 MaxPacketBytes 約束
單流情況正是天真的 XML 處理會栽跟頭的地方。設計器常把 <xfa:datasets> 帶命名空間前綴寫出,而 template 沒有前綴,於是按字面標籤名切塊會找到一個封包、漏掉另一個。轉換器按本地名稱比對元素與屬性名,之前另有一項檢查確認開閉標籤按完整前綴名成對。只想查看封包而不轉換的話,用 PDFium VCL 偵測 XFA 表單並擷取其封包的指南涵蓋那條唯讀路徑
欄位值來自兩處。template 裡的 <value> 元素供應預設值,datasets 封包供應填好的資料,按欄位名稱綁定。真實表單裡欄位名稱會重複,所以綁定器比較具名 <subform> 祖先鏈與資料中的祖先路徑,取最長的無衝突匹配。兩個葉欄位在不同 subform 下共用名稱時,AcroForm 的 /T 變成 SubformPath.Name,兩個欄位轉換後仍然互不混淆
choiceList 為什麼需要顯式的 Combo 旗標?
XFA 與 PDF 對選擇欄位的預設相反,一個什麼旗標都不複製的轉換器會把每個下拉式選單變成清單方塊。帶預設 open="userControl" 或 open="onEntry" 的 XFA <choiceList> 是下拉式;不帶旗標的 PDF /FT /Ch 欄位是可捲動清單方塊,只有 Combo 旗標(ISO 32000-1 表 230 的位元位置 18,值 131072,即 PDFium 稱為 FPDF_FORMFLAG_CHOICE_COMBO 的那一位)能讓它成為下拉式方塊。所以轉換器為下拉情況設 Combo 旗標,只有 template 說 open="always" 或 open="multiSelect"——保持清單永久展開——時才留空旗標。3.181.1 之前的版本把 Combo 與 Pushbutton 的值弄反了,還在找非標準的 open="on",用它們扁平化過的表單值得重跑
其餘 UI 映射遵循 ISO 32000-1 §12.7.4 的欄位旗標表:
textEdit、numericEdit與dateTimeEdit變成/FT /Tx;多行文字欄位帶 Multiline 旗標(/Ff 4096,表 228)與按行拆開的外觀流passwordEdit變成帶 Password 旗標(/Ff 8192)的/FT /Tx,外觀為每個 Unicode 純量畫一個星號,而不是每個 UTF-16 代碼單元checkButton變成/FT /Btn核取方塊,綁定資料為1時值為/Yes,否則/Off<exclGroup>變成一個/FT /Btn /Ff 32768的單選父欄位(表 226),帶/V、/DV、/Opt與/Kids,外加每個成員一個子介面元件,各自帶/AS與雙態/APimageEdit變成一個外觀繪出影像的按鈕;基線 JPEG 原樣嵌在/DCTDecode底下,PNG 則被解碼並重新編碼為/FlateDecode像素,PNG 帶 alpha 或tRNS透明時再加/SMask
不是純可列印 ASCII 的值,包括換行,都以 <FEFF...> UTF-16BE 字串寫出,日文姓名或兩行地址因此安然過關。Fields 輸出鏡像寫下的內容,當審計軌跡很方便
procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
KindNames: array[TPdfXfaFlattenFieldKind] of string = (
'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
I: Integer;
F: TPdfXfaFlattenField;
begin
for I := 0 to High(Fields) do
begin
F := Fields[I];
Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
[F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
case F.Kind of
fxfkChoice:
Writeln(Format(' %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
fxfkRadio:
Writeln(Format(' %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
fxfkImage:
Writeln(Format(' %d image bytes', [Length(F.ImageData)]));
else
Writeln(' value "' + F.Value + '"');
end;
end;
end;
幾何、量測單位與頁面路由
XFA 幾何轉成 PDF 使用者空間的方式,是把每個欄位對它自己頁面的 MediaBox 高度翻轉,因為 XFA 從頁面頂部向下量,PDF 從底部向上量。巢狀 <subform> 元素的偏移沿 template 向下累加,每個欄位最終必須有正的寬與高,否則轉換被拒
量測接受真實設計器會輸出的單位:裸數字與 pt,加上 in(72 點)、mm(72 / 25.4)、cm(72 / 2.54)、pc(12)、px(0.75)與 mp(0.001,毫點)。解析與地區設定無關,德文 Windows 不會把 8.5in 變成亂碼。未知的單位後綴被拒收,而不是當成點數讀——那會悄悄把欄位放錯位置
頁面路由跟隨 <pageSet>:同層的 <pageArea> 元素按宣告順序從一編號,認領嵌在其下的欄位,沒有 <pageArea> 祖先的欄位留在第一頁。每頁有自己的 /Annots 更新。被路由到 PDF 最後一頁之後的欄位是錯誤,而不是把介面元件丟上第一頁
轉換器拒收哪些 XFA 表單?
凡是必須跑 XFA 版面引擎才知道版面的東西,轉換器一律拒收。也就是說:<subform> 或 <exclGroup> 上不是 position 的 layout 屬性(流式 tb、lr-tb 及其同類,動態表單正是用它們搭的)、任何 <script> 元素,以及上面七種之外任何 UI 類型。執行期長頁的動態表單屬於另一套工具鏈;PDFium Component 的動態 XFA 重分頁與頁數一文展示了那種即時排版涉及什麼
拒收是刻意的。流式 subform 沒有固定座標,任何「轉換」都得編造位置;帶腳本的表單常常在計算總計或切換可見性,靜態 AcroForm 重現不了。產出一份看起來完整、卻悄悄丟了計算總計的 PDF,比 Report.ErrorText 裡一條清楚的錯誤更糟。XFA 3.3 規範定義的遠多於這個子集,轉換器也不假裝不是如此
寫出並驗證 AcroForm 修訂版的陷阱
打造寫入器時冒出兩個問題,對任何手工產增量更新的人都適用。第一:catalog 必須永遠在更新集合裡,即使它的位元組沒變——因為 AcroForm 是在別處改寫的間接物件。稀疏更新寫入器從它攜帶的物件導出 trailer 的 /Root,缺根物件的更新產出的修訂版,檢視器錨定不了
第二個關於驗證。文件一帶 /AcroForm,透過 PDFium 包裝載入轉換後的檔案就會觸發表單環境初始化,而測試環境裡的宿主 pdfium.dll 缺 FORM_GetTextDirection 匯出,初始化在任何欄位被檢查之前就失敗了。所以讀回檢查改用元件的物件讀取器,它解析作用中修訂版而不碰表單層
uses
System.SysUtils, System.Classes, FPdfCompress;
function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
Stream: TBytesStream;
Objects: TPdfActiveObjects;
Revision: TPdfSparseRevisionInfo;
ErrorText: string;
begin
Stream := TBytesStream.Create(Pdf);
try
if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
Objects, Revision, ErrorText) then
raise Exception.Create(ErrorText);
// 作用中 catalog 必須參照 AcroForm;轉換器寫出的
// 內嵌 AcroForm 不帶 /XFA 條目
Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
finally
Stream.Free;
end;
end;
外觀流刻意簡單:文字與選擇值用 10 點的 Helvetica,核取方塊畫描邊方塊,單選選中態畫填色內方塊。它們讓轉換後的表單處處可讀,但不是原 XFA 渲染的像素複本,template 裡定義的字型、顏色與邊框不會帶過來。如果表單必須與設計器輸出長得一樣,先用 XFA 建置渲染一次、比對過,再把生產管線切過來
FAQ
FlattenPdfXfaToAcroForm 需要 V8 或 XFA 版的 PDFium 嗎?
不需要。FlattenPdfXfaToAcroForm 用 PDFium Component 的代碼解析封包、寫出修訂版,所以標準的 pdfium.dll 就能用,也不執行任何 XFA 腳本
轉換器處理得了加密的 XFA 文件嗎?
不能直接處理。加密來源會以錯誤被拒,請先解密文件,再轉換解密後的副本
轉換後的欄位還能編輯嗎?
可以。輸出含真正的 AcroForm 文字、核取方塊、單選、選擇與按鈕欄位,帶值與外觀流,使用者可以在任何支援 AcroForm 的檢視器裡繼續填表
靜態 XFA 轉換、XFA 封包擷取與啟用 V8 的 XFA 執行環境都是PDFium Component for Delphi and C++Builder 的一部分,它為 Delphi、C++Builder 與 Lazarus 專案包裝 PDFium