Delphi のビューアでユーザーが PDF フォームを記入し、Print を押すと、出てきた紙は全フィールドが空欄で、Letter サイズのページが A4 に合わせて引き伸ばされています。どちらの故障も印刷ループの中にあります。PDFium Component はフォームの値をビットマップにだけ描画し(あるいはフラット化したコピーに焼き込み)、各ページはアスペクト比を保つただ 1 つの倍率を求めています
フォームレイヤーはビットマップを要る第 2 パス
Windows では、デバイスコンテキストのオーバーロード TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) がプリンターの HDC をそのまま FPDF_RenderPage へ渡します。この呼び出しはページのコンテンツを描き、reAnnotations が設定されていればマークアップ注釈も描きます。インタラクティブなフォームレイヤーは DC には決して届きません。PDFium の form-fill 環境に属し、FPDF_FFLDraw が描くもので、その唯一のターゲットは FPDF_BITMAP です。HDC 版は存在しません。TPdf.RenderPage の 2 つのビットマップオーバーロードは、文書が FormFill を True(既定)にして開かれていれば、同じピクセルの上で FPDF_RenderPageBitmap と FPDF_FFLDraw を走らせます。オプションに rePrinting があると、PDFium Component はフォームパスの前にフィールドのハイライトを取り除き、後で既定のハイライト色とアルファを復元します。編集可能フィールドを囲む着色ボックスは画面に残るわけです。rePrinting はエンジンの FPDF_PRINTING フラグにも対応し、出力を印刷として印を付けます。ISO 32000-1 §12.5.3(Table 165)が注釈フラグのビット 3 で扱うのがこのケースです。適合リーダーは、Print ビットの立っていない注釈を、画面でどう見えていようと紙から外します
残りはプラットフォームが決めます。PDFium エンジンでは、FPDF_RenderPage は Windows GDI を通してデバイスコンテキストへ描き、Windows ビルドにしか存在しません。FPDF_FFLDraw はビットマップ以外の何ものにも描かず、どのプラットフォームでも動きます。Linux や他の非 Windows ターゲットでは GDI の呼び出しがそもそもないので、HDC オーバーロードはビットマップの描画——フォームレイヤー込み——で賄われ、ハンドルの背後のキャンバスへ転写されます。したがってデバイスコンテキストの呼び出しは、Windows の外ではフォームの値を運び、Windows の上では取り残します。Delphi と Lazarus で共有するコードでは、分岐を明示的に保ちましょう。文書がフォーム環境を持ち、その値が紙に載るべきなら、どのプラットフォームでもビットマップを要求します
印刷ジョブはどの経路を取るべきか
紙が何を運ぶべきかで選びます。生のフォームの値ならビットマップ経路、ベクター出力なら HDC 経路、そしてフラット化コピーなら、保存と再読み込み 1 回の値段で両方が手に入ります。SetPdfPrintMode はライブラリ全体に効くので、変えるのはジョブの間であって、ページの間ではありません
| 経路 | 紙に載るフォームの値 | 出力 | コスト |
|---|---|---|---|
RenderPage(DC, …) | Windows では載らない | ベクター GDI。SetPdfPrintMode が EMF(既定)か PostScript レベルを選ぶ | スプール最小、最速 |
TBitmap への RenderPage、続いて StretchDIBits | 載る。未保存の編集も含む | 要求したピクセルサイズのラスタ | 1 ページあたり 幅 × 高さ × 4 バイト、DIB コピーも加算 |
RenderPageBanded | 載る | ラスタ。1 度に 1 ストリップ | 全シートの代わりに 1 バンドだけメモリに |
フラット化コピー、続いて RenderPage(DC, …) | 載る。焼き込まれたページコンテンツとして | ベクター | 保存、再読み込み、GenerateFormAppearances、FlattenAllPages(FLAT_PRINT) |
600 dpi の A4 全ページは約 4960 × 7016 のデバイスピクセルで、32 ビット/ピクセルでは 1 ページ 139,197,440 バイトになります。StretchDIBits のために DIB へコピーすればピークは倍になり、32 ビットプロセスには重い荷です。それでもビューアの Print ボタンの既定はビットマップ経路が正解です。フィールドに打ち込んで Print を押したユーザーは、その値が紙に現れることを期待しますし、バンディングがメモリを有界に保ちます。アーカイブされた明細書のようにアプリケーションの外へ出ていく文書には、フラット化コピーのほうが合っています。値は普通のページコンテンツになり、プリンターはベクター出力を受け取ります
uses
System.SysUtils, System.IOUtils, PDFium, FPdfUtils; // FPdfUtils: FLAT_PRINT 用
function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
if not Source.SaveAs(TempName) then
raise Exception.Create('Could not write the print copy');
Result := TPdf.Create(nil);
try
Result.FileName := TempName; // FormFill は True のまま(既定)
Result.Password := Source.Password; // コピーは暗号化を引き継ぐ
Result.Active := True;
if not Result.Active then // 読み込み失敗は TPdf を非 active のままにする
raise Exception.Create('Could not reopen the print copy');
Result.GenerateFormAppearances; // 現在の /V から /AP を再構築する
if not Result.FlattenAllPages(FLAT_PRINT) then
raise Exception.Create('Flattening failed');
except
Result.Free;
TFile.Delete(TempName);
raise;
end;
end;
フラット化経路には、フラグよりも大事な順序の規則があります。FlattenAllPages は各 widget の appearance stream(ISO 32000-1 §12.5.5)をページコンテンツへ焼き込みますが、appearance stream は最後の作成者が書いたものそのままです。/V のフィールド値は関与しません。appearance を一度も再生成しなかったツールが記入したフォームは、古いテキストか、何もない状態にフラット化されます。だから GenerateFormAppearances が先に走ります。現在の値からテキストフィールドの appearance を再構築し、チェックボックスとラジオの /AS 状態を同期します。そのためには文書を開いた時点で FormFill が True であったことが要ります。これらはすべてコピーの上でやりましょう。フラット化は実行対象の TPdf のページを書き換え、開かれたビューアは自分のフィールドが編集可能なままでいることを望みますから。コピーは、下の印刷ルーチンのベクター分岐をフォームレイヤー切りのまま通ります。ジョブが終わったら解放し、一時ファイルを削除します
各ページを印刷可能領域へ収める
倍率は 2 つの係数の小さいほうで 1 回だけ掛け、結果を中央へ置きます。Printer.PageWidth × Printer.PageHeight への引き伸ばしは、紙と形の違うページをすべて歪めます。A4 に載る Letter のページ(612 × 792 ポイント)、縦のシートに載る横のページです。プリンター側はデバイスピクセルでの印刷可能領域(HORZRES と VERTRES)で、ドライバーのマージンはすでに除外済みです。PDF 側では、TPdf.PageWidth と PageHeight は表示どおりのページをすでに報告します。/Rotate 90 付きで格納された A4 縦のページは 842 × 595 ポイントで返ってきます。ISO 32000-1 §7.7.3.3(Table 30)は、表示や印刷のときページを /Rotate どおりに回すべきと言い、PDFium はそれを render 呼び出しの内側で適用します。RenderPage の Rotation 引数は、ユーザーがビューを回したときにビューアが足す回転だけです。そこへページ自身の PageRotation を渡すと、ページは 2 回回ります。幅と高さの入れ替えは、その追加回転が 90 度か 270 度のときだけです。回転下でのサムネイルの収め方とズームを支配するのと同じ 2 つの規則です。下のルーチンは、PDFium Component での PDF 文書の印刷の基本ループに、フォーム値の分岐と、ジョブを捨てるキャンセル経路を足したものです
uses
Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;
procedure FitRect(PageW, PageH: Double; Swap: Boolean;
AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
T, Scale: Double;
begin
if Swap then
begin
T := PageW; PageW := PageH; PageH := T;
end;
if (PageW <= 0) or (PageH <= 0) then
begin
X := 0; Y := 0; W := AreaW; H := AreaH;
Exit;
end;
Scale := AreaW / PageW;
if AreaH / PageH < Scale then
Scale := AreaH / PageH;
W := Round(PageW * Scale);
H := Round(PageH * Scale);
X := (AreaW - W) div 2;
Y := (AreaH - H) div 2;
end;
procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
InfoSize, ImageSize: DWORD;
Info: PBitmapInfo;
Bits: Pointer;
begin
GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
Info := AllocMem(InfoSize);
try
Bits := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, Info^, Bits^);
StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
finally
FreeMem(Bits);
end;
finally
FreeMem(Info);
end;
end;
function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
Opts: TRenderOptions = [reAnnotations, rePrinting];
var
Page, X, Y, W, H: Integer;
Bitmap: TBitmap;
begin
Result := False;
Printer.BeginDoc;
try
for Page := FromPage to ToPage do
begin
if Page > FromPage then
Printer.NewPage;
Pdf.PageNumber := Page;
FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
if FormLayer then
begin
Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
if Bitmap <> nil then // 縮退したページサイズでは nil
try
BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
finally
Bitmap.Free;
end;
end
else
Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
Application.ProcessMessages; // Cancel ボタンがフラグを立てられるようにする
if Cancel then
Break;
end;
Result := not Cancel;
finally
if Result then
Printer.EndDoc // ジョブをスプーラへコミットする
else
Printer.Abort; // キャンセルか例外:何も印刷されない
end;
end;
ビューアは Pdf.FormFill を FormLayer として渡します。フラット化コピーは False を渡します。EndDoc はコミットであって、後片付けの工程ではありません。BeginDoc の後、Printer.EndDoc はそれまでに描画したすべてを、正当なジョブとしてスプーラへ引き渡します。だから finally の無条件 EndDoc は、キャンセルされた印刷を半分印刷された印刷に変えます。ルーチンは代わりに成功フラグから finally で決めます。最後のページの後は EndDoc、キャンセルか例外の後は Abort です。ループの脱出にも同じ手の注意が要ります。Delphi も Free Pascal も、for ループの終値は入場時に 1 回だけ評価します。Cancel ハンドラから ToPage や部数を 0 にしても、すでに走っているループは止まりません。フラグを試して Break しましょう。部数と照合のネストしたループでは、各段を抜けます。さらに Application.ProcessMessages はメニューショートカットまで発火させるので、ジョブの実行中は Open、Save、Print を無効にしましょう
ラスタ経路をメモリの中に収める
TPdf.RenderPageBanded はページを、好きな高さの横帯に分けて描画し、各帯をコールバックへ渡します。各帯はフルサイズページの一片で、再スケールは何もなく、フルページのビットマップと同じフォームの値を運びます。512 行の帯なら、幅 4960 ピクセルのページはシート全体の 139 MB の代わりに、帯 1 本につき約 10 MB で済みます。およそ 14 分の 1 のメモリです。TPdfBandCallback はメソッドポインタで、帯インデックス、ページピクセルでの帯の上端オフセット、ビットマップを受け取り、True を返すと続行します。RenderPageBanded は帯数を返し、失敗時は 0 です
type
TBandPrinter = class
private
FDC: HDC;
FLeft, FTop: Integer;
public
constructor Create(DC: HDC; Left, Top: Integer);
function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
end;
constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
inherited Create;
FDC := DC;
FLeft := Left;
FTop := Top;
end;
function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
Result := True; // False で残りのバンドが止まる
end;
// 全ページビットマップ分岐の代わりにページループから呼ばれる
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
ExtraRotation: TRotation);
var
Bands: TBandPrinter;
begin
Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
try
if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
[reAnnotations, rePrinting]) = 0 then
raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
finally
Bands.Free;
end;
end;
ViewerPreferences と権限ビットをホストはどう扱うか
読んで、自分のコードで対応してください。PDFium は報告するだけで、ページはどのみち描画します。作成者は印刷のヒントを catalog の /ViewerPreferences 辞書(ISO 32000-1 §12.2、Table 150)に格納でき、TPdf は印刷ダイアログの形を作るものを表面に出します:
PrintScaling(Boolean)は、ファイルが/PrintScaling /Noneと言うとき False になります。原寸で印刷という意味です。/AppDefaultか欠落エントリでは True。規格は、印刷ダイアログを出さないときでもこれを尊重することを求めますPrintPaperHandling(TPrintPaperHandling)は/DuplexをphSimplex、phDuplexFlipShortEdge、phDuplexFlipLongEdge、phUndefinedとして運びます。SetPdfPrintPaperHandlingDevModeはこれをDEVMODE.dmDuplexに書きDM_DUPLEXを立てます。phUndefinedでは False を返してレコードに触れないので、ドライバーの既定が生き延びますPrintCopies(Integer)は/NumCopies。ダイアログに入れる部数です
権限ビットも同じ規則に従います。標準セキュリティハンドラでは、ISO 32000-1 §7.6.3.2(Table 22)が /P のビット 3 を印刷に、revision 3 以降のハンドラではビット 12 をフルフィデリティの印刷に割り当てます。ビット 3 が立ってビット 12 が 0 なら、印刷は appearance の低水準の表現に限られ、劣化していることもあります。TPdf.GrantedPermissions はマスクを permPrint と permPrintHighQuality へ解読し、HasPermission が 1 つの値を試します。エンジンは要求されたものを何でも描くので、強制はホストの仕事で、たいていは無効化された Print ボタンです。permPrint だけが与えられているとき、規格に合う読みは、全許可が開放するベクターや PostScript 出力ではなく、控えめな解像度のビットマップ経路です。ビットは開いたときの文書を記述します。owner パスワードで解錠されたファイルはフルアクセスを報告し、UserHasPermission は user レベルのビットだけに答えます
uses
Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;
procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
out ActualSize, HighQuality: Boolean);
var
Device, Driver, Port: array[0..MAX_PATH] of Char;
DevModeHandle: THandle;
DevMode: PDeviceMode;
begin
if not Pdf.HasPermission(permPrint) then
raise Exception.Create('This document does not permit printing');
HighQuality := Pdf.HasPermission(permPrintHighQuality);
ActualSize := not Pdf.PrintScaling; // /PrintScaling /None
Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
if DevModeHandle <> 0 then
begin
DevMode := GlobalLock(DevModeHandle);
try
SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
finally
GlobalUnlock(DevModeHandle);
end;
Printer.SetPrinter(Device, Driver, Port, DevModeHandle); // ダイアログはここから始まる
end;
Dialog.MinPage := 1;
Dialog.MaxPage := Pdf.PageCount;
Dialog.Options := Dialog.Options + [poPageNums];
if Pdf.PrintCopies > 1 then
Dialog.Copies := Pdf.PrintCopies;
end;
// ActualSize が True のときは FitRect の代わりに使う
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
// PDF 側は 1 インチ 72 ポイント、プリンターは 1 インチあたりのデバイスピクセル
W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
X := (Printer.PageWidth - W) div 2; // ページのほうが大きければ負になり、
Y := (Printer.PageHeight - H) div 2; // 印刷可能領域より大きい:端はクリップされる
end;
will-print と did-print のアクションを本当の印刷境界で走らせる
文書レベルの印刷スクリプトは、ホストが走らせるときに走ります。catalog の additional-actions 辞書(ISO 32000-1 §12.6.3、Table 197)は、印刷の前に実行される JavaScript アクションの WP と、後の DP を定義します。PDFium Component は両方を ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) と dlaDidPrint で公開し、render 呼び出しからは遠ざけます。サムネイル、スナップショット、変換も描画しますが、どれも文書スクリプトにはユーザーの印刷に見えてはいけないからです。順序を正すのはあなたの仕事です。日付を押したり「printed by」フィールドを埋めたりする will-print スクリプトはフォームの値を変えます。最初のページが描画される前に走らせ、フラット化経路ではコピーを保存する前に。did-print は EndDoc がジョブをコミットした後だけ発火させましょう。フォーム環境がなければ、あるいは LifecycleActionPolicy が拒めば、呼び出しはメッセージ付きで False を返します。信頼できないファイルには lapDisabled が文書アクションを切ります。スクリプトの全体像はホストイベント付きの AcroForm JavaScript 実行にあります
Windows では、フォームの値を運ぶべきジョブはビットマップ分岐に属します。通常の解像度では TBitmap への RenderPage、600 dpi 以上では RenderPageBanded です。PDFium Component のデバイスコンテキストオーバーロードは、フラット化コピーと、フィールドのない文書のために取っておきましょう。そちらではベクター出力のほうが良い結果になります