技術記事

DelphiでTDataSetとレポートをPDFにエクスポートする

DelphiでTDataSetまたはレポートをPDFにエクスポートするために、losLab PDF Libraryは2つのアプローチを提供しています。PDFlibTableExportは、FireDACクエリ、ClientDataSet、インメモリテーブルなどの任意のTDataSetをページ分割されたPDFの表に変換します。また、Addonsに含まれる3つのブリッジが、作成済みのFastReport、QuickReport、またはReportBuilderレポートを同じPDFライターに渡します。どちらもプリンタードライバーを使用せず、ウィンドウを表示することなく本物のPDFを生成します

これら2つの課題は似ているようで異なります。DBGridや生のクエリ結果には独自のレイアウトがないため、エクスポートする際には列、ヘッダー、改ページなどのレイアウトを独自に設計する必要があります。一方、FastReportやReportBuilderのドキュメントにはすでに設計されたレイアウトが存在するため、エクスポートするとは、描画コマンドをPDF空間に忠実に再現することを意味します。losLab PDF Libraryは、障害モードが異なるため、これらの处理を別々のユニットに分けて保持しています。この記事の残りの部分では、それぞれの処理手順と信頼性が低下するケースについて解説します

DelphiでTDataSetをPDFにエクスポートするにはどうすればよいですか?

PDFlibTableExportは、1回の呼び出しでデータセットを表に変換します。エクスポート処理では、フィールドリストをスキャンし、バイナリBLOBフィールド(ftBlobftGraphicftBytesなど、セルのテキストとして意味をなさないもの)を自動的にスキップし、数値列を右揃えにし、スタイル設定されたヘッダーバンドの上にオプションのゼブラストライプを描画します。内部的には、CreateTableでグリッドを構築し、SetTableCellContentでセルを埋め、手動で操作するのと同じ公開Table APIであるDrawTableRowsでレンダリングします。便利なラッパーであるPDFlibExportDataSetは、ミリメートル単位と左上の原点を自動的に設定するため、呼び出し元はページを指定するだけで済みます

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // any TDataSet works here

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // millimetres
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // redraw the header on each page

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

自動的なBLOBのスキップはデフォルトの動作であり、制限ではありません。カスタム列ラベルの設定、幅の縮小、BLOB以外の内部キー列の除外など、フィールドごとの制御が必要な場合は、TPDFlibTableExporterを直接構築し、そのOnFieldFilterイベントをフックします。このイベントはフィールドごとに1回呼び出され、変更可能な仕様を渡します。フィールドを除外するにはIncludeFalseに設定し、デフォルトをオーバーライドするにはColumnWidthDisplayLabelを設定します。このフックは、ページを肥大化させたくない幅広のメモ列を除外する場所でもあります

ページ分割:ページ間での描画状態の受け渡し

DrawTableRowsはページ分割の状態を保持しており、その動作仕様を理解することが最も重要です。この関数は開始行と終了行を指定して呼び出します。終了行に1未満の値を指定すると、指定された高さの範囲内でテーブルの最後まで描画することを意味します。関数は実際に描画された高さを返し、GetTableLastDrawnRowは収まった最後の行を報告します。この情報のペアが、次のページに引き渡す状態となります。最後に描画された行が総行数に達していない場合、新しいページを開き、その次の行から再开します。再測定は行われず、前回の呼び出しが終了した正確な位置から処理が続行されます

// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Last row = 0 means "draw to the end", bounded by DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // the whole table fit on this page
  if LastDrawn < Row then
    Break;                       // safety: no forward progress, bail out
  PDF.NewPage;
  Row := LastDrawn + 1;          // continue from the first undrawn row
  Y := PageHeight - Options.Top;
end;

ループ処理は、繰り返されるヘッダーが配置される場所でもあります。継続ページごとに、エクスポート処理はデータ行の前にオプションで1行目(ヘッダー)を再度描画し、最初のDrawTableRows呼び出しによって返された高さを使用して、その下の本文をオフセットします。これが、DrawTableRowsが次の行の座標ではなく高さを返す理由です。この戻り値により、いずれのサイズもハードコーディングすることなく、再描画されたヘッダーと継続する本文を重ねて配置することができます

FastReportまたはReportBuilderレポートをPDFにエクスポートするにはどうすればよいですか?

レポートブリッジは逆のアプローチを取ります。レイアウトを独自に設計することはなく、既存のレイアウトを再現します。各ブリッジは、それぞれのエンジンのネイティブなエクスポート機能にアタッチされます。PDFlibFRExportTfrxCustomExportFilterを継承し、FastReportのメモ、画像、図形、線オブジェクトを受け取って、それぞれをPDFlibPasのプリミティブに変換します。PDFlibRBDeviceTppFileDeviceを継承し、ReportBuilderのDrawCommandリストをスキャンします。PDFlibQRExportは、次のセクションで説明するまったく別の第3のルートを採用しています。これら3つはすべてヘッドレスで実行されます。これが、サーバー上でこれらを利用する最大の理由です

ヘッドレス実行には注意が必要です。FastReportはその典型例です。TfrxReport.Exportはプレビューページを経由し、フィルターのShowDialogプロパティを参照しますが、このプロパティのデフォルト値はTrueを継承しています。インタラクティブなデスクトップがないマシンでは、モーダルダイアログがキャンセル結果を返し、エクスポートが静かに失敗します。Exportを呼び出す前にShowDialogFalseに設定すると、レポートはサイレントにレンダリングされます。ReportBuilderにも同様のスイッチ(AllowPrintToFileShowPrintDialog)があり、すべてをPrepareで処理するQuickReportでは、ダイアログの抑制は一切不要です

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // build the pages first
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: skip the modal, no cancel
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

3つのエンジン、3つの座標系

各ブリッジはそれぞれ独自の座標計算を行います。これは、エンジンごとに測定基準が異なるためです。PDFlibFRExportは、FastReportのオブジェクト位置を96 PPIのピクセルとして扱い、96/25.4でスケールしてミリメートルに変換します。PDFlibRBDeviceは、ReportBuilderの描画コマンドを1/1000ミリメートル単位で読み取って1000で除算し、ppRegisterDeviceを介して自身を登録します。そのため、ppReport.DeviceType'PDFlibPas'に設定してPrintを呼び出すだけで、出力をそこにルーティングできます。PDFlibQRExportは座標計算を一切行いません。作成済みのQuickReportページはすでにEMFメタファイルであるため、ブリッジは各ページをストリームに保存し、それをImportEMFFromStreamに渡すことで、ライブラリのデバイスコンテキストおよびメタファイルレンダリングパスにあらゆるグリフと罫線の配置を委ねます。対照的に、FastReportおよびReportBuilderのブリッジは、ネイティブのベクター描画プリミティブとネイティブテキストを出力します

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepares the report and appends every page in one call; the bridge
    // turns each page's EMF into PDF through ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

再現性の限界

これらのブリッジにワークフローを依存させる前に、その制限事項を把握してください。データセットエクスポート処理はバイナリ列をサイレントにドロップするため、埋め込み画像を表示する必要があるレポートには、単純な表とは異なるアプローチが必要です。QuickReportのパスはEMFを経由するため、テキストがベクターグリフに変換されます。そのため、ページの見栄えは正しくなりますが、選択や検索が可能なテキストは含まれず、アクセシビリティのためのタグ付きPDF構造も作成されません。FastReportとReportBuilderは実際のテキストを保持しますが、型定義されたハンドラーは一般的なビュー(メモ、画像、図形、線)のみをカバーし、特殊なオブジェクトはバウンディングボックスにフォールバックされるかスキップされるため、リッチテキスト、バーコード、グラデーションの塗りつぶしを多用するレポートでは詳細が失われます。これらは欠陥ではなく、変換レイヤーにおける限界仕様です

運用上の最大の注意点は次のとおりです。3つのエンジンはいずれもlosLab PDF Libraryに同梱されておらず、ブリッジはメインのビルドから意図的に除外されています。これらは、検索パスにすでにFastReport VCL 6.x、QuickReport 8、またはReportBuilder 20が存在するプロジェクトからのみコンパイルできます。対象となるメタファイルおよびDrawCommand의 インターフェースは、いくつかの主要なエンジンバージョンにわたって安定していますが、バージョンの適合確認は開発者自身で行う必要があります。適切に対応すれば、これら2つのパスによって、一時的なDBGridダンプからデザインされた請求書まで、幅広い用途をカバーできます。テーブルエクスポート処理とレポートブリッジは、いずれもDelphiおよびC++Builder向けのlosLab PDF Libraryに含まれています