技術記事

HotPDFを使用したDelphiでのPDFフォームの自動計算

明細項目が自動的に合算され、日付が自動的にフォーマットされるような請求書や経費精算書のPDFを実現するには、AcroFormに3つの要素を組み込む必要があります。DelphiのHotPDFは、ロードされたファイルに対してこれら3つすべてを追加できます。AFSimple_Calculateによる合計値計算のためのApplyLoadedFieldCalculation、AcrobatのAFフォーマットおよび検証ヘルパー用のApplyLoadedFieldValidation、およびビューアーが実際に描画する値を確定させるためのEnsureLoadedFieldAppearanceStreamです

HotPDFはDelphiおよびC++Builder用のネイティブVCL PDFコンポーネントであり、これら3つのメソッドはゼロから作成するドキュメントではなく、LoadFromFileで開いたドキュメントに対して動作します。各メソッドは、AcroFormの/Fields配列における0ベースのフィールドインデックス(GetLoadedFormFieldNamesが返す順序と同じ)を受け取り、フィールドディクショナリに直接書き込みを行います。そのため、全体の作業手順は、ロード、インデックス指定、アタッチ、SaveLoadedDocumentのようになります。フィールドセット自体の構築は別のタスクであり、DelphiでロードされたPDFにAcroFormフィールドを追加するで解説しています。このページでは、既存のフィールドセットに計算ロジックとフォーマットを適用する方法について説明します

PDFフォームが独自のフィールドを合算するためにスクリプトが必要なのはなぜですか?

PDFにはスプレッドシートのようなレイヤが存在しないためです。テキストフィールドが保持するのは文字列であり、数式ではありません。スクリプトが添付されていない限り、明細項目が変更されても合計値が自動的に再計算されることはありません。この仕組みを実現するのが、トリガーイベントをアクションにマッピングするフィールドの追加アクションディクショナリである/AA(ISO 32000-1 §12.7.5)です。これには、文字が入力されたときに発生するキーストロークトリガー(K)、表示用のフォーマットトリガー、コミット時の検証トリガー(V)、およびフォーム上のいずれかのフィールドが変更されたときに常に実行される計算トリガー(C)があります。各アクションはJavaScriptアクション/S /JavaScript(ISO 32000-1 §12.6.4.14)であり、実行されるスクリプトが実際の合計や再フォーマットを処理します。Acrobatには、AFSimple_CalculateAFNumber_FormatAFDate_FormatExなどの構築済みのヘルパーライブラリが付属しているため、添付するスクリプトは独自のコードではなく、通常はそのライブラリを呼び出す1行のコードになります

複数段階のフォームが正しい計算結果を出力するかどうかは、計算順序という1つの詳細によって決まります。Acrobatは、計算アクションを持つすべてのフィールドを実行しますが、それらをページ順ではなくAcroFormの/CO配列にリストされている順序で実行します。ApplyLoadedFieldCalculationは指定されたフィールドに計算アクションをアタッチします。ある計算フィールドが別のフィールドの入力値となる場合(たとえば、税額行が参照する小計があり、さらにその両方を参照する総合計がある場合)、/COの順序において各入力フィールドがそれを消費するフィールドよりも前に配置されていることを確認する必要があります。そうしないと、総合計が古い小計を参照してしまい、更新が1ステップ遅れてしまいます

PDFフォームで合計を自動的に計算するにはどうすればよいですか?

ApplyLoadedFieldCalculationは、ターゲットフィールドのインデックス、計算操作、および集計対象となるソースフィールドの名前を受け取ります。HotPDFは、ターゲットフィールドの計算アクションにAFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...])を書き込みます。THPDFFieldCalcOp列挙型によって計算関数(AcrobatのSUM、AVG、PRD、MIN、MAXにマッピングされるfccSumfccAveragefccProductfccMinimumfccMaximum)を選択します。ソース名はAcroFormのフィールド名であり、これはGetLoadedFormFieldNamesが返す文字列と同じであるため、3つの明细項目と送料の合計を求める処理を1回の呼び出しで記述できます

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

AFヘルパースイートを使用したフィールドのフォーマットと検証

ApplyLoadedFieldValidationは、フォーマット、キーストローク、および検証スクリプトを1回の呼び出しでアタッチし、指定されたTHPDFFieldValidationKindによって関連付けられるAcrobatヘルパーが決定されます。fvkCurrencyを渡すと、HotPDFはフォーマットスクリプトとしてAFNumber_Format(2,0,0,0,"$ ",true)と対応するAFNumber_Keystrokeを書き込みます。fvkNumberはセパレータ引数に"."を使用し、fvkPercentageAFPercent_Formatを出力し、fvkDateDateFormat引数にカスタムマスクを渡さない限りAFDate_FormatEx("dd/mm/yyyy")を出力します。特殊なフォーマット(fvkZipCodefvkSSNfvkPhone)は、Acrobatがそれぞれ予約しているコードを使用してAFSpecial_Formatにマッピングされ、fvkArbitraryMaskはこれらのプリセットでカバーされないフォーマット用にMask引数を介してマスク文字列を受け取ります。数値型の場合、HotPDFはAFRange_Validateスクリプトもアタッチするため、コミット時に範囲外の入力が拒否されます

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

AFヘルパーは一般的なパターンをカバーしているため、個別のJavaScriptを手動で作成する必要はありません。フィールドにプリセットで表現できないロジック(チェックサム、フィールド間ルール、カスタム正規表現など)が必要な場合は、AcroFormのフィールドおよびアクションの構築メソッドを使用し、JavaScript文字列を直接アタッチしてください。どちらの方法も同じ/AAディクショナリに書き込まれるため、あるフィールドでのプリセット通貨フォーマットと別のフィールドでの手動検証コードが同じドキュメント内で共存できます

アピアランスストリームを再生成しなければならないのはなぜですか?

PDFにおいては、値を示す文字列とビューアーが描画するピクセルデータが全く異なるものであり、一方を変更しても他方は自動的に変更されないためです。フィールドはその値を/Vに保存しますが、リーダーがレンダリングするのはフィールドのアピアランスストリームである/AP /Nから取得したデータです。オブジェクトグラフを介して/Vを編集しても、厳密なビューアーでは再生成が行われるまで古いアピアランスが表示されたままになります。フォームディクショナリは、開いたときにすべてのアピアランスを再構築するようビューアーに要求する/NeedAppearancesフラグを保持しており、Acrobatはこのフラグを尊重します。しかし、ブラウザ埋め込み型のレンダラー、モバイルリーダー、プリントサーバー、およびサムネイルジェネレーターは、このフラグを無視してすでに存在するストリームを描画するか、あるいは何も描画しないことがよくあります。EnsureLoadedFieldAppearanceStreamは、フィールドの/DA/V、および/Rectから自身で/AP /NフォームXObjectを構築することでこの問題を解決し、ビューアーの再描画処理に依存することなくレンダリングされた値をファイル内に保持させます

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

AFスクリプトが動作しなくなるケース

すべてのAF*スクリプトはビューアーにJavaScriptエンジンが搭載されていることに依存していますが、ほとんどのビューアーには搭載されていません。Acrobatや一部のデスクトップ製品はこれらのヘルパーを実行できますが、Chrome、Edge、Firefoxに組み込まれているPDFビューアーや主要なモバイルプラットフォーム、サーバー側のラスタライザー、印刷パイプラインなどは、フォームのJavaScriptを一切実行しません。それらのリーダーでは計算が実行されず、フォーマットも適用されません。フィールドは単に最後に保存されたアピアランスストリームを表示するだけです。これが、/AP /Nをベイクしておくことが極めて重要である理由です。AF*レイヤーは、スクリプト実行に対応したリーダーを使用している一部のユーザー向けの利便性向上策として捉え、決して動作を保証するものとして依存しないでください。サーバー側で正確である必要がある合計値(請求額や税額など)は、フォームが返ってきた後に自社システム側で再計算する必要があります。フォームに入力したリーダーがスクリプトを1行でも実行したとは仮定できないからです

もう1つ言及すべきケースは、XFAとして提供されたフォームです。動的なXFAフォームは、AF*ヘルパーが関与できないXMLペイロード内に独自の計算エンジンを保持しているため、XFAフィールドにAFSimple_Calculateをアタッチしても意味がありません。まず、DelphiでのXFAからAcroFormへのフラット化で説明されている方法に従ってXFAフォームを通常のAcroFormにフラット化してください。その後、ここで紹介した3つのメソッドを、他のロードされたフォームと同様にAcroFormフィールドに適用できるようになります

自動計算および自動フォーマットを行うAcroFormの実装は、ロードされたドキュメントに対して3つの呼び出し(計算のアタッチ、フォーマットと検証のアタッチ、アピアランスのベイク)を行い、その後にSaveLoadedDocumentを呼び出すだけで完了します。ここで紹介したロード済みフォームAPIは、DelphiおよびC++Builder用のHotPDF Componentの一部であり、そこにはAcroFormの他の機能とともにTHPDFFieldValidationKindおよびTHPDFFieldCalcOp列挙型の詳細な仕様がドキュメント化されています