技術記事

HotPDFのResolution:描画単位とUserWidth(Delphi)

HotPDF Componentでは、THotPDF.Resolutionが描画単位を定義します。すべてのX、Y座標、すべてのマージン、SetFontへ渡すサイズ、そしてTextWidthとGetWideTextWidthの結果は、1/Resolutionインチで測られます。THPDFPage.WidthとHeightはこれに従わず、ポイントのままです。だからレイアウトの境界は、読み取り専用のUserWidthとUserHeightから取る必要があります。Resolutionに触れるいつもの理由は移植です。1/96や1/144インチで考えるレポートエンジンは、PDF側が同じ単位で話すときの方が、すべての呼び出し箇所に変換係数を差し込むときより、移しやすいものです。どの数値が新しい単位へ移り、どれが取り残されたかを知っていれば、それはうまく機能します

THotPDF.Resolutionが実際に変えるもの

THotPDF.Resolutionが変えるのは、HotPDFがあなたの渡す数値をどう読むかだけです。書き出されるPDFは同じです。セッターは2行です。SetResolutionが値を保存し、DocScale := Value / 72を設定します。以後、XProjectionとYProjectionは、コンテンツストリームへの道すがらですべての座標をDocScaleで割り、SetFontはサイズを同じやり方で割ってから記録します。PDFユーザー空間のデフォルトは1/72インチ(ISO 32000-1 §8.3.2.3)なので、デフォルトのResolution 72では射影は恒等写像、144では1描画単位は半ポイントです。/UserUnitエントリーは書かれません。PDF 1.6で追加されたこのページ属性は別物で、HotPDFはTHPDFPage.SetUserUnitとして公開します。TextOutのチュートリアルから来た人がひっかかる細部が1つあります。ページ座標は左上コーナーから始まり、Yは下向きに増えます。YProjectionがMediaBoxの上端からスケール済みのYを引くためで、これはあらゆるResolutionで変わりません

DelphiでTHotPDF.Resolutionが描画単位を定義する方法。セッターはDocScaleをResolution÷72として保存し、XProjection、YProjection、SetFontはコンテンツストリームへの道すがらすべての座標とサイズを割ります。Resolution 72は恒等写像、Resolution 144では1描画単位が半ポイントになりながら、ページは左上原点、Y下向きのままです
出力ファイルの中では何も動きません。変わるのはあなたの渡す数値の意味だけで、だから同じコンテンツストリームが72でも144でも現れるのです
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1描画単位 = 1/144インチ
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4:Width = 595、UserWidth = 1190
    Margin := 144;                       // 描画単位で1インチ
    Page.SetFont('Arial', [fsBold], 28); // 28/144インチ、14ポイントのフォント
    Title := 'INVOICE 2026-0417';
    // 同じ単位で測ったページ端に対して右寄せ
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // 1ポイントの罫線
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Resolution 144でPage.Widthが座標と食い違う理由

THPDFPage.WidthとHeightは、ドキュメントのResolutionが何であれページをポイントで報告します。一方あなたの座標は1/Resolutionインチです。だから144では、ページは実際の半分の幅に見えます。A4ページはResolution 72でWidth = 595、Height = 842と読め、144でも595と842のままです。実際には右端はX = 1190にあります。v2.766.0で追加されたUserWidthとUserHeightは、Width * DocScaleを返します。つまりあなたが描く単位でのページサイズです。これらが存在する前、ライブラリは内部で2つを混ぜていました。Resolution 144での症状は劇的です。段落が文字ごとに折り返し、THPDFTable.Renderは各行を新しいページへ押しやり、HTMLインポーターとXFAフラットナーはコンテンツを半分のサイズで描き、フラット化されたフォームは左上コーナーに密集しました。段落レイアウト、テーブル描画、HTMLインポート、EMFのセンタリング、WMFのページクリップ、レイアウト診断は、今はすべてユーザー単位のサイズを読みます。あなたのレイアウトコードもそうすべきです。描画座標と比較するもの(右マージン、改ページテスト、センタリング計算)は、UserWidthとUserHeightの側に属し、決してWidthとHeightの側には属しません

罠その1:WidthやHeightの代入がページをポイントへ切り替える

Page.WidthやPage.Heightの設定は、ページを黙ってUserDefinedへ変え、UserDefinedのページはDocScaleを完全に無視します。だから以後そこへ描くすべては、1/Resolutionインチでなくポイントです。セッターは古く、設計上ポイントを取ります。意味がそのままにされたのはそのためです。UserDefinedページの射影は素のX + MinXで、SetFontはサイズをそのまま保存します。Resolution 144では、コンテンツが突然、直前のページの2倍の大きさで出るページができます。ライブラリ自身がまさにこの間違いをしました。段落の続きページは以前、前のページのサイズをWidth経由でコピーし、すべてのオーバーフローページがポイントへ切り替わっていました。今はそれらのページがSize、Orientation、そしてページのResolutionをコピーし、元のページがすでにUserDefinedだったときだけWidthとHeightへフォールバックします

出口は2つあります。何が必要かによります。標準の用紙で足りるなら、Page.SizeとPage.Orientationを設定し、Resolution単位のまま描き続けてください。本当にカスタムサイズが要るなら、それがポイントのページであることを受け入れ、ポイントで描いてください。そこではUserWidthはWidthと等しいので、常にUserWidthを読むレイアウトコードは、両方の種類のページで動き続けます。ユニットテストはこれを固定します。Resolution 144でA4ページは1190のUserWidthを報告しますが、Width := 500とHeight := 400の後は500と400を報告します。ロード済みページも同じように振る舞います。既存PDFから再構築されたページは、ポイントでのMediaBoxしか知らず、ポイントで描くからです。このドキュメントが作ったページは、CurrentPageNumberで離れて戻ってきても、自分の単位を保ちます。v2.766.26以来の挙動です

HotPDFでResolution 144のときPage.Widthが座標と食い違う理由。WidthとHeightはポイントのままで、描画は1/144インチを使います。だからA4ページは595と読めるのに、右端はUserWidth 1190にあります。Widthの代入はページをUserDefinedへ切り替え、DocScaleを無視させるため、段落は文字ごとに折り返し、テーブルは行ごとに壊れ、SetFontサイズは半分になります
描画座標と比較するものは、UserWidthとUserHeightの側へ置いてください。UserDefinedのポイントページでは両者は一致するので、同じレイアウトコードが両方で生き延びます

罠その2:フォントサイズが半分のサイズになる理由

ポイントとして始まったフォントサイズが、Resolution 144で半分のサイズになるのは、SetFontがサイズ引数を描画単位として扱い、保存の前にポイントへ変換するからです。内部では、SetFontは現在のフォントオブジェクトへASize / DocScale * DPIを保存します。保存される値は常にポイントです。ライブラリはこれに2度つまずきました。WideTextOutBoxExのフォントフォールバックと段落の続きページが、どちらも保存済みのポイント値をSetFontへ返し、もう1度スケールされてテキストが半分になりました。あなたのコードは保存サイズを読めませんが、同じバグは、どこか他からのポイント値がSetFontへ届くたびに現れます。VCLフォームのTFont.Size、レポート定義のサイズ、CSSのptの長さ。まず変換し、係数にページ自身のResolutionとUserDefinedの場合を含めてください。メタファイル再生がページのCanvasを再生するときにやるように(その経路はHotPDFがEMFとWMFベクターグラフィックスをインポートする方法を見てください):」

// 現在のページの1ポイントあたり描画単位数。HotPDFが使う射影を
// 鏡像する:Width/Heightでサイズ化されたページでは1、それ以外は
// (document Resolution / 72) * (page Resolution / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // TFont.Sizeはポイント。SetFontは描画単位を期待する
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

ライブラリは同じルールを、自分のポイント定数へ適用します。すべての新しいページが始まる12ポイントのフォントは、今や内部のユニット/ポイント係数と乗算されるので、あらゆるResolutionで12ポイントです。マージン、ラベルサイズ、線幅がすべてハードコードされたポイントであるDrawChartは、スケールを一時的に1にして走ります。意図的に描画単位のまま残るのは、DrawQRCodeのモジュールサイズやデフォルトのテーブルフォントサイズといった、公開パラメーターのデフォルトです。API契約の一部なので、Resolution 144では72での意味の半分になります。テンプレートからレポートのサイズを決めるなら、HotPDFでフォントと画像を使ったレポート出力のガイドが、それらの値がどこから来るかを扱います

レイアウトがResolution非依存だとどう検証するか

最も信頼できる検査はバイト比較です。同じページをResolution 72で、そして144で、すべての座標とサイズを2倍にして描くと、非圧縮のコンテンツストリームは同一でなければなりません。両方の実行は射影後に同じポイント値へ着地するので、差異があればそれは変換をスキップした値です。HotPDFのテストスイートが段落、テーブル、HTMLインポート、XFAフラット化、円弧、メタファイル、画像を検査するのは、この方法です。ほとんどハーネスなしで、自分のレポートコードにも同じ技法が使えます

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // 読めるコンテンツストリーム
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// RenderPage('r72.pdf', 72, 1) と RenderPage('r144.pdf', 144, 2) は
// バイト単位で同一のページコンテンツストリームを出さなければならない
HotPDF DelphiコードでResolution非依存を検証する方法。同じレイアウトを2度描きます。1度はResolution 72、スケール1で、もう1度は144で、すべての座標とフォントサイズを2倍にします。それからバイト単位で同一の非圧縮コンテンツストリームを要求します。不一致は、Width経由でUserDefinedへ切り替えられたページか、変換されないままSetFontへ届いたポイント値を指します
両方の実行は射影後に同じポイント値へ着地します。だから差異は変換をスキップした数値です。HotPDFのテストスイートが頼るのと同じハーネスです

数値を運ぶ演算子を調べてください。Td、Tm、Tf、re、w、そしてTJ配列です。ファイルレベルのバイトは作成日時と/IDでまだ違います。だからファイル全体でなくストリームを比較してください。不一致はほぼ常に上の2つの罠のどちらかを指します。Width経由でリサイズされたページか、そのままSetFontへ渡されたポイント値です。描画呼び出し自体が初めてなら、まずサイズ、スタイル、回転を扱うHotPDF TextOut解説から始め、レイアウトがUserWidthを読むようになったら戻ってきてResolutionを切り替えてください。API詳細の全文とトライアルダウンロードは、HotPDF Delphi PDF component pageにあります