HotPDF Delphi Componentは、読み込み済みAcroFormフィールドの/FT、/Ff、/V、/DVを継承可能な属性として扱い、/Parentチェーンをたどって解決します。v2.754.3とv2.754.4以降、型を親から受け取る名前付き子フィールドは個別にアドレス可能なまま保たれ、RemoveFormFieldは兄弟フィールドに触れず、ResetLoadedFormFieldは継承されたデフォルトを元のPDFオブジェクト型のままコピーします。それ以前は、驚くほど多くの普通のフォームが誤読されていました
このすべてを暴き出すフォームは、変わったものではありません。オーサリングツールは、/FT /Chとフィールドフラグとオプションリストを1回だけ運ぶグループノードgroupを作り、その下に名前付きの子aとbをぶら下げます。各子は/T、/Parent、/Rectと自分の/Vしか持たない、フィールド+ウィジェットのマージ辞書です。これは属性を共有する完全に合法なやり方であり、Delphiで読み込み済みPDFのフォームフィールドに値を設定するのLimitsセクションが未処理として挙げた、まさにそのケースです。ボタンの整合はローカルの/FTしか見ていませんでした。本記事はその記事が止まったところから先を扱います。フィールドツリーがどう分類されるか、継承された値がどう読まれるか、そして単一フィールドのリセットは何を書いてよいか
フィールドは親からどのAcroFormエントリーを継承できるか
ISO 32000-1 §12.7.3.1のTable 220は/FT、/Ff、/V、/DVを継承可能とし、§12.7.4.3のTable 229はテキストフィールドの/MaxLenについて同じことを述べています。ローカルの辞書しか見ないリーダーは、完全に有効な子フィールドに対して、間違った型と間違ったフラグと空の値を報告します。HotPDFはこれらの読み取りをすべて、1つの内部リゾルバーHPDFLoadedInheritedFieldObjectに集めます。辞書にキーがあるか確認し、間接参照があれば解決し、なければ/Parentを最大128階層までたどります。不正なファイルは/Kidsとは無関係の/Parentサイクルを作り得るからです。公開getterはその上に載っています。GetFormFieldType、GetFormFieldValue、GetLoadedFormFieldFlags、IsFormFieldRequired、IsFormFieldNoExport、GetLoadedFormFieldMaxLength、GetLoadedFormFieldDefaultValue、そしてオプションのヘルパーGetLoadedFormFieldOptionCountとGetLoadedFormFieldOptions。後ろの2つは、親に格納された/Opt配列も拾います。リゾルバーの規則の1つは間違いやすいものです。たどりは、値が空文字列であっても、キーを含む最初の辞書で止まります。ローカルの/V ()は親を覆い隠す意図的なオーバーライドであって、さらに上から埋めるべき空白ではありません
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group'は/FT /Chと/Ff 131078と/Optを運ぶ。子の
// 'group.b'は/Tと/Parentと/Rectと自分の/Vだけを運ぶ
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo(ビット18)+ NoExport(ビット3)+ Required(ビット2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // ローカルの/V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
末端フィールドの判定にローカルの/FTは誤りなのはなぜか
親が型を供給しながら、名前付きの子フィールドも所有し得るからです。つまり/FTの存在は、フィールドツリーがどこで終わるかについて何も語りません。旧トラバーサルは、ノードが自分の/FTを持つか、/Kidsを持たないときはいつでも、末端と宣言していました。上のフォームでは、groupは/FT /Chと/Kidsの両方を持つので、2つのウィジェットを持つgroupという名前の1フィールドとして登録され、完全修飾名のgroup.aとgroup.bは単に消えました。GetFormFieldCountは1を返し、子の名前でのルックアップは失敗し、SetFormFieldValueは共有の親にしか書けませんでした。代わりのテストHPDFLoadedFieldHasChildFieldsは、親ではなくkidを見ます。kidは、自分の/Tを持つ、自分の/Kidsを持つ、あるいは/Subtype /Widget辞書ですらないとき、子フィールドです。どのkidも当てはまらないときだけノードは末端となり、kidは自分のウィジェット注釈として扱われます
その規則を形作った2つのエッジケースは、どちらもマージ辞書から来ています。§12.7.3.1は、フィールドが単一のウィジェットを持つときにこれを許します。名前付きのマージ辞書は/Subtype /Widgetを運びながら、それでも子フィールドです。だからsubtypeだけでは、それを親の匿名ウィジェットリストへ送り込むことはできません。/Tが勝つのです。逆も起こります。プロデューサーの中には、親の/FTを匿名ウィジェットごとに繰り返すものがあり、だから/FTは、ウィジェットが新しいフィールドを始める証拠にも使えません。この分類は、リレーションシップキャッシュとFormFieldExistsとRemoveFormFieldで共有され、それぞれのたどりはいまや、すでに訪れた辞書を記録し、128階層を超えると止まります。グループが自分自身を2回列挙するリグレッションファイル、/Kids [5 0 R 5 0 R 6 0 R 7 0 R]は、永遠に再帰したり同じノードを2回数えたりする代わりに、今もきちんと2フィールドを報告します
RemoveFormFieldが兄弟フィールドの削除を避ける仕組み
RemoveFormFieldはいまや、名指した子だけを削除します。発見と削除が、末端フィールドとは何かについてようやく一致したからです。この一致は見た目以上に重要です。名前指定のオーバーロードは、リレーションシップキャッシュを通してインデックスを解決し、それから/AcroForm /Fieldsの2回目のたどりで末端フィールドを数えます。キャッシュがgroup.aとgroup.bを見られるようになったあとも、削除側のたどりが未修正のままだと、groupは依然として単一の末端フィールド扱いになり、インデックス0は親を、すべての兄弟とそのウィジェットもろとも削除していたでしょう。削除のたどりはいまや、同じHPDFLoadedFieldHasChildFieldsテストと同じvisited集合を使い、削除される子のウィジェット注釈だけを収集し、各ページの/Annotsからそれらを剥がし、親の/Kids配列が空になったときだけ親を削除します。リグレッションは、間違いが現れる3つの場所をすべて確認します。親の/Kids、ページの/Annots、そして生き残った兄弟の値と外観。完全な書き直しの後でも、インクリメンタル更新の後でも
// 名前付き子を1つ削除する。兄弟と共有の親は生き残る
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// 型、フラグ、オプションは引き続き親経由で解決される
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
デフォルトが継承されているときResetLoadedFormFieldは何を書くか
ResetLoadedFormFieldは、継承された/DVの新鮮なコピーで、同じPDFオブジェクト型を持つローカルの/Vを書き込みます。そしてフィールドに触れる前に、デフォルト全体を検証します。オブジェクト型が重要なのは、スカラーgetterがすべてをテキストへ平坦化するからです。チェックボックスのデフォルトは/Yesのような名前オブジェクト、MultiSelectのリストボックスのデフォルトは文字列の配列、テキストのデフォルトは16進UTF-16文字列かもしれません。これらをGetLoadedFormFieldDefaultValue経由でコピーすると、名前は文字列になり、配列は空文字列になり、16進文字列はそのリテラルな数字になってしまいます。だからリセットは継承された型で分岐します。テキストとchoiceフィールドはIsHexadecimalフラグを保つ新しい文字列オブジェクトを受け取り、配列デフォルトを持つchoiceフィールドは新しい文字列からなる新しい配列を受け取り、プッシュボタン以外のボタンは新しい名前オブジェクトを受け取ります。親のオブジェクトを指すのではなくコピーするのは意図的です。親の/DV配列やそのオブジェクト番号を共有する/Vは、次に誰かが値を編集したときにデフォルトまで変えてしまいます。型の違うデフォルトや、文字列以外を含むchoice配列は、例外を上げて、/Vと/Iをあったがままに置きます。値を持たないプッシュボタン(Table 226、ビット17)と署名フィールドは、古い文字列限定の経路へフォールバックします
チェーンのどこにも/DVが存在しないとき、メソッドはクリアの契約を守るため、ローカルの空文字列を書きます。チェックボックスやラジオフィールドなら/Offです。ローカルの/Vを削除するほうがきれいに見えますが、それは間違いです。親は現在の値を保持しているかもしれず、子のオーバーライドを取り除くと、その値が静かに戻ってきます。単一フィールドのリセットが§12.7.5.3のResetFormアクションではないのは、そのためでもあります。ResetFormはユーザーがボタンをクリックしたとき、ビューアーがフィールドの集合に対して実行するもので、HotPDFでAcroFormフィールドとアクションを作るで説明しています。ResetLoadedFormFieldは1つの読み込み済みフィールドへの編集操作で、デフォルトなしのケースについて自分の規則を持ち、NoteLoadedFormFieldDirty経由でフィールドを記録するので、インクリメンタル再計算はこの変更を見ます
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// 親はMultiSelectリストボックスに/DV [(b) (r)]を持つ。group.aは
// 自分の/V [(b) (r)]と新しい/I [0 2]を得る。親は無傷
Pdf.ResetLoadedFormField(Field.Index);
// スカラーgetterは配列デフォルトを表現できない
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // 空
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
/Vと/Iと/ASを一致させ続ける
リセットが正しいのは、選択インデックスと外観状態が値に従うときだけです。だからResetLoadedFormFieldは、SetFormFieldValueと同じ2つのリコンサイラーで締めくくります。HPDFReconcileChoiceSelectionはいまや配列値を受け付けます。ローカルの/Iを中身に触れずに削除し、すべての値を各/Optエントリーのエクスポート側と照合し、ソート済みの新しい/Iを1つ書きます。だから[(b) (r)]へのリセットは、オプションb、g、rに対して/I [0 2]を生みます。ReconcileLoadedButtonAppearanceStatesはいまや継承された型を要求するので、/FT /Btnが親に住む子のチェックボックスは、ついに/ASを設定されます。書き込み側では、SetFormFieldValueとSetLoadedFormFieldDefaultValueは、継承されたプッシュボタン以外のボタンに対して、子に型をコピーするローカルのエントリーがなくても、名前オブジェクトを格納します。そしてEnsureLoadedFieldAppearanceStreamがボタンの外観を作り直すときは、値がオン状態と一致しない限り/AS /Offを書き、各状態ストリームに正しい/Type /XObject、/Subtype /Form、/BBoxを与えます。v2.754.4より前は、リセット後に外観を再生成すると、ファイルが保存される前にチェックが再び付いてしまえるのでした
これを土台にする前に知っておくべき限界
スカラーgetterはスカラーのままです。GetFormFieldValueとGetLoadedFormFieldDefaultValueは、配列値には空文字列を返し、数値と真偽値は42やtrueと文字列化し、16進エンコードされた文字列は16進の綴りのまま報告します。/Parentのサイクルは例外なしにたどりを終えるので、型がサイクルの中で失われたフィールドは、失敗する代わりにlfftUnknownとフラグ0を報告します。SetFormFieldValueとResetLoadedFormFieldは常に、アドレスした子を書き、値を共有の親へ昇格させることはありません。独立した子にはそれが正しいのですが、ラジオグループは選択を所有するフィールドを通して扱うべきだということでもあります。そして各呼び出しは1つのフィールドを単独でコミットします。ここには、リセットのバッチをトランザクションにするものは何もありません
ここで述べた継承属性の解決、統一されたフィールドツリーの分類、そして型付きリセットは、DelphiとC++Builder向けのHotPDF Delphi Componentの読み込み済みフォームAPIの一部です。Delphiで読み込み済みPDFにAcroFormフィールドを追加するで扱うフィールド作成と並んでいます