Trong HotPDF Component, THotPDF.Resolution định nghĩa đơn vị vẽ: mọi tọa độ X và Y, mọi margin, cỡ chữ truyền vào SetFont, và kết quả của TextWidth cùng GetWideTextWidth đều được đo theo 1/Resolution inch. THPDFPage.Width và Height thì không theo nó và vẫn tính theo point, nên biên layout phải lấy từ UserWidth và UserHeight chỉ-đọc. Lý do thường gặp để đụng tới Resolution là một cuộc port: một engine báo cáo vốn đã tư duy theo 1/96 hay 1/144 inch sẽ dễ chuyển sang hơn khi phía PDF nói cùng đơn vị, hơn là khi mọi call site phải mang theo một hệ số quy đổi. Điều đó chạy rất ổn, miễn là bạn biết con số nào đã chuyển sang đơn vị mới và con số nào ở lại
THotPDF.Resolution thực sự đổi điều gì?
THotPDF.Resolution chỉ đổi cách HotPDF đọc những con số bạn truyền vào; PDF nó ghi ra vẫn vậy. Setter chỉ hai dòng: SetResolution lưu giá trị và đặt DocScale := Value / 72. Từ đó, XProjection và YProjection chia mọi tọa độ cho DocScale trên đường vào content stream, và SetFont chia cỡ chữ theo cùng cách trước khi ghi lại. PDF user space mặc định là 1/72 inch (ISO 32000-1 §8.3.2.3), nên ở Resolution mặc định 72 phép chiếu là đồng nhất, còn ở 144 một đơn vị vẽ bằng nửa point. Chẳng có entry /UserUnit nào được ghi. Thuộc tính trang đó, thêm vào ở PDF 1.6, là chuyện riêng mà HotPDF lộ ra dưới dạng THPDFPage.SetUserUnit. Một chi tiết bắt đúng những người đến từ các hướng dẫn TextOut: tọa độ trang chạy từ góc trên-trái với Y tăng dần xuống, vì YProjection tính đỉnh MediaBox trừ đi Y đã scale, và điều đó đúng ở mọi Resolution
var
Pdf: THotPDF;
Page: THPDFPage;
Margin: Single;
Title: WideString;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Resolution := 144; // 1 đơn vị vẽ = 1/144 inch
Pdf.BeginDoc;
Page := Pdf.CurrentPage; // A4: Width = 595, UserWidth = 1190
Margin := 144; // một inch theo đơn vị vẽ
Page.SetFont('Arial', [fsBold], 28); // 28/144 inch, một font 14 pt
Title := 'INVOICE 2026-0417';
// Canh phải theo mép trang đo bằng cùng đơn vị
Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
Margin, 0, Title);
Page.SetLineWidth(2); // đường kẻ 1 pt
Page.MoveTo(Margin, Margin + 48);
Page.LineTo(Page.UserWidth - Margin, Margin + 48);
Page.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Vì sao Page.Width bất đồng với tọa độ của bạn ở Resolution 144?
THPDFPage.Width và Height báo trang theo point bất kể document Resolution là bao nhiêu, trong khi tọa độ của bạn theo 1/Resolution inch, nên ở 144 trang trông rộng bằng một nửa thực tế. Một trang A4 đọc Width = 595 và Height = 842 ở Resolution 72 và vẫn đọc 595 cùng 842 ở 144, nơi mép phải thực ra nằm ở X = 1190. UserWidth và UserHeight, thêm vào từ v2.766.0, trả về Width * DocScale, tức kích thước trang theo đúng đơn vị bạn đang vẽ. Trước khi chúng tồn tại, thư viện trộn lẫn hai đơn vị nội bộ, và các triệu chứng ở Resolution 144 rất hoành tráng: đoạn văn xuống dòng sau từng ký tự, THPDFTable.Render đẩy từng row sang một trang mới, còn cả bộ import HTML lẫn bộ làm phẳng XFA vẽ content của mình với nửa cỡ, form bị làm phẳng chui gọn vào góc trên-trái. Layout đoạn văn, render bảng, import HTML, căn giữa EMF, clip trang WMF và các phép chẩn đoán layout giờ đều đọc kích thước theo user-unit. Code layout của bạn cũng nên vậy: bất cứ thứ gì so sánh với một tọa độ vẽ (một margin phải, một phép thử ngắt trang, một phép tính căn giữa) đều thuộc về UserWidth và UserHeight, đừng bao giờ dùng Width và Height
Bẫy một: gán Width hay Height chuyển trang sang point
Đặt Page.Width hay Page.Height lặng lẽ đổi trang thành UserDefined, và một trang UserDefined bỏ qua DocScale hoàn toàn, nên mọi thứ bạn vẽ lên nó sau đó đều theo point chứ không phải 1/Resolution inch. Setter này rất cũ và nhận point một cách có chủ đích, đó là lý do ý nghĩa của nó được giữ nguyên. Phép chiếu cho một trang UserDefined chỉ đơn giản là X + MinX, và SetFont lưu cỡ chữ nguyên trạng. Ở Resolution 144, kết quả là một trang mà content bỗng nhảy ra to gấp đôi trang đứng trước nó. Thư viện đã tự mắc đúng lỗi này: các trang tiếp diễn của đoạn văn từng chép kích thước của trang trước qua Width, và mọi trang tràn đều chuyển sang point. Những trang đó giờ chép Size, Orientation cùng Resolution trang thay vào, và chỉ rơi về Width với Height khi trang gốc vốn đã UserDefined
Hai lối thoát, tùy bạn cần gì. Nếu một khổ giấy chuẩn là đủ, hãy đặt Page.Size và Page.Orientation và cứ vẽ theo đơn vị Resolution của bạn. Nếu thực sự cần một cỡ trang riêng, hãy chấp nhận nó là một trang point và vẽ theo point; UserWidth sẽ bằng Width ở đó, nên code layout luôn đọc UserWidth chạy tốt trên cả hai loại trang. Unit test ghim chặt chuyện này: ở Resolution 144 một trang A4 báo UserWidth là 1190, nhưng sau Width := 500 và Height := 400 nó báo 500 và 400. Các trang đã nạp hành xử theo cùng cách, vì một trang dựng lại từ một PDF có sẵn chỉ biết MediaBox của mình theo point và vẽ theo point. Những trang do tài liệu này tạo ra giữ đơn vị riêng của chúng khi bạn rời đi rồi quay lại qua CurrentPageNumber, điều đúng từ v2.766.26
Bẫy hai: vì sao cỡ font lại ra nửa cỡ?
Một cỡ font khởi đầu theo point sẽ ra nửa cỡ ở Resolution 144 vì SetFont coi tham số cỡ của mình là đơn vị vẽ và chuyển nó thành point trước khi lưu. Nội bộ, SetFont lưu ASize / DocScale * DPI vào object font hiện tại, nên giá trị được lưu luôn là point. Thư viện từng trượt chân ở chỗ này hai lần: font fallback trong WideTextOutBoxEx và trang tiếp diễn của đoạn văn cùng trả giá trị point đã lưu đó ngược lại cho SetFont, thứ scale nó lần thứ hai và làm chữ nhỏ đi một nửa. Code của bạn không đọc được giá trị đã lưu, nhưng cùng bug này lộ diện mỗi khi một giá trị point từ nơi khác chạm tới SetFont: một TFont.Size từ một form VCL, một cỡ trong một định nghĩa báo cáo, một độ dài CSS pt. Hãy quy đổi trước, và tính cả Resolution riêng của trang cùng ca UserDefined vào hệ số, như metafile playback làm khi nó replay Canvas của trang (xem cách HotPDF import đồ họa vector EMF và WMF cho đường đi đó):
// Đơn vị vẽ trên mỗi point ở trang hiện tại. Phản chiếu đúng phép
// chiếu HotPDF dùng: 1 trên trang đặt cỡ qua Width/Height, nếu không
// (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 tính theo point; SetFont đòi đơn vị vẽ
Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
Font.Size * UnitsPerPoint(Pdf));
end;
Thư viện áp cùng luật cho các hằng point riêng của nó. Font 12 point mà mọi trang mới khởi đầu với giờ được nhân với hệ số units-per-point nội bộ, nên nó luôn là 12 point ở bất kỳ Resolution nào. DrawChart, với margin, cỡ nhãn và bề rộng đường đều là point cứng, giờ chạy với scale tạm đặt về 1. Những gì ở lại theo đơn vị vẽ, một cách có chủ đích, là các mặc định tham số public như cỡ module của DrawQRCode hay cỡ font bảng mặc định: chúng là một phần hợp đồng API, nên ở Resolution 144 chúng có nghĩa bằng nửa so với ở 72. Nếu bạn dựng báo cáo từ một mẫu, hướng dẫn output báo cáo với font và ảnh trong HotPDF bàn nơi những giá trị ấy thường đến từ đâu
Kiểm chứng một layout độc lập với Resolution bằng cách nào?
Phép kiểm đáng tin nhất là so sánh byte: render cùng một trang ở Resolution 72 rồi lại ở 144 với mọi tọa độ và cỡ chữ nhân đôi, và các content stream chưa nén phải giống hệt nhau. Cả hai lần chạy đều đáp xuống cùng các giá trị point sau phép chiếu, nên bất kỳ khác biệt nào đều là một giá trị đã bỏ qua phép quy đổi. Đây chính là cách bộ test HotPDF kiểm đoạn văn, bảng, import HTML, làm phẳng XFA, cung tròn, metafile và ảnh. Kỹ thuật tương tự áp dụng được cho code báo cáo riêng của bạn với harness gần như bằng không:
procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.Compression := cmNone; // content stream đọc được
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) và RenderPage('r144.pdf', 144, 2)
// phải cho ra content stream trang giống nhau từng byte
Hãy soi các toán tử mang số: Td, Tm, Tf, re, w và các mảng TJ. Byte cấp tệp vẫn khác nhau ở ngày tạo và /ID, nên hãy so các stream, đừng so nguyên tệp. Một điểm khác biệt hầu như luôn chỉ về một trong hai cái bẫy trên: một trang bị đổi cỡ qua Width, hay một giá trị point được nhét thẳng vào SetFont. Nếu bạn còn mới với các lệnh vẽ, hãy bắt đầu từ hướng dẫn HotPDF TextOut cho cỡ, style và xoay, rồi quay lại và bật Resolution khi layout của bạn đã đọc UserWidth. Chi tiết API đầy đủ cùng bản tải dùng thử nằm trên trang HotPDF Delphi PDF component