技術記事

DelphiのPDFium Componentを使用してPDFドキュメントを印刷する

PDFの座標はポイント単位であり、プリンターの座標はデバイス単位であり、これらは意図的に変換するまで互いに何の関係もありません。その不一致が、Delphiアプリケーションにおけるほとんどの不適切な印刷出力の根本原因です。コードは正しいファイルを送信しますが、ページが切り取られたり、引き伸ばされたり、空白になったりします。PDFium Componentはレンダリング側をきれいに処理します。プリンターの配管部分は標準のVCLです。両側が何を期待しているかを理解すれば、適度な量のコードで2つはうまく適合します

レンダリング後印刷のパイプラインはどのように機能するか

PDFium Componentはプリンターと直接通信しません。パターンは次のようになります。必要な解像度でページを TBitmap にレンダリングし、StretchDIBits を使用してそのビットマップをプリンターのキャンバスに転送します。TPdf.RenderPage は呼び出し元が所有するビットマップを返すため、ピクセル寸法を制御できます。オプションセットに [rePrinting] を渡すと、PDFiumはそのレンダリングパスをLCDサブピクセルヒンティングなどの画面専用の効果を省略するものに切り替え、印刷出力のためにページのMediaBoxを正しく処理します。rePrinting を省略してプリンターに送信するものは画面レンダリングになります。これはモニター上では問題なく見えますが、96 DPIの画面用に下されたヒンティングの決定は300または600 DPIの印刷には適していないため、高DPIプリンターでは出力がぼやける傾向があります

TPdf.Active は、ページのプロパティに触れる前にチェックする唯一のゲートです。このコンポーネントはロードエラーを暗黙のうちに飲み込みます。破損したファイルやパスワードで保護されたファイルで Active := True を設定しても例外は発生しません。単に ActiveFalse のままにするだけです。代入後は常にチェックしてください。アクティブでないドキュメントで PageCount または PageWidth を読み取るとゼロが返され、スプーラーに到達すると診断が非常に困難なサイレントなノーオペレーション(no-op)が生成されます

最小の印刷ループ

最も単純な動作ケースは、ファイルを読み込み、印刷ジョブを開き、ページを反復処理し、そして閉じることです。唯一の厄介な詳細は、最初のページの前に Printer.NewPage を呼び出してはならないことなので、FirstPage フラグがあります。StretchDIBits 転送は GetDIBSizesGetDIB を経由して、ビットマップハンドルからデバイス非依存のビット(DIB)を取得し、フルページサイズでプリンターキャンバスにペイントします

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

ビットマップ寸法として Printer.PageWidthPrinter.PageHeight を渡すことは、デバイスDPIをすでに考慮しているプリンターのネイティブピクセルサイズでレンダリングすることを意味します。その後、StretchDIBits 呼び出しは、それらのピクセルをページ上に1対1でマッピングします。これにより、明示的なDPI計算なしで達成可能な最高の忠実度が得られますが、PDFページと物理的な用紙がたまたま同じサイズである場合にのみ機能します。それらが異なる場合は、明示的なスケーリングが必要です

ページサイズと用紙サイズが異なる場合のスケーリング

A4縦のPDFページはUS Letterのプリンターに自動的に収まるわけではなく、横向きのページを縦向きのプリンターに送ると切り取られます。標準的なアプローチは、プリンターのピクセル数とPDFのポイント数の比率から均一なスケール係数を計算し、それを両方の寸法に適用して縦横比を維持することです。Pdf.PageWidthPdf.PageHeight は現在のページ寸法をポイント単位で公開します。1ポイントは1/72インチです。ターゲットDPIを掛けて72で割ると、その解像度でのピクセルに変換されます。印刷可能領域に収まる最大スケールを得るには、XとYの比率の Min を取ります

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Dpi = 300 でのレンダリングは、ほとんどのオフィスプリンターに適しています。600 DPIでは、単一のA4ページのビットマップは約34メガピクセルに達し、32ビットのビットマップとして約100 MBになります。通常のテキストドキュメントの場合、品質の向上は最小限であり、ページごとのメモリコストは大きくなります。600 DPIは、印刷所やそれが純粋に重要となるベクターを多用する技術図面のために取っておきましょう

2番目のコードブロックの reAnnotations フラグは、rePrinting とは独立しています。スタンプ、ハイライト、およびコメントボックスが紙に表示されることをユーザーが期待する場合は含めます。コンテンツのみの出力の場合は省略します。両方のフラグは自由に組み合わせることができます

ページの回転

PDFiumは、PDF内のページの回転を /Rotate エントリとして保存し、Pdf.PageRotation 経由でアクセスでき、TRotation 値(ro0ro90ro180ro270)を返します。プリンターの座標系は、画面に対して90度および270度の回転を反転させます。調整なしで生の PageRotation 値をそのまま RenderPage に渡すと、縦向きドキュメントに埋め込まれた横向きページは、ほとんどのWindowsプリンタードライバーで逆さまに印刷されます。修正方法は、レンダリング呼び出し前の単純な入れ替えです。ro90ro270 にマップし、ro270ro90 に戻し、ro0ro180 はそのままにします

出荷する前に、特定のターゲットプリンターでこの動作を確認してください。回転に関するドライバーの動作はベンダー間で統一されておらず、一部のドライバーはGDIレベルで独自の回転補正を適用します。二重の回転が見られる場合は、入れ替えを削除します。補正がまったく見られない場合は、入れ替えを追加します。縦向きと横向きのページが交互に配置された混合方向のドキュメントは、テスト中にどちらの障害モードも最も早くキャッチする方法です

長い印刷ジョブ全体でのメモリ管理

RenderPage への各呼び出しは、呼び出し元が所有し解放しなければならない新しい TBitmap を割り当てます。上記のループでは、try/finally Bitmap.Free ブロックが一度に1ページずつこれを正しく処理します。ページをまたいでビットマップを蓄積しないでください。200ページのドキュメントの300 DPIレンダリングは、最初のページがスプーラーに到達する前にギガバイトを消費します。次のページに進む前に、各ビットマップを解放してください

転送ブロック内の AllocMem / FreeMem のペアは同じルールに従います。GetDIBSizes は、DIBヘッダーとピクセルデータに必要なメモリ量を教えてくれます。これらはすべて1つのページのスコープ内で、割り当て、入力、ペイント、解放を行います。どちらのブロックでもリークが発生すると、数十ページを超えるドキュメントの印刷ジョブでプロセスヒープが使い果たされます

バックグラウンドスレッドで印刷ジョブを実行する必要がある場合は、TPdf とすべてのVCLプリンター呼び出しを同じスレッドに維持してください。TPdf 自体は、PDFium DLLのグローバル状態を共有するインスタンス間でスレッドセーフではありません。最も安全なモデルは、スレッドごとに1つの TPdf を用意し、それぞれがファイルの独自のコピーを読み込むことです

ここで紹介するレンダリングとドキュメントAPIは、DelphiとC++Builder向けの PDFium Component の一部です