Skip to main content
Table of Contents

Text and fonts in a PDF document

Text is where a PDF stops being a drawing and becomes a document — and where most of the layout decisions live. TTMSFNCPDFLib draws text three ways: plain text at a point, wrapped text in a rectangle or a set of columns, and Mini HTML for anything that needs mixed formatting inside one block. Around those calls sit the tools that make a layout reliable: measuring before drawing, the overflow count that tells you when to start another page, line-break and truncation modes, and font handling — embedding, and the fallback list that finds glyphs your primary font does not have. Reach for plain DrawText for labels, table cells and running text in one style; reach for DrawHTMLText as soon as a paragraph mixes weights, colours, links or images, because it lays the fragments out for you. Everything on this page applies to generating a new document between BeginDocument and EndDocument.

Drawing text

Use DrawText to draw plain or word-wrapped text. Its appearance comes from p.Graphics.Font — Name, Size, Color and Style:

Drawing TextPascalC++
p.Graphics.Font.Name := 'Segoe UI';
p.Graphics.Font.Size := 16;
p.Graphics.Font.Color := gcRed;
p.Graphics.Font.Style := [TFontStyle.fsBold];
p.Graphics.DrawText('Hello World !', PointF(10, 50));
p->Graphics()->Font->Name = "Segoe UI";
p->Graphics()->Font->Size = 16;
p->Graphics()->Font->Color = gcRed;
p->Graphics()->Font->Style = TFontStyles() << TFontStyle::fsBold;
p->Graphics()->DrawText("Hello World !", PointF(10, 50));

Text drawn on a PDF page

Which overload you call decides the layout behaviour and what comes back:

Call Returns Behaviour
DrawText(Text, Point, Calculate = False) TRectF One line at the point; no wrapping and no alignment. Explicit line breaks in Text still start new lines
DrawText(Text, Rect, Calculate = False) TRectF Wraps within the rectangle and applies Alignment; lines that do not fit above Rect.Bottom are not drawn
DrawText(Text, Rect, Columns, Padding = 5, DetectOverflow = False) Integer Splits Rect into Columns equal columns, flows the text through them, and returns the number of characters that did not fit
DrawText(Text, Rects, Padding = 5, DetectOverflow = False) Integer Flows the text through an array of rectangles in order, and returns the overflow count

Passing a TRectF instead of a TPointF is what enables word wrapping. Line breaks inside the string (#13#10 or sLineBreak) force a new line in the wrapping modes. Pass an integer column count as a third argument to flow text across multiple columns:

Pascal
p.Graphics.DrawText(s, RectF(10, 50, 500, 400), 3);

Text flowed across three columns

Padding in the column and rectangle-array overloads is a horizontal inset applied to both sides of every column, so it doubles as the gutter between columns.

Measuring text before drawing

Layout decisions usually need a size before anything is committed to the page: how tall a shaded box behind a title must be, whether a paragraph still fits on this page, how wide a label column should be. Measuring is always a separate, side-effect-free call, so it is cheap to measure first and draw second.

Call What it measures
CalculateText(Text) Single-line extent of Text in the current font
CalculateText(Text, Rect) The rectangle the wrapped text needs inside Rect's width, including lines that would fall below Rect.Bottom
CalculateTextOverflow(Text, Rect, Columns, Padding = 5) How many characters would not fit in the columns
CalculateTextOverflow(Text, Rects, Padding = 5) The same, for an array of rectangles
CalculateHTMLText(Text, Rect, Scale = 1) / CalculateHTMLText(Text, Scale = 1) The extent of Mini HTML text, constrained to a rectangle or unconstrained
GetTextRect The rectangle of the most recent DrawText or DrawHTMLText call

CalculateText is the same as calling the matching DrawText overload with Calculate = True, and CalculateTextOverflow is the column overload with DetectOverflow = True. With the general engine used on Windows, Linux, VCL and WEB, a measured rectangle is as wide as its widest line and as tall as the number of lines multiplied by Font.Size * LineHeightFactor.

The overflow count is also returned by the drawing column overload, which turns it into a pagination loop: draw what fits, keep the tail of the string, and start a new page while anything is left. The characters that did not fit are always the last Overflow characters of the string you passed.

Measure text, then flow a long body across pagesPascalC++
procedure TForm1.GenerateFlowedArticle(const AFileName, ATitle, ABody: string);
var
  p: TTMSFNCPDFLib;
  TitleRect, Body: TRectF;
  Remaining: string;
  Overflow: Integer;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { Measure first. CalculateText lays the title out inside the available
        width and returns the rectangle it needs - nothing is drawn yet. }
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 20;
      p.Graphics.Font.Style := [TFontStyle.fsBold];
      TitleRect := p.Graphics.CalculateText(ATitle, RectF(50, 50, 545, 400));

      { A band exactly as tall as the measured title, then the title on top. }
      p.Graphics.Fill.Kind := gfkSolid;
      p.Graphics.Fill.Color := gcWhitesmoke;
      p.Graphics.Stroke.Kind := gskNone;
      p.Graphics.DrawRectangle(RectF(40, 40, 555, TitleRect.Bottom + 10));
      p.Graphics.DrawText(ATitle, RectF(50, 50, 545, TitleRect.Bottom));

      { The column overload draws what fits and returns the number of
        characters that did not; those are the tail of the string. }
      p.Graphics.Font.Size := 11;
      p.Graphics.Font.Style := [];
      Body := RectF(40, TitleRect.Bottom + 30, 555, 750);
      Remaining := ABody;
      Overflow := p.Graphics.DrawText(Remaining, Body, 1);
      while Overflow > 0 do
      begin
        { Guard against a rectangle too small to take even one line. }
        if Overflow >= Length(Remaining) then
          Break;
        Remaining := Copy(Remaining, Length(Remaining) - Overflow + 1, Overflow);
        p.NewPage;
        Body := RectF(40, 40, 555, 750);
        Overflow := p.Graphics.DrawText(Remaining, Body, 1);
      end;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::GenerateFlowedArticle(const String AFileName, const String ATitle, const String ABody)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // Measure first. CalculateText lays the title out inside the available
      // width and returns the rectangle it needs - nothing is drawn yet.
      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 20;
      p->Graphics()->Font->Style = TFontStyles() << TFontStyle::fsBold;
      TRectF TitleRect = p->Graphics()->CalculateText(ATitle, RectF(50, 50, 545, 400));

      // A band exactly as tall as the measured title, then the title on top.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
      p->Graphics()->Fill->Color = gcWhitesmoke;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
      p->Graphics()->DrawRectangle(RectF(40, 40, 555, TitleRect.Bottom + 10));
      p->Graphics()->DrawText(ATitle, RectF(50, 50, 545, TitleRect.Bottom));

      // The column overload draws what fits and returns the number of
      // characters that did not; those are the tail of the string.
      p->Graphics()->Font->Size = 11;
      p->Graphics()->Font->Style = TFontStyles();
      TRectF Body = RectF(40, TitleRect.Bottom + 30, 555, 750);
      String Remaining = ABody;
      int Overflow = p->Graphics()->DrawText(Remaining, Body, 1);
      while (Overflow > 0)
      {
        // Guard against a rectangle too small to take even one line.
        if (Overflow >= Remaining.Length())
          break;
        Remaining = Remaining.SubString(Remaining.Length() - Overflow + 1, Overflow);
        p->NewPage();
        Body = RectF(40, 40, 555, 750);
        Overflow = p->Graphics()->DrawText(Remaining, Body, 1);
      }
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

Note

DetectOverflow = True makes the column overloads measure only — nothing is drawn. Leave it False in a loop like the one above, where the same call must both draw the part that fits and report what is left.

HTML text

DrawHTMLText renders Mini HTML-formatted text, which is the way to mix bold, colour, and links inside a single paragraph without splitting it into separate DrawText calls and computing each fragment's width. It wraps words, advances lines, and places inline images for you.

Pascal
p.Graphics.DrawHTMLText(s, RectF(10, 50, 300, 400));

HTML-formatted text in a PDF

The engine understands this tag set:

Tag Effect
<b>, <i>, <u>, <s> Bold, italic, underline, strike-through
<sup>, <sub> Superscript and subscript, drawn at 70 % of the current size
<br> Line break
<p align="left\|center\|right" indent="n" bgcolor="..."> Paragraph with alignment, left indent in points and background colour; </p> ends the line
<font face="..." size="n" color="..." bgcolor="..."> Font name, size, text colour and highlight. size 1–5 map to 8, 10, 12, 14 and 16 pt; any other value is used as points
<a href="..."> Link text, drawn blue and underlined, with a clickable link annotation
<img src="..." width="n" height="n" align="left\|center\|right"> Inline image. Give one dimension to scale proportionally
<ul> + <li type="circle\|square\|star\|arrow\|tick"> Bulleted list; without type the bullet is a dash
<hr color="..."> Horizontal rule
<ind x="n"> Indent the current line by n points

Colours are either #RRGGBB or a colour name. The entities &amp;, &lt;, &gt;, &nbsp;, &quot;, &copy;, &reg;, &trade;, &euro;, &pound;, &sect;, &para; and &dollar; are decoded.

The src of an image is looked up by name in the document's bitmap container — assign p.BitmapContainer (or p.Graphics.BitmapContainer) before drawing. On desktop targets a file:// path is accepted as well; the WEB target has no local file system and resolves container names only.

Three rules about how the markup combines with the canvas state explain most surprises:

  • Only Font.Name and Font.Size carry over. Text colour and style come from the markup; unformatted HTML text is black and regular whatever Graphics.Font.Color and Graphics.Font.Style say.
  • </font> resets everything. Closing a <font> returns the text to plain black — bold, italic and the other styles opened before it end too. Nest <font> outermost and close it last.
  • Use <br> for line breaks. Raw line-break characters in the markup are not a reliable way to break lines.
Mini HTML formatting, images and pagingPascalC++
procedure TForm1.GenerateNewsletter(const AFileName: string);
var
  p: TTMSFNCPDFLib;
  Html: string;
  I: Integer;
begin
  { A heading, then a paragraph that mixes weight, colour and superscript.
    </font> resets every formatting tag in effect, so it closes last. }
  Html :=
    '<p align="center"><font size="18" color="#1F4E79"><b>Quarterly update</b></font></p><br>' +
    '<p>Revenue grew <b>12%</b> year over year, driven by <i>subscription</i> ' +
    'renewals. Churn fell to <font color="#1F7A1F"><b>2.1%</b></font>, the lowest ' +
    'in five years.<sup>1</sup></p>' +
    '<hr color="#1F4E79">' +
    { List items are written without </li>: the item ends at the next tag. }
    '<ul><li type="tick">Three new regions opened' +
    '<li type="tick">Support response under four hours' +
    '<li type="circle">Mobile app in public beta</ul>' +
    '<p><font bgcolor="#FFF2CC">Next review: 14 October.</font> Details at ' +
    '<a href="https://www.example.com/q3">www.example.com/q3</a>.</p><br>' +
    { src names an image in the BitmapContainer; align belongs to the img tag. }
    '<img src="logo" width="80" align="right"><br>';

  { Enough regional notes to run past the first page. }
  for I := 1 to 40 do
    Html := Html + Format('<p><b>Region %d.</b> Targets met; ' +
      'pipeline &amp; staffing on plan.</p>', [I]);

  Html := Html + '<br><font size="1"><sup>1</sup> Excluding one-off migrations.</font>';

  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    { A TTMSFNCBitmapContainer on the form that holds an image named 'logo'. }
    p.BitmapContainer := TMSFNCBitmapContainer1;

    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { DrawHTMLText takes only the font name and size from Graphics.Font;
        colour and style come from the markup. }
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 11;

      { Paging = True: when the text overflows the rectangle, a new page is
        started and the text continues there. }
      p.Graphics.DrawHTMLText(Html, RectF(40, 40, 555, 750), True);

      { DrawHTMLText leaves Alignment set by the last paragraph it drew. }
      p.Graphics.Alignment := gtaLeading;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::GenerateNewsletter(const String AFileName)
{
  // A heading, then a paragraph that mixes weight, colour and superscript.
  // </font> resets every formatting tag in effect, so it closes last.
  String Html =
    "<p align=\"center\"><font size=\"18\" color=\"#1F4E79\"><b>Quarterly update</b></font></p><br>"
    "<p>Revenue grew <b>12%</b> year over year, driven by <i>subscription</i> "
    "renewals. Churn fell to <font color=\"#1F7A1F\"><b>2.1%</b></font>, the lowest "
    "in five years.<sup>1</sup></p>"
    "<hr color=\"#1F4E79\">"
    // List items are written without </li>: the item ends at the next tag.
    "<ul><li type=\"tick\">Three new regions opened"
    "<li type=\"tick\">Support response under four hours"
    "<li type=\"circle\">Mobile app in public beta</ul>"
    "<p><font bgcolor=\"#FFF2CC\">Next review: 14 October.</font> Details at "
    "<a href=\"https://www.example.com/q3\">www.example.com/q3</a>.</p><br>"
    // src names an image in the BitmapContainer; align belongs to the img tag.
    "<img src=\"logo\" width=\"80\" align=\"right\"><br>";

  // Enough regional notes to run past the first page.
  for (int I = 1; I <= 40; I++)
    Html = Html + Format("<p><b>Region %d.</b> Targets met; "
      "pipeline &amp; staffing on plan.</p>", ARRAYOFCONST((I)));

  Html = Html + "<br><font size=\"1\"><sup>1</sup> Excluding one-off migrations.</font>";

  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    // A TTMSFNCBitmapContainer on the form that holds an image named "logo".
    p->BitmapContainer = TMSFNCBitmapContainer1;

    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // DrawHTMLText takes only the font name and size from Graphics.Font;
      // colour and style come from the markup.
      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 11;

      // Paging = true: when the text overflows the rectangle, a new page is
      // started and the text continues there.
      p->Graphics()->DrawHTMLText(Html, RectF(40, 40, 555, 750), true);

      // DrawHTMLText leaves Alignment set by the last paragraph it drew.
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

Flowing HTML text across pages

A long HTML body does not have to be cut into page-sized pieces by hand. Pass Paging = True (the third argument of the rectangle overload) and, when the text reaches the bottom of the rectangle, DrawHTMLText starts a new page and continues there, so headers, footers and page numbers are added exactly as they are for NewPage. Give the rectangle the full body area of the page — its horizontal bounds are reused on every continuation page. Without paging, the bottom of the rectangle is not a reliable limit for HTML text; when a block has to fit a fixed area, measure it first with CalculateHTMLText and shorten or split it yourself.

The Scale argument multiplies every font size and image dimension in the markup, which is a quick way to reuse one block of HTML at a different size.

Rich text is not the portable route

p.GraphicsEx.RichText and DrawRichText render attributed text through the platform's native text engine. The general engine used on Windows, Linux, VCL and WEB does not implement it — DrawRichText returns without drawing there. Use DrawHTMLText for formatted text that must render on every target.

Alignment

Set p.Graphics.Alignment to gtaLeading (default), gtaCenter, or gtaTrailing. Alignment is a property of the canvas, not an argument to DrawText, so it applies to every subsequent call until it is changed — set it, draw, and set it back. Right-aligned currency in a table is the usual reason to reach for it.

Alignment only takes effect in the rectangle overloads: it positions each line between the rectangle's left and right edges. The point overload has no width to align within and always starts the text at the point, so a right-aligned figure needs a rectangle whose right edge is the column edge.

Note

For HTML text, align with <p align="..."> rather than Graphics.Alignment. DrawHTMLText sets the alignment per paragraph from the markup and leaves Graphics.Alignment at the value of the last paragraph it drew — reset it before the next DrawText call.

Total page count placeholder

A footer that reads "Page 3 of 12" is impossible to draw honestly in a single pass: the total is only known once the last page has been written. Rather than buffering the whole document or making two passes, embed the PDFPageCountRef constant in the text and let the library substitute the real total during finalisation. Reach for it whenever a running header or footer has to state the document length; for anything else, plain Format with a number you already know is simpler.

Draw a "Page N of M" footer with the total-page placeholderPascalC++
uses
  FMX.TMSFNCPDFLib, FMX.TMSFNCPDFCoreLibBase, FMX.TMSFNCGraphicsTypes;

procedure TForm1.GenerateNumberedReport(const AFileName: string;
  const ALines: TStrings);
var
  p: TTMSFNCPDFLib;
  I: Integer;
  Footer: TRectF;
begin
  p := TTMSFNCPDFLib.Create;
  try
    { Header and Footer default to the literal captions 'Header' and 'Footer';
      clear them so only the footer drawn below appears. }
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      for I := 0 to ALines.Count - 1 do
      begin
        p.NewPage;

        p.Graphics.Alignment := gtaLeading;
        p.Graphics.Font.Size := 12;
        p.Graphics.DrawText(ALines[I], RectF(40, 40, 555, 740));

        { PDFPageCountRef is a literal placeholder token. It survives into the
          content stream and is replaced with the real total page count when
          the document is finalised - so it can be drawn on page 1 even though
          the total is not known yet. }
        Footer := RectF(40, 760, 555, 790);
        p.Graphics.Alignment := gtaTrailing;
        p.Graphics.Font.Size := 9;
        p.Graphics.DrawText(Format('Page %d of %s', [I + 1, PDFPageCountRef]),
          Footer);
        p.Graphics.Alignment := gtaLeading;
      end;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::GenerateNumberedReport(const String AFileName,
  TStrings *ALines)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    // Header and Footer default to the literal captions "Header" and "Footer";
    // clear them so only the footer drawn below appears.
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      for (int I = 0; I < ALines->Count; I++)
      {
        p->NewPage();

        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
        p->Graphics()->Font->Size = 12;
        p->Graphics()->DrawText(ALines->Strings[I], RectF(40, 40, 555, 740));

        // PDFPageCountRef is a literal placeholder token. It survives into the
        // content stream and is replaced with the real total page count when
        // the document is finalised - so it can be drawn on page 1 even though
        // the total is not known yet.
        TRectF Footer = RectF(40, 760, 555, 790);
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
        p->Graphics()->Font->Size = 9;
        p->Graphics()->DrawText(Format("Page %d of %s", ARRAYOFCONST((I + 1, PDFPageCountRef))),
          Footer);
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
      }
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

The mechanism has one subtlety worth understanding, because it explains the only way this can look wrong. Text width is measured while the page is written, but the placeholder's width is not yet known, so measurement substitutes PageCountHint — the library's running estimate of the total. For gtaLeading text that is harmless: the line starts at the left edge regardless of how wide it ends up. For gtaCenter and gtaTrailing text the start x depends on the final width, so instead of committing a position the library writes an x-position placeholder and records it in the output writer's PageCountXPosInfos array, together with the font, font size, character spacing, and the width of the line excluding the page-count token. When the document is finalised, each recorded entry is replayed with the true total and the correct x is written back. This is why a centred or right-aligned "Page N of M" stays correctly aligned even when the total rolls from 9 to 10 pages.

Practical consequences:

  • PDFPageCountRef is a plain string constant from FMX.TMSFNCPDFCoreLibBase — compose it with Format or ordinary concatenation, do not try to escape it.
  • It is substituted by the general PDF engine used by TTMSFNCPDFLib on Windows. On platforms that render through a native PDF engine, compute the total yourself and draw it as literal text.
  • PageCountXPosInfos and PageCountHint on TTMSFNCPDFGraphicsLibOutputWriter are the bookkeeping for this pass. They are read-only diagnostics from an application's point of view; the correction runs automatically and there is nothing to trigger by hand.

Line break mode

Graphics.LineBreakMode decides what happens when text is wider than its rectangle: wrap onto more lines, or stay on one line and give something up. Wrapping suits paragraphs; the single-line modes suit fixed-height cells, where an ellipsis reads better than a clipped glyph or a row that grows.

Value Description
bmLineBreakModeWordWrap Wrap at word boundaries — spaces, hyphens and line breaks (default)
bmLineBreakModeCharacterWrap Wrap at any character
bmLineBreakModeClip One line, cut at the rectangle edge
bmLineBreakModeHeadTruncation One line; the start is replaced by ... so the end stays visible
bmLineBreakModeMiddleTruncation One line; the middle is replaced by ... so both ends stay visible
bmLineBreakModeTailTruncation One line; the end is replaced by ...
bmLineBreakModeNone One line, cut at the rectangle edge, with no truncation marker

The modes are fully supported on macOS and iOS. Windows, Linux and Android support them in a limited form: in the general engine used on Windows, Linux, VCL and WEB, the three truncation modes insert three full stops, Clip and None both cut at the edge, and in every single-line mode line breaks in the text are drawn as spaces. LineBreakMode is canvas state like Alignment, so restore bmLineBreakModeWordWrap after a truncated cell. DrawHTMLText always wraps at words, whatever the mode is set to.

Line height

Graphics.LineHeightFactor multiplies the font size to give the distance between lines — raise it for airy body text, lower it for dense tables. Its default is 1.4 with the general engine (Windows, Linux, VCL, WEB) and 1.0 on macOS, iOS and Android, so a layout tuned on one platform can run longer or shorter on another; set it explicitly when line counts matter. Call LineHeightFactorResetToDefault to restore the platform default.

A link in a generated document is two things at once: visible text, and a clickable annotation over it. Graphics.AddURL(AText, AURL, ARect) does both — it draws AText in ARect and places the link over the drawn text, honouring Alignment. It draws with Graphics.URLFont, not Graphics.Font; URLFont is blue and underlined by default and is a separate font object, so give it the name and size the surrounding text uses.

Inside Mini HTML, write <a href="..."> instead. The HTML engine styles link text itself — blue and underlined in the current font — and does not use URLFont.

Links that jump to another page of the same document (AddGoTo) are covered with the other link annotations in Links.

Fonts

Font embedding

By default, a font subset is embedded in the PDF for portability. Set EmbedFonts := False to disable embedding — smaller files and faster generation, at the cost of correct rendering anywhere the font is missing. That trade is only safe for print-only workflows on machines you control.

EmbedFontType controls the embedding strategy:

Value Description
eftNative Platform native embedding (default)
eftCustom TMS cross-platform embedding

On Android, set Graphics.Font.FileName to a TTF file path to use a font that is not registered in the system directory.

The automatic header, footer and page number have fonts of their own — HeaderFont, FooterFont and PageNumberFont on the document — see Headers, footers, and page numbering.

Non-Latin text and font fallback

Strings are Unicode, so Cyrillic, Greek, Chinese, Japanese or symbol characters are drawn by passing them to DrawText or DrawHTMLText like any other text. The one requirement is a font that contains the glyphs, and a document font rarely covers every script. That is what FontFallBackList is for: when a string contains a character the current font lacks, the general engine tries the fonts in this list in order and switches to the first one that has it.

The list starts with platform defaults, so common scripts work without any setup:

Platform Default fallback fonts
Windows Nirmala UI, Leelawadee UI, Tahoma, SimSun, Segoe UI Symbol, Arial Unicode MS
Linux Noto Sans, Noto Sans CJK (SC, TC, JP), Noto Serif CJK SC, WenQuanYi Micro Hei and Zen Hei, AR PL UMing and UKai
WEB Roboto, Nirmala UI, Leelawadee UI, Tahoma, SimSun, Segoe UI Symbol, Arial Unicode MS — only fonts present in the font cache count

A name that is not installed is skipped, so adding a font you prefer is safe: Insert it at the front to try it first, or Add it to try it last. Assigning a new list to FontFallBackList replaces the defaults. Embedding is what makes the substituted glyphs render on the reader's machine, so keep EmbedFonts enabled for multilingual documents.

Warning

Fallback supplies glyphs; it does not shape text. The general engine draws characters left to right in the order they are stored, with one glyph per character, so right-to-left scripts such as Arabic and Hebrew, and scripts whose letters change form or combine, do not come out in correct reading form. Test such output on each target platform before relying on it.

Common mistakes

  • Alignment "leaking" into later text. Alignment is canvas state, not a per-call argument. Reset it after the aligned block — and after DrawHTMLText, which leaves it at the last paragraph's alignment.
  • Aligning with the point overload. DrawText(Text, Point) ignores Alignment; give right-aligned or centred text a rectangle.
  • Expecting wrapping from the point overload. DrawText(Text, Point) never wraps — pass a rectangle.
  • Text silently missing at the bottom of a rectangle. The rectangle overload drops lines that do not fit. Use the column overload and its overflow count, or measure with CalculateText first.
  • Setting DetectOverflow := True in a pagination loop. It turns the call into a measurement and nothing is drawn.
  • Styling HTML through Graphics.Font. Only the font name and size reach DrawHTMLText; set colour and weight with tags.
  • Closing </font> inside a bold run. It ends the bold as well.
  • An <img> that does not appear. Its src must match a bitmap name in the assigned bitmap container.
  • Leaving a truncation mode switched on. Wrapped paragraphs after a truncated cell come out as single lines. Restore bmLineBreakModeWordWrap.
  • AddURL text in the wrong font. It draws with URLFont, which does not follow Graphics.Font.
  • Replacing the fallback list by assignment. FontFallBackList := MyList drops the platform defaults; Insert or Add instead.
  • A missing font on the target machine. That is what EmbedFonts prevents; do not disable it for documents that leave your control.
  • Using DrawRichText for cross-platform output. It draws nothing on Windows, Linux, VCL and WEB; use DrawHTMLText.

Putting it together

A multilingual price list

City names in four scripts resolved through the fallback list, descriptions truncated to one line, right-aligned prices, and a clickable source link — font fallback, line break mode, alignment and hyperlinks in one table. Note how LineBreakMode and Alignment are set per column and restored before the link:

A multilingual price list with fallback fonts, truncation, alignment and a linkPascalC++
procedure TForm1.GeneratePriceList(const AFileName: string);
const
  Descriptions: array[0..3] of string = (
    'City centre, three nights, breakfast and airport transfer included',
    'Old town walking tour with a local guide',
    'Island ferry pass valid for all routes during the whole summer season',
    'High-speed rail day return');
  Prices: array[0..3] of string = ('1.240,00', '85,00', '310,50', '96,00');

  function U(const ACodes: array of Word): string;
  var
    I: Integer;
  begin
    { Build non-Latin text from code points so the source stays ASCII. }
    Result := '';
    for I := 0 to High(ACodes) do
      Result := Result + Char(ACodes[I]);
  end;

var
  p: TTMSFNCPDFLib;
  Cities: array[0..3] of string;
  I: Integer;
  y: Single;
begin
  Cities[0] := 'Paris';
  Cities[1] := U([$041C, $043E, $0441, $043A, $0432, $0430]);  { Moscow }
  Cities[2] := U([$0391, $03B8, $03AE, $03BD, $03B1]);         { Athens }
  Cities[3] := U([$5317, $4EAC]);                              { Beijing }

  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    { Tried in order when a glyph is missing from the current font; a name
      that is not installed (or, on WEB, not cached) is skipped. }
    p.FontFallBackList.Insert(0, 'Microsoft YaHei');

    p.BeginDocument(AFileName);
    try
      p.NewPage;
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 11;

      y := 60;
      for I := 0 to High(Cities) do
      begin
        { Column 1: characters Segoe UI lacks (the Chinese name) come from
          the first fallback font that has them. }
        p.Graphics.Alignment := gtaLeading;
        p.Graphics.LineBreakMode := bmLineBreakModeWordWrap;
        p.Graphics.DrawText(Cities[I], RectF(40, y, 140, y + 18));

        { Column 2: one line, cut with an ellipsis instead of wrapping. }
        p.Graphics.LineBreakMode := bmLineBreakModeTailTruncation;
        p.Graphics.DrawText(Descriptions[I], RectF(150, y, 440, y + 18));

        { Column 3: right-aligned. Alignment only applies to rectangle
          overloads, so the price gets a rectangle, not a point. }
        p.Graphics.LineBreakMode := bmLineBreakModeWordWrap;
        p.Graphics.Alignment := gtaTrailing;
        p.Graphics.DrawText(Char($20AC) + ' ' + Prices[I], RectF(450, y, 555, y + 18));
        y := y + 24;
      end;

      { Canvas state back to the defaults before the next block. }
      p.Graphics.Alignment := gtaLeading;
      p.Graphics.LineBreakMode := bmLineBreakModeWordWrap;

      { AddURL draws the text with URLFont (blue and underlined by default)
        and places a clickable link annotation over it. URLFont is a font of
        its own - it does not follow Graphics.Font - so name and size it. }
      p.Graphics.URLFont.Name := 'Segoe UI';
      p.Graphics.URLFont.Size := 9;
      p.Graphics.AddURL('Prices from www.example.com/tariffs',
        'https://www.example.com/tariffs', RectF(40, y + 20, 555, y + 34));
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
// Build non-Latin text from code points so the source stays ASCII.
static String U(const Word *ACodes, int ACount)
{
  String Result = "";
  for (int I = 0; I < ACount; I++)
    Result = Result + WideChar(ACodes[I]);
  return Result;
}

void __fastcall TForm1::GeneratePriceList(const String AFileName)
{
  const String Descriptions[4] = {
    "City centre, three nights, breakfast and airport transfer included",
    "Old town walking tour with a local guide",
    "Island ferry pass valid for all routes during the whole summer season",
    "High-speed rail day return"};
  const String Prices[4] = {"1.240,00", "85,00", "310,50", "96,00"};

  const Word Moscow[] = {0x041C, 0x043E, 0x0441, 0x043A, 0x0432, 0x0430};
  const Word Athens[] = {0x0391, 0x03B8, 0x03AE, 0x03BD, 0x03B1};
  const Word Beijing[] = {0x5317, 0x4EAC};

  String Cities[4];
  Cities[0] = "Paris";
  Cities[1] = U(Moscow, 6);
  Cities[2] = U(Athens, 5);
  Cities[3] = U(Beijing, 2);

  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    // Tried in order when a glyph is missing from the current font; a name
    // that is not installed (or, on WEB, not cached) is skipped.
    p->FontFallBackList->Insert(0, "Microsoft YaHei");

    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();
      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 11;

      float y = 60;
      for (int I = 0; I < 4; I++)
      {
        // Column 1: characters Segoe UI lacks (the Chinese name) come from
        // the first fallback font that has them.
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
        p->Graphics()->LineBreakMode = TTMSFNCPDFGraphicsLibLineBreakMode::bmLineBreakModeWordWrap;
        p->Graphics()->DrawText(Cities[I], RectF(40, y, 140, y + 18));

        // Column 2: one line, cut with an ellipsis instead of wrapping.
        p->Graphics()->LineBreakMode = TTMSFNCPDFGraphicsLibLineBreakMode::bmLineBreakModeTailTruncation;
        p->Graphics()->DrawText(Descriptions[I], RectF(150, y, 440, y + 18));

        // Column 3: right-aligned. Alignment only applies to rectangle
        // overloads, so the price gets a rectangle, not a point.
        p->Graphics()->LineBreakMode = TTMSFNCPDFGraphicsLibLineBreakMode::bmLineBreakModeWordWrap;
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
        p->Graphics()->DrawText(String(WideChar(0x20AC)) + " " + Prices[I], RectF(450, y, 555, y + 18));
        y = y + 24;
      }

      // Canvas state back to the defaults before the next block.
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
      p->Graphics()->LineBreakMode = TTMSFNCPDFGraphicsLibLineBreakMode::bmLineBreakModeWordWrap;

      // AddURL draws the text with URLFont (blue and underlined by default)
      // and places a clickable link annotation over it. URLFont is a font of
      // its own - it does not follow Graphics.Font - so name and size it.
      p->Graphics()->URLFont->Name = "Segoe UI";
      p->Graphics()->URLFont->Size = 9;
      p->Graphics()->AddURL("Prices from www.example.com/tariffs",
        "https://www.example.com/tariffs", RectF(40, y + 20, 555, y + 34));
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

The full report

Plain text, Mini HTML, and trailing alignment in one page — the source of the screenshot on the chapter index. Note how Alignment is set immediately before each currency figure and restored straight afterwards:

A complete one-page PDF reportPascalC++
procedure TForm1.BuildReport(const AFileName: string);
const
  Rows: array[0..7, 0..2] of string = (
    ('Northern Europe', 'Alice Moreau', '184.000'),
    ('Southern Europe', 'Bob Devlin',   '72.500'),
    ('North America',   'Carol Yang',   '412.800'),
    ('South America',   'Dan Pereira',  '43.000'),
    ('Middle East',     'Eve Novak',    '159.000'),
    ('South Asia',      'Frank Bauer',  '91.000'),
    ('East Asia',       'Grace Lim',    '213.000'),
    ('Oceania',         'Hugo Marsh',   '66.500'));
var
  p: TTMSFNCPDFLib;
  i: Integer;
  y: Single;
  Euro: string;
begin
  { Build the euro sign from its code point so the source stays ASCII-safe. }
  Euro := Char($20AC);

  p := TTMSFNCPDFLib.Create;
  try
    { Header/Footer default to a non-empty value - clear them before drawing
      your own, or the built-in caption appears at the top of every page. }
    p.Header := '';
    p.Footer := '';

    { A custom page fitted to the content, so the PDF has no trailing empty
      space below the table. }
    p.PageSize := psCustom;
    p.PageWidth := 595;
    p.PageHeight := 440;

    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { Header band: a filled rectangle with no stroke, drawn before the text
        that sits on top of it. }
      p.Graphics.Fill.Color := gcSteelblue;
      p.Graphics.Fill.Kind := gfkSolid;
      p.Graphics.Stroke.Kind := gskNone;
      p.Graphics.DrawRectangle(RectF(40, 40, 555, 92));

      p.Graphics.Font.Color := gcWhite;
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 18;
      p.Graphics.DrawText('Regional sales report', PointF(54, 55));
      p.Graphics.Font.Size := 10;
      p.Graphics.DrawText('Fiscal year 2026 - all regions', PointF(54, 76));

      { Mini HTML for a paragraph that mixes weight and colour inline. }
      p.Graphics.Font.Color := gcBlack;
      p.Graphics.Font.Size := 10;
      p.Graphics.DrawHTMLText(
        'Revenue by region for the full year. Figures in <b>EUR</b>, ' +
        'rounded to the nearest thousand. Regions above ' +
        '<font color="#1F7A1F"><b>150.000</b></font> are on track.',
        RectF(40, 108, 555, 148));

      { Table header. Alignment is canvas state, so it is set for the currency
        column and restored immediately afterwards. }
      y := 158;
      p.Graphics.Fill.Color := gcGainsboro;
      p.Graphics.Stroke.Kind := gskSolid;
      p.Graphics.Stroke.Color := gcSilver;
      p.Graphics.DrawRectangle(RectF(40, y, 555, y + 24));
      p.Graphics.Font.Size := 10;
      p.Graphics.DrawText('Region', PointF(50, y + 6));
      p.Graphics.DrawText('Account manager', PointF(220, y + 6));
      p.Graphics.Alignment := gtaTrailing;
      p.Graphics.DrawText('Revenue', RectF(390, y + 6, 545, y + 22));
      p.Graphics.Alignment := gtaLeading;

      { Rows, with alternating band fills. }
      y := y + 24;
      for i := 0 to High(Rows) do
      begin
        if i mod 2 = 1 then
          p.Graphics.Fill.Color := gcWhitesmoke
        else
          p.Graphics.Fill.Color := gcWhite;
        p.Graphics.DrawRectangle(RectF(40, y, 555, y + 22));
        p.Graphics.DrawText(Rows[i][0], PointF(50, y + 5));
        p.Graphics.DrawText(Rows[i][1], PointF(220, y + 5));
        p.Graphics.Alignment := gtaTrailing;
        p.Graphics.DrawText(Euro + ' ' + Rows[i][2],
          RectF(390, y + 5, 545, y + 21));
        p.Graphics.Alignment := gtaLeading;
        y := y + 22;
      end;

      { Totals row. }
      p.Graphics.Fill.Color := gcLightsteelblue;
      p.Graphics.DrawRectangle(RectF(40, y, 555, y + 24));
      p.Graphics.DrawText('Total', PointF(50, y + 6));
      p.Graphics.Alignment := gtaTrailing;
      p.Graphics.DrawText(Euro + ' 1.241.800', RectF(390, y + 6, 555, y + 22));
      p.Graphics.Alignment := gtaLeading;

      { Footer rule and caption, clear of the table. }
      p.Graphics.Stroke.Color := gcSilver;
      p.Graphics.DrawLine(PointF(40, 398), PointF(555, 398));
      p.Graphics.Font.Size := 8;
      p.Graphics.Font.Color := gcGray;
      p.Graphics.DrawText('Generated with TMS FNC PDF Library', PointF(40, 406));
      p.Graphics.Alignment := gtaTrailing;
      p.Graphics.DrawText('Page 1 of 1', RectF(390, 406, 555, 420));
      p.Graphics.Alignment := gtaLeading;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::BuildReport(const String AFileName)
{
  const String Rows[8][3] = {
    {"Northern Europe", "Alice Moreau", "184.000"},
    {"Southern Europe", "Bob Devlin",   "72.500"},
    {"North America",   "Carol Yang",   "412.800"},
    {"South America",   "Dan Pereira",  "43.000"},
    {"Middle East",     "Eve Novak",    "159.000"},
    {"South Asia",      "Frank Bauer",  "91.000"},
    {"East Asia",       "Grace Lim",    "213.000"},
    {"Oceania",         "Hugo Marsh",   "66.500"}};

  // Build the euro sign from its code point so the source stays ASCII-safe.
  String Euro = WideChar(0x20AC);

  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    // Header/Footer default to a non-empty value - clear them before drawing
    // your own, or the built-in caption appears at the top of every page.
    p->Header = "";
    p->Footer = "";

    // A custom page fitted to the content, so the PDF has no trailing empty
    // space below the table.
    p->PageSize = TTMSFNCPDFLibPageSize::psCustom;
    p->PageWidth = 595;
    p->PageHeight = 440;

    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // Header band: a filled rectangle with no stroke, drawn before the text
      // that sits on top of it.
      p->Graphics()->Fill->Color = gcSteelblue;
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
      p->Graphics()->DrawRectangle(RectF(40, 40, 555, 92));

      p->Graphics()->Font->Color = gcWhite;
      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 18;
      p->Graphics()->DrawText("Regional sales report", PointF(54, 55));
      p->Graphics()->Font->Size = 10;
      p->Graphics()->DrawText("Fiscal year 2026 - all regions", PointF(54, 76));

      // Mini HTML for a paragraph that mixes weight and colour inline.
      p->Graphics()->Font->Color = gcBlack;
      p->Graphics()->Font->Size = 10;
      p->Graphics()->DrawHTMLText(
        "Revenue by region for the full year. Figures in <b>EUR</b>, "
        "rounded to the nearest thousand. Regions above "
        "<font color=\"#1F7A1F\"><b>150.000</b></font> are on track.",
        RectF(40, 108, 555, 148));

      // Table header. Alignment is canvas state, so it is set for the currency
      // column and restored immediately afterwards.
      float y = 158;
      p->Graphics()->Fill->Color = gcGainsboro;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
      p->Graphics()->Stroke->Color = gcSilver;
      p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 24));
      p->Graphics()->Font->Size = 10;
      p->Graphics()->DrawText("Region", PointF(50, y + 6));
      p->Graphics()->DrawText("Account manager", PointF(220, y + 6));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
      p->Graphics()->DrawText("Revenue", RectF(390, y + 6, 545, y + 22));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;

      // Rows, with alternating band fills.
      y = y + 24;
      for (int i = 0; i < 8; i++)
      {
        if (i % 2 == 1)
          p->Graphics()->Fill->Color = gcWhitesmoke;
        else
          p->Graphics()->Fill->Color = gcWhite;
        p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 22));
        p->Graphics()->DrawText(Rows[i][0], PointF(50, y + 5));
        p->Graphics()->DrawText(Rows[i][1], PointF(220, y + 5));
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
        p->Graphics()->DrawText(Euro + " " + Rows[i][2],
          RectF(390, y + 5, 545, y + 21));
        p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
        y = y + 22;
      }

      // Totals row.
      p->Graphics()->Fill->Color = gcLightsteelblue;
      p->Graphics()->DrawRectangle(RectF(40, y, 555, y + 24));
      p->Graphics()->DrawText("Total", PointF(50, y + 6));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
      p->Graphics()->DrawText(Euro + " 1.241.800", RectF(390, y + 6, 555, y + 22));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;

      // Footer rule and caption, clear of the table.
      p->Graphics()->Stroke->Color = gcSilver;
      p->Graphics()->DrawLine(PointF(40, 398), PointF(555, 398));
      p->Graphics()->Font->Size = 8;
      p->Graphics()->Font->Color = gcGray;
      p->Graphics()->DrawText("Generated with TMS FNC PDF Library", PointF(40, 406));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaTrailing;
      p->Graphics()->DrawText("Page 1 of 1", RectF(390, 406, 555, 420));
      p->Graphics()->Alignment = TTMSFNCGraphicsTextAlign::gtaLeading;
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

See also