Technical Article

HotPDF TextOut in Delphi: Size, Style, Rotation, and Spacing

A string drawn at (72, 72) lands one inch below the top edge of the page, which catches out anyone who learned PDF from the content-stream side, where y = 72 sits an inch above the bottom. The second surprise shows up a few hundred lines later. A report laid out for US Letter comes back with a first page of 612 by 792 points and every page after it at 595 by 842, because the line that set the paper size ran exactly once

Both effects have the same root. Text in a PDF is a short run of state changes (font and size, character spacing, word spacing, a text matrix for position and rotation) followed by one show-text operator, and a high-level text call is a thin wrapper over that sequence. The call reads the state. It never resets it for you, and a new page starts from a fixed set of defaults rather than from whatever the previous page ended with

Where TextOut puts (X, Y) on the page

HotPDF draws text with THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), reached as Pdf.CurrentPage.TextOut. X and Y are measured from the top-left corner of the page with Y growing downward, and with the default Resolution of 72 one unit is one point, 1/72 of an inch. The page converts each Y into PDF user space, which runs bottom-up, by subtracting it from the page height. The angle is in degrees, and because the text is a WideString, Unicode goes straight through. A second overload takes a PWORD plus a length for code that already holds UTF-16 buffers

The page geometry is fixed too. BeginDoc adds the first page, and every page created by BeginDoc or AddPage starts as A4, 595 by 842 points, with Arial 12 as the current font. US Letter (612 by 792) is one assignment away through CurrentPage.Size := Letter, but that assignment belongs to the page, so it has to be repeated for each new page

By default the Y you pass is the top of the text, and the baseline sits one font ascent below it. TextFromBaseLine(True) switches the page to baseline positioning. Turn it on for anything that mixes sizes on one line. In top-edge mode a 9-point currency code next to a 14-point amount has its top aligned with the amount's top, so it rides visibly high; in baseline mode both runs sit on the same line, the way a typesetter would set them. The last block of the next example does exactly that

Size and style live in SetFont

TextOut has no size or style parameter. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) carries all of it, the last two arguments defaulting to ANSI_CHARSET and horizontal text, and whatever it sets stays in force for every TextOut that follows until the next SetFont. Most first-day confusion traces back to that. A line comes out bold because three calls earlier something set [fsBold] and nothing cleared it

var
  W: Single;
begin
  Pdf.BeginDoc;                                     // first page: A4, Arial 12
  Pdf.CurrentPage.TextFromBaseLine(True);           // every Y below is a baseline

  Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
  Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report');        // 24 pt regular

  Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
  Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue');                // 12 pt bold

  Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
  Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands');   // 11 pt italic

  Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
  Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY');             // styles combine

  // Mixed sizes on one baseline: measure while the 14 pt face is current
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
  Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR');            // 3 pt gap
  ...
end;

The style argument is an ordinary TFontStyles set, so [] is plain and [fsBold, fsItalic] is bold italic. fsUnderline and fsStrikeOut are drawn as thin filled bars under or through the run, and they follow the run when it is rotated. Size is in points, the same unit as the coordinates, which keeps vertical arithmetic simple: 11-point body text with a 16-point step leaves about five points of air between lines. There is no automatic line advance. Every Y is yours to compute, which is tedious for a paragraph and exactly right for a form whose fields sit at fixed positions

GetWideTextWidth measures with whatever font is current at the moment of the call, so in the last block the measurement has to happen while the 14-point bold face is still set. Move it below the second SetFont and the 9-point suffix lands on top of the amount's last digits

The font name is resolved against the fonts installed on the machine that builds the PDF, and a face that resolves on a developer desktop may be absent on a build server, so the face in the finished file depends on which machine produced it. Script coverage is the other half. A Latin-only face asked to draw Cyrillic or CJK produces missing-glyph boxes, and that is a property of the font file, not of the call. When one string mixes scripts that no single face covers, TextOutWithFallback takes the same four arguments, switches faces run by run for characters the primary font lacks, and restores the original font afterwards

HotPDF TextOut page showing Arial, Times New Roman, and Courier New rendered with regular, bold, and italic styles across several character sets

How does the angle argument rotate text?

A nonzero angle turns the run counterclockwise by that many degrees around its anchor, and the anchor is the top-left corner of the text in the text's own frame. The anchor does not move. Only the direction the glyphs march changes, so the coordinate that placed a horizontal label also places its rotated twin. Rotated runs keep that top-left anchor even when the page is in baseline mode; TextFromBaseLine shifts horizontal runs only

Diagram of HotPDF TextOut rotation geometry: runs at 0, 45, and 90 degrees pivoting counterclockwise around a fixed anchor, with Delphi TextOut call examples for spine labels, watermarks, and tilted column headers
A nonzero angle turns the run counterclockwise about its unmoving anchor, the top-left corner of the text, and shared baselines step X exactly the way horizontal lines step Y
var
  W, AxisMidY: Single;
begin
  AxisMidY := 400;                                // middle of the chart's Y axis

  // A 90-degree run reads bottom to top and grows upward from Y,
  // so centering it on the axis means starting half its width below the middle
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
  W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
  Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');

  // A diagonal watermark
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
  Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

  // Column headers tilted 60 degrees: same Y, step X
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
  Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;

Ninety degrees covers axis labels and spine titles. At 90 degrees the glyphs stand to the right of the anchor's X, one line height wide, and the run climbs toward the top of the page by its own width. That is why centering adds half the width to Y instead of subtracting it, and why a label anchored at the midpoint itself sits entirely above it. Sixty degrees suits column headers, letting a long label stand over a narrow column without spilling into its neighbor, and 45 degrees is the usual watermark angle. Runs that share a baseline get the same Y and a stepped X, exactly as stacked horizontal lines share an X and step Y

Why does the second page come out as A4 in Arial 12?

AddPage returns the new page's index and makes it current, and the new page starts from defaults: A4, Arial 12, top-edge positioning, zero character and word spacing. Nothing about the previous page carries across. That design keeps each page self-contained, since a PDF page's content stream starts with a fresh graphics state anyway, but it means any layout code that configures the page once at the top of the document will quietly switch paper size and typeface on page two. The fix is to put every per-page setting in one routine and call it after BeginDoc and after every AddPage

HotPDF: US Letter page diagram showing line-position arithmetic with a left margin, a top start line, and a fixed Leading stepping toward a bottom-margin limit, beside the named-constant layout loop with its manual AddPage guard and SetFont reset
Named anchors turn a column of magic numbers into arithmetic: advance the running Y by Leading each line and guard the bottom margin yourself with AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
  Pdf.CurrentPage.Size := Letter;               // a new page starts as A4
  Pdf.CurrentPage.TextFromBaseLine(True);       // a new page starts in top-edge mode
  Pdf.CurrentPage.SetFont('Arial', [], 11);     // a new page starts in Arial 12
end;

procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
  LeftMargin   = 72;   // 1 inch
  TopLine      = 72;   // first baseline, 1 inch down
  BottomMargin = 72;
  Leading      = 16;
var
  I: Integer;
  Y: Single;
begin
  StartReportPage(Pdf);
  Y := TopLine;
  for I := 0 to Lines.Count - 1 do
  begin
    if Y > Pdf.CurrentPage.Height - BottomMargin then   // 720 on Letter
    begin
      Pdf.AddPage;
      StartReportPage(Pdf);
      Y := TopLine;
    end;
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
    Y := Y + Leading;
  end;
end;

There is no flow layout underneath TextOut, and a Y past the bottom of the page is still a valid coordinate as far as the content stream is concerned: the text is written, it just falls outside the media box and no viewer shows it. So the loop checks Y before every line and opens a new page itself. Reading the limit from CurrentPage.Height instead of hard-coding 720 keeps the loop correct if someone later switches the helper back to A4, where the same one-inch margin puts the limit at 770

The quickest check is the one a reader's print dialog would do. Open the output, go to page two, and look at the document properties: a Letter page reports 8.5 by 11 inches, an A4 page about 8.27 by 11.69. If the first page and the second disagree, some per-page setting lives outside the helper

Character and word spacing are page state too

Sometimes the string is right and the width is wrong: a heading that should span a fixed rule, a code that reads better with looser digits, a line of labels whose gaps need opening up. PDF has two text-state parameters for this in ISO 32000-1, character spacing Tc (§9.3.2), added after every glyph, and word spacing Tw (§9.3.3), added at each space. HotPDF sets them with SetCharacterSpacing(Spacing: Single) and SetWordSpacing(Spacing: Single), in points at the default resolution, and like the font they stay in force until changed

HotPDF: Character spacing Tc spreading extra room evenly after every glyph of SUMMARY versus word spacing Tw pooling it at space characters only, wired into the set, draw, reset cycle that keeps spacing state from leaking into later paragraphs
Tc spreads its adjustment across every glyph while Tw acts only on the space character, and resetting in the next line keeps drawing state clean

Word spacing has a catch written into the specification. §9.3.3 applies Tw only to the single-byte character code 32, and explicitly not to a byte 32 inside a multi-byte code, so on its own the operator does nothing for text written with two-byte glyph codes, which is how embedded Unicode fonts are normally written. HotPDF writes Unicode runs that way, and on that path it applies the word spacing as explicit position adjustments after each space glyph, so SetWordSpacing widens the gaps either way. The trigger is still the plain space, U+0020. A run of Chinese or Japanese with no ASCII spaces gets no extra room at all, and an ideographic space (U+3000) is treated as an ordinary glyph

Character spacing is the right tool when a heading must reach a target width, because it spreads the change evenly instead of pooling it at the spaces. GetWideTextWidth counts the character and word spacing currently in force, so measure with both at zero, then derive the spacing:

const
  Heading = 'SUMMARY';
  Target  = 200;        // width of the rule the heading should span, in points
var
  Natural, Tc: Single;
begin
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // measure without leftover spacing
  Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);

  // Tc goes after every glyph, but only the gaps between glyphs are visible
  Tc := (Target - Natural) / (Length(Heading) - 1);

  Pdf.CurrentPage.SetCharacterSpacing(Tc);      // negative values tighten
  Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // reset before the body text

  Pdf.CurrentPage.SetWordSpacing(6);
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
  Pdf.CurrentPage.SetWordSpacing(0);
end;

The reset lines matter more than the set lines. Leave a 4-point character spacing in place after one heading and every paragraph below it inherits the stretch, which reads as a vague wrongness that survives a quick proofread and then shows up in the printed copy. Set the value, draw the one run that needs it, and zero it on the next line, so no later code has to know what an earlier section did. The same page offers SetHorizontalScaling (Tz, 100 by default) for condensing a run without changing its height, and SetTextRenderingMode with values such as trStroke for outlined text, both of which persist in exactly the same way

HotPDF TextOut page comparing horizontal text scaling, character spacing, word spacing, and fill versus stroke rendering modes

If this is your first page of output, the HotPDF Hello World example covers document setup and saving. For text that has to fill a column edge to edge, full justification for PDF text has its own walkthrough, and top-to-bottom CJK is covered in vertical text layout in HotPDF. TextOut, SetFont, the spacing calls and the page-size settings above all ship with the HotPDF Delphi Component for Delphi and C++Builder