技術文章

Delphi 中的 XFA SOM 路徑運算式:動態表單資料

losLab PDF 函式庫透過 XFA 3.3 SOM 路徑運算式讀取和寫入動態 XFA 表單中的重複資料節點:GetXFAFormFieldValueSetXFAFormFieldValue 接受 $data$record!data 根節點、[*] 出现次數萬用字元、子代 and 子萬用字元選擇器、parent 屬性、#dataGroup/#dataValue 類別選擇器,以及簡單的 FormCalc 樣式述詞,因此 Delphi 程式只需呼叫一次即可更新政府稅務表單中的每一行發票。本篇文章為定址參考;有關在檔案之間移動整個資料集的資訊,請參閱關於 FDF、XFDF 和 XFA 表單資料交換的隨附文章

當表單不再是扁平結構時,問題就會顯現出來。建立為動態 XFA 表單的增值稅申報單或海關申報單,不會有 12 個名為 Total_Price_1Total_Price_12 的欄位,而是包含一個在範本 DOM 中宣告一次,並根據資料需求多次產生的 Detail 子表單。這些值存在於另一個樹狀結構中,即 datasets 封包內部的 Data DOM,其中每個單一項目是 <Receipt> 底下重複的 <Detail> 元素。扁平的欄位名稱無法定址到「第三行上的價格」或「所有超過 200 的行」;這正是 Adobe XFA 3.3 規格書中「指令碼處理物件模型」(Scripting Object Model)章節賦予 SOM 運算式的工作,而這也正是 losLab PDF 函式庫 XFA 欄位值 API 所支援的語法。

如何從 Delphi 中定址 XFA 資料節點?

每個資料集路徑都從根節點開始,而 losLab PDF 函式庫可交替接受標準拼法:$dataxfa.datasets.data 的 XFA 3.3 簡寫形式,!data 是以 xfa.datasets 為根節點的簡寫形式,而在不使用記錄處理的封包中,$record 會解析為外部資料記錄(資料節點下的第一個元素)。範本端 API(例如 SetXFAFormFieldAccess)也以同樣方式接受 $templatexfa.template。路徑段可以是用點分隔或用斜線分隔($data.Receipt.Tax$data/Receipt/Tax),這非常重要,因為 GetXFAFormFieldNames 會將欄位列舉為斜線分隔的路徑,這些路徑可直接用於值和範本 API。根據規格,出現次數索引是以 0 為基準,因此 Detail[0] 是第一列。有兩個逸出規則可保持特殊名稱的可定址性:在以點分隔的路徑中,\. 表示字面上的句點(Line\.Item),而以斜線分隔的路徑則將句點視為普通字元。帶有其自身 XML 命名空間前綴的商務資料會透過本地名稱進行比對,因此 <m:Receipt> 仍會對應到 Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Equivalent addresses for the same data node
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Slash form with a zero-based occurrence index
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

如何透過程式設計方式填寫 XFA 表單中的重複列?

[*] 出現次數萬用字元是批次處理工具。當數字索引選取一個同級節點時,[*] 會選取所有同名的同級節點,因此 $data.Receipt.Detail[*].Total_Price 可以一次定址到每個單一項目上的價格欄位。在讀取時,GetXFAFormFieldValue 會傳回以 | 分隔字元連接的所有相符節點的值;在寫入時,SetXFAFormFieldValue 會使用相同的值更新每個相符的節點,並在成功時傳回 1。未比對到任何內容的路徑會讀回為空字串,這是在寫入分支之前探測其是否存在的簡便方法。

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // All Detail rows at once: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Reset one column across every repeated row in a single call
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

子代、子萬用字元與父選擇器

三個結構選擇器涵蓋了您知道欄位名稱但不知道其確切深度的情況。子代選擇器 .. 可比對目前節點下方任何深度的節點:$data..Total_Price 會在資料根節點底下的任何地方尋找第一個 Total_Price,而透過子代路徑進行的寫入會更新該第一個相符的節點。注意其界限:子代比對後的出現次數索引不會跨越同級分支,因此如果 $data..Total_Price[0] 在第一個 Detail 列內部解析,$data..Total_Price[1] 會傳回空值,而不會跳到下一列;當您需要所有同級節點時,請使用 Detail[*]。子萬用字元 .* 會比對每個直接子節點,並允許剩餘的路徑段進行篩選:$data.Receipt.*.Total_Price 會觸及 Receipt 的每個子分支內部的 Total_Price,而不會同時選取直接位於 Receipt 上的同名摘要欄位。最後,parent 屬性會向上提升一級,XFA 3.3 故意將其與 .. 分開:$data.Receipt.Detail[1].parent.Tax 從第二個單一項目開始,向上移至 Receipt,並落在其同級的 Tax 值上。

類別選擇器與述詞:#dataGroup、#dataValue、.[expression]

#class 語法透過物件類別而不是名稱來定址 Data DOM 節點,這在 XML 標籤包含難以拼寫為指令碼名稱的字元時非常實用。losLab PDF 函式庫將 #dataGroup 對應到具有子元素的元素,將 #dataValue 對應到沒有子元素的分葉元素,且兩者皆可與數字或 [*] 出現次數結合:$data.Receipt.#dataGroup[0].#dataValue[0] 選取 Receipt 底後第一個群組內的第一個值。述詞選擇器 .[expression] 依內容篩選同名的同級節點。支援的述詞是子節點值與字面值的簡單比較,使用關係運算子如 ><<=;當述詞比對到多列時,讀取會傳回以 | 連接的結果,而寫入會更新每個相符項。

// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

哪些 XFA SOM 功能不受支援?

losLab PDF 函式庫中的 SOM 支援僅限於資料集和範本欄位路徑,且其界限是明確規定的,而不是盡力而為。提前了解這些限制可以避免對靜默傳回空字串的路徑進行偵錯。

  • 述詞不執行任意的 FormCalc 或 JavaScript:不支援函式呼叫(contains(...) 述詞會傳回空值),不支援複合布林運算式,僅支援單一的 child op literal(子節點 運算子 字面值)比較
  • $record 僅在非記錄處理封包中運作;未實作 dataWindow 記錄分頁和記錄群組旋轉
  • 解析是直接針對 Data DOM 和範本 DOM 進行;不執行 Form DOM 相對解析(檢視器在執行階段建置的合併檢視)
  • 未套用 Data DOM 屬性載入語義;函式庫將資料集封包讀取為原始 XML,因此路徑定址的是元素,而不是提升為節點的屬性

這些限制在批次填寫工作中很少會造成困擾,因為填寫器是透過結構而不是指令碼邏輯來定址資料。當它們確實造成限制時,替代方案是使用封包層級:讀取整個資料集 XML,在 Delphi 中進行轉換,然後將其寫回。

使用 SetXFAFromString 寫入整個封包

SetXFAFromString 是該封包層級的進入點:它會將完整的 XDP 字串、範本和資料集安裝在一起,在尚未存在 AcroForm 容器時於新文件中建立該容器,且它可接受帶有位元組順序標記(BOM)的 UTF-16 編碼封包以及 UTF-8。常見的生產模式是將機構發布的 XDP 作為固定資源保留,使用 SetXFAFromString 載入它,然後在儲存前針對變動的發票值執行 SOM 定址的 SetXFAFormFieldValue 呼叫。由於 XFA 是 PDF 可以承載的兩種表單模型之一,經典的 AcroForm 端也有其自動化模式,已在互動式表單動作與 JavaScript 文章中介紹。

這裡展示的 XFA 欄位值、列舉和封包 API 隨附在適用於 Delphi、C# 和 VB.NET 的 losLab PDF 函式庫中;產品頁面提供了完整的表單處理參考。