PDFiumコンポーネントのTPdf.SetFocusedFormFieldTextは、現在フォーカスされているフォームフィールドのライブな編集バッファに書き込むが、XFAフォームの場合、そのバッファはディスクにシリアライズされるdatasetsパケットには決して到達しない——そのため、ユーザーが入力し、あなたのコードが受理されたことを確認した値は、次にファイルが開かれたときに静かに消えている。AcroFormのフィールドにはこの問題はない:同じ呼び出しは、フォーカスが離れた瞬間にフィールドの/Vエントリにコミットされる。XFAの受付フォームに入力し、保存し、再度開いて金額フィールドが再び空白であることに気づいたユーザーは、レンダリングの不具合にぶつかっているのではない——彼らはPDFiumエンジン自身がフォームデータの書き込みのために公開しているものの限界にぶつかっているのだ
これは、そもそもXFAフォームを検出することや、そのJavaScriptを実行させることよりも狭い問題である:「PDFiumはXFAをサポートしているか」でも「AcroFormのスクリプトをどう実行するか」でもなく、具体的にはSetFocusedFormFieldTextが成功を報告した後、値に何が起こるかである。手短に言えば、AcroFormとXFAは、PDFiumの書き込み経路に関する限り、同じフォームモデルの2つの方言ではない——それらは、ユーザーが入力したものと保存が実際に捕捉するものの間に全く異なる2つの関係を持つ2つのフォームモデルであり、この2つを混同することが、1行のAPI呼び出しを、顧客のパイロット導入が本番稼働してから3週間後のサポートチケットに変えてしまう。AcroForm JavaScriptの記事はその1行の呼び出しを示し、コードコメントの中でAcroForm対XFAの結果を述べている;本稿は同じAPIに留まり、内部の書き込み経路、XFAの書き込みが決して着地しないことを示すdatasetsパケットの証拠、そのギャップがなぜDelphiバインディングではなくPDFium自体にあるのか、そして編集が保存を生き延びる必要がある文書のための自前でXMLをパッチする回避策を歩く
SetFocusedFormFieldTextはどうやってフィールド値を書き込むのか
TPdf.SetFocusedFormFieldTextは、値を文書モデルに直接突っ込むのではなく、キーストロークレベルの編集をシミュレートすることで機能する。内部では、フォーカスされたフィールドの現在の内容を選択するためにFORM_SelectAllTextを呼び、次に選択範囲を新しい文字列で上書きするためにFORM_ReplaceSelectionを呼ぶ——これはキーボード駆動の「すべて選択して入力」がトリガーするのと同じ2つの操作である。この書き込みはそれを迂回するのではなくPDFiumの対話的テキスト編集経路を通るため、そのフィールドに結びついたどんなキーストローク・フォーマット・計算スクリプトも、人間が入力したときと全く同じように発火する。これが、JavaScriptを生かし続けるビューアにおけるプログラム的なフォーム入力にとってこのAPIを有用にしているものである。読み取り側の対応物はFocusedFormFieldTextであり、これはFORM_GetFocusedTextに裏付けられ、SetFocusedFormFieldTextがちょうど書いたのと同じライブなバッファを反映する
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
なぜAcroFormは値を保持しXFAはそれを失うのか
AcroFormのテキストとコンボのフィールドが永続化するのは、PDFium自身のフォーム入力環境があなたの代わりに編集バッファをコミットするからだ:フィールドがフォーカスを失った瞬間、そのバッファはフィールドの/Vエントリに書き込まれる。これは、あらゆる準拠したPDFリーダーが、あるフィールドの保存された値を知るために見る同じキーである。内部でFORM_ForceToKillFocusを呼ぶTPdf.ClearFormFieldFocusは、そのコミットを要求に応じて強制する。そのため値をプログラム的に設定するコードは、UIの他の場所での本物のマウスクリックを待つ必要がない。その直後に保存すると、新しいテキストはTPdf.SaveAsが実行される前にすでに文書オブジェクトグラフの一部になっている。なぜなら/Vは本物のフィールド辞書内の本物のエントリであり、事後に取り付けられた何かではないからだ
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
XFAフィールドの編集は実際にどこに存在するのか
XFAフィールドにはそのような配線が存在しない。ユーザーが入力したテキストは、PDFiumのXFAレンダリング・対話層に属するCPWL_Editバッファに着地し、その層には、そのバッファをPDFに保存されているdatasetsパケットにコピーし戻すコードパスが一切ない。TPdf.GetXfaDatasetsはこのギャップを可視化する:XFAフィールドの編集の前後でそれを呼んでみると、それが返すバイトは同一である。なぜならこのメソッドは、あなたがたった今編集したウィジェットのライブな状態ではなく、文書が開かれたときの元のパケットを読むからだ。それはキャッシュのバグでもリフレッシュのタイミングの問題でもない——ディスク上のdatasetsパケットとメモリ上の編集バッファは、単にPDFiumの公開APIが決して接続しない2つの異なる状態にすぎない
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
これはPDFiumコンポーネントのバグなのかPDFiumの制限なのか
欠けているピースは、その上のDelphiバインディングではなくPDFium自体にある。PDFiumの公開APIには、更新されたパケットを注入するFPDF_SetXFAPacketも、保存前にXFAエンジンに現在のDOMをdatasets XMLへシリアライズし直すよう求めるFPDF_SaveAsXFAも存在しない。TPdf.SaveAsを裏付けるエクスポートであるFPDF_SaveAsCopyは、PDFiumがすでに持っている文書オブジェクトグラフを書き出す;それには、XFAエンジンにまず自身のライブな状態をフラッシュするよう求めるフックがない、なぜならそのフックは上流に存在しないからだ。PDFiumコンポーネントは、PDFium自体が一度も実装していない調整を追加できず、PDFiumの内部XFA状態を推測する自家製のDOM-to-XMLシリアライザを出荷することは、正直なギャップよりも悪いことになる:それは次のPDFiumバージョンがプロジェクトの外の誰にも見えない何かを変更するまで動くように見えるだけだからだ
この境界は、そもそもSetFocusedFormFieldTextを構築したのと同じv2.13.2の監査の間に表面化した。FORM_ReplaceSelectionは、Pascalコードから一度も呼ばれることなく数バージョンにわたってDLLインポートテーブルにバインドされていた。そして最終的にそれを使う書き込み経路を追加したことが、この永続化のギャップを理論上のものではなく、文書化するに値するほど具体的なものにした。同じ監査のラウンドは、精神的には関連するが無関係のギャップも見つけた:AcroFormのJavaScriptは、v2.13.0以降静かに無効化されていた。なぜならJSプラットフォームはXFA初期化の分岐の内部にしか配線されておらず、app.alertや計算フィールドを持つ通常のAcroForm文書は、そもそもスクリプトエンジンを全く得られなかったからだ。そちらは修正可能だった——JSプラットフォームをXFAの有無にかかわらずすべての文書に拡張すること——そしてそれは同じバージョンで出荷された;ここで扱った永続化のギャップは、上記の理由により修正可能ではなかった。JavaScriptの修正とその周りのホスト拒否権イベントは、PDFiumコンポーネントでAcroForm JavaScriptを実行するで扱われている
Delphiでこれについて何をすべきか
AcroForm文書については、その修正は良い習慣以外の何物でもない:値がプログラム的に設定されたときはいつでも、後のUI操作があなたの代わりにコミットを引き起こしてくれると仮定するのではなく、SaveAsの前にClearFormFieldFocusを呼ぶ(あるいは他の方法でフォーカスを移す)こと。AcroFormかもしれずXFAかもしれない文書について——これは汎用ビューアではよくあるケースだ——呼び出し元に保存が定着すると約束する前にFormTypeかXFAブール値をチェックすること。そして、XFAコンテンツが/Vを尊重する他は普通のAcroFormウィジェットの上に重ねられているXFAFのケースを含む一連のプローブについては、XFAフォームの検出とXFAパケットの抽出を読んでほしい
編集された値が保存を生き延びる必要がある本物の動的XFAフォームについては、対話的な編集バッファは全く正しいツールではない。持続的な経路は、GetXfaDatasetsをあなたの結果ではなくベースラインとして扱うことである:文書が開いたときに一度それを読み、ユーザーがフィールドごとに何を変更したかの自分自身の記録を保持する——これはまさにあなたのUIがすでに持っている値である、なぜならPDFiumは事後にそれをあなたに返してくれないからだ——それらを自分でそのベースラインXMLにパッチし、自分自身の出力を駆動する。あなた自身のコードが制御するXMLを通る書き込みは、CPWL_Editバッファが決してできなかった保存を生き延びる
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
顧客より先にこのギャップを捕まえる
TPdf.SaveAsは、XFAフィールドの値が生き延びたかどうかにかかわらずTrueを返す。なぜならPDFiumの視点からは、その保存は本当に成功したからだ——それは求められたすべてのバイトを書いた。これがまさに、このバグをスモークテストをすり抜けさせて顧客に届かせる類の欠陥にしている:何も例外を投げず、何もログに残らず、ファイルは問題なく開き、特定の値だけが間違っている。実際に保存されたファイルを再度開き、フィールドの値を比較するか、あるいは先の例の通り前後でGetXfaDatasetsを比較する往復テストは、ユーザーがXFAコンテンツを編集できるあらゆるビューアの回帰テストスイートに属するべきである。既定で動作するAcroFormの経路だけではない
これはPDFiumコンポーネントに対して報告すべき欠陥というよりは、その周りを設計すべき境界である:SetFocusedFormFieldTextは両方のフォームモデルに対してその名前が言う通りのことを正確に行い、結果の違いは、AcroFormとXFAがそれぞれPDFium側でそのバッファを何に結びつけているかにきれいに遡る。ここで参照したAPI、フォーカスと保存のプリミティブ、そしてパケットリーダーは、DelphiおよびC++Builder向けPDFiumコンポーネントの一部である