技術記事

Delphi での losLab PDF Library を使用した RTF から PDF への変換

RTF はかなり以前から存在しているため、レガシーなレポートジェネレーター、差し込み印刷のパイプライン、最新のワードプロセッサより前の法的文書のアーカイブなど、誰も計画していなかった場所に現れます。オンザフライで PDF に変換することは繰り返し発生する要件であり、Windows 上で実際に機能するアプローチは、専用の RTF パーサーではなく、Windows 自体が TRichEditEM_FORMATRANGE を通じて既に提供しているレンダリングパスです。losLab PDF Library DLL エディションは、そのパイプラインに直接スロットインする仮想デバイスコンテキストを公開しています

メカニズム:仮想 DC と EM_FORMATRANGE

Rich Edit コントロールは、物理プリンターだけでなく、任意のデバイスコンテキストに対してコンテンツをページ分割できます。EM_FORMATRANGE メッセージは、指定された DC に文字の範囲をレイアウトするようにコントロールに指示し、何とか収まった最後の文字の位置を返します。cpMin を毎回進めながらこれを繰り返し呼び出すことで、ページごとの出力が得られます。losLab PDF Library の GetCanvasDC は、指定したページサイズのインメモリ DC を提供します。ページをその DC にレンダリングした後、LoadFromCanvasDc がその結果を PDF ページとしてキャプチャします。これがパイプラインの全体です

最初から正しく設定すべきことの1つは、ターゲットのページに合わせて TRichEdit コントロールのサイズを設定する必要があるということです。コントロールが DC の寸法よりも小さいか大きい場合、ページ分割が PDF の最終的な結果と一致しません。A4 出力の場合の標準的なアプローチは、RTF ファイルをロードする前に、DC のサイズ設定に使用するのと同じスケールヘルパーを使用して、コントロールのピクセル寸法を 96 DPI で 210 x 297 mm に一致するように設定することです

Delphi の実装

以下では、ライブラリの DLL エディションをラップする PDFlibAX_TLB インポートユニットを使用しています。フォームには TRichEdit とボタンが配置されており、フォームの OnCreate ハンドラーでコントロールのサイズを設定し、RTF を読み込み、ボタンのクリックによって変換ループが開始されます

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // ページ分割が DC と一致するように、画面の DPI でコントロールのサイズを A4 に設定します
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // A4 サイズの仮想 DC を取得します
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // RTF コンテンツの次のページを DC にレンダリングします
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // DC の内容を PDF ドキュメントとしてキャプチャします
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // 最後のページをシグナルします
end;

end.

ループの処理内容

PrintRtfBoxTFormatRange 構造体を埋め、SendMessage 経由で Rich Edit コントロールに渡します。コントロールは cpMin から始まる文字をレンダリングし、DC がいっぱいになると停止して、収まりきらなかった最初の文字の位置を返します。戻り値がテキストの全長以上になると、すべての文字がレンダリングされたことになり、関数はゼロを返します。これにより、repeat...until ループが終了します

各反復処理で、Output1.pdfOutput2.pdf などの名前で1つの PDF ファイルが生成されます。代わりに単一の複数ページドキュメントが必要な場合は、ライブラリのページ追加 API を使用して事後に組み立てるか、単一のドキュメントセッション内で AddPage を呼び出すようにループを再構築できます。上記の反復ごとの SaveToFile に続く RemovePdfDocument のパターンは、ピークメモリを1ページ分のコンテンツに抑えます。これは、非常に長い RTF ファイルの場合に重要になります

つまずきやすいサイズ設定の詳細

LoadFromCanvasDc の 96 DPI の引数は、DC がどの画面解像度でレンダリングされたかをライブラリに伝えるため、PDF ページの正しいポイントからピクセルへのマッピングを計算できます。これを間違えると、画面上では画像が正しく見えていても、出力ではテキストが間違ったサイズで表示されます

RcPage.RightRcPage.Bottom に追加された +100 は、コントロールの表示可能な端を越えた小さな余白です。Rich Edit は rcPage の四角形を使用してページを分割する場所を決定します。余白がないと、境界に正確に位置する行が2つのページにまたがって重複する可能性があります。これは魔法の定数ではありません。ページの境界が最後のピクセルではなく、コントロールのレイアウト領域内にきれいに収まるように、十分に大きな値を設定する必要があります

最後に、SendMessage の最初の呼び出しの前にウィンドウハンドルが有効になるように、FormCreate が実行されるときに、コントロールが既に表示されているフォームウィンドウにアタッチされている必要があります。実行時に動的に作成された TRichEdit は、フォームがまだ表示されていない場合、レンダリングループを開始する前に明示的な HandleNeeded 呼び出しを必要とします

フォントと RTF 機能の処理

レンダリングは Windows の Rich Edit エンジンによって行われるため、フォントの置換は、表示や印刷に使用されるのと同じルールに従います。RTF ファイルで参照され、マシンにインストールされているフォントは忠実にレンダリングされます。見つからないフォントは静かに置換されるため、行の長さやページ分割がずれる可能性があります。本番環境のバッチ変換では、これを明示的にテストする価値があります。RTF ソースで使用される各書体のドキュメントをロードし、出力ページ数が手動の印刷プレビューから期待されるものと一致することを確認してください

テーブル、埋め込み画像、およびほとんどの Rich Text フォーマット機能は、Rich Edit がネイティブにレンダリングするため、特別な処理なしで機能します。驚くかもしれない1つの領域は、twips で表現されたカスタムの段落間隔や最初の行のインデントを使用するテキストです。Rich Edit の内部座標系は twips(1/1440 インチ)ですが、TFormatRange で設定する DC 座標は現在の DPI におけるピクセルです。コントロールは内部で変換を行いますが、RTF をプログラムで構築している場合は、マージン値が正しい単位であることを確認する必要があります

DPI 認識と高 DPI ディスプレイ

150% のスケーリング (144 DPI) で動作するディスプレイでは、ScaleX(210, mmPixel) は 100% のディスプレイよりも大きなピクセル数を返します。PDF Library は、GetCanvasDC に渡されたピクセル寸法をすべて記録し、LoadFromCanvasDc の DPI 引数を使用して、PDF の物理ページサイズを逆算します。渡す DPI 値がアプリケーションが実行されている DPI と一致している限り、ディスプレイのスケーリングに関係なく、出力ページサイズは正しくなります

アプリケーションが DPI 非認識 (古いデフォルト) の場合、Windows は画面の DC をスケーリングし、高 DPI マシンではピクセル計算が間違ってしまいます。最も簡単な修正方法は、アプリケーションマニフェストで DPI 認識を宣言することです。そうすれば、アプリケーションは真のデバイスピクセルを受け取り、LoadFromCanvasDc に渡す 96 は、GetDeviceCaps(GetDC(0), LOGPIXELSX) から取得した実際のディスプレイ DPI に置き換える必要があります。上記のコードサンプルで 96 をハードコードしているのは、それが 100% のスケーリング環境に適しており、例を短く保つためです

出力構造:ページごとに1つのファイルか、統合されたドキュメントか

上記のループは、各ページを別々の PDF ファイルに書き込みます。これが目的かどうかは、ダウンストリームの用途によって異なります。レポート生成システムでは、後でページをマージしたり並べ替えたりして最終的なドキュメントを組み立てるため、個別のページが必要になることがよくあります。最初から単一の PDF が必要な場合は、ライブラリを使用して、1回のセッションで複数ページのドキュメントを作成できます。ループの外側でドキュメントを一度作成し、ループの内側で SaveToFile の代わりにページ追加メソッドを呼び出し、ループを抜けた後に完全なドキュメントを保存します。これにより、中間ファイルが回避され、ほとんどの単一ドキュメント変換シナリオに適した構造になります

変換速度はページ数にほぼ比例し、200ページのドキュメントには数秒かかる可能性があるため、大きな RTF ファイルの場合は、ループ内に進行状況のフィードバックを追加する価値があります。repeat...until 構造は拡張が簡単です。反復ごとにプログレスバーの更新で文字オフセットを追跡し、LastCharRichEdit1.GetTextLen からの合計文字数で割った値を使用します

ここに示されている GetCanvasDC および LoadFromCanvasDc メソッドは、Delphi および C++Builder 用の losLab PDF Library の一部です