Generating a report comes down to placing three things on a page and getting them to agree on where they sit: text at known coordinates, fonts that render the same on the server as on your desktop, and images sized to fit. Everything else a report library does is arranged around those three. HotPDF, losLab's PDF generation library for Delphi and C++Builder, gives you each of them as a direct call on the page object, and the only real friction is the coordinate system underneath, which runs the opposite way from the VCL canvas you are used to. Get that orientation settled first and the rest of the layout work stops fighting you.
Text placement and the bottom-left origin
Almost everyone's first report comes out upside down. The title lands near the bottom edge and each line below it climbs toward the top. Nothing is malfunctioning. PDF user space, defined in ISO 32000-1 §8.3, puts the origin at the bottom-left corner with Y growing upward, which is the mirror image of the GDI canvas where Y grows downward from the top-left. Five minutes spent making peace with that saves a layout you would otherwise rewrite once the numbers stop making sense.
The page object's central call is TextOut(X, Y, Angle, Text). X and Y locate the text in points from the bottom-left corner, and Angle rotates it in degrees, which is how a diagonal DRAFT or COPY stamp gets drawn without any special support. The trick that lets VCL-trained intuition keep working is to express Y as the page height minus the distance you want from the top:
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 from top of Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // rotated stamp
Pdf.AddPage; // CurrentPage now points here
Pdf.CurrentPage.SetFont('Arial', [], 10); // font state does not carry over
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
The two stateful behaviors in that listing are responsible for most of the bugs that only show up on page two. AddPage repoints CurrentPage at the page it just created, so a page reference you cached earlier no longer draws where you expect. Font selection is also per page rather than per document. If you skip the SetFont after an AddPage, the first TextOut on the fresh page falls back to whatever default the page started with, not the bold heading font you set three pages ago. The safe habit is to treat "start a new page" and "re-establish the text state" as one inseparable step in the report loop.
Fonts that exist on the server, not just your desktop
Most font problems are really deployment problems wearing a disguise. Your development box has the corporate font installed, so the report looks right on your screen and ships. The production host runs the job under a service account that has never had that font installed, the renderer quietly substitutes something it can find, and the first anyone hears of it is a customer asking why the letterhead changed. The way out is to stop trusting the OS font directory and load the font from a file your installer puts on disk. HotPDF's Unicode registration call takes a path and does precisely that:
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 accepts a WideString directly, which matters more than it first looks. A customer name with an accent, a German street, a Polish city: these are not edge cases, they are the normal contents of a customer table, and they go through the same call as the ASCII labels you hard-code, as long as the registered font actually contains the glyphs. One version constraint rides along with embedded fonts: the document has to be PDF 1.5 or later, so if an unrelated requirement is pinning you to an older version, that is the thing that will quietly break. Right-to-left scripts such as Arabic and Hebrew need real shaping rather than a straight glyph lookup, and that has its own pipeline; see our article on complex script text shaping with HotPDF.
When no installed font can express what you need, think MICR characters on a check or a proprietary symbol set, Type 3 fonts fill the gap. You define each glyph as a small content stream through RegisterType3Font and AddType3Glyph. It is a specialized corner of the API and you will rarely reach for it, but it is far cleaner than scattering hundreds of tiny symbol bitmaps across a page.
Images: the middle arguments are a width and height, not a corner
Image handling splits into two steps, and keeping them separate is the whole point. AddImage takes a TBitmap or TJPEGImage, embeds it once, and hands back an index. PNG artwork has to be decoded to a bitmap before it gets there. ShowImage then draws that index wherever and however often you want it. The argument order on ShowImage is the one place worth slowing down to read:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // decode PNG to a bitmap
LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle): not (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
The two numbers after the position are a width and a height. They are not the coordinates of the opposite corner, and the trailing argument is a rotation angle in degrees. Read the signature as an X1/Y1/X2/Y2 box and a 120-by-40 logo placed at (50, 700) stretches from there to (120, 40) instead, sprawling across most of the page. The output makes the mistake obvious while the source code looks entirely reasonable, which is what makes it waste an afternoon. KeepImageAspectRatio defaults to True, so a box with the wrong proportions letterboxes the image instead of distorting it; flip it to False only when you actually mean to stretch.
The split between registering and placing pays off on long runs. Because AddImage embeds the pixels once and every ShowImage with that index points back at the same embedded object, where you call AddImage decides the file size. Call it inside the page loop for a 500-page statement and the same logo gets embedded 500 times. Call it once before the loop, keep the index, and the logo is stored a single time. A small dictionary keyed by the asset path is enough to make sure each distinct image is registered exactly once.
The choice of codec is the other size lever. Photographic content, scanned attachments and the like, belongs in JPEG: pass icJpeg to AddImage and drop JpegQuality to around 85, since the property starts at 100 and the difference at 85 is invisible on a printed page. Flat-color artwork such as logos, charts, and line drawings belongs in icFlate, where lossless compression is already compact and JPEG would smear visible ringing around the hard edges. A statement run that pushes one full-quality photo onto every page can balloon into gigabytes; the same content at JPEG 85 lands around a tenth of the size, and no reader can tell.
Rules, boxes, and shading with path primitives
The horizontal line under a table header and the gray box behind a totals figure do not need to be images. Draw them as vectors and they stay crisp at any zoom, print sharply, and add almost nothing to the file. HotPDF follows the same model raw PDF content streams use: build a path, then call an operator that paints it.
// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
The order is not optional: set the paint state, construct the path, then call Stroke or Fill. A path you build but never paint contributes nothing to the page, which is almost always the answer when a rule "isn't showing up." SetRGBFillColor takes a single TColor, so the familiar VCL constants like clNavy and clBlack drop straight in, and Rectangle uses the same width-and-height arguments as image placement rather than two corners. One caution about thin lines: anything below roughly half a point can look elegant on a monitor and then disappear on a 600 dpi office printer, so 0.75pt is a reasonable floor for any rule that has to survive being printed.
Pagination against real data, not sample data
One detail to get right before the layout sets: numeric columns should be aligned on their right edge, and the way to do that is to measure the rendered width of each value and position it back from the column boundary, not to pad the string with leading spaces. Space padding only lines up in a monospaced font, and nobody sets a financial report in a monospaced font. Run the values through Delphi's locale-aware routines such as FormatFloat first, so the thousands separator you measure the width of is the same one the customer's locale will actually display.
The danger with pagination is that you write it against the demo dataset, where ten short rows fit on one page and the loop never has to break. Production hands you a customer whose company name runs 140 characters and a statement with 4,000 line items, and now the loop has to break correctly every time. The pattern that holds up is a single Y cursor that moves downward as you subtract each row's height, and a check that starts a new page the moment the cursor would cross the bottom margin. Downward here means decreasing Y, which is the one place the bottom-left origin stays counterintuitive. Keep all of that in one routine that also re-issues SetFont and redraws the running header on the new page, and the off-by-one-page bugs never get a foothold. When the same reports also have to meet archival or accessibility rules, the choices you make right here, which fonts you embed, whether the output is tagged, which color spaces you use, are the ones those standards police; the HotPDF PDF/A, PDF/X, and PDF/UA guide is worth reading before the template hardens.
Every call shown here, the text positioning, font registration, image embedding, and path drawing, ships in the HotPDF Component for Delphi and C++Builder, whose reference documents the full output API along with the forms, encryption, and signing features it sits beside.