技術記事

DelphiでのPDFiumによるXFAフォームの検出と抽出

はい、PDFiumはXFAフォームをサポートしていますが、1つ重要な区分があります。PDFiumコンポーネントは、XFAフォームを検出し、そのtemplatedatasets、およびconfigパッケージを生のXMLとして、実行時依存関係なしで標準のDLLを使用して抽出できます。動的なXFAフォームを実行すること(XMLの記述をライブでインタラクティブなページに配置すること)は別の問題であり、これにはV8対応のビルドが追加で必要となります。コミュニティでは、PDFiumが「XFAをサポートしているか」という質問がまるで単一のイエスかノーかの機能であるかのように繰り返されていますが、実情を言えば、XFAデータの読み取りとXFAフォームのレンダリングは、依存関係のフットプリントがまったく異なる2つの全く別の機能です

この記事では、DelphiおよびC++Builder用のPDFiumネイティブVCLラッパーであるPDFiumコンポーネントを使用して、この両方の側面について解説します。まず、検出と静的抽出のパスであるFormTypeXFAプロパティ、およびGetXfaPacket*リーダーについて説明し、次にV8による制限を伴う動的ランタイムパス、最後に配置されたDLLがエンジンを実行できない場合に安全に処理をフォールバックさせるための機能検出について説明します

PDFiumはXFAフォームをサポートしているか

これは1つの機能ではなく、2つの機能として捉えてください。静的なXFA抽出は常に利用可能です。GetXfaPacketCountGetXfaPacketName、およびGetXfaPacketContentをサポートする低レベルのFPDF_GetXFAPacket*エクスポートは、XFA機能がオフにされているビルドであっても、比較的モダンなpdfium.dllにはすべて組み込まれています。そのため、フォームから生のtemplatedatasets、およびconfig XMLを取り出す処理には実行時依存関係が一切ありません。これは純粋な構造的読み取りであり、エンジンは関与しません。もう一方の機能が動的XFAです。実際にフォームを実行し、その計算を評価し、XFAレイアウトエンジンに改ページを処理させます。このパスは、V8 JavaScriptエンジンの上にXFAサポートを組み込んでコンパイルされたPDFiumビルドにのみ存在し、これこそが「PDFiumはXFAをサポートしているか」という議論の大部分が実際に指している部分です

規格はこの区分を明確に規定しています。XFAは、Adobe XFA 3.3(2012年)においてPDFのコア文法の外で定義されており、PDF 1.7 §8.6.7は、XFAフォームがPDF内でどのように保持されるかを説明しています。フォームパッケージはAcroForm辞書の/XFAエントリの下に存在し、意味を持つ前にXFAプロセッサによってレイアウトされる必要があるドキュメントは、カタログ内に/NeedsRendering trueを設定します。これら2つのマーカーは、まさにPDFiumコンポーネントが検索するものであり、これまで見たことのないファイルを処理する際に信頼できる指標となります

XFAはAcroFormとどのように違うのか

AcroFormは従来のPDFフォームです。テキストフィールド、チェックボックス、ラジオグループなどのウィジェットアノテーションが、通常のPDFページの固定座標に配置されます。画面に表示されるページがファイル内のページであり、フィールドはその上に配置されます。XFAはこれとは逆のアプローチを取ります。インタラクティブフォームは完全にXMLで記述され、完全な(動的)XFAドキュメントでは、PDFページは事実上のプレースホルダーになります。実際のコンテンツは、XMLテンプレートを読み取ってレイアウトするXFAエンジンによってオープン時に生成され、データに合わせてサイズが伸縮します。これが/NeedsRendering trueが示す内容です。XFAプロセッサがない場合、静的ページは空白であるか、「互換性のあるビューアで開いてください」という通知のみが表示される可能性があります

XFAペイロード自体は名前付きのパッケージのセットであり、そのうちの3つにほとんどの統合で必要となるすべての情報が含まれています。templateパッケージはフォームの定義(フィールド、レイアウト、計算、およびスクリプト)です。datasetsパッケージは実際のインスタンスデータ(入力されたフィールド値をXMLツリーにしたもの)を保持します。configパッケージはXFAエンジン向けの処理指示を保持します。他にもlocaleSet、connectionSet、xdpラッパーなどが存在しますが、フォームの監査やデータの移行においては、通常templateとdatasetsだけで用は足ります。このデータはストリーム内に存在する単なるXMLであるため、読み取り時にプログラムを実行する必要は一切なく、これが静的抽出が依存関係フリーである根本的な理由です

DelphiでのXFAフォームの検出

検出は、PDFiumコンポーネントの他のタスクと同様に始まります。FileNameを設定し、Active := Trueに切り替え、実際に開かれたことを確認します。Active := Trueは例外を発生させません。ファイルが見つからない、パスワードが違う、またはDLLがない場合は、单にActiveFalseのままになるため、このガード処理は必須です。ドキュメントが開かれると、FormTypeによってどのフォームモデルが使用されているかがわかります。TPdfFormTypeの値のうち2つがXFAです。ftXfaFullはレンダリングが必要な完全な動的XFAフォームであり、ftXfaForegroundはXFAF(通常のAcroFormページの上にXFAコンテンツを描画するフォアグラウンドサブセット)で、静的ページにも意味が残ります。XFAブーリアン是、「いずれかの形式のXFAドキュメントである」ことを示すショートカットです。XFAFまたはAcroFormドキュメントのウィジェットレベルのフィールドも操作する場合は、PDFiumフォームフィールドナビゲーションのガイドでその仕組みを説明しています

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // damaged, encrypted, or DLL missing
    case Pdf.FormType of
      ftXfaFull:
        Log('Full (dynamic) XFA form');
      ftXfaForeground:
        Log('XFAF: XFA layered over static AcroForm pages');
      ftAcroForm:
        Log('Classic AcroForm');
    else
      Log('No interactive form');
    end;
    if Pdf.XFA then
      Log('Document carries an XFA packet payload');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

XFAパッケージを生のXMLとして抽出する

抽出は、価値が高く依存関係のない半分側の機能です。ロードされたDLLでパッケージリーダーのエクスポートが解決されていることを確認するXfaStaticReadableでガードし、インデックスでパッケージを巡回します。GetXfaPacketCountはフォームに含まれるパッケージの数を返し、GetXfaPacketNameは各パッケージの名前を返し、GetXfaPacketContentは生のバイトデータを返します。よく知られたパッケージの場合、これはUTF-8 XMLです。必要なパッケージがわかっている場合は、名前付きヘルパーであるGetXfaTemplateGetXfaDatasets、およびGetXfaConfigが名前を解決し、TBytesを直接返します。データ移行作業で重要なのはGetXfaDatasetsです。これは、入力されたフィールド値をXMLドキュメントとして返すため、解析して独自のスキーマにマッピングできます。これは、PDFからのテキスト抽出の後に行うような下流作業と同様です

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // packet reader exports not present
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // raw UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Or jump straight to the filled-in field values by name:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

なぜ動的なXFAレンダリングにはV8エンジンが必要なのか

結論から言うと、動的なXFAフォームは静的な描画ではなく、1つの小さなプログラムであるためです。テンプレートパッケージには、FormCalcやJavaScriptの計算、検証、およびイベントスクリプトが含まれており、XFAレイアウトエンジンはそれらを実行してページがどのように見えるかを決定する必要があります。PDFiumは、GoogleのJavaScriptエンジンであるV8の上にXFAサポートを組み込んでコンパイルされたビルドでのみこのエンジンを実装しています。これが、V8対応DLL(pdfium32v8.dllまたはpdfium64v8.dll)が標準ビルドよりも著しく大きい理由です。V8がなければスクリプトを実行する存在がないため、レンダリングされたページを表示することはできず、静的に読み取ることができる生のXMLのみが存在することになります

このビルドの選択には、回避できない厳密な制限があります。それは、プロセスごとに1回限りの決定であるということです。単一のプロセスで標準のpdfium.dllとV8対応ビルドの両方をロードすることはできません。これらは同じシンボルをエクスポートし、V8はグローバルなアイソレート状態を維持するためです。そのため、PDFiumコンポーネントは、ライブラリが最初にロードされる前にV8ビルドを選択する必要があります。これを自動化するため、LoadDocumentはファイルを事前にスキャンして/XFAおよび/NeedsRendering trueマーカー(スタンドアロンのPdfFileLooksXfa関数として公開されているものと同じチェック)を検索し、それらが見つかった場合はロード前にEnableV8Engine := Trueを設定します。通常のpdfium.dllがすでにロードされている場合、この切り替えは機能せず、その状態はランタイム欠落パスによって報告されます

uses
  PDFium, FPdfAsync;

begin
  // Peek for XFA before PDFium is loaded for the first time, so the
  // V8-enabled build can still be selected. PdfFileLooksXfa scans for
  // /XFA and /NeedsRendering true without parsing the whole file.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // engine is live: dynamic layout
      else
        ExtractXfaData(Pdf, 'C:\out'); // fall back to static packets
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

XFAランタイム欠落の安全な処理

3つの検出機能によって、どの程度のXFA機能が利用可能かを正確に把握できます。信頼できるコードは想定に頼らずこれらをチェックします。XfaFeaturesAvailableは、ロードされたDLLにXFA/V8ヘルパーのエクスポートが存在するかどうかのグローバルチェックです。XfaStaticReadableは、ドキュメントごとにパッケージリーダーが解決されたかを確認する抽出用のガードです。最も強力なのがXfaRuntimeAvailableです。核心となるこのプロパティは、この特定のドキュメントが実際にXFAエンジンをアクティブ化した場合(つまり、ドキュメントがXFAであり、V8ビルドがロードされ、FPDF_LoadXFAが成功した場合)にのみtrueになります。配置されたDLLにXFAがない場合や、標準ビルドがすでにロードされている場合、ドキュメントがXFA = Trueを報告してもXfaRuntimeAvailable = Falseになることがあります

XFAドキュメントが開いたもののエンジンが実行できない場合、PDFiumコンポーネントは無理な処理を行いません。対応できない動的ランタイムを要求するのではなく、フォームを安全な静的モードに固定し、ホストが対応できるようにOnXfaRuntimeMissingを一度発生させます。通常は、その間に静的に読み取り可能なデータを提供しつつ、V8対応ビルドで再起動するようユーザーに促します。このハンドラーを接続しておくことが、クリーンなフォールバックと、何も表示されない状態の境界線になります

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires once when an XFA document opens but the engine cannot run:
  // a plain pdfium.dll was already loaded, or the build lacks XFA/V8.
  ShowMessage('This is a dynamic XFA form. Restart with the ' +
    'V8-enabled build to render it; its packet data is still readable.');
end;

自社製品においても、この境界線を明確にしてください。V8対応のDLLを出荷しない場合、ユーザー環境で動的なXFAフォームがレンダリングされることはありません。しかし、フォームを検出し、すべてのパッケージを抽出し、入力されたデータを取り出すことは可能です。これは、フォームをインタラクティブに提示することよりも、レガシーな行政機関や金融機関のフォームからデータを取り出すことが目的である多くのタスクにとって十分な機能です。実行可能なフォームが本当に必要なケースにのみ重いV8ビルドを予約し、機能検出を利用して各ドキュメントを実際に処理可能なパスにルーティングします。抽出パイプラインが埋め込みファイルも処理する場合は、同じ読み取り専用の考え方がDelphiでのPDF添付ファイルにも適用されます

ここで紹介したFormTypeXFAGetXfaPacketCountGetXfaPacketNameGetXfaPacketContentGetXfaTemplateGetXfaDatasetsGetXfaConfig、および機能検出APIは、DelphiおよびC++Builder用のPDFiumコンポーネントの一部です