アンケートフォームがAcrobatで2つのドロップダウンリスト、group.aとgroup.bを見せているとします。自分の/FTを運ぶノードをどれも完成したフィールドとして扱うコードで/AcroForm /Fieldsを歩くと、カウントは1で返ってきます。2つのウィジェットを持つgroupという単一のフィールドです。group.bを名前で要求すると、ルックアップは失敗します。子の辞書を開くと、/FTはおろか/Ffもオプションリストもありません。何も壊れてはいません。型、フラグ、オプションは1階層上の親に座っており、PDF仕様はそれらがそこに住むことを明示的に許しています
このレイアウトは合法かつ経済的です。オーサリングツールは/FT /Ch、/Ff 131078、/Opt配列を持つグループノードを1度書き、その下に2つの名前付きの子をぶら下げます。各子は/T、/Parent、/Rectと自分の/Vだけを保持する、フィールドとウィジェットのマージ辞書です。そのようなフォームを正しく読むには3つのことが要ります。継承可能なキーごとに/Parentをたどること、フィールドツリーが本当にどこで終わるかを判定すること、そしてリセットが正しいPDF型の値を書くことを確実にすることです。この記事は、Delphiで読み込み済みPDFのフォームフィールドに値を設定するが止まったところから続きます
フィールドは親からどのAcroFormエントリーを継承できるか
ISO 32000-1 §12.7.3.1(Table 220)は/FT、/Ff、/V、/DVを継承可能と印付け、§12.7.4.3のTable 229はテキストフィールドの/MaxLenについて同じ扱いをします。オプション配列はここでは同じ扱いを受けます。choiceリストを子の間で共有する生成者は、/Optをグループに保存するためです。ローカル辞書だけを検査するリーダーは、完全に正当な子に対して、間違った型、0のフラグ、空の値を報告することになります
HotPDF Delphi Componentは、それらの読み取りのどれにも同じ方法で答えます。キーをフィールド自身の辞書で探し、そこにあれば間接参照を解決して止まります。なければ/Parentをたどって再試行します。最大128階層までです。この上限は飾りではありません。破損したファイルの/Parentポインタは、/Kidsとは何の関係もないループを形作ることがあり、それらを無条件に信頼するリーダーは、最初に遭遇したそのようなファイルで固まります
このウォークの1つの規則は、逆に覚えやすいものです。キーを含む最初の辞書が勝ちます。そこに保存された値が空文字列であってもです。group.bのローカルな/V ()は、作者が「この子は空欄だ」と言っているのであり、ウォークはそこで止まり、親が保持しているかもしれない値へは登りません。空を「欠落」として扱うと、クリアされた子が親の選択を表示することになり、作者が書いたことの逆になります
公開getterはすべてそのルックアップに座っています。GetFormFieldType、GetFormFieldValue、GetLoadedFormFieldFlags、IsFormFieldRequired、IsFormFieldNoExport、GetLoadedFormFieldMaxLength、GetLoadedFormFieldDefaultValue、そしてオプションヘルパーのGetLoadedFormFieldOptionCountとGetLoadedFormFieldOptionsです。だからアンケートグループの子を読むのに特別なコードは要りません
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;
GetFormFieldは呼び出しごとに新しいTHPDFLoadedFormFieldラッパーを返し、名前が不明なときは例外を出します。呼び出しはラッパーのtryブロックの外に置き、ラッパーは自分で解放してください
フィールドツリーの終わりを見つける
ローカルな/FTは末端フィールドの検査としては間違っています。親が型を供給しながら、名前付きの子フィールドもまだ所有できるからです。魅力的な走査は、ノードが自分の/FTを持つ、あるいは/Kidsを持たないときはいつでも降りるのを止めます。アンケートフォームでは、groupは/FT /Chと/Kidsの両方を持つため、その走査はこれを2つのウィジェット注釈を持つ1つのフィールドとして登録します。名前group.aとgroup.bは消え、フィールド数は1です。子の名前によるルックアップは失敗し、値の書き込みは共有の親にしか着地できません
HotPDFは代わりにkidsを見ます。kidは、自分の/Tを持つ、自分の/Kidsを持つ、あるいは/Subtype /Widget辞書でない場合に、子フィールドと数えます。どのkidも該当しないときにだけノードは末端であり、そのkidsはウィジェットとして扱われます。その規則の背後にある2つのエッジケースはどちらもマージ辞書から来ています。§12.7.3.1は、フィールドがちょうど1つのウィジェットを持つときにこれを許します
- 名前付きのマージ辞書は
/Subtype /Widgetを運びながら、それでも子フィールドです。subtypeだけではそれを親の匿名ウィジェットリストへ押し込めません。決めるのは/Tです - 生成者の中には、親の
/FTをすべての匿名ウィジェットに繰り返すものがあります。だからウィジェット上の/FTも「ここから新しいフィールドが始まる」とは読めません
同じ分類が、フィールドインデックス、FormFieldExists、RemoveFormFieldを駆動します。それらのウォークはそれぞれ、すでに訪れた辞書を記録し、128階層を過ぎると諦めます。グループがオブジェクト5で、自分自身を自分のkidsの中に2回挙げているとしましょう。/Kids [5 0 R 5 0 R 6 0 R 7 0 R]です。それでも報告されるのは、子の6と7という、ちょうど2つのフィールドです。無限再帰もなく、自己参照から来る幻の3つ目のフィールドもありません
RemoveFormFieldはどうやって兄弟フィールドの削除を避けるのか
RemoveFormFieldが名指した子だけを削除するのは、発見と削除が末端フィールドの1つの定義を共有しているからです。名前指定のオーバーロードはフィールドリストからフィールドインデックスを解決し、それから/AcroForm /Fieldsを2度目に歩いて、末端フィールドを数えながら、削除すべき辞書を見つけます。2度目のウォークが魅力的な/FT規則を使い、1度目がkidベースの規則を使っていたら、インデックスは食い違います。削除のウォークはgroupをフィールド0と見て、group.aの削除が親も兄弟も両ウィジェットも道連れにします
だから削除のウォークは、発見と同じ子フィールドのテストと同じvisited集合を適用します。削除される子に属するウィジェット注釈を集め、各ページの/Annotsからそれらだけを剥がし、親はその/Kids配列が空になったときにだけ取り除きます。group.bがまだ挙がっている間は親は残り、group.bが継承する型、フラグ、オプションも共に残ります
// 名前付き子を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');
自分のフォームでの削除を確認するときは、3つの場所を見てください。それぞれが別の間違いを捕まえます。親の/Kids、ページの/Annots、そして生き残った兄弟の値と外観です。SaveLoadedDocumentの後に1回、SaveIncrementalUpdateの後に1回実施します。増分保存は変更されたオブジェクトだけを追記し、親の/Kidsはその中に含まれていなければならないためです
ResetLoadedFormFieldは継承された/DVから何をコピーするのか
ResetLoadedFormFieldは、継承された/DVの新鮮なコピー、つまり同じPDFオブジェクト型を持つローカルな/Vを書き込み、フィールドに触れる前にデフォルト全体を検証します。チェックボックスのデフォルトは/Yesのような名前です。マルチセレクトリストボックスのデフォルトは文字列の配列です。テキストのデフォルトは<FEFF...>のような16進UTF-16文字列かもしれません。それらをGetLoadedFormFieldDefaultValueとSetFormFieldValueで往復させると、名前があるべき場所に文字列、配列があった場所に空文字列、あるいは生の16進数字が得られます
それゆえリセットは、継承された/DVが実際に何であるかで分岐します
- 文字列デフォルトを持つテキストフィールドとchoiceフィールドは、元の16進フラグを保つ新しい文字列オブジェクトを受け取ります
- 配列デフォルトを持つchoiceフィールドは、新しい文字列の新しい配列を受け取ります。文字列以外を含む配列は例外を出します
- チェックボックスとラジオボタンは新しい名前オブジェクトを受け取ります
- その他の型のデフォルトは例外を出し、
/Vと/Iは元のまま保たれます
参照ではなくコピーなのは意図的で、それが唯一の安全な選択です。親の/DV配列を指す子の/Vは、「現在値」と「デフォルト値」の間で1つのオブジェクトを共有することになります。その子への次のSetFormFieldValueが、すべての兄弟のデフォルトを静かに書き換えてしまうのです。リセットフィールドごとの数バイトの余分は、2つを別々に保つための安い値段です。値を持たないプッシュボタン(Table 226、ビット17)と署名フィールドは、自分の/DVの文字列コピーを書くプレーンな経路に留まります
チェーンのどこにも/DVが存在しないとき、メソッドはローカルな空文字列を書きます。チェックボックスやラジオフィールドでは/Offです。子の/Vを削除する方が整って見え、間違った答えを与えます。親が現在値を保持している場合、子のオーバーライドを取り除くとその値がすぐに戻り、「リセットされた」フィールドが、ユーザーがそこに入力したことのないものを見せ始めます
var
Field: THPDFLoadedFormField;
begin
// 親の'palette'はMultiSelectリストボックスで、
// /Opt [(b) (g) (r)] と /DV [(b) (r)] を持つ。子は/Tと/Vだけ
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// 子は今や自分の /V [(b) (r)] と /I [0 2] を持ち、
// 親の/DV配列は手つかずのまま
except
on E: Exception do
// 不正なデフォルト(型の間違い、文字列でない配列項目):
// /Vと/Iはそのまま残された
Writeln('Reset skipped: ', E.Message);
end;
// スカラーgetterは配列のデフォルトを表現できない
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // empty
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
単一フィールドのリセットは、§12.7.5.3のResetFormアクションとも別物です。こちらはビューアがユーザーのボタンクリックで一連のフィールドに走らせるものです。そのアクションはHotPDFでAcroFormフィールドとアクションを作るで扱っています。ResetLoadedFormFieldはロード済みの1つのフィールドを編集し、デフォルトなしの場合には自分の規則に従い、増分再計算が拾えるようフィールドを変更済みとして印付けます
/V、/I、/ASを一致させ続ける
リセットが正しいのは、選択インデックスと外観状態が値に従うときだけです。ResetLoadedFormFieldはSetFormFieldValueと同じ2つの後処理で締めくくります。choiceフィールドでは、子のローカルな/Iを取り除き、各値をすべての/Optエントリのエクスポート半分と照合し、ソート済みの新しい/Iを書きます。オプションb、g、rに対して[(b) (r)]へリセットすると、/I [0 2]になります。親から継承された/Iがあれば、それは手つかずのままです
ボタンは継承された型から働きます。/FT /Btnが親に住む子のチェックボックスも、自分の/ASを更新されます。書き込み側では、SetFormFieldValueとSetLoadedFormFieldDefaultValueは、継承された非プッシュボタンのボタンに対して、子が型をコピーすべきローカルエントリを持たなくても、名前オブジェクトを保存します。そしてEnsureLoadedFieldAppearanceStreamがボタンの外観を再構築するとき、値がon状態と一致しない限り/AS /Offを書き、各状態ストリームに/Type /XObject、/Subtype /Form、/BBoxを与えます。その/Offの検査がなければ、リセット後の外観の再生成は、ファイルが保存される前に箱を再びチェック済みで見せることになります
スカラーgetterと、計画に入れておくべきその他の端
文字列getterはスカラーのままです。出力を比較するコードは、それらが何を平坦化するかを知っておくべきです。GetFormFieldValueとGetLoadedFormFieldDefaultValueは、配列値には空文字列を返し、数値と真偽値は42やtrueへ変え、16進エンコードされた文字列は16進の綴りで報告します。リストボックスのデフォルトが2つの選択を保持するのか何も保持しないのかを知る必要があるなら、デフォルトgetterには答えられません。どちらも空文字列で返ってくるからです
/Parentのループは例外を出さずにルックアップを終えます。型をループの中で失ったフィールドはlfftUnknownと0のフラグを報告します。名前付きフィールドのlfftUnknownは、ログに値する破損ファイルの兆候として扱ってください。SetFormFieldValueとResetLoadedFormFieldは、常にアドレスした子に書き込み、値を共有の親へ昇格させることは決してありません。2つのアンケートドロップダウンのような独立した子にはそれが正しい動作です。ラジオグループは別です。選択はグループを所有するフィールドに属するため、kidの1つではなくそのフィールドをアドレスしてください
各呼び出しは1つのフィールドだけを自分でコミットします。20のフィールドにわたるリセットのループは20の独立した編集であり、5番目が不正なデフォルトで例外を出したら、最初の4つはすでに適用済みです。バッチ作業では、まず全ターゲットの型とデフォルトを読み、どのフィールドに触れるかを決めてから、書き始めてください
継承属性のルックアップ、kidベースのフィールドツリー分類、型付きリセットは、すべてHotPDF Delphi Componentのロード済みフォームAPIの一部です。既存のドキュメントへ新しいフィールドを足す必要のあるDelphiとC++Builderのプロジェクトもカバーしており、Delphiで読み込み済みPDFにAcroFormフィールドを追加するにその様子があります