XML Forms ArchitectureであるXFAは非推奨となりました。ISO 32000-1は、PDF 2.0で削除されたという注記を伴ってこれを保持しており、現代のビューアはXFAエンジンを一つずつ廃止しています。しかし、アーカイブが空になったわけではありません。政府の申請書、保険の申請書、銀行の明細書などは2年近くにわたりXFAとして作成されており、これらのファイルは今日もインボックスやドキュメントパイプラインに届き続けています。これらをレンダリングしていたビューアがサポートを停止すると、フォームは「別のリーダーで開いてください」というプレースホルダーを伴う空白のページになります。永続的な解決策は、XFAを静的なPDFコンテンツにフラット化し、どのリーダーでも描画できるようにすることです。
そのフラット化における難関はフィールドではありません。テキストボックスやチェックボックスは、AcroFormウィジェットに比較的簡単にマッピングできます。難しいのは、XFAが描画要素(draw element)内の<exData contentType="text/html">ブロックに格納するリッチテキストです。このブロックは、インラインのスタイリングとアンカー(リンク)を含むHTMLのサブセットです。これをページ上に出力するということは、スタイル設定されたテキストとライブハイパーリンクの両方を再現することを意味し、ハイパーリンクこそが、ほとんどの実装が静かに諦めてしまう場所です。
XFAリッチテキストの実際の構成
exDataの本体は、XHTMLの小さな断片です。段落は<p>、スタイル設定された文字列のランは、太さ、傾き、色、サイズを指定する独自のインラインCSSを持つ<span>、そしてハイパーリンクは、表示テキストをラップする<a href="...">です。単一の行に、それぞれ異なるスタイリングを持つ複数のスパンが並ぶことがあり、そのうちの1つがアンカーになる場合があります。スタイリングは、削除しても構わない単なる装飾ではありません。法的な警告であるために太字の赤でレンダリングされた条項は、フラット化した後も太字で赤のままでなければならず、そうでなければフラット化されたドキュメントはオリジナルを誤って伝えてしまいます。
したがって、フラット化エンジンはブロックを1つの文字列として扱うことはできません。インライン構造を巡回し、スパンのインラインCSSを描画要素の基本フォントの上にレイヤー化することで、各ランの有効なスタイルを解決し、行に沿ってランを次々とレイアウトする必要があります。HotPDFは、これらのレイアウトされた各断片を内部のTXFARichRunレコードとしてモデル化します。このレコードは、ランのテキスト、解決されたスタイル、測定されたボックス、およびアンカーの場合はそれが指すHrefを保持します。
ランを左から右へレイアウトする
配置処理は、リッチテキストがパースの問題から組版(Typesetting)の問題へと移行する場所です。ランは行を共有するため、各ランは前のランが終了した場所から始まります。これらの位置を記録するマークアップは存在しないため、測定する必要があります。エンジンの内部ルーチンLayoutRichTextは、後にそれを描画するのと同じフォントメトリクスを使用してすべてのランを測定し、ランの水平オフセットをそれ以前のすべてのランの幅の累積値に設定します。ラン1は描画ボックスの原点から始まり、ラン2はラン1の幅の場所から始まり、ラン3は最初の2つの幅を組み合わせた場所から始まり、以下同様に行に沿って進みます。
これが、測定フォントの配置が非常に重要である理由です。レイアウトパスはアドバンス(文字幅)を測定し、別個の描画パスがグリフを描画します。もしこれら2つのパスでフォントが一致しない場合、レイアウトが計算したボックスはレンダラーが描画するグリフの下に配置されません。HotPDFは、各ランの解決されたスタイルを、レンダラーのデフォルトであるArialの10ポイントに一致するフォント仕様(内部ヘルパーRunStyleToFontSpecを介して)にマッピングすることで、これらを同期させます。測定された文字幅と描画されたテキストが一致し、ランの計算されたボックスが読者の見るキャラクターを正確にカバーするようになります。
// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
TRichRunInfo = record
Dx, Dy : Double; // top-left, relative to the draw-box origin
W, H : Double; // measured run box (width from the layout pass)
Text : AnsiString; // the run's visible characters
Href : AnsiString; // URI target for an <a> run, '' otherwise
end;
アンカーランからPDFリンク注釈へ
完成したPDFにおけるハイパーリンクは、ページコンテンツの一部ではありません。それは、ISO 32000-1 §12.5.6.5で説明されているように、独立したオブジェクトであるリンク注釈(Link annotation)です。この注釈は、ページ上のクリック可能な矩形を定義する/Rectと、矩形がクリックされたときに実行されるアクションを保持します。外部リンクの場合、アクションはURIアクションであり、ターゲットアドレスを/URI文字列として持つ/S /URIです。下に見えるテキストは通常のページコンテンツであり、注釈はその上に重ねられた見えないホットゾーン(反応領域)です。
フラット化パスはまさにこのモデルに従います。ランがHrefを保持している場合、HotPDFはまずスタイル設定されたテキストを描画し、次にランのボックスの上にリンク注釈を構築します。その注釈の公開エントリーポイントは、ページメソッドのAddURILinkです。これは/Type /Annot /Subtype /Linkオブジェクトを/URIアクションとともに作成し、注釈辞書を返します。その矩形はランの測定されたボックスであり、描画要素のローカル座標からページ座標へと変換されます。その結果、アンカーテキストのみに正確に配置されるリンクが生成されます。
// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
LinkRect: TRect;
Annot: THPDFDictionaryObject;
begin
LinkRect := Rect(72, 690, 268, 706); // page-space hit box for the run
Annot := Pdf.CurrentPage.AddURILink(LinkRect,
'https://www.example.gov/appeal', 'File an appeal online');
end;
ヒットボックスを測定幅から取得する必要がある理由
ページ上で表示されるテキストを検索し、見つかったものを囲むように矩形を描画することで、リンクの位置を特定できると考えるかもしれません。しかしそれは動作しません。その理由は、フラット化されたテキストがどのように保存されるかという根本的な仕組みにあります。スタイル設定されたランは、埋め込まれたサブセットフォントを使用して描画されます。サブセットフォントは保持するグリフの番号を振り直すため、ページコンテンツストリームは元の文字コードではなく、16進数のCIDコードを保持します。ページ上のバイトデータは人間が読む文字ではなく、テキストとして検索することはできません。アンカーのキャプションを検索しても何も見つかりません。そのキャプションは、ストリーム内のどこにもテキストリテラルとして存在しないためです。
矩形を特定する唯一の信頼できる情報は、行を流し込む(フロー)際にグリフ番号が振り直される前に計算された、レイアウトパスが生成したジオメトリ(位置と幅)です。したがって、HotPDFはテキスト検索からではなく、ランの配置ボックスからリンク矩形を直接取得します。測定には描画フォントが使用されたため、サブセット化に関係なくボックスは正確です。ジオメトリはエンコーディングを生き残りますが、テキストは生き残りません。これこそが、測定幅に基づく配置を行うべき理由であり、テキスト検索によってリンクを後からはめ込もうとするフラット化処理が、反応領域のズレや消失を引き起こす原因です。
コードからフラット化を実行する
XFAパケットを既に含んでいるPDFの場合、エントリーポイントはFlattenLoadedXFAです。ドキュメントを読み込み、このメソッドを呼び出し、結果を保存します。Editableパラメータはフォームフィールドの扱いを決定します。入力可能なAcroFormウィジェットとして残すにはTrueを渡し、すべてのウィジェットを読み取り専用としてマークして出力を固定レコードにするにはFalseを渡します。リッチテキスト描画ブロックは、スタイル設定されたランとリンク注釈を伴って、どちらのモードでも生成されます。関数は出力されたウィジェットの数を返します。
var
Pdf: THotPDF;
Emitted, i: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('xfa_appeal_form.pdf');
// True keeps fields fillable; False freezes them read-only.
Emitted := Pdf.FlattenLoadedXFA(True);
// Anything the engine could not map is reported, not raised.
for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);
Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
Writeln('Widgets emitted: ', Emitted);
finally
Pdf.Free;
end;
end;
呼び出しの後は必ずXFAFlattenWarningsを読み取ってください。このリストは各フラット化処理の開始時にクリアされ、エンジンがレンダリングを拒否した要素(サポートされていないフィールドタイプ、デコードできない描画画像、使用可能なスパンを持たないexDataブロックなど)ごとに警告行が累積されます。これらによって例外は発生しないため、警告リストが空であることは、すべてが正常にマッピングされた証拠であり、空でない場合はどのオリジナルファイルを検査すべきかを正確に示しています。生のXFAをPDFとしてではなくXDPバイトとして保持している場合は、シスターメソッドであるApplyXFAAsAcroFormがそれらのバイトを直接受け取り、同じコードパスと警告動作を共有します。補完的なAddXFAPacketメソッドは逆の動作を行い、作成中のドキュメントにXFAパケットを埋め込みます。
リーダーでの結果の確認
フラット化されたファイルをAcrobatなどの現在のビューアで開き、2つの点を確認します。第1に、リッチテキストがそのスタイル(太字のスパンは太字になり、色付きのスパンは色を保持し、スパンがボックス内で重なったりはみ出したりせずに行内に正しく配置されていること)を維持してレンダリングされていること。第2に、ハイパーリンクが機能していること。アンカーの上にカーソルを置くとステータスバーにターゲットアドレスが表示され、クリックするとURIアクションが起動してアドレスが開くはずです。ビューアの注釈インスペクタを使用して、それぞれがアンカーテキストを正確に囲む本物の/Link注釈であり、その下にあるコンテンツがフォーム描画されたXFAではなくプレーンな描画グリフになっていることを確認します。この「スタイル設定された静的テキスト」と「正しい矩形上の本物のリンク注釈」の組み合わせにより、フラット化されたドキュメントは、不要となったXFAエンジンよりも長く生き残ることができます。
これらのリッチテキストを取り囲むフィールド(テキストボックス、チェックボックス、選択リストなど)自体のフラット化については、XFAフォームのAcroFormウィジェットへのフラット化に関するチュートリアルでカバーしています。フラット化パスが自動生成するもの以外に、リンク注釈を手動で構築して配置する詳細なストーリーについては、HotPDFでのPDF注釈の操作を参照してください。どちらも、DelphiおよびC++Builder向けのHotPDF Componentに付属するのと同じ注釈およびフォームモデル上に構築されています。