XFA (XML Forms Architecture) 已被棄用。ISO 32000-1 在第 12.7 節中保留了它,並註明它已從 PDF 2.0 中移除,且現代的檢視器正逐漸捨棄它們的 XFA 引擎。然而這一切並沒有清空檔案庫。政府的受理表單、保險申請書和銀行對帳單在過去二十年的大半時間裡都是以 XFA 格式製作的,而這些檔案今天仍然不斷湧入收件匣和檔案處理流程中。當過去用來渲染它們的檢視器停止支援時,表單就會變成一個空白頁面,上面只有一個「請使用其他閱讀器開啟」的佔位符號。長久的解決方案是將 XFA 平面化 (flatten) 為任何閱讀器都能繪製的靜態 PDF 內容
平面化困難的部分不在於欄位。文字方塊和核取方塊可以很乾淨地對應到 AcroForm 小工具 (widgets)。困難的部分是 XFA 儲存在 draw 元素內、位於 <exData contentType="text/html"> 區塊中的豐富文字。該區塊是一個帶有行內樣式 (inline styling) 且通常帶有錨點 (anchors) 的 HTML 子集。要將其呈現在頁面上,意味著要重現帶有樣式的文字以及活動的超連結,而超連結正是大多數實作默默放棄的地方
XFA 豐富文字實際看起來像什麼
一個 exData 主體是一小段 XHTML。段落是 <p>;帶有樣式的字元範圍是 <span>,其具有自己設定粗細、姿態、顏色和大小的行內 CSS;而超連結是 <a href="..."> 包覆著其可見的文字。單一行可以連續包含數個 span,每個都有不同的樣式,其中一個可以是錨點。這些樣式並不是可以捨棄的裝飾。因為是法律警告而以粗體紅色渲染的條款,在平面化後必須保持粗體和紅色,否則平面化的檔案會歪曲原始內容
因此,平面化引擎不能將該區塊視為單一字串。它必須走訪行內結構,透過將 span 的行內 CSS 疊加在 draw 元素的基底字型上來解析每個文字段 (run) 的有效樣式,然後將這些文字段一個接一個地排成一行。HotPDF 將每個這些排版好的片段塑模 (model) 為一個內部的 TXFARichRun 記錄。該記錄包含文字段的文字、其解析後的樣式、其測量的外框,以及對於錨點而言它所指向的 Href
從左到右排列文字段
定位是豐富文字從解析問題變成排版問題的地方。文字段共享一行,因此每個文字段都從前一個文字段結束的地方開始。沒有標記來記錄這些位置;它們必須被測量。引擎內部的 LayoutRichText 常式使用與稍後繪製它時相同的字型度量 (font metrics) 來測量每個文字段,然後將文字段的水平偏移量設定為所有先前文字段寬度的累計總和。文字段一從 draw 外框的原點開始,文字段二從文字段一的寬度處開始,文字段三從前兩個文字段的總寬度處開始,依此類推排列整行
這就是為什麼測量時的字型對齊如此重要。排版階段 (layout pass) 測量步進 (advances);獨立的渲染階段 (render pass) 繪製字形。如果這兩個階段在字型上不一致,排版所計算出的外框將不會位於渲染器繪製的字形下方。HotPDF 透過內部的 RunStyleToFontSpec 輔助函式,將每個文字段的解析樣式對應到符合渲染器自身預設值 (10 點的 Arial) 的字型規格,使兩者保持同步。這樣測量出的步進與繪製的文字就會一致,而文字段計算出的外框也能真實涵蓋讀者所看到的字元
// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
TRichRunInfo = record
Dx, Dy : Double; // top-left, relative to the draw-box origin
W, H : Double; // measured run box (width from the layout pass)
Text : AnsiString; // the run's visible characters
Href : AnsiString; // URI target for an <a> run, '' otherwise
end;
從錨點文字段到 PDF 連結註解
完成的 PDF 中的超連結不是頁面內容的一部分。它是一個獨立的物件,即 ISO 32000-1 第 12.5.6.5 節中描述的連結註解 (Link annotation)。該註解有一個 /Rect 定義了頁面上可點擊的矩形,以及點擊矩形時會觸發的動作。對於外部連結,該動作是 URI 動作:/S /URI 及其目標位址作為 /URI 字串。其下方可見的文字是一般的頁面內容;該註解則是覆蓋其上的不可見熱區 (hot zone)
平面化路徑完全遵循這個模型。當文字段帶有 Href 時,HotPDF 首先繪製帶有樣式的文字,然後在文字段的外框上建立一個連結註解。該註解的公用進入點是頁面方法 AddURILink,它會建立帶有 /URI 動作的 /Type /Annot /Subtype /Link 物件,並傳回註解字典。它的矩形就是文字段測量出的外框,從 draw 元素的區域座標轉換為頁面座標。結果是一個精準落在錨點文字上而不偏離的連結
// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
LinkRect: TRect;
Annot: THPDFDictionaryObject;
begin
LinkRect := Rect(72, 690, 268, 706); // page-space hit box for the run
Annot := Pdf.CurrentPage.AddURILink(LinkRect,
'https://www.example.gov/appeal', 'File an appeal online');
end;
為什麼點擊區域 (hit box) 必須來自測量的寬度
我們很容易想像透過在頁面上搜尋超連結的可見文字,並在找到的任何內容周圍繪製矩形來定位連結。這行不通,其原因與平面化文字的儲存方式息息相關。帶有樣式的文字段是使用內嵌的子集字型 (subset fonts) 進行繪製的。子集字型會重新編號其保留的字形,因此頁面內容串流 (content stream) 包含的是十六進位的 CID 碼,而不是原始字元碼。頁面上的位元組並不是人類閱讀的字母,而且它們無法作為文字進行搜尋。對錨點標題的搜尋會找不到任何東西,因為該標題並不存在於串流的任何地方作為文字常值 (literal text)
矩形唯一可靠的錨點 (anchor) 是排版階段已經產生的幾何形狀。每個文字段的偏移量和測量寬度是在對文字行換行時計算出來的,遠在任何字形被重新編號之前,且它們描述了文字實際出現的位置。因此,HotPDF 直接從文字段放置的外框中取得連結矩形,而不是從任何文字查詢中取得。因為測量使用的是渲染字型,無論是否使用子集化,外框都是正確的。幾何形狀能在編碼中留存下來;文字則不能。這就是使用測量寬度定位的全部理由,這也是為什麼試圖透過文字搜尋來加裝 (retrofit) 連結的平面化工具會產生漂移或消失的點擊區域 (hit zones)
從您的程式碼驅動平面化
對於已經包含 XFA 封包的 PDF,進入點是 FlattenLoadedXFA。載入檔案,呼叫該方法,並儲存結果。Editable 參數決定表單欄位會發生什麼事:傳入 True 以將它們保留為可填寫的 AcroForm 小工具,或傳入 False 將每個小工具標記為唯讀,讓輸出成為凍結的記錄。這兩種方式都會產生豐富文字的 draw 區塊,以及它們帶樣式的文字段和連結註解。此函數傳回它所產生的小工具數量
var
Pdf: THotPDF;
Emitted, i: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('xfa_appeal_form.pdf');
// True keeps fields fillable; False freezes them read-only.
Emitted := Pdf.FlattenLoadedXFA(True);
// Anything the engine could not map is reported, not raised.
for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);
Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
Writeln('Widgets emitted: ', Emitted);
finally
Pdf.Free;
end;
end;
呼叫之後請務必讀取 XFAFlattenWarnings。該清單在每次平面化開始時會被清除,並為引擎拒絕渲染的每個元素累積一行:不支援的欄位種類、無法解碼的 draw 圖片、沒有可用 span 的 exData 區塊。這些都不會引發例外,因此空的警告清單是您所有內容都成功對應的證據,而非空的清單則能明確告訴您需要檢查哪些原始內容。當您將原始 XFA 作為 XDP 位元組而不是載入的 PDF 來持有時,同級方法 ApplyXFAAsAcroForm 會直接接受這些位元組,並共用相同的程式碼路徑和相同的警告行為。而互補的方法 AddXFAPacket 則是反向操作,將 XFA 封包內嵌到您正在建立的檔案中
在閱讀器中確認結果
在 Acrobat 或任何目前的檢視器中開啟已平面化的檔案,並檢查兩件事。首先,豐富文字渲染後的樣式是否完好無缺:粗體的文字段是否為粗體,有顏色的文字段是否帶有顏色,且 span 是否按正確的順序排列在文字行上,而不是重疊或超出外框。其次,超連結是否有效。將滑鼠游標懸停在錨點上,狀態列應該會顯示目標位址;點擊它,URI 動作應該會開啟它。使用檢視器的註解檢查工具 (annotation inspector) 來確認每一個都是真正的 /Link 註解,其 /Rect 緊貼著錨點文字,並覆蓋在現在已是單純繪製的字形而非表單渲染的 XFA 內容上。這個組合(帶樣式的靜態文字加上在正確矩形上的真實連結註解),正是讓平面化檔案能比它不再需要的 XFA 引擎活得更久的原因
將環繞這些豐富文字的欄位本身(文字方塊、核取方塊和選取清單)進行平面化,已在我們關於將 XFA 表單平面化為 AcroForm 小工具的逐步解說中探討。關於在平面化路徑產生的註解之外,手動建立和放置連結註解的更廣泛故事,請參閱 在 HotPDF 中使用 PDF 註解。兩者都建立在隨附於適用於 Delphi 和 C++Builder 的 HotPDF 元件 的相同註解和表單模型之上