Việc tạo một báo cáo quy về việc đặt ba thứ lên một trang và làm cho chúng thống nhất với nhau về vị trí: văn bản tại các tọa độ đã biết, các phông chữ hiển thị giống nhau trên máy chủ và trên máy tính để bàn của bạn, và hình ảnh có kích thước vừa vặn. Mọi thứ khác mà một thư viện báo cáo làm đều được sắp xếp xung quanh ba thứ đó. HotPDF, thư viện tạo PDF của losLab cho Delphi và C++Builder, cung cấp cho bạn mỗi thứ đó dưới dạng một lệnh gọi trực tiếp trên đối tượng trang, và ma sát thực sự duy nhất là hệ tọa độ bên dưới, hệ tọa độ này chạy ngược chiều với canvas VCL mà bạn đã quen thuộc. Hãy giải quyết ổn thỏa hướng tọa độ đó trước, và phần còn lại của công việc bố cục sẽ ngừng chống lại bạn
Đặt văn bản và gốc tọa độ dưới-trái
Gần như báo cáo đầu tiên của mọi người đều ra lộn ngược. Tiêu đề rơi gần mép dưới và mỗi dòng bên dưới nó leo dần lên phía trên. Không có gì bị trục trặc cả. Không gian người dùng PDF, được định nghĩa trong ISO 32000-1 §8.3, đặt gốc tọa độ ở góc dưới-trái với trục Y tăng dần lên trên, đây là hình ảnh phản chiếu của canvas GDI nơi trục Y tăng dần xuống dưới từ góc trên-trái. Năm phút bỏ ra để làm hòa với điều đó sẽ tiết kiệm cho bạn một bố cục mà nếu không bạn sẽ phải viết lại một khi các con số ngừng có ý nghĩa
Lệnh gọi trung tâm của đối tượng trang là TextOut(X, Y, Angle, Text). X và Y định vị văn bản theo point từ góc dưới-trái, và Angle xoay nó theo độ, đây là cách một dấu đóng chéo DRAFT hoặc COPY được vẽ mà không cần bất kỳ hỗ trợ đặc biệt nào. Mẹo giúp trực giác được đào tạo theo VCL tiếp tục hoạt động là diễn đạt Y như chiều cao trang trừ đi khoảng cách bạn muốn từ đỉnh:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-0001.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE'); // 50pt từ đỉnh của khổ Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // dấu đóng đã xoay
Pdf.AddPage; // CurrentPage giờ trỏ đến đây
Pdf.CurrentPage.SetFont('Arial', [], 10); // trạng thái phông chữ không mang sang trang mới
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Hai hành vi có trạng thái trong đoạn liệt kê đó chịu trách nhiệm cho phần lớn các lỗi chỉ xuất hiện ở trang hai. AddPage trỏ lại CurrentPage sang trang nó vừa tạo, nên một tham chiếu trang bạn đã lưu trước đó không còn vẽ ở nơi bạn mong đợi nữa. Việc chọn phông chữ cũng là theo từng trang chứ không phải theo từng tài liệu. Nếu bạn bỏ qua SetFont sau một AddPage, lệnh TextOut đầu tiên trên trang mới sẽ quay về bất cứ mặc định nào mà trang đó bắt đầu với, không phải phông chữ tiêu đề in đậm bạn đã đặt ba trang trước đó. Thói quen an toàn là coi "bắt đầu một trang mới" và "thiết lập lại trạng thái văn bản" như một bước duy nhất không thể tách rời trong vòng lặp báo cáo
Các phông chữ tồn tại trên máy chủ, không chỉ trên máy tính để bàn của bạn
Hầu hết các vấn đề về phông chữ thực ra là các vấn đề triển khai đội lốt. Máy phát triển của bạn đã cài đặt phông chữ của công ty, nên báo cáo trông đúng trên màn hình của bạn và được phát hành. Máy chủ sản xuất chạy tác vụ dưới một tài khoản dịch vụ chưa bao giờ cài phông chữ đó, trình kết xuất âm thầm thay thế bằng thứ gì đó nó có thể tìm thấy, và điều đầu tiên ai đó nghe được về việc đó là một khách hàng hỏi tại sao tiêu đề thư lại thay đổi. Lối thoát là ngừng tin tưởng thư mục phông chữ của hệ điều hành và tải phông chữ từ một tệp mà trình cài đặt của bạn đặt trên đĩa. Lệnh đăng ký Unicode của HotPDF nhận một đường dẫn và làm chính xác điều đó:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));
TextOut chấp nhận trực tiếp một WideString, điều này quan trọng hơn vẻ ngoài ban đầu của nó. Một tên khách hàng có dấu, một con phố Đức, một thành phố Ba Lan: đây không phải là các trường hợp biên, chúng là nội dung bình thường của một bảng khách hàng, và chúng đi qua cùng một lệnh gọi như các nhãn ASCII bạn viết cứng, miễn là phông chữ đã đăng ký thực sự chứa các glyph đó. Có một ràng buộc phiên bản đi kèm với các phông chữ được nhúng: tài liệu phải là PDF 1.5 trở lên, nên nếu một yêu cầu không liên quan nào đó đang ghim bạn vào một phiên bản cũ hơn, đó chính là thứ sẽ âm thầm hỏng. Các văn tự viết từ phải sang trái như tiếng Ả Rập và tiếng Hebrew cần định hình (shaping) thực sự thay vì một lần tra cứu glyph trực tiếp, và điều đó có pipeline riêng của nó; xem bài viết của chúng tôi về định hình văn bản văn tự phức tạp với HotPDF
Khi không có phông chữ đã cài đặt nào có thể diễn đạt được thứ bạn cần, hãy nghĩ đến các ký tự MICR trên một tấm séc hoặc một bộ ký hiệu độc quyền, các phông chữ Type 3 lấp đầy khoảng trống đó. Bạn định nghĩa mỗi glyph như một luồng nội dung nhỏ thông qua RegisterType3Font và AddType3Glyph. Đây là một góc chuyên biệt của API và bạn hiếm khi cần dùng đến nó, nhưng nó sạch sẽ hơn nhiều so với việc rải hàng trăm bitmap ký hiệu nhỏ xíu khắp trang
Hình ảnh: các tham số ở giữa là chiều rộng và chiều cao, không phải một góc
Xử lý hình ảnh tách thành hai bước, và giữ chúng riêng biệt chính là điểm mấu chốt. AddImage nhận một TBitmap hoặc TJPEGImage, nhúng nó một lần, và trả về một chỉ mục. Ảnh PNG phải được giải mã thành bitmap trước khi đến đó. ShowImage sau đó vẽ chỉ mục đó ở bất cứ đâu và bao nhiêu lần bạn muốn. Thứ tự tham số trên ShowImage là nơi đáng để chậm lại và đọc kỹ:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // giải mã PNG thành bitmap
LogoIdx := Pdf.AddImage(Logo, icFlate); // không mất dữ liệu cho hình ảnh màu phẳng
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle): không phải (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
Hai con số sau vị trí là chiều rộng và chiều cao. Chúng không phải là tọa độ của góc đối diện, và tham số cuối cùng là một góc xoay theo độ. Đọc chữ ký hàm như một hộp X1/Y1/X2/Y2 và một logo 120 x 40 được đặt tại (50, 700) sẽ kéo dài từ đó đến (120, 40) thay vào đó, tràn lan khắp phần lớn trang. Đầu ra làm cho lỗi trở nên rõ ràng trong khi mã nguồn trông hoàn toàn hợp lý, đây chính là điều khiến nó ngốn mất một buổi chiều. KeepImageAspectRatio mặc định là True, nên một hộp có tỷ lệ sai sẽ tạo viền hộp thư (letterbox) cho hình ảnh thay vì làm méo nó; chỉ chuyển nó thành False khi bạn thực sự có ý định kéo giãn
Sự tách biệt giữa đăng ký và đặt vị trí mang lại lợi ích trên các lượt chạy dài. Vì AddImage nhúng các điểm ảnh một lần và mỗi ShowImage với chỉ mục đó trỏ trở lại cùng một đối tượng đã nhúng, nơi bạn gọi AddImage quyết định kích thước tệp. Gọi nó bên trong vòng lặp trang cho một bảng sao kê 500 trang và cùng một logo sẽ được nhúng 500 lần. Gọi nó một lần trước vòng lặp, giữ chỉ mục, và logo được lưu trữ đúng một lần. Một từ điển nhỏ được khóa theo đường dẫn tài sản là đủ để đảm bảo mỗi hình ảnh riêng biệt được đăng ký đúng một lần
Việc chọn codec là đòn bẩy kích thước khác. Nội dung nhiếp ảnh, các tệp đính kèm đã quét và những thứ tương tự, thuộc về JPEG: truyền icJpeg cho AddImage và hạ JpegQuality xuống khoảng 85, vì thuộc tính này bắt đầu ở 100 và sự khác biệt ở mức 85 là không thể nhận ra trên một trang in. Hình ảnh màu phẳng như logo, biểu đồ, và bản vẽ đường nét thuộc về icFlate, nơi nén không mất dữ liệu vốn đã gọn nhẹ và JPEG sẽ làm nhòe các viền cứng có thể nhìn thấy. Một lượt chạy sao kê đẩy một ảnh chất lượng đầy đủ lên mỗi trang có thể phình to lên hàng gigabyte; cùng nội dung đó ở JPEG 85 chỉ còn khoảng một phần mười kích thước, và không trình đọc nào có thể nhận ra
Đường kẻ, hộp, và tô bóng với các nguyên hình đường dẫn
Đường kẻ ngang dưới tiêu đề bảng và hộp xám sau một con số tổng không cần phải là hình ảnh. Vẽ chúng dưới dạng vector và chúng vẫn sắc nét ở bất kỳ mức zoom nào, in sắc nét, và gần như không thêm gì vào kích thước tệp. HotPDF theo cùng mô hình mà các luồng nội dung PDF thô sử dụng: xây dựng một đường dẫn, rồi gọi một toán tử tô nó
// Đường kẻ ngang dưới tiêu đề bảng
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Hộp tổng có tô bóng: X, Y, chiều rộng, chiều cao
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
Thứ tự này không phải là tùy chọn: đặt trạng thái tô, xây dựng đường dẫn, rồi gọi Stroke hoặc Fill. Một đường dẫn bạn xây dựng nhưng không bao giờ tô không đóng góp gì vào trang, đây gần như luôn luôn là câu trả lời khi một đường kẻ "không hiện ra." SetRGBFillColor nhận một TColor duy nhất, nên các hằng số VCL quen thuộc như clNavy và clBlack dùng được ngay, và Rectangle sử dụng cùng các tham số chiều rộng-và-chiều cao như khi đặt hình ảnh thay vì hai góc. Một lời cảnh báo về các đường mảnh: bất cứ thứ gì dưới khoảng nửa point có thể trông thanh lịch trên màn hình rồi biến mất trên một máy in văn phòng 600 dpi, nên 0,75pt là một sàn hợp lý cho bất kỳ đường kẻ nào cần sống sót qua việc in ấn
Phân trang dựa trên dữ liệu thực, không phải dữ liệu mẫu
Một chi tiết cần làm đúng trước khi bố cục được cố định: các cột số nên được căn theo mép phải của chúng, và cách làm điều đó là đo chiều rộng đã hiển thị của mỗi giá trị và định vị nó lùi lại từ ranh giới cột, không phải đệm chuỗi bằng các khoảng trắng ở đầu. Đệm bằng khoảng trắng chỉ thẳng hàng trong một phông chữ đơn cách (monospaced), và không ai đặt một báo cáo tài chính bằng phông chữ đơn cách cả. Hãy chạy các giá trị qua các hàm nhận biết địa phương hóa của Delphi như FormatFloat trước, để dấu phân cách hàng nghìn mà bạn đo chiều rộng chính là dấu mà locale của khách hàng thực sự sẽ hiển thị
Nguy hiểm với việc phân trang là bạn viết nó dựa trên bộ dữ liệu demo, nơi mười hàng ngắn vừa vặn trên một trang và vòng lặp không bao giờ phải ngắt. Môi trường sản xuất đưa cho bạn một khách hàng có tên công ty dài 140 ký tự và một bảng sao kê với 4.000 dòng mục, và giờ vòng lặp phải ngắt đúng cách mỗi lần. Mẫu hình đứng vững là một con trỏ Y duy nhất di chuyển xuống dưới khi bạn trừ đi chiều cao của mỗi hàng, và một phép kiểm tra bắt đầu một trang mới ngay khi con trỏ sẽ vượt qua lề dưới. "Xuống dưới" ở đây nghĩa là Y giảm dần, đây là nơi duy nhất mà gốc tọa độ dưới-trái vẫn phản trực giác. Hãy giữ tất cả điều đó trong một quy trình cũng phát lại SetFont và vẽ lại tiêu đề đang chạy trên trang mới, và các lỗi lệch-một-trang sẽ không bao giờ có chỗ đứng. Khi cùng các báo cáo đó cũng phải đáp ứng các quy tắc lưu trữ hoặc khả năng tiếp cận, những lựa chọn bạn đưa ra ngay tại đây, phông chữ nào bạn nhúng, đầu ra có được gắn thẻ hay không, không gian màu nào bạn dùng, chính là những gì các tiêu chuẩn đó giám sát; hướng dẫn PDF/A, PDF/X, và PDF/UA của HotPDF đáng đọc trước khi mẫu được cố định
Mọi lệnh gọi được trình bày ở đây, định vị văn bản, đăng ký phông chữ, nhúng hình ảnh, và vẽ đường dẫn, đi kèm trong HotPDF Delphi Component cho Delphi và C++Builder, tài liệu tham khảo của nó ghi lại đầy đủ API đầu ra cùng với các tính năng biểu mẫu, mã hóa, và ký mà nó nằm cạnh