The first invoice almost every team renders with a PDF library comes out wrong in the same way: the header text sits along the bottom edge of the page, and each subsequent line climbs upward. Nothing is broken. PDF user space, per ISO 32000-1 §8.3, puts the origin at the bottom-left corner with Y increasing upward, the exact opposite of the GDI canvas a VCL developer has been drawing on for years. HotPDF, losLab's PDF generation library for Delphi and C++Builder, exposes that coordinate model directly, so the five minutes you spend internalizing it now saves a layout rewrite later. This guide walks through the output primitives a report generator actually needs: positioned text, fonts that survive deployment, image placement, and vector drawing.
UK teams should align this hotpdf report output fonts images delphi workflow with local governance, audit, and data quality requirements before production release
Text placement and the bottom-left origin
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 diagonal DRAFT and COPY stamps are done without any extra machinery. The idiom that keeps VCL-trained intuition working is to compute Y as page height minus the distance 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;
Two stateful behaviours in that listing cause most page-two bugs. AddPage repoints CurrentPage at the new page, so any reference you cached to the previous page object is now stale for drawing purposes. And font selection is per page: call SetFont again after every AddPage, or the first TextOut on the new page will not use the font you think it will. A report loop should treat 'new page' as 're-establish text state' as a single unit.
Fonts that exist on the server, not just your desktop
Font failures are deployment failures. The development machine has the corporate font installed; the Windows service account on the production host does not, and the output silently substitutes. The defensive pattern is to load fonts from files your installer controls instead of trusting the OS font directory, and HotPDF's Unicode registration call does exactly 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 ✓'));
Note that TextOut takes a WideString directly, so customer data that contains anything beyond the local code page — which in practice means all customer data — travels through the same call as ASCII report furniture, provided the selected font covers the glyphs. Embedded Unicode fonts also require the document to be PDF 1.5 or later, so keep that version floor in mind if some other requirement is pinning you to an older version. For scripts that need shaping instead of just glyph lookup, Arabic and Hebrew in particular, the dedicated right-to-left pipeline is covered by our article on complex script text shaping with HotPDF.
For the rare case where no font file can represent what you need, MICR-like marks, proprietary symbols, HotPDF supports Type 3 fonts via RegisterType3Font and AddType3Glyph, where each glyph is a small content stream you define. It is a niche tool, but it beats shipping symbol artwork as hundreds of tiny images.
Images: the middle arguments are a width and height, not a corner
HotPDF separates image registration from placement. AddImage ingests a TBitmap or TJPEGImage once — decode PNG artwork to a bitmap first — and returns an index; ShowImage places that index as many times as needed. The signature is the part to read twice:
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, not an opposite corner, and the final argument is a rotation angle. Code written against an X1/Y1/X2/Y2 assumption produces logos stretched across most of the page, a bug that is obvious in output and baffling in source. Related: KeepImageAspectRatio defaults to True, so a mismatched box letterboxes instead of distorts; set it to False only when stretching is genuinely intended.
Registration-versus-placement also matters for performance and size: AddImage embeds the bitmap data once, and every ShowImage with the same index reuses that single embedded object. A 500-page statement run that calls AddImage per page for the same logo embeds the logo 500 times; the same run that registers once and reuses the index embeds it once. Cache the indices in a small dictionary keyed by asset path and the problem never appears.
File size lives here too. Photographic content should go through JPEG encoding — pass icJpeg to AddImage and set JpegQuality to around 85, since the property defaults to 100 — which is visually clean for scanned attachments and photos at a fraction of lossless size. Keep PNG for flat-colour artwork like logos and charts, where JPEG ringing artefacts are visible and Flate compression is already efficient. A statement run that embeds one photo per page at the wrong settings ships gigabytes; the same run at JPEG 85 ships a tenth of that with no complaint from anyone's eyes.
Rules, boxes, and shading with path primitives
Table rules and totals boxes do not need images at all; the vector primitives produce crisper output at any zoom and cost almost nothing in file size. The model is path construction followed by a painting operator:
// 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 ordering discipline is the same as in raw PDF content streams: set the paint state, build the path, then call Stroke or Fill. A path that is never painted simply vanishes, which is the usual explanation when a rule 'doesn't show up'. SetRGBFillColor takes a single TColor, so the VCL constants — clNavy, clBlack — work directly, and Rectangle follows the same width-and-height convention as image placement. Hairlines deserve one caution: line widths below about half a point look elegant on screen and can drop out entirely on a 600 dpi office printer, so 0.75pt is a sensible floor for table rules that must survive paper.
Pagination against real data, not example data
Numeric columns expose another habit worth building early: align amounts on their right edge by computing the X position from the column's right boundary and the rendered width of each value, instead of padding strings with spaces. Space-padding only lines up in monospaced fonts, and financial reports are never set in monospaced fonts. Format values through Delphi's locale-aware routines such as FormatFloat before measuring, so that the thousands separator the customer's locale expects is the one whose width you measured.
The demo dataset has ten short rows; production has a customer whose company name is 140 characters and a statement with 4,000 line items. A robust report loop tracks a Y cursor downward, subtracting each row's height, and breaks to a new page when the cursor would cross the bottom margin, remembering that 'downward' means decreasing Y in this coordinate system. Put the page-break handling in one place, re-issue SetFont and redraw the running header within it, and the off-by-one-page bugs disappear. If your reports must also satisfy archival or accessibility requirements, generation choices made here, embedded fonts, tagged output, colour spaces, are exactly what the standards constrain; see the HotPDF PDF/A, PDF/X, and PDF/UA guide before the template hardens.
FAQ
Why does my text render at the bottom of the page?
PDF's origin is the bottom-left corner with Y increasing upward. Convert top-relative positions with PageHeight - Offset, or design your layout code around the bottom-left origin from the outset.
Why is the font wrong on page 2 but correct on page 1?
Font selection does not carry across pages, and AddPage switches CurrentPage to the new page. Call SetFont after every AddPage before the first TextOut.
How do I keep file size sane with many embedded photos?
Pass icJpeg to AddImage and set JpegQuality near 85 for photographic content; reserve lossless icFlate for flat-colour logos and line art. Register each distinct image once with AddImage and reuse the index.
Product reference
Every call in this article ships with the HotPDF Component for Delphi and C++Builder, which documents the full text, font, image, and drawing API alongside the forms, encryption, and signing features covered elsewhere on this blog.