PDFium Component は、FPdfDocumentXfaFlatten ユニットの FlattenPdfXfaToAcroForm を通じて、Delphi から静的 XFA フォームを普通の AcroForm フィールドへ変換します。この関数は XFA の template と datasets パケットを読み、絶対位置指定されたすべてのフィールドを、独自のアピアランスストリームを持つ AcroForm ターミナルフィールドへ写像し、結果をインクリメンタルリビジョンとして追記します。そのため、変換された PDF は、XFA を一度もサポートしなかったビューアで普通のフォームとして開きます
動機はたいていロードマップ項目ではなくサポートチケットです。政府や保険のフォームが XFA として保存するデザイナーで作られ、ある 1 つのデスクトップリーダーでは動くのに、他のどこでも「この文書にはより新しいビューアが必要です」と言う白紙ページとして開きます。XFA は PDF 2.0(ISO 32000-2)で非推奨になり、ブラウザとモバイルビューアは実装したことがなく、PDFium 自体も V8 有効ビルドでのみ XFA を描画します。レイアウトが設計時に固定されている静的 XFA フォームは、テンプレートに AcroForm フィールドとして再構築するのに足るジオメトリを運んでおり、このコンバータが扱うのはまさにそのスライスです
FlattenPdfXfaToAcroForm は実際に何を作るのか
FlattenPdfXfaToAcroForm が作るのは、変更されていないソースのコピーに追記されるバイトレベルの AcroForm リビジョンであって、元ファイルの書き直しでは決してありません。PDFium はフォームフィールドを作る公開 API を持たないため、コンポーネントはこの仕事を PDFium を通じてまったく行いません。アクティブリビジョンを自前のオブジェクトリーダーで読み、フィールド辞書、ウィジェット注釈、アピアランス XObject、/Helv フォントリソースを生の PDF オブジェクトとして組み立て、ISO 32000-1 §7.5.6 の意味でのスパースなインクリメンタル更新を通じてシリアライズします
新しい /AcroForm 辞書は /Fields、/DA、/DR を運び、/XFA エントリはありません。ソースが AcroForm を間接オブジェクトとして保持している場合、そのオブジェクトはその場で書き直されます。インライン辞書はカタログ内で置き換えられます。どちらのやり方でも、アクティブリビジョンはもう XFA パケットを指さないため、両方のモデルを理解するビューアは、XFA フォームのレイアウトを試す代わりに AcroForm フィールドを開きます。元の XFA バイトはまだ以前のリビジョンの中にいます。それがインクリメンタル更新というものですが、現行リビジョンの何もそれらを参照しません
uses
System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;
procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
Source: TFileStream;
Options: TPdfXfaFlattenOptions;
OutputPdf: TBytes;
Report: TPdfXfaFlattenReport;
Fields: TPdfXfaFlattenFields;
begin
Options := TPdfXfaFlattenOptions.Default;
Options.MaxFields := 1024; // 既定は 512、許容範囲は 1..4096
Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
try
if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
finally
Source.Free;
end;
TFile.WriteAllBytes(TargetFile, OutputPdf);
Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
[Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;
この関数は、拒否されたか不正な入力に対して False を返し Report.ErrorText を埋め、出るときにソースストリームの位置を復元します。伝播する唯一の例外経路はキャンセルです。Options.CancellationToken が発火すると、レポートが埋められた後で EPdfOperationCancelled が再送出されます。TPdfXfaFlattenReport はさらに SourceSha256 と OutputSha256 を運び、バッチジョブがどの入力がどの変換ファイルを作ったかを証明しなければならないときに便利です。TPdfXfaFlattenOptions.Default は 8 MiB のパケット予算、512 フィールド、512 MiB の出力上限を設定します。ソースは暗号化されていてはなりません
template と datasets パケットを読む
PDFium Component は、ISO 32000-1 §12.7.8 が /XFA エントリに許す両方のレイアウトを受け付けます。パケット名とストリーム参照を交互に並べる配列形式と、1 つのストリームが <xdp:xdp> 文書全体を保持する単一ストリームの XDP 形式です。パケットストリームは非圧縮でも予測子なしの /FlateDecode でもよく、展開は MaxPacketBytes で制限されます
単一ストリームのケースこそ、素朴な XML 処理が壊れる場所です。デザイナーは、template が接頭辞なしで座っているのに、<xfa:datasets> を名前空間接頭辞付きで書くことがよくあります。リテラルのタグ名で切り出すと、1 つのパケットは見つかりもう 1 つは見逃します。コンバータは、開きタグと閉じタグが完全な接頭辞付き名前でペアになることを別途確認した上で、要素名と属性名をローカル名で照合します。変換ではなくパケットを眺めるだけなら、PDFium VCL で XFA フォームを検出しパケットを抽出するガイドがその読み取り専用経路を扱います
フィールド値は 2 つの場所から来ます。テンプレートの <value> 要素が既定値を供給し、datasets パケットが記入済みデータをフィールド名で束ねて供給します。実在のフォームではフィールド名は繰り返されるため、バインダーは名前付き <subform> 先祖の連鎖をデータの先祖パスと比較し、最も長い非競合の一致を取ります。異なるサブフォームの下で 2 つのリーフフィールドが名前を共有するとき、AcroForm の /T は SubformPath.Name になり、2 つのフィールドは変換後も区別されたままです
choiceList に明示的な Combo フラグが要る理由
XFA と PDF は選択フィールドの既定が正反対なので、フラグを何も写さないコンバータはすべてのドロップダウンをリストボックスに変えます。既定の open="userControl" か open="onEntry" の XFA <choiceList> はドロップダウンです。フラグのない PDF の /FT /Ch フィールドはスクロールするリストボックスで、コンボボックスにするのは Combo フラグ(ISO 32000-1 Table 230 のビット位置 18、値 131072、PDFium が FPDF_FORMFLAG_CHOICE_COMBO と名付けるのと同じビット)だけです。したがってコンバータは、ドロップダウンのケースで Combo フラグを立て、テンプレートが open="always" か open="multiSelect"、つまりリストを永久に開いたままにする場合だけフラグを空のままにします。3.181.1 より前のビルドは Combo と Pushbutton の値を入れ替え、非標準の open="on" を探していたため、それらでフラット化されたフォームは再実行する価値があります
残りの UI 写像は ISO 32000-1 §12.7.4 のフィールドフラグテーブルに従います
textEdit、numericEdit、dateTimeEditは/FT /Txになります。複数行テキストフィールドには Multiline フラグ(/Ff 4096、Table 228)と行分割のアピアランスストリームが付きますpasswordEditは Password フラグ(/Ff 8192)付きの/FT /Txになり、アピアランスは UTF-16 コードユニットごとではなく Unicode スカラーごとに 1 つのアスタリスクを描きますcheckButtonは/FT /Btnのチェックボックスになり、束縛されたデータが1なら値は/Yes、それ以外は/Offです<exclGroup>は/V、/DV、/Opt、/Kidsを持つ 1 つの/FT /Btn /Ff 32768ラジオ親(Table 226)と、メンバーごとの、独自の/ASと 2 状態の/APを持つ子ウィジェットになりますimageEditは、アピアランスが画像を描くプッシュボタンになります。ベースライン JPEG は/DCTDecodeの下にそのまま埋め込まれ、PNG はデコードされて/FlateDecodeピクセルとして再エンコードされ、PNG がアルファかtRNS透明を運ぶときは追加の/SMaskが付きます
改行を含め、素の印字可能 ASCII でない値は <FEFF...> の UTF-16BE 文字列として書かれるため、日本語の名前も 2 行の住所も旅を生き延びます。Fields 出力は書かれたものを映すため、便利な監査証跡になります
procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
KindNames: array[TPdfXfaFlattenFieldKind] of string = (
'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
I: Integer;
F: TPdfXfaFlattenField;
begin
for I := 0 to High(Fields) do
begin
F := Fields[I];
Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
[F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
case F.Kind of
fxfkChoice:
Writeln(Format(' %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
fxfkRadio:
Writeln(Format(' %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
fxfkImage:
Writeln(Format(' %d image bytes', [Length(F.ImageData)]));
else
Writeln(' value "' + F.Value + '"');
end;
end;
end;
ジオメトリ、測定単位、ページの割り当て
XFA のジオメトリは、各フィールドを自分のページの MediaBox の高さに対して反転することで PDF ユーザー空間へ変換されます。XFA はページの上端から下向きに測り、PDF は下端から上向きに測るからです。ネストした <subform> 要素からのオフセットはテンプレートを下りながら累積され、すべてのフィールドは正の幅と高さで終わらなければならず、そうでなければ変換は拒否されます
測定は実在のデザイナーが出す単位を受け付けます。素の数値と pt に加え、in(72 ポイント)、mm(72 / 25.4)、cm(72 / 2.54)、pc(12)、px(0.75)、mp(0.001、ミリポイント)です。パースはロケール非依存のため、ドイツ語の Windows 環境が 8.5in をナンセンスに変えたりしません。未知の単位接尾辞は、ポイントとして読まれて静かにフィールドをずらす代わりに拒否されます
ページの割り当ては <pageSet> に従います。兄弟の <pageArea> 要素は宣言順に 1 から番号が振られ、その下にネストされたフィールドを請求し、<pageArea> 先祖のないフィールドは 1 ページ目に留まります。各ページは独自の /Annots 更新を受け取ります。PDF の最終ページを越えて割り当てられたフィールドは、1 ページ目に落とされたウィジェットではなくエラーです
コンバータはどんな XFA フォームを拒否するのか
コンバータは、XFA レイアウトエンジンを実行しなければ分からないレイアウトのものをすべて拒否します。つまり、<subform> や <exclGroup> 上の position 以外の layout 属性(フローする tb、lr-tb など、動的フォームが作られるもの)、すべての <script> 要素、上の 7 つ以外のすべての UI 種類です。実行時にページを増やす動的フォームは別のツールチェーンの領分で、PDFium Component の動的 XFA 再ページネーションとページ数の記事が、そのライブレイアウトに何が関わるかを示します
拒否は意図的です。フローするサブフォームには固定座標がなく、どんな「変換」も位置をでっち上げることになりますし、スクリプト付きフォームはたいてい、静的 AcroForm では再現できない合計の計算や可視性の切り替えをします。計算された合計を静かに失った、完成品に見える PDF は、Report.ErrorText の明確なエラーより悪いものです。XFA 3.3 仕様はこのサブセットよりはるかに多くを定義しており、コンバータはそうではないふりをしません
AcroForm リビジョンの書き込みと検証の罠
ライターを構築している間に 2 つの問題が現れ、どちらも手でインクリメンタル更新を作る人すべてに当てはまります。1 つ目は、カタログはバイトが変わらない場合でも、常に更新集合の一部でなければならないということです。AcroForm が別の場所で書き直される間接オブジェクトでもです。スパース更新ライターはトレーラーの /Root を、運ぶオブジェクトから導出するため、ルートオブジェクトなしの更新は、ビューアが錨を下ろせないリビジョンを作ります
2 つ目は検証に関わります。変換されたファイルを PDFium ラッパー経由でロードすると、文書が /AcroForm を持った瞬間にフォーム環境の初期化が始まり、テスト環境のホスト pdfium.dll には FORM_GetTextDirection エクスポートがなかったため、1 つのフィールドも検査できる前に初期化が失敗しました。そこで読み戻しチェックはコンポーネントのオブジェクトリーダーを使います。フォーム層に触れずにアクティブリビジョンをパースするものです
uses
System.SysUtils, System.Classes, FPdfCompress;
function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
Stream: TBytesStream;
Objects: TPdfActiveObjects;
Revision: TPdfSparseRevisionInfo;
ErrorText: string;
begin
Stream := TBytesStream.Create(Pdf);
try
if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
Objects, Revision, ErrorText) then
raise Exception.Create(ErrorText);
// アクティブなカタログは AcroForm を参照しなければならない。コンバータが
// 書いたインライン AcroForm は /XFA エントリを運ばない
Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
finally
Stream.Free;
end;
end;
アピアランスストリームは意図的に単純です。テキストと選択の値には 10 ポイントの Helvetica、チェックボックスには線を引いた正方形、選択されたラジオ状態には塗られた内側の正方形です。変換されたフォームはどこでも読める形になりますが、元の XFA 描画のピクセルコピーではないため、テンプレートで定義されたフォント、色、ボーダーは引き継がれません。フォームがデザイナー出力と同一でなければならないなら、本番パイプラインを切り替える前に、XFA ビルドで一度描画して比較してください
FAQ
FlattenPdfXfaToAcroForm には V8 か XFA ビルドの PDFium が要るか
いいえ。FlattenPdfXfaToAcroForm はパケットをパースし、PDFium Component コードでリビジョンを書くため、標準の pdfium.dll で機能し、XFA スクリプトを一切実行しません
コンバータは暗号化された XFA 文書を扱えるか
直接には扱えません。暗号化されたソースはエラーで拒否されるため、まず文書を復号し、復号されたコピーを変換してください
変換されたフィールドは今も編集可能か
はい。出力には、値とアピアランスストリームを持つ本物の AcroForm テキスト、チェックボックス、ラジオ、選択、プッシュボタンフィールドが含まれるため、ユーザーは AcroForm 対応のどんなビューアでもフォームへの記入を続けられます
静的 XFA 変換、XFA パケット抽出、V8 有効の XFA ランタイムはすべて、Delphi、C++Builder、Lazarus プロジェクト向けに PDFium を包む PDFium Component for Delphi and C++Builder の一部です