技術記事

Delphi で読み込んだ PDF に AcroForm フィールドを追加する

第三者が用意した請求書テンプレートや、何年も前に誰ももう見つけられないソフトで生成されたアーカイブ済み契約書があり、それを対話型にする必要があるとします。コーナーに署名欄を置き、いくつかのテキストフィールドを追加し、平坦なチェックリストを本物のチェックボックスに変えるのです。問題は、この PDF をゼロから作成しているわけではないことです。すでに存在し、ページもコンテンツストリームも、こちらが制御できないフォントも入っています。そのオブジェクトグラフを作り直さずに AcroForm ウィジェットを後付けする必要があります。これは新規ドキュメントにフォームを作るのとは別の問題で、うまくいかない理由は、ビューアーで結果を開いてみるまで見えません。書いたフィールドがページ上のどこにも現れないのです

HotPDF は Delphi と C++Builder 向けのネイティブ VCL PDF コンポーネントで、v2.247.0 ではまさにこの用途に向けた専用メソッド群を備えています。つまり、LoadFromFile で読み込んだ文書上に、6 種類すべての標準フィールドを直接作成できます。この記事では、それらのメソッドの働き、生成される ISO 32000-1 の辞書、そして 1 つでも欠けると処理全体が静かに空のようなファイルを出力してしまうフラグについて説明します

読み込んだ文書へのフィールド作成が独自の経路になる理由

PDF をゼロから作る場合、HotPDF がオブジェクトモデル全体を管理します。各ページは書き込み可能な THPDFPage ラッパーであり、AddTextField を通じてテキストフィールドを追加すると、新しいウィジェットがページの注釈オブジェクト、ページオブジェクト、そしてフォームのフィールドコレクションに結び付けられ、その後、文書のフォントリソースから外観ストリームが生成されます。外観ストリームはウィジェットの見える面であり、枠や境界線、既定テキストなどが、ビューアーがそのまま描画する PDF の描画オペレーターとして描かれます

読み込んだ文書では、そうした土台は何も得られません。ページは生の辞書として取り込まれているため、ウィジェットをぶら下げるための書き込み可能な THPDFPage ラッパーはなく、さらに重要なのは、外観ストリームを描画するためのフォントリソースのパイプラインが待機していないことです。そのため読み込み済みパスは別の経路を取ります。フィールド辞書を解析済みのオブジェクトグラフへそのまま書き込み、ページはページオブジェクトではなく 0 ベースのインデックスで指定します。フィールド種別とフラグビットはゼロから作る経路とまったく同じなので、Text field はどちらでも Text field です。違うのは、その下の配線と、決定的に重要なウィジェット表面の描画方法です

/NeedAppearances フラグはここでは省略できない

これが、作業結果が表示されるかどうかを決める唯一の要素です。読み込み済みパスは外観ストリームを生成しないため、新しく追加されたウィジェットにはビューアー側で /AP エントリがないまま到着します。つまり、表面の説明がないフィールドです。外観も作成指示もないウィジェットの描画を求められたビューアーの多くは、何も描きません。フィールドはファイル内にあり、構造的には正しく、フォーム入力ツールからは参照できますが、人間には完全に見えません

回避策は ISO 32000-1 §12.7.3 で定義されています。AcroForm 辞書は /NeedAppearances ブール値を持ち、それが true のとき、準拠リーダーは各フィールドの /DA(既定外観)文字列と値から、不足している外観ストリームを自分で構築しなければなりません。HotPDF はこれを自動で設定します。読み込み済み文書に最初のフィールドを追加すると、EnsureLoadedAcroForm が実行されます。catalog に /AcroForm がなければ作成し、/Fields 配列がなければそれも作成し、/NeedAppearances true を強制的に true にします。これを直接呼ぶ必要はありませんが、存在を知っておくと挙動の説明がつきます。運用上の注意点もはっきり述べておく価値があります。ごく一部の最小限または非準拠のビューアーは /NeedAppearances を無視し、それでも何も描画しません。主要な閲覧環境ではこのフラグが機能しますが、対象が珍しい組み込みレンダラーなら、保証する前にそこで必ずテストしてください

6 種類のフィールドを追加する

すべてのメソッドは同じ形です。0 ベースのページインデックス、PDF ユーザー空間座標でのウィジェット矩形の 4 隅、フィールド名、そして型ごとに必要な追加引数を渡します。矩形は X1, Y1, X2, Y2 で、PDF の原点はページ左下にあるため、Y 値が大きいほど上に配置されます。これはファイル形式側の座標規則であって、画面の左上原点規則ではありません。これを逆に扱うのは、フラグを忘れる次に多いミスです。各呼び出しは新しいフィールドの 0 ベースのインデックスを返し、ページインデックスが範囲外か、ページオブジェクトを解決できない場合は -1 を返します

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

テキストフィールドの 3 番目と 4 番目の文字列引数は、フィールド名と初期 /V 値です。整数は /MaxLen で、0 より大きいときだけ書き込まれます。HotPDF は編集可能な各フィールドに、既定の外観文字列として /Helv 12 Tf 0 0 0 rg を与えます。これは /NeedAppearances 対応ビューアーが、その値を描画する際のフォントと色を決めるために読むものです。チェックボックスには、チェック時にフォームが送信する文字列である export 値と、初期状態を示すブール値を渡します。内部では対応する /V/AS/DV の各名前エントリを書き込み、ファイルを開いた瞬間に on/off 状態が一致するようにします。export 値を空にすると既定で Yes になり、これはチェックボックスの慣用的な “on” 名です

選択フィールドと /Ff ビットフラグ

ComboBox と ListBox はどちらも選択フィールドで、ISO 32000-1 §12.7.4 ではフィールド型 /Ch です。ドロップダウンとスクロール可能な一覧の違いは、フィールドフラグ整数 /Ff の 1 ビット、つまり Combo フラグのビット 18、値 $40000 にあります。HotPDF は AddLoadedComboBox にはこのビットを立て、AddLoadedListBox には立てません。それ以外は両者は同一で、どちらも候補を /Opt エントリに書き込んだ文字列の可変配列として受け取ります

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

候補リストについて 2 点あります。HotPDF は各 /Opt エントリを単純な文字列として書き込み、export 値と表示ラベルは同じテキストになります。ISO 32000-1 §12.7.4.4 では、送信値を表示内容と分けたい場合に 2 要素の [export display] 形式も認められていますが、読み込み済みの作成メソッドはより簡単な単一文字列形式を使うため、export 値と表示値を分けたいなら、生成後の辞書に自分で設定する必要があります。また、フィールドの現在の選択値として渡す値は、提示した候補のいずれかであるべきです。ビューアーはそれを一覧と照合するからです

Push button はもう 1 つのフラグ駆動のケースです。フィールド型 /Btn に bit 17、つまり PushButton フラグ、値 $10000 を持たせます。このビットが、クリック可能なボタンとチェックボックスを分けています。チェックボックスも /Btn フィールドですが、このビットは持ちません。渡したキャプションは外観特性辞書 /MK に、通常キャプション /CA として書き込まれます。ここではスコープを正直に言っておくべきです。ボタンはラベルと矩形を持って作成されますが、読み込み済み作成メソッドはアクションを付与しないため、単体では見た目だけ正しく、クリックしても何も起きません。送信、リセット、JavaScript アクションの配線は別の話です。ゼロからの作成側では、フィールドとアクションを組み合わせる手順は Delphi で AcroForm フィールドとアクションを作成する で扱っています。こちらの方が、読み込み済みパスがあえて含めていないものを比較するのに適しています

すべてのフィールドが共有する辞書

6 つのメソッドの下には、ウィジェット注釈を構築して 2 か所に登録する共通ビルダーがあります。ここで /Type /Annot/Subtype /Widget/Rect 配列を 4 つの座標から書き込み、注釈フラグ /F 4 を設定してフィールドが画面だけでなく紙にも出るようにし、フィールド名 /T、フィールド型 /FT、フラグ /Ff、そしてページオブジェクトへの /P 逆参照を書き込みます。その後、新しいフィールドを AcroForm の /Fields 配列と、そのページの /Annots 配列に追加し、途中で間接参照を解決して、ウィジェットを孤立させずに実際の配列を拡張します

この二重登録が重要なのは、2 つの一覧のうち片方にしか存在しないウィジェットは、微妙な形で壊れているからです。/Fields にあるがページの /Annots にないフィールドはフォームには認識されるのに描画されず、逆の場合は描画されるのにフォームロジックには未知です。HotPDF は追加のたびに両方を同期させます。これは、規格に対して手作業で完全に正しくやらなければならなくなる種類の台帳管理です

いくつかの率直な制約

これを使ったワークフローを組む前に、期待値をはっきりさせてください。flatten-and-regenerate の挙動は、ビューアーが /NeedAppearances を尊重するかどうかに依存します。これは Acrobat、現代のブラウザ PDF エンジン、一般的なデスクトップリーダーをカバーしますが、世の中のあらゆるレンダラーで絶対に保証されるわけではありません。フラグを無視するビューアーも含め、どこでも同じようにフィールドを描画するファイルが必要なら、外観ストリームの領域に入ります。その場合は、/AP を描画してくれるゼロからの作成パスの方が適しています。署名フィールドも同様で、これは署名可能な空の署名ウィジェットとして作成されるだけです。フィールドを置くことと、暗号署名を適用することは同じではありません

すでにあるものを追加ではなく変更したい場合に対応する操作はフォームのフラット化です。対話型フィールドを静的なページ内容へ焼き込み、値を永続的で編集不能にします。XFA を含むフォームの扱いを含め、この往復処理については flattening XFA and AcroForm fields in Delphi。フィールドを追加することとフィールドをフラット化することは、同じライフサイクルの両端です。この記事は、元々インタラクティブでなかった文書に対話性を与える方法であり、フラット化はフォームの役目が終わったあとにそれを取り除く方法です

ここで示す読み込み済み文書フォーム API は、標準の HotPDF Component の一部として、Delphi と C++Builder 向けに提供されており、フィールドフラグ、外観処理、そして AcroForm モデルの残りすべてに関する完全なリファレンスも含まれています