技術文章

Delphi 中的 FDF、XFDF 和 XFA 表單資料交換

losLab PDF Library 透過三種不同的方式在 PDF 內外移動表單資料:適用於 AcroForm 欄位值的 FDF 與 XFDF、適用於註解批註的相同兩種格式,以及直接寫入表單的完整 XFA XDP 封包。ExportFormDataToXFDFImportFormDataFromXFDFImportAnnotationsFromFDFExportAnnotationsToXFDFSetXFAFromString 是進入點,且其中每一個都有檔案變體和字串變體

之所以有這麼多方法,是因為 PDF 表單資料並非單一事物。填寫好的 AcroForm 具有欄位值,它也可能包含審查註解,而舊版的 XFA 表單則內嵌了與這兩者完全無關的完整 XML 應用程式描述。losLab PDF Library 特意將這三種功能保留在獨立的 API 上,因為將它們合併會將錯誤的資料模型強加到其中至少兩者之上。決定您需要哪一個系列是第一個設計選擇,而且通常在您知道另一側的接收系統實際使用什麼時就已經決定了

FDF、XFDF 和 XFA 表單資料有何不同?

FDF 和 XFDF 在兩種不同的語法中攜帶相同的資訊,而 XFA 則是另一個獨立的世界。FDF 是一個微型 PDF 語法文件(ISO 32000-2 §12.7.8):一個由 << /T (name) /V (value) >> 字典組成的 /Fields 陣列,其中的字串轉義方式與 PDF 內部字面值字串的轉義方式完全相同。XFDF 是相同資料的 XML 形式(ISO 19444),一個 <fields> 樹狀結構,Acrobat 和大多數表單後端都能原生讀寫。XFA 兩者皆非:它是一個 XML 表單架構範本及其資料,儲存為 PDF 從 /AcroForm/XFA 參照的 XML 資料封包(XDP)。當您在欄位值上進行互通時請選擇 FDF 或 XFDF,並且只有在維護最初作為 XFA 表單建立的文件時才使用 XFA

在 FDF 和 XFDF 內, losLab PDF Library 劃分了第二條界線:欄位值與註解批註。表單資料系列(ExportFormDataToFDFImportFormDataFromFDFExportFormDataToXFDFImportFormDataFromXFDF)僅讀寫 /Fields 子樹,不涉及其他內容。註解系列(ExportAnnotationsToFDFImportAnnotationsFromFDFExportAnnotationsToXFDFImportAnnotationsFromXFDF)則讀寫 /Annots 子樹,這就是 Acrobat 中「匯出註解」的意思。兩者絕不重疊,因此匯出欄位值不會掃進零星的檢閱筆記,而匯入註解也不會干擾使用者已經輸入的值。控制項在被按一下或重新計算時的行為是第三個關注點,隨附的互動式表單動作與 JavaScript 筆記中對此進行了介紹

如何在 Delphi 中從 FDF 或 XFDF 填入 PDF 表單?

載入文件,呼叫一個匯入方法,然後儲存。ImportFormDataFromXFDFImportFormDataFromFDF 分別解析傳入的表單資料,透過其完全限定名稱將每個項目與 AcroForm 欄位進行比對,設定其值,並傳回實際更新的欄位數量。這兩個方法都只更新目標 PDF 中已存在的欄位;兩者都不會為表單未定義的名稱建立欄位,這可以防止零星或惡意的資料檔案在無形中增加您的表單欄位

var
  Lib: TPDFlib;
  FieldsSet: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('application-blank.pdf', '');
    // 案件管理系統產生的 XFDF,在磁碟上已是 UTF-8
    FieldsSet := Lib.ImportFormDataFromXFDF('applicant-1042.xfdf');
    if FieldsSet > 0 then
      Lib.SaveToFile('application-filled.pdf');
  finally
    Lib.Free;
  end;
end;

兩個細節決定了重要資料能否在傳輸過程中完好無損。第一個是轉義:FDF 將值儲存為 PDF 字面值字串,因此包含括弧、反斜線或不可列印位元的字值在傳入時會被包裝在 ISO 32000-2 §7.3.4.2 中定義的八進位轉義中,而 losLab PDF Library 在匯入時會反轉該轉義,以便將括弧和斜線還原為使用者輸入的字面字元。第二個是編碼:基於檔案的方法將 XFDF 和 FDF 作為 UTF-8 進行讀寫,這正是 XFDF 宣告所承諾的,也是任何非 ASCII 欄位值(例如帶有重音符號的名稱、貨幣符號、中日韓地址)為了在往返過程中不損壞所需要的。如果您自己建立 XFDF,請宣告 UTF-8並寫入 UTF-8,匯入就會與其相容

階層式欄位、多重選取選項和 Rich Text

階層式欄位名稱是普通匯出器最先出錯的地方。AcroForm 使用點號分隔的完整標題(例如 Applicant.FullName)來定址巢狀欄位,但 XFDF 並不將該點號字串放在單一名稱屬性中;ISO 19444 將其巢狀化,表示為 <field name="Applicant"><field name="FullName">。losLab PDF Library 在匯出時將點號標題拆分為巢狀的 <field> 元素,並在匯入時將巢狀元素重新組合成完整標題,使這兩個方向保持對稱。在匯出時,它還會跳過非終端母欄位,因為 AcroForm 中的母節點僅承載命名層級,其本身沒有值;輸出它會產生一個空的 <value></value>,這與實際表單不符。多重選取清單方塊是第二個陷阱:選項欄位可以同時包含多個選定的值,XFDF 將其表示為重複的 <value> 元素,而 FDF 將其表示為 /V 陣列,且 losLab PDF Library 僅在欄位確實是多重選取選項時才將其拆分為多個值,因此一般的多行文字欄位會保留其分行,而不是碎裂成錯誤的值

Rich Text 是第三個陷阱,也是人們最常弄錯的地方。具有格式的欄位會將其標記以 XHTML 子樹形式儲存在 RV 項目中,而 XFDF 則將其攜帶在 <value-richtext> 中。losLab PDF Library 將該子樹寫入為即時 XML 片段,而不是對其進行轉義,因此下游工具讀取的是真正的 Rich Text,而不是一串可見的標籤;在匯入時,它會保留原始的 RV 子樹,並仍將純文字 <value> 套用至 V。如果同一個欄位同時提供兩者,則純文字值會優先套用於 V,而 Rich Text 則並存於 RV 中,這項互通性規則可以防止 Rich Text 匯入時悄悄覆寫另一個工具已設定的值。當您的 Rich Text 是為了向輔助技術傳達文件結構時,請以處理 標記 PDF 與可存取性結構 筆記中討論的閱讀順序的相同方式來對待它

var
  Lib: TPDFlib;
const
  XFDF =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">' +
    '<fields>' +
    '  <field name="Applicant">' +
    '    <field name="FullName"><value>Alice Example</value></field>' +
    '  </field>' +
    '  <field name="Skills">' +
    '    <value>Delphi</value><value>PDF</value>' +
    '  </field>' +
    '  <field name="Notes">' +
    '    <value>See attachment</value>' +
    '    <value-richtext><body><p>See <b>attachment</b></p></body></value-richtext>' +
    '  </field>' +
    '</fields></xfdf>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('intake.pdf', '');
    // Applicant.FullName 重新組合;Skills 填入 /V + /I 陣列;
    // Notes 從  取得 V,並從  取得 RV
    Lib.ImportFormDataFromXFDFString(XFDF);
    Lib.SaveToFile('intake-filled.pdf');
  finally
    Lib.Free;
  end;
end;

如何將註解批註在 FDF 或 XFDF 之間往返?

註解在註解系列中傳遞,且支援的子集特意設得很小。ExportAnnotationsToXFDFImportAnnotationsFromXFDF 及其 FDF 對應方法僅攜帶能乾淨往返的欄位與文字樣式標記:註解子類型、其矩形、其從 0 開始的頁面索引、T 中的作者、Subj 中的主題、純文字 Contents,以及顏色(這會將 PDF /C DeviceRGB 三元組對應至 XFDF #RRGGBB 屬性,反之亦然)。匯入僅接受已知的元素名稱,並跳過頁面超出範圍或缺少矩形的任何項目,因此手動編輯或外部的 XFDF 無法將格式錯誤的註解丟入文件中。值得清楚說明該子集目前尚未攜帶的內容:Rich Text 內容(RC)、快顯視窗、四邊形頂點(quadpoints)、筆跡列表、頂點和烘焙的外觀串流目前不在範圍內,因此醒目提示幾何形狀和自訂印章外觀將無法在此路徑中保留。若要確認實際寫入了什麼,請走訪頁面註解或文字搜尋與頁面元素列舉中涵蓋的更寬廣的元素樹

var
  Src, Dest: TPDFlib;
  Xfdf: WideString;
begin
  Src := TPDFlib.Create;
  Dest := TPDFlib.Create;
  try
    Src.LoadFromFile('reviewed.pdf', '');
    Xfdf := Src.ExportAnnotationsToXFDFString;   // 僅限  子集
    
    Dest.LoadFromFile('clean-copy.pdf', '');     // 頁面索引必須對齊
    Dest.ImportAnnotationsFromXFDFString(Xfdf);
    Dest.SaveToFile('clean-copy-commented.pdf');
  finally
    Dest.Free;
    Src.Free;
  end;
end;

使用 SetXFAFromString 寫入完整的 XFA XDP 封包

XFA 是一個例外,而 SetXFAFromString 是您一次寫入完整內容的方法。該方法接收一個完整的 XML 資料封包,即包含其範本與資料集封包的 <xdp:xdp> 文件,並將其儲存為 /AcroForm/XFA 參照的資料流。當文件沒有 AcroForm 容器時,losLab PDF Library 會根據需要建立該容器,因此您不必為了給 XFA 提供掛載位置而新增一個丟棄的欄位,並且它會捨棄其保留的任何已解析 XFA 狀態,以便後續的讀取能反映您剛剛寫入的封包,而不是過期的快取。由於 XFA 封包是 XML 且不一定是 UTF-8,程式庫會偵測 UTF-16 位元組順序標記(BOM),並在解析前正確解碼封包,這對於預設為 UTF-16 的工具產生的封包至關重要

一旦封包到位,GetXFAFormFieldValueSetXFAFormFieldValue 就會透過其指令碼物件模型(SOM)路徑來定址個別欄位。losLab PDF Library 接受標準 SOM 根目錄以及單純的相對路徑,因此 form1.FullName$data.form1.FullNamexfa.datasets.data.form1.FullName 都解析為同一個資料節點,且範本端也以相同的管道接受 $template and xfa.template 的方式。當 SOM 路徑是由另一個始終輸出完全限定格式的系統產生時,這種容錯能力就顯得尤為重要

var
  Lib: TPDFlib;
const
  Xdp =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">' +
    '  <template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">' +
    '    <subform name="form1">' +
    '      <field name="FullName"><ui><textEdit/></ui></field>' +
    '    </subform>' +
    '  </template>' +
    '  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">' +
    '    <xfa:data><form1><FullName>Alice Example</FullName></form1></xfa:data>' +
    '  </xfa:datasets>' +
    '</xdp:xdp>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetXFAFromString(Xdp, 0);      // 如果不存在 AcroForm 容器,則建立它
    // 透過 SOM 路徑讀回資料 DOM
    if Lib.GetXFAFormFieldValue('form1.FullName') = 'Alice Example' then
      Lib.SetXFAFormFieldValue('form1.FullName', 'Bob Example');
    Lib.SaveToFile('xfa-packet.pdf');
  finally
    Lib.Free;
  end;
end;

坦白說,結論是欄位值可透過 FDF 和 XFDF 完整地雙向往返,包括階層架構、多重選取和 Rich Text;註解批註則以具有明顯間隙的定義文字標記子集形式往返;而 XFA 則是作為一個完整封包進行讀寫,並在此之上提供了每個欄位的 SOM 存取。請使格式與取用端保持一致,為您手動建立的任何內容宣告 UTF-8,並記住匯入方法只會觸發已符合文件特徵的欄位和註解。這裡描述的表單資料、註解和 XFA 方法是適用於 Delphi 和 C++Builder 的 losLab PDF Library 的一部分,其參考手冊中包含每個匯出和匯入進入點的完整參數清單