HotPDFは、座標ではなく宣言的なツリーからページ分割済みのドキュメントを構築できます。セクション、スタック、テキスト、リスト、テーブルからTHPDFDOMDocumentを組み立ててTHPDFDOMRendererに渡すと、レンダラーが測定、ページ分割、ページ装飾の描画を行い、要求すればアクセシブルな結果を実現するPDF/UA構造ツリーも出力します。レイアウトのコードは一切y座標を計算しません
座標駆動のレポート生成器を保守した経験があれば、これが重要な理由がわかるはずです。最初のバージョンはうまく動きます。しかしやがて、顧客住所が3行に伸び、テーブルに行が増え、ローカライズされた見出しが折り返すようになると、それ以降のあらゆるy座標がずれてしまいます。修正はビジネスロジックのあちこちに散らばった手作業の改ページチェックとして積み重なっていき、2年後にやってくるタグ付きPDFの要件は、段落が何かすら理解していないコードには後付けできません
ツリーが所有するもの、そして所有権が厳格である理由
DOMはあらゆる階層で単一所有権を強制します。ドキュメントはセクションを所有し、セクションは本文・ヘッダー・フッターを所有し、スタック、コンテナ、テーブルはそれぞれの子要素を所有します。再利用はCloneを通じて、あるいは登録済みのファクトリを通じて行うのであって、同じオブジェクトを2つの親に取り付けることは決してありません。この規則は形式的なものではありません。ツリー内に2回現れるコンポーネントは、異なる制約の下で2回測定され、解体時には2回解放されてしまいます
呼び出し側のコードにとっての実務上の帰結は、ヘルパーが常に新しいインスタンスを返すということです。RegisterComponentでファクトリを登録しCreateComponentを呼び出すと、毎回新しいコンポーネントを生成する名前付きレシピが得られます。署名欄や法的フッターのような繰り返し使う装飾要素をツリーに組み込むには、この方法を使います
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // PDF/UA構造ツリーを出力する
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4サイズ(ポイント単位)
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // 重み比であって絶対幅ではない
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // ヘッダー行
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
ページ分割はどのようにして2乗のコストを避けているのか?
ツリーをページ分割する素朴なやり方は、収まらなかった部分をクローンして次のページへ運ぶというものです。1万行のテーブルでこれを行うと、残りの行をページごとにクローンすることになり、線形だったドキュメントが2乗の処理になってしまいます
HotPDFは代わりに、必要な部分だけを狭く分割します。トップレベルのレンダラーは本文の子要素をインデックスでたどり、セクションや本文全体をクローンすることはありません。実際にページ境界をまたぐネストしたスタックやコンテナだけが、影響を受ける部分木をクローンされます。そして重量級の2つの葉ノード型は、コピーではなくカーソルを持ち回ります。テキストの継続は、まだ処理していないソース文字範囲を保持し、テーブルの継続は、まだ配置していない行のスライスを保持します。長いドキュメントは線形のままであり、長い段落は、1回で分割されようが5回で分割されようがコストは変わりません
測定処理は副作用について正直であり続けます。THPDFLayoutElement.Measureは描画の副作用を持たないことが要求されており、実際の配置は常にTHotPDF.PlaceLayoutElementを通じて行われます。これは、配置済みのフラグメントを再測定し、オーバーフローの所有権を設定し、診断情報を記録する、同一の中心的なルーチンです。DOMレンダラーが決めるのは、新規ページのポリシー、ページ装飾、間隔、継続の生存期間だけです
無限ドキュメントを防ぐテーブルヘッダーの規則
ページをまたいでテーブルヘッダーを繰り返すというのは簡単そうに聞こえますが、実は2つの失敗モードを隠しています。HotPDFは、ヘッダー行が連続する行の最初のひとまとまりにのみ現れることと、最初の分割にはすべてのヘッダー行と少なくとも1つの本文行が収まることを要求します。2つ目の規則がなければ、残りスペースより高いヘッダーは、ヘッダーしか含まないページを生み出し、その後にまったく同じページが延々と続くことになります
継続先のページではヘッダーが再描画されますが、その再描画されたコピーはコンテンツではなくアーティファクトとしてマークされます。これは、アクセシビリティとテキスト抽出の両方にとって正しい答えです。オリジナルのヘッダー行は、論理的なテーブル構造の中にちょうど1回だけ残ります。これを省略すると、スクリーンリーダーはデータの途中でもう一度列見出しを読み上げてしまい、テキスト抽出ツールは本文行の間に重複したヘッダー行を挿入してしまいます
継続の深さには防御的な上限も設けられています。カスタムコンポーネントはSplitを、常に同等の末尾部分を返すように実装することも自由にできてしまうからです。レンダラーは、末尾部分を切り離した後、次のページを開始する前にこの上限をチェックし、現在のイテレーションはその末尾部分を自身のfinallyブロックで解放します。そのため、不適切に振る舞うサードパーティ製コンポーネントは、ディスクを埋め尽くすのではなく、診断可能なエラーで失敗します
1つの論理要素、複数のページフラグメント
自動タグ付けは、ページ分割モデルと構造モデルが一致しなければならない箇所です。2ページにまたがって分割された段落は、論理的には1つの段落であるため、構造要素としても1つのままでなければなりません。しかし、マークコンテンツ識別子はページごとのものであるため、目に見える各フラグメントには、それが現れるページ上で独自のMCIDが必要になります
HotPDFはこれを、単一の構造要素を保持したまま、各フラグメントについてその/K配列にマークコンテンツ参照を追加することで解決しています。ページと識別子を特定する/Pgと/MCIDのペアが付随します。そのMCIDに対応するParentTreeのスロットは、同じ要素を指し示します。これはまさにISO 14289が求めている挙動であり、継続によるクローンが通常のクローンとは区別される理由でもあります。通常のCloneは新しい論理コンテンツを意味し、新しい意味的アイデンティティを得ますが、内部的な継続クローンは、それが継続する元のコンポーネントのアイデンティティを引き継ぎます
要素の再利用は、コンポーネントポインタでソートされた意味的アイデンティティのインデックスを通じて検索され、二分比較によって探索されるため、大規模なツリーでも検索は対数時間で済みます。このインデックスが保持するのは所有権を持たない参照だけであり、構造オブジェクト自体の生存期間はPDFオブジェクトグラフに委ねられています
レンダラーが事前に強制する構造規則
GenerateStructureを有効にすると、いくつかのPDF/UAの規則は、ファイルが出来上がった後ではなく、ツリーがレンダリングされている最中にチェックされます。見出しはレベル1から始まり、レベルを飛ばすことはできません。LIはLの内部にのみ現れることができ、LblとLBodyはLIの内部にのみ現れることができます。TRはテーブルに属し、THとTDは行に属します。代替テキストを持たない図は、PDF/UAモードでは拒否されます
ここで早期に拒否するのは意図的な選択です。ドキュメントが書き出された後に代替テキストの欠落を報告するバリデーターは、1万通の明細書のバッチをすべて再生成する必要があると教えてくれるだけです。一方、コンポーネントを拒否するレンダラーは、それを生み出したデータがまだスコープ内にあるうちに、どのコンポーネントが原因かを教えてくれます。適合性の検証は、それでも別のステップとしてパイプラインに含めるべきであり、その仕組みはPDF/A、PDF/X、PDF/UAの検証で扱っています
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
この統計レコードは、見た目以上に有用です。テンプレート変更の後にSplitCountが急上昇するのは、たいてい、あるコンポーネントがコンテナより高く測定され始めたことを意味します。MaximumContinuationDepthがじわじわと増えていくのは、1ページあたりの進捗がわずかしかないSplitを持つコンポーネントの早期警告です。ArtifactCountと継続ページ数を比較すれば、繰り返されるヘッダーが本当にアーティファクトとしてタグ付けされたことを確認できます
DOMは直接APIとどう共存するのか
DOMは直接描画を置き換えるものではなく、同じページオブジェクトの上に構築されています。レンダラーが配置するものは何であれ、THotPDFへの直接呼び出しと織り交ぜることができます。これは、署名画像のような、正確な位置に手動で配置する要素が1つだけ必要なレポートで重要になります。ページのクローズは引き続きAddPageとEndDocの管理下にあるため、即時フラッシュモードでは完了済みのページをメモリ上に保持することはなく、常駐メモリは現在の継続、フォントリソース、通常のドキュメントオブジェクトグラフによって管理されたままです
コンテンツがデータ駆動でレイアウトがルール駆動である場合はDOMを選び、固定のアートワークには直接描画を使い続けてください。現在の悩みが特にテーブルのページ分割にあるなら、PDFでのテーブル生成で扱っているより絞り込んだアプローチを先に読む価値があります。行揃えのようなテキストレベルの挙動についてはテキストの行揃えで説明しています
宣言的レイアウト、自動タグ付け、直接描画APIは、Delphiおよび C++Builder向けの同じコンポーネントに同梱されています。完全な機能一覧はHotPDF Delphi PDFコンポーネントページに掲載されています