技術記事

Delphi で PDF ページを 1 ビット白黒にレンダリングする

FAX ゲートウェイは 24 ビットのページレンダリングを欲しがりません。100 万件のスキャン風請求書を保存するアーカイブパイプラインも同様ですし、文字認識の前にすべてを白黒へしきい値処理する OCR フロントエンドも同じです。3 者が欲しいものは一致しています。1 ピクセル 1 ビット、各ドットがインクか紙かのどちらかである、きれいな 1 ビットビットマップです。フルカラー BMP を渡しても、先方はどうせ 1 ピクセルあたり 23 ビットを捨てます。しかも、たいていは自分でやるより質の悪いディザ処理でです。面白いのは、そのダウンコンバートをどこで行うべきかという点で、PDFlibPas の答えは、できれば書き換えたくないレンダラをどう拡張するかについて有用な示唆を与えてくれます

PDFlibPas は Delphi と C++Builder 向けのネイティブ Object Pascal PDF ライブラリです。そのレンダリングコアはページをビットマップへラスタライズし、BMP、PNG、JPEG、WMF などいくつかの形式で出力できます。最近までできなかったのは、本当のモノクロビットマップを返すことと、ページの一部だけをレンダリングすることでした。どちらも v3.83.0 で入りましたが、どちらもラスタライザそのものを変えるのではなく、既存レンダラの上に載る薄い便利層として実装されました。この制約こそが話の中心です

なぜダウンコンバートはレンダラ内ではなく、レンダリング後に行うのか

1 ビット画像を作る分かりやすい方法は、ラスタライザに 1 ビットで描画させることです。しかし、それは同時にほかのすべてを壊す方法でもあります。レンダラ内部のビットマップは PixelFormat := pf24bitpf24bitPDFlibRendererTPDFPageRendererpf1bitpf1bit

そのため RenderPageToMonochromeFileRenderPageToMonochromeBitmap

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI is the classic Group 4 fax resolution; page index is 1-based
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

は逆の道を取ります。ページはまず通常どおり一時的な 24 ビット BMP にレンダリングし、その後でだけ 1 ビットへ畳み込みます。レンダラ自体には一切触れません。モノクロ化の振る舞いは完全に便利メソッドの中に閉じているため、それを呼ばない利用者には何の影響もありません。この種のトレードオフは明示しておく価値があります。後処理方式はビットマップ割り当てと一時ファイルを 1 つ余分に払いますが、その代わりに重荷を支えるコアを完全にスコープ外へ置けます。FAX やアーカイブの端ケースに応えるための機能としては、こちらが正しい判断です

1 ビット化が実際にどう行われるかTBitmapTBitmapTBitmapTBitmapPixelFormat := pf1bitpf1bit

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

肝は SetStretchBltModeStretchBltHALFTONEHALFTONEHALFTONEHALFTONEBLACKONWHITECOLORONCOLOR

ここで 1 つ譲れず、しかも間違えやすい点があります。一時レンダリングは必ず BMP でなければなりません。RenderPageToMonochromeFileRenderPageToMonochromeBitmap01RenderPageToFileRenderPageToFile0112253657TBitmap.LoadFromStreamTBitmap.LoadFromFile25「Bitmap image is not valid」です。Windows Metafile はベクタのレコードストリームであって DIB ではありません。モノクロへのダウンコンバートは最初から最後までラスタ処理なので、中間形式もラスタでなければなりません

ページの部分領域だけをレンダリングする

もう 1 つのメソッド RenderPageRegionToFileRenderPageRegionToFile

// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

StrToFloatDelimitedTextwidth * dpi / 72Round(Width * DPI / 72)height * dpi / 72Round(Height * DPI / 72)24 ビットpf24bitRenderPageToDCRenderPageToDCClip を通して描画します。結果ファイルには、ページ全体ではなく、切り出した矩形だけがその領域サイズで収まります

何もしなかった clip パラメータ

見た目以上に鋭い作業だったのはここです。RenderPageToDCClipRenderPageToDCClipClipRectTPDFPageTree.RenderPageToDCRenderPageToDCExRenderPageToDCClipRenderPageRegionToFile

v3.83.0 で、その線がつながりました。RenderPageToDCRenderPageToDCEx"Left,Top,Width,Height"4 つの double からなるDPI / 72dpi / 72

// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);

SaveDCSaveDCRestoreDCRestoreDC(-1)RestoreDCRestoreDC(TargetDC, -1)RenderPageToMonochromeBitmapRenderPageRegionToFile1 ビット化も同じ経路を通るため、ここが直ったことで副次的に恩恵を受けています

理解しておくべき挙動が 1 つあります。この clip は 切り抜くのであって、拡大縮小する わけではありません。ページは指定した DPI のまま通常位置でラスタライズされ、clip 領域の外側が捨てられるだけです。領域を出力いっぱいにズームしているわけではありません。拡大したいなら DPI を上げてください。矩形座標はポイントからピクセルへ変換されたあとのデバイス空間で解釈され、レンダリング面の左上を基準とします。したがって LeftTopTopBottom はページ上端から下向きに計画してください。PDFlibPas が画面出力のためにデバイスコンテキストをどう駆動するかをより詳しく追うなら、関連稿 印刷プレビューとデバイスコンテキスト出力

で同じ DC 配管を表示側から辿っています

正直な境界: 1 ビット BMP であって、G4 TIFF ではないRenderPageToMonochromeFile誇張して「FAX 対応出力」と言いたくなるところですが、ここで制限を率直に述べます。pf1bitRenderPageToMonochromeBitmapencoder がありません。エンコーダがなければ圧縮済みのモノクロランを書き出す先がないため、モノクロ経路は非圧縮の 1 ビット DIB で止まります

それでも実務上は有用です。1 ビット BMP は正しいピクセル形式であり、ディザ済みで、そのまま FAX、アーカイブ、OCR の各ツールチェーンへ渡すか、下流で G4 へ変換できます。ただし要件が「ライブラリから直接 Group 4 TIFF を出すこと」なら、現時点ではそこまでは到達していません。自前の圧縮段を計画してください。機能がどこで止まるかを知ることは、何ができるかを知るのと同じくらい重要です

どちらのメソッドも意図的に小さく、そのこと自体がこのページから持ち帰るべき設計上の教訓です。レンダラの上に載る便利 API でも、モノクロ出力や領域切り抜きといった実際の能力を、ラスタライザへ踏み込んで他の呼び出し元すべてを不安定にすることなく追加できます。基礎ラスタライズにどのレンダリングエンジンを選ぶかまで比較したいなら、Delphi におけるマルチエンジン PDF レンダリング の概要が、そのトレードオフを詳しく扱っています。完全なレンダリング面と残りの API は、PDFlibPas Delphi PDF Library の製品ページに全体像があります