Table of Contents

Drawing graphics on a PDF page

After NewPage, p.Graphics is the drawing surface for the current page. It offers two levels: the low-level primitives on this page — lines, rectangles, paths, images — and, when you would rather reuse code you already have, the graphics engine, which exposes the same API as any other FNC canvas so an existing paint routine can render straight into the PDF.

Coordinates are PDF points (1/72 inch) with the origin at the top-left of the page. See Working in millimetres or inches when your measurements come from a print spec.

Lines, rectangles, and paths

p.Graphics.Stroke.Color := gcRed;
p.Graphics.Stroke.Width := 3;
p.Graphics.Stroke.Kind := gskDashDotDot;
p.Graphics.DrawLine(PointF(10, 50), PointF(100, 50));

Lines, rectangles and paths drawn on a PDF page

Rectangles support fill with optional gradients:

p.Graphics.Fill.Kind := gfkGradient;
p.Graphics.Fill.Color := gcBlue;
p.Graphics.Fill.ColorTo := gcOrange;
p.Graphics.DrawRectangle(RectF(10, 50, 100, 150));

Rectangles with gradient fills

Custom paths

Build paths with DrawPathBegin / DrawPathMoveToPoint / DrawPathAddLineToPoint / DrawPathClose / DrawPathEnd:

p.Graphics.DrawPathBegin;
p.Graphics.DrawPathMoveToPoint(PointF(x, y));
for I := 1 to 5 do
begin
  x := rad * sin((i * 4 * pi + angle) / 5) + st.X;
  y := rad * cos((i * 4 * pi + angle) / 5) + st.Y;
  p.Graphics.DrawPathAddLineToPoint(PointF(x, y));
end;
p.Graphics.DrawPathClose;
p.Graphics.DrawPathEnd;

A custom path drawn on a PDF page

Path drawing modes

DrawPathEnd takes the mode that decides whether the path is stroked, filled, or used as a clip. This is the parameter that most often explains "my path is invisible" — a path ended in a fill-only mode with no fill colour set draws nothing.

Clipping paths

Ending a path in a clipping mode restricts every subsequent drawing operation to that shape until the clip is reset. Use it for a rounded-corner photo, a masked chart area, or any shape a rectangle cannot express.

Linear gradient paths

A path can be filled with a linear gradient rather than a solid colour, which is what produces a banded header or a shaded chart series without drawing dozens of adjacent rectangles.

Transforms

Transforms apply a matrix to subsequent drawing — translation, rotation, scaling — so a repeated element (a rotated watermark, a rotated column header) is described once and positioned by the matrix rather than recomputed per instance. See TTMSFNCGraphicsMatrix for the coefficient layout.

Clearing a region

DrawClear(Rect) erases the content inside a rectangle by painting it with the background colour — useful when stamping over existing page content.

Images

Draw images with DrawImage, DrawImageFromFile, or DrawImageWithName (the last requires a BitmapContainer, which is the right choice when the same logo appears on many pages — it is embedded once and referenced per use):

p.BitmapContainer := TMSFNCBitmapContainer1;
p.Graphics.DrawImageWithName('MyImage', PointF(10, 50));
p.Graphics.DrawImageFromFile('MyImage.jpg', PointF(160, 50));
MyImage.LoadFromFile('MyImage.jpg');
p.Graphics.DrawImage(MyImage, PointF(310, 50));

Images drawn on a PDF page

The graphics engine

TTMSFNCGraphicsPDFEngine exposes the same drawing API as a regular FNC canvas, eliminating the need to build raw PDF paths manually. This is the shortest route from "I already draw this on screen" to "I can put it in a PDF": point the engine at the document and run the same paint code.

Add FMX.TMSFNCGraphicsPDFEngine to the uses clause:

uses
  FMX.TMSFNCPDFLib, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCGraphicsPDFEngine;

var
  p: TTMSFNCPDFLib;
  g: TTMSFNCGraphicsPDFEngine;
  pth: TTMSFNCGraphicsPath;
begin
  p := TTMSFNCPDFLib.Create;
  g := TTMSFNCGraphicsPDFEngine.Create(p);
  pth := TTMSFNCGraphicsPath.Create;
  try
    p.BeginDocument('output.pdf');
    p.NewPage;
    pth.MoveTo(PointF(200, 200));
    pth.AddLine(PointF(200, 200), PointF(300, 300));
    pth.ClosePath;
    g.DrawPath(pth);
    g.DrawText(RectF(100, 200, 300, 400), 'Hello!', False, gtaCenter, gtaCenter, gttNone, -45);
    p.EndDocument(True);
  finally
    pth.Free;
    g.Free;
    p.Free;
  end;
end;

Output produced through the FNC graphics engine

Tip

If the goal is to export a whole existing control or canvas rather than to run custom paint code, use TTMSFNCGraphicsPDFIO instead — it wraps the same engine in a one-call export. See the PDF Library overview.

Common mistakes

  • A path that draws nothing. Check the mode passed to DrawPathEnd and that a fill or stroke colour is actually set for that mode.
  • A clip that never ends. A clipping path stays in effect for subsequent drawing; reset it before drawing content that should not be masked.
  • Text hidden behind a shape. Drawing order is painter's order — draw fills first, text last.
  • A missing image with no error. DrawImageFromFile needs a path that exists at generation time; verify it rather than assuming the draw failed silently for another reason.

Putting it together: the full report

The header band, the ruled and alternately-filled table rows, and the footer rule in the chapter index screenshot are all shapes from this page, drawn before the text that sits on them:

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;

See also