在 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 是同一份。setter 只有两行:SetResolution 存值并设 DocScale := Value / 72。此后,XProjection 和 YProjection 在坐标进内容流的路上都除以 DocScale,SetFont 同样先除字号再记录。PDF 用户空间默认 1/72 英寸(ISO 32000-1 §8.3.2.3),所以默认 Resolution 72 时投影是恒等映射,144 时一个绘图单位是半磅。不会写 /UserUnit 条目。那个 PDF 1.6 加入的页面属性是另一回事,HotPDF 把它暴露为 THPDFPage.SetUserUnit。一个从 TextOut 教程过来的人容易踩的细节:页面坐标从左上角起、Y 向下增长,因为 YProjection 算的是 MediaBox 顶减去缩放后的 Y,这在任何 Resolution 下都成立
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; // 绘图单位下的一英寸
Page.SetFont('Arial', [fsBold], 28); // 28/144 英寸,14 pt 的字体
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,也就是你绘图所用单位下的页面尺寸。它们出现之前,库自己在内部混用两者,Resolution 144 的症状很戏剧化:段落每个字符一换行,THPDFTable.Render 把每行推上新页,HTML 导入器和 XFA 摊平器都把内容画成一半大小,摊平后的表单挤在左上角。段落排版、表格渲染、HTML 导入、EMF 居中、WMF 页面裁剪和布局诊断现在都读用户单位尺寸。你自己的布局代码也该如此:凡是要和绘图坐标比较的东西(右边距、分页判断、居中计算)都该落在 UserWidth 和 UserHeight 上,永远不要用 Width 和 Height
陷阱一:给 Width 或 Height 赋值会把页面切到点数
设置 Page.Width 或 Page.Height 会无声把页面变成 UserDefined,而 UserDefined 页面完全无视 DocScale,所以之后你画的一切都是点数、不是 1/Resolution 英寸。这个 setter 年代久远、按设计收点数,所以它的含义没被动过。UserDefined 页面的投影就是朴素的 X + MinX,SetFont 原样存字号。在 Resolution 144 下,结果是一个页面内容突然比前一页大一倍的文件。库自己犯过一模一样的错:段落的续页曾经过 Width 拷贝前一页的尺寸,于是每个溢出页都切到了点数。这些页现在拷贝 Size、Orientation 和页面 Resolution,只有原页本就是 UserDefined 时才回落到 Width 和 Height
两条出路,看你需要什么。标准纸张够用就设 Page.Size 和 Page.Orientation,继续用你的 Resolution 单位画。真需要自定义页面尺寸就接受它是点页、用点数画;那里 UserWidth 等于 Width,所以始终读 UserWidth 的布局代码在两种页面上都能跑。单元测试钉住了这一点:Resolution 144 下一张 A4 页报告 UserWidth 为 1190,而 Width := 500、Height := 400 之后报告 500 和 400。已加载页面同理,因为从既有 PDF 重建的页面只知道点数 MediaBox、按点数绘制。这份文档自己创建的页面在你切走再经 CurrentPageNumber 回来时保持自己的单位,v2.766.26 起就是如此
陷阱二:为什么字号出来只有一半?
本来是点数的字号在 Resolution 144 下输出成一半,因为 SetFont 把字号参数当绘图单位、存之前先换算成点数。内部,SetFont 在当前字体对象里存 ASize / DocScale * DPI,存进去的值永远是点数。库在这上面绊过两次:WideTextOutBoxEx 里的字体回退和段落续页都把那个存的点数值交回给 SetFont,它又缩了一次,文字减半。你的代码读不到那个存下的值,但同一个 bug 会在任何别处来的点数值抵达 SetFont 时现身:VCL 窗体的 TFont.Size、报表定义里的尺寸、CSS 的 pt 长度。先换算,并把页面自己的 Resolution 和 UserDefined 情形算进系数里,metafile 回放重放页面 Canvas 时就是这么做的(那条路径见HotPDF 如何导入 EMF 和 WMF 矢量图形):
// 当前页面每点对应的绘图单位数。镜像 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 磅字体现在乘上内部的 units-per-point 系数,任何 Resolution 下都是 12 磅。DrawChart 的边距、标签尺寸和线宽全是硬编码点数,现在运行时把缩放临时设为 1。刻意留在绘图单位里的,是 DrawQRCode 模块尺寸、默认表格字号这类公共参数默认值:它们是 API 契约的一部分,所以在 Resolution 144 下它们的含义是 72 下的一半。如果你从模板定报表尺寸,HotPDF 中带字体与图像的报表输出指南讲了那些值通常从哪来
怎么验证一个布局与 Resolution 无关?
最可靠的检查是字节比较:同一页面在 Resolution 72 渲染一次、在 144 下把每个坐标和字号翻倍再渲染一次,未压缩内容流必须相同。两次运行投影后落在同样的点数值上,所以任何差异都是一个漏掉换算的值。HotPDF 测试套件就是这样检查段落、表格、HTML 导入、XFA 摊平、圆弧、metafile 和图像的。同样的技术几乎不用搭架子就能用在你的报表代码上:
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) and RenderPage('r144.pdf', 144, 2)
// 必须产出逐字节相同的页面内容流
检查带数字的操作符:Td、Tm、Tf、re、w 和 TJ 数组。文件级字节在创建日期和 /ID 上仍会不同,所以比较流、别比较整个文件。不一致几乎总是指向上面两个陷阱之一:经 Width 改过尺寸的页面,或直接传进 SetFont 的点数值。如果你对绘图调用本身还陌生,先看HotPDF TextOut 实战:尺寸、样式与旋转,等你布局读的都是 UserWidth 再回来切 Resolution。完整 API 细节和试用版下载在 HotPDF Delphi PDF component 页面