losLab PDF Library は、XFA 3.3 SOM(Scripting Object Model)パス式を介して、動的XFAフォーム内の繰り返しデータノードの読み書きに対応しています。GetXFAFormFieldValue および SetXFAFormFieldValue は、$data、$record、!data などのルートノード、[*] によるオカレンスワイルドカード、子孫セレクターおよび子ワイルドカードセレクター、parent プロパティ、#dataGroup/#dataValue クラスセレクター、さらに FormCalc風の簡易な述語(Predicate)フィルターを受け取ることができます。これにより、Delphiのプログラムから1回の呼び出しで、政府の申告書などのすべての請求項目行を一括更新できます。この記事では、これらのアドレス指定方法について解説します。ファイル間でのデータセット全体の移行方法については、姉妹記事のFDF、XFDF、およびXFAフォームデータのやり取りに関する記事を参照してください。
この課題は、フォームがフラットな構造でなくなった瞬間に現れます。動的XFAフォームとして構築された納税申告書や税関申告书には、Total_Price_1 から Total_Price_12 のような平坦な名前を持つ12個 of フィールドが存在するわけではありません。テンプレートDOM内で一度だけ定義され、データの量に応じて何回でもインスタンス化される Detail サブフォームが1つだけ存在します。実際の値は、データセットパッケージ内の別のツリーである Data DOM に保存され、各ラインアイテムは <Receipt> の下で繰り返される <Detail> 要素となります。平坦なフィールド名では「3行目の金額」や「200を超えるすべての行」といった指定ができません。これこそが、Adobe XFA 3.3 仕様書の Scripting Object Model(SOM)の章で定義されているSOM式の役割であり、losLab PDF Library のXFAフィールド値操作APIが受け取る構文そのものです。
DelphiからXFAデータノードのアドレスを指定する方法
datasets 内のすべてのパスはルートノードから始まります。losLab PDF Library は標準の表記法を相互に認識します。$data は XFA 3.3 における xfa.datasets.data の短縮形、!data は xfa.datasets をルートとする短縮形であり、レコード処理を使用しないパッケージにおいては $record が外側のデータレコード(data ノードの直下の最初の要素)に解決されます。SetXFAFormFieldAccess などのテンプレート側のAPIも同様に $template や xfa.template を受け入れます。セグメントの区切り文字はドット(.)またはスラッシュ(/)のいずれも使用できます(例: $data.Receipt.Tax 或いは $data/Receipt/Tax)。GetXFAFormFieldNames はフィールド名をスラッシュ区切りのパスとして列挙するため、この出力をそのまま値設定やテンプレートのAPIに入力することができます。インデックスは仕様に従い 0 から始まるため、Detail[0] は最初の行を指します。特殊な名前のアドレス指定に対応するために2つのエスケープ規則が用意されています。ドット区切りのパスにおいて \. はリテラルのピリオドを表し(例: 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;
子孫セレクター、子ワイルドカード、および親セレクター
フィールドの名前は分かっているものの、その正確な階層の深さが不明な場合に役立つ3つの構造的セレクターが用意されています。子孫セレクター .. は、現在のノードより下にある任意の深さのノードに一致します。$data..Total_Price は、データのルート以下の任意の場所にある最初の Total_Price を検出し、そのパスへの書き込みは最初の一致ノードを更新します。ただし、制限として、子孫一致の後に指定されたオカレンスインデックスは兄弟ブランチをまたいでステップ実行されません。もし $data..Total_Price[0] が最初の Detail 行内で解決された場合、$data..Total_Price[1] は次の行に進むのではなく空を返します。すべての行を対象とする場合は Detail[*] を使用してください。子ワイルドカード .* は、直接の子ノードすべてに一致し、残りのセグメントでフィルター処理を行います。$data.Receipt.*.Total_Price は、Receipt の直下にある summary フィールドなどを誤って選択することなく、Receipt の各子ブランチ内にある Total_Price にアクセスできます。最後に、parent プロパティは階層を1つ上に登ります。XFA 3.3 はこれを .. と明確に区別しています。例えば $data.Receipt.Detail[1].parent.Tax は、2番目のラインアイテムから開始して Receipt に登り、その兄弟である Tax の値に到達します。
クラスセレクターと述語:#dataGroup、#dataValue、.[式]
#class 構文は、名前の代わりにオブジェクトクラスで Data DOM ノードを指定します。これは、XMLのタグ名にスクリプト名として記述しにくい文字が含まれている場合に実用的です。losLab PDF Library は、要素の子を持つ要素を #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 Library における SOM サポートは、datasets および template のフィールドパスに限定されており、サポート外の機能はベストエフォートではなく明確に機能が制限されています。あらかじめ制限を把握しておくことで、パスが警告なしに空の文字列を返すバグの調査にかける時間を削減できます。
- 述語内での任意の FormCalc や JavaScript の実行はサポートされていません。関数呼び出し(例: 述語での
contains(...)は空を返します)や複合的な論理式は使用できず、単一のchild op literalの比較のみが実行可能です。 $recordはレコード処理を使用しないパッケージでのみ機能します。dataWindow レコードページングやレコードグループのローテーションは実装されていません。- アドレスの解決は Data DOM および template DOM に対して直接実行されます。ビューアが実行時に構築するマージされたビューである Form DOM の相対解決は実行されません。
- Data DOM の属性ロードのセマンティクスは適用されません。ライブラリは datasets パッケージを生のXMLとして読み取るため、パスはノードに昇格された属性ではなく要素を指定します。
データ入力処理においては、スクリプトの動的ロジックではなく構造に基づいてアドレスを指定するため、これらの制限が問題になることはほとんどありません。問題が発生した場合は、パケットレベルの回避策として、datasets のXML全体を読み込み、Delphi側で加工した後に書き戻してください。
SetXFAFromStringを使用したパケット全体の書き込み
SetXFAFromString は、パケットレベルでの書き込みを行うAPIです。これはテンプレートと datasets が含まれた完全な XDP 文字列をインポートし、新しいドキュメント内に AcroForm コンテナが存在しない場合は自動的に作成します。バイトオーダーマーク付きの UTF-16 エンコードパケットのほか、UTF-8 も受け入れます。一般的な業務パターンとしては、官公庁が発行した XDP ファイルをテンプレートとして読み込み、SetXFAFromString でインポートした後に、SOM指定した SetXFAFormFieldValue を呼び出して変動する請求書データを入力し、ファイルを保存します。XFAはPDFが保持できる2つのフォームモデルの片方に過ぎないため、従来の AcroForm 側にも独自の自動化機能が用意されています。これについては、インタラクティブフォームアクションとJavaScriptに関する記事で説明されています。
ここで紹介した XFA のフィールド値、列挙、およびパケットに関するAPIは、Delphi、C#、およびVB.NET向けのlosLab PDF Libraryに含まれています。製品ページには完全なフォーム処理に関するリファレンスが掲載されています。