技術記事

DelphiでのPDFフォーム結合:重複フィールドの規則

PDF Library for Delphiは、名前を共有するフィールドに対する明示的なポリシーを指定して、2つのAcroFormドキュメントを結合します。MergeDocumentExは、ソースドキュメントの識別子と、3つの戦略のうちの1つを受け取ります。dfsRejectは結合を拒否し、dfsMergeは共有名をそのまま維持して値を同期させ、dfsAutoNumberは取り込まれるフィールドを決定論的にリネームします。この名前のスキャンは、オブジェクト番号がずれる前に行われるため、拒否された結合は両方のドキュメントを完全に使用可能な状態のまま残します

PDFの申込書一式を組み立てたことがある人なら、この問題に必ず遭遇しています。それぞれにSignatureDateTotalという名前のフィールドを持つ3つのフォームが、1つのファイルに結合されます。AcroFormでは、完全修飾フィールド名がフィールドの同一性そのものであるため、同じ名前を持つ2つのフィールドは、まったく2つのフィールドではありません。片方に入力すればもう片方にも入力され、片方に適用された署名は、誰も意図していなかった範囲を覆ってしまいます

なぜ名前の衝突は結合の前に判定されるのか?

従来のMergeDocumentは、2つのAcroFormのルートフィールド配列を単純に連結するだけで、選択の余地を与えません。さらに悪いことに、結果が使い物にならない場合、それが判明するのはオブジェクト番号がすでに振り直され、ページツリーがすでに縫い合わされた後です。呼び出し側の手元には、どちらの元のドキュメントとも異なる状態のドキュメントが残ってしまいます

MergeDocumentExは、この順序を逆にします。両方のドキュメントからトップレベルのフィールド名を収集し、それらを比較し、何かを動かす前に戦略を適用します。そのため、拒否は完全に副作用のない操作です。対象ドキュメントは無傷のまま、ソースドキュメントも無傷のまま、両方とも開いたまま使用可能な状態を保ちます。これは、拒否された結合の後にソースからフィールド値を読み戻すことで検証されています

この比較には、順序付けられた大文字小文字を区別する名前の集合を使用するため、コストは2つのフィールド数の積ではなく、両者を合わせたフィールド数に対数係数を掛けたものに比例します。ここで大文字小文字を区別するのが正しい選択なのは、PDFのフィールド名が大文字小文字を区別するものだからです。それらを畳み込んでしまうと、仕様上は別物として扱われるフィールドが結合されてしまいます

3つの戦略、それぞれが適切な場面

dfsRejectは、曖昧なドキュメントを生成してはならない自動化パイプライン向けの戦略です。結合はゼロを返し、LastErrorCodeは705を報告します。これは専用のコードであり、重複した名前を他のあらゆる結合失敗と区別し、特定の対処——たいていは上流でのフィールドのリネーム——へ振り分けられるようにします

dfsMergeは、共有された名前を意図的にそのまま維持し、ターゲット側の値とデフォルト値をソースのフィールドへ同期させます。そのため、準拠したビューアは、複数のウィジェットを1つの論理的な名前を持つフィールドとして扱います。これは、複数のウィジェットアノテーションを持つフィールドに対する標準的なAcroFormの挙動です。この戦略が行わないのは、異なるフィールド辞書を1つのオブジェクトに畳み込むことです。各フィールドは、自身のページとの関連付け、外観、アクションを保持したままです。それらを統合してしまうと、取り込まれるドキュメントに属する書式や挙動が黙って失われてしまうからです

dfsAutoNumberは、取り込まれる重複フィールドに、_2から始まる数値の接尾辞を付けてリネームし、最初に空いている番号を使用します。この結果は再現可能です。存在する名前だけに依存し、フィールドのオブジェクト番号には決して依存しないため、同じ組み合わせのドキュメントを2回結合しても、両方とも同じ名前が得られます。この性質は、下流のコード、FDFインポート、データベースマッピングがフィールドを名前で参照する場合に重要になります

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    TargetDoc := Lib.SelectedDocument;
    Lib.LoadFromFile('application-part1.pdf', '');

    SourceDoc := Lib.NewDocument;
    Lib.LoadFromFile('application-part2.pdf', '');

    Lib.SelectDocument(TargetDoc);
    if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
    begin
      if Lib.LastErrorCode = 705 then
      begin
        // どちらのドキュメントも無傷のまま——ポリシーを変えて再試行する
        Log('duplicate field names; retrying with auto-numbering');
        Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
      end;
    end;

    Lib.SaveToFile('application-complete.pdf');
  finally
    Lib.Free;
  end;
end;

このコード内の2段階のパターンに注目してください。これが可能なのは、拒否が非破壊的だからです。まず厳格なポリシーを試し、エラーを調べ、それから判断します。途中で失敗する結合であれば、フォールバックのために両方のファイルを再読み込みして最初からやり直さなければならなかったはずです

結合後のフォームはどうなるか

dfsMergeの下では、「Target value」という値を持つSharedという名前のターゲット側フィールドと、同じ名前を持つソース側フィールドから、両方ともSharedという名前を持ち、両方ともターゲット側の値を報告する2つのフィールドが生まれます。ターゲットの値とデフォルト値が、取り込まれるフィールドへ同期されるからです。これが、共有名に対して意図された意味論です。1つの論理フィールド、複数のウィジェット、1つの値です

dfsAutoNumberの下では、同じ入力からSharedShared_2という、独立した値を持つ別々のフィールドが生まれます。どちらを選ぶかは、たった1つの問いで決まります。「片方のコントロールに入力したら、もう片方にも入力されるべきか?」パック内のすべてのパーツに繰り返し現れる署名者名であれば、答えはイエスであり、dfsMergeが正解です。フォームごとに意味の異なる合計金額であれば、答えはノーであり、自動採番が正解です

// 結合後、実際に何が得られたかを列挙する
for I := 1 to Lib.FormFieldCount do
  Log(Format('%d: %s = %s',
    [I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));

フォーム一式を組み立てる際の実務上の注意点

結合に成功すると、ソースドキュメントは消費されます。ライブラリのドキュメントリストから取り除かれるため、DocumentCountは2から1に減ります。その後、ソースの識別子を使い続けないでください。ドキュメントのバージョンは両者のうち高い方に引き上げられるため、PDF 2.0のフォームを1.7のドキュメントに結合すると、2.0のファイルが生まれます

名前に関しては、結合する順序が重要です。AをBに結合する場合と、BをAに結合する場合とでは、自動採番の結果が異なります。結合を行う側のドキュメントは自身の名前を変更しないままだからです。一式の中に基準となる主フォームがある場合は、それをターゲットにしてください

署名フィールドには、それ自体の考慮が必要です。結合前に適用された署名は、それが署名した版だけをカバーするため、結合を行うと、署名以降にファイルが変化したという実務上の意味で、その署名は無効になります。署名済みの部分同士を結合するのではなく、まず組み立てを行い、組み立て済みのドキュメントに署名してください。結合がフォームではなくページ内容に関するものである場合は、バイト参照シフトによる高速PDF結合で説明したより高速な経路の方が適したツールです

最後に、一式のデータ側の計画も、結合と合わせて立てておいてください。フィールドの値が外部システムから届く場合は、そのシステムがフィールドを名前でアドレスするかどうかを、自動採番を選ぶ前に確認してください。Shared_2は、Sharedを期待するマッピングとは一致しません。インポート・エクスポート形式についてはFDF、XFDF、XFAによるフォームデータ交換で説明しており、リネームの影響を受け得るフィールドレベルのスクリプト挙動についてはインタラクティブフォームアクションとJavaScriptで説明しています

フォームの結合、データ交換、署名は、いずれもDelphi、C++Builder、Free Pascal向けの同じライブラリの中で動作します。完全な機能一覧はPDF Library for Delphiページに掲載されています