Skip to main content
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, ellipses, paths, images, and links — map almost one to one onto PDF operators, so you control exactly what is written. When you would rather reuse code you already have, the graphics engine exposes the same API as any other FNC canvas, so an existing paint routine renders straight into the PDF and you also get shapes the primitives do not offer, such as rounded rectangles.

Reach for the primitives when you lay out a document from data — a report, an invoice, a certificate — and want predictable output with no dependency on a control. Reach for the graphics engine when the drawing code already exists for a screen control, or when you need its richer shape set.

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 ellipses

DrawLine, DrawRectangle, and DrawEllipse cover most layout needs: rules, table bands, boxes, and status dots. They all read the current Fill and Stroke settings, so set those first and then draw.

Lines, Rectangles, and PathsPascalC++
p.Graphics.Stroke.Color := gcRed;
p.Graphics.Stroke.Width := 3;
p.Graphics.Stroke.Kind := gskDashDotDot;
p.Graphics.DrawLine(PointF(10, 50), PointF(100, 50));
p->Graphics()->Stroke->Color = gcRed;
p->Graphics()->Stroke->Width = 3;
p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskDashDotDot;
p->Graphics()->DrawLine(PointF(10, 50), PointF(100, 50));

Lines, rectangles and paths drawn on a PDF page

DrawRectangle and DrawEllipse decide for themselves what to paint: fill and outline when both are active, only the fill when Stroke.Kind is gskNone, only the outline when Fill.Kind is gfkNone. DrawLine uses only the stroke and draws nothing when Stroke.Kind is gskNone.

Rectangles support fill with optional gradients:

Lines, Rectangles, and PathsPascalC++
p.Graphics.Fill.Kind := gfkGradient;
p.Graphics.Fill.Color := gcBlue;
p.Graphics.Fill.ColorTo := gcOrange;
p.Graphics.DrawRectangle(RectF(10, 50, 100, 150));
p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkGradient;
p->Graphics()->Fill->Color = gcBlue;
p->Graphics()->Fill->ColorTo = gcOrange;
p->Graphics()->DrawRectangle(RectF(10, 50, 100, 150));

Rectangles with gradient fills

Fill and stroke

Graphics.Fill and Graphics.Stroke are the canvas state every shape and path reads. They persist until you change them, which is convenient for a run of identical rows and a common source of surprises when one shape's settings leak into the next — set them explicitly before each distinct element.

Setting Values Notes
Fill.Kind gfkNone, gfkSolid, gfkGradient gfkNone turns the fill off for DrawRectangle / DrawEllipse
Fill.Color / Fill.ColorTo any TTMSFNCGraphicsColor A gradient runs from Color to ColorTo across the shape's bounds
Fill.Orientation gfoHorizontal, gfoVertical Direction of a gradient fill
Stroke.Kind gskNone, gskSolid, gskDash, gskDot, gskDashDot, gskDashDotDot gskNone turns the outline off
Stroke.Color / Stroke.Width colour; width in points

The sample below draws one line per dash pattern, then shows the three outcomes of the automatic mode choice — a fill-only dot, an outline-only ring, and a gradient box with an outline:

Fill and stroke settingsPascalC++
procedure TForm1.DrawFillAndStrokeSamples(const AFileName: string);
const
  DashKinds: array[0..4] of TTMSFNCGraphicsStrokeKind =
    (gskSolid, gskDash, gskDot, gskDashDot, gskDashDotDot);
var
  p: TTMSFNCPDFLib;
  I: Integer;
  y: Single;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { One line per dash pattern. DrawLine only uses the stroke, and draws
        nothing at all when Stroke.Kind is gskNone. }
      p.Graphics.Stroke.Color := gcDarkslategray;
      p.Graphics.Stroke.Width := 2;
      for I := 0 to High(DashKinds) do
      begin
        y := 50 + I * 18;
        p.Graphics.Stroke.Kind := DashKinds[I];
        p.Graphics.DrawLine(PointF(40, y), PointF(240, y));
      end;

      { DrawEllipse and DrawRectangle choose the drawing mode themselves:
        a fill without a stroke, a stroke without a fill, or both. }
      p.Graphics.Fill.Kind := gfkSolid;
      p.Graphics.Fill.Color := gcSeagreen;
      p.Graphics.Stroke.Kind := gskNone;
      p.Graphics.DrawEllipse(RectF(280, 44, 330, 94));

      p.Graphics.Fill.Kind := gfkNone;
      p.Graphics.Stroke.Kind := gskSolid;
      p.Graphics.Stroke.Color := gcFirebrick;
      p.Graphics.Stroke.Width := 3;
      p.Graphics.DrawEllipse(RectF(350, 44, 400, 94));

      { A vertical gradient with a thin outline. The gradient runs from
        Fill.Color to Fill.ColorTo across the shape's bounds. }
      p.Graphics.Fill.Kind := gfkGradient;
      p.Graphics.Fill.Color := gcWhite;
      p.Graphics.Fill.ColorTo := gcGoldenrod;
      p.Graphics.Fill.Orientation := gfoVertical;
      p.Graphics.Stroke.Color := gcDimgray;
      p.Graphics.Stroke.Width := 1;
      p.Graphics.DrawRectangle(RectF(420, 44, 560, 94));
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::DrawFillAndStrokeSamples(const String AFileName)
{
  const TTMSFNCGraphicsStrokeKind DashKinds[5] =
    {TTMSFNCGraphicsStrokeKind::gskSolid, TTMSFNCGraphicsStrokeKind::gskDash,
     TTMSFNCGraphicsStrokeKind::gskDot, TTMSFNCGraphicsStrokeKind::gskDashDot,
     TTMSFNCGraphicsStrokeKind::gskDashDotDot};

  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // One line per dash pattern. DrawLine only uses the stroke, and draws
      // nothing at all when Stroke.Kind is gskNone.
      p->Graphics()->Stroke->Color = gcDarkslategray;
      p->Graphics()->Stroke->Width = 2;
      for (int I = 0; I < 5; I++)
      {
        float y = 50 + I * 18;
        p->Graphics()->Stroke->Kind = DashKinds[I];
        p->Graphics()->DrawLine(PointF(40, y), PointF(240, y));
      }

      // DrawEllipse and DrawRectangle choose the drawing mode themselves:
      // a fill without a stroke, a stroke without a fill, or both.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
      p->Graphics()->Fill->Color = gcSeagreen;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
      p->Graphics()->DrawEllipse(RectF(280, 44, 330, 94));

      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkNone;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
      p->Graphics()->Stroke->Color = gcFirebrick;
      p->Graphics()->Stroke->Width = 3;
      p->Graphics()->DrawEllipse(RectF(350, 44, 400, 94));

      // A vertical gradient with a thin outline. The gradient runs from
      // Fill.Color to Fill.ColorTo across the bounds of the shape.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkGradient;
      p->Graphics()->Fill->Color = gcWhite;
      p->Graphics()->Fill->ColorTo = gcGoldenrod;
      p->Graphics()->Fill->Orientation = TTMSFNCGraphicsFillOrientation::gfoVertical;
      p->Graphics()->Stroke->Color = gcDimgray;
      p->Graphics()->Stroke->Width = 1;
      p->Graphics()->DrawRectangle(RectF(420, 44, 560, 94));
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

Custom paths

A path is the shape you cannot get from a rectangle or ellipse: a star, a pennant, an arrow, a chart line. Build it between DrawPathBegin and DrawPathEnd, adding segments in between, and close it with DrawPathClose when the outline should return to its start point.

A gradient-filled star pathPascalC++
procedure TForm1.DrawGradientStar(const AFileName: string);
var
  p: TTMSFNCPDFLib;
  Center, Vertex: TPointF;
  Radius, Angle: Single;
  I: Integer;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;
      Center := PointF(160, 190);
      Radius := 110;

      { Fill and stroke are applied when the path begins, so configure them
        before DrawPathBegin. A gradient fill spans the bounds of the path. }
      p.Graphics.Fill.Kind := gfkGradient;
      p.Graphics.Fill.Color := gcLightblue;
      p.Graphics.Fill.ColorTo := gcSteelblue;
      p.Graphics.Fill.Orientation := gfoHorizontal;
      p.Graphics.Stroke.Kind := gskDot;
      p.Graphics.Stroke.Color := gcBlue;
      p.Graphics.Stroke.Width := 1;

      p.Graphics.DrawPathBegin;
      { Every second vertex of a pentagon, so the outline crosses itself. }
      for I := 0 to 4 do
      begin
        Angle := I * 4 * Pi / 5;
        Vertex := PointF(Center.X + Radius * Sin(Angle),
          Center.Y - Radius * Cos(Angle));
        if I = 0 then
          p.Graphics.DrawPathMoveToPoint(Vertex)
        else
          p.Graphics.DrawPathAddLineToPoint(Vertex);
      end;
      p.Graphics.DrawPathClose;

      { Fill (non-zero winding, so the centre is filled as well) and then
        stroke the outline. This is also the default when no mode is passed. }
      p.Graphics.DrawPathEnd(dmPathFillStroke);
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::DrawGradientStar(const String AFileName)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();
      TPointF Center = PointF(160, 190);
      float Radius = 110;

      // Fill and stroke are applied when the path begins, so configure them
      // before DrawPathBegin. A gradient fill spans the bounds of the path.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkGradient;
      p->Graphics()->Fill->Color = gcLightblue;
      p->Graphics()->Fill->ColorTo = gcSteelblue;
      p->Graphics()->Fill->Orientation = TTMSFNCGraphicsFillOrientation::gfoHorizontal;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskDot;
      p->Graphics()->Stroke->Color = gcBlue;
      p->Graphics()->Stroke->Width = 1;

      p->Graphics()->DrawPathBegin();
      // Every second vertex of a pentagon, so the outline crosses itself.
      for (int I = 0; I < 5; I++)
      {
        float Angle = I * 4 * M_PI / 5;
        TPointF Vertex = PointF(Center.X + Radius * sin(Angle),
          Center.Y - Radius * cos(Angle));
        if (I == 0)
          p->Graphics()->DrawPathMoveToPoint(Vertex);
        else
          p->Graphics()->DrawPathAddLineToPoint(Vertex);
      }
      p->Graphics()->DrawPathClose();

      // Fill (non-zero winding, so the centre is filled as well) and then
      // stroke the outline. This is also the default when no mode is passed.
      p->Graphics()->DrawPathEnd(TTMSFNCPDFGraphicsLibPathDrawingMode::dmPathFillStroke);
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

A custom path drawn on a PDF page

DrawPathBegin applies the current fill and stroke to the path it starts, so configure Fill and Stroke before calling it, not between the segments.

Path segments

Method Adds
DrawPathMoveToPoint(Point) Moves the current point without drawing; starts a new sub-path
DrawPathAddLineToPoint(Point) A straight line from the current point
DrawPathAddLines(Points) A polyline: moves to the first point, then a line to each following point
DrawPathAddCurveToPoint(Control1, Control2, EndPoint) A cubic Bezier curve
DrawPathAddQuadCurveToPoint(Control, EndPoint) A quadratic curve
DrawPathAddRectangle(Rect) A closed rectangle sub-path
DrawPathAddEllipse(Rect) A closed ellipse sub-path; start the path with DrawPathMoveToPoint at the ellipse's left-middle point when it is the first segment
DrawPathClose A straight line back to the start of the current sub-path

Curves are what turn a sequence of data points into a smooth trend line, and a polyline is the compact way to draw an axis or a sparkline. The sample draws an axis as one polyline, a trend line from two cubic segments, and a filled shape closed from a quadratic curve:

Curves, polylines and a filled curved shapePascalC++
procedure TForm1.DrawTrendSketch(const AFileName: string);
var
  p: TTMSFNCPDFLib;
  Axis: TTMSFNCPDFGraphicsLibPointArray;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { Stroke-only paths: turn the fill off so the mode below is the only
        thing that decides what is painted. }
      p.Graphics.Fill.Kind := gfkNone;
      p.Graphics.Stroke.Kind := gskSolid;

      { The axis as one polyline. DrawPathAddLines moves to the first point
        and adds a line to every following point. }
      SetLength(Axis, 3);
      Axis[0] := PointF(40, 60);
      Axis[1] := PointF(40, 220);
      Axis[2] := PointF(420, 220);
      p.Graphics.Stroke.Color := gcGray;
      p.Graphics.Stroke.Width := 1;
      p.Graphics.DrawPathBegin;
      p.Graphics.DrawPathAddLines(Axis);
      p.Graphics.DrawPathEnd(dmPathStroke);

      { A smooth trend line: two cubic Bezier segments that share an end point. }
      p.Graphics.Stroke.Color := gcSeagreen;
      p.Graphics.Stroke.Width := 2;
      p.Graphics.DrawPathBegin;
      p.Graphics.DrawPathMoveToPoint(PointF(40, 200));
      p.Graphics.DrawPathAddCurveToPoint(PointF(100, 120), PointF(160, 120),
        PointF(220, 170));
      p.Graphics.DrawPathAddCurveToPoint(PointF(280, 220), PointF(340, 90),
        PointF(400, 110));
      p.Graphics.DrawPathEnd(dmPathStroke);

      { A closed shape from a quadratic curve and the closing line, filled only. }
      p.Graphics.Fill.Kind := gfkSolid;
      p.Graphics.Fill.Color := gcLightsteelblue;
      p.Graphics.DrawPathBegin;
      p.Graphics.DrawPathMoveToPoint(PointF(460, 200));
      p.Graphics.DrawPathAddQuadCurveToPoint(PointF(500, 80), PointF(540, 200));
      p.Graphics.DrawPathClose;
      p.Graphics.DrawPathEnd(dmPathFill);
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::DrawTrendSketch(const String AFileName)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // Stroke-only paths: turn the fill off so the mode below is the only
      // thing that decides what is painted.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkNone;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;

      // The axis as one polyline. DrawPathAddLines moves to the first point
      // and adds a line to every following point.
      TTMSFNCPDFGraphicsLibPointArray Axis;
      Axis.Length = 3;
      Axis[0] = PointF(40, 60);
      Axis[1] = PointF(40, 220);
      Axis[2] = PointF(420, 220);
      p->Graphics()->Stroke->Color = gcGray;
      p->Graphics()->Stroke->Width = 1;
      p->Graphics()->DrawPathBegin();
      p->Graphics()->DrawPathAddLines(Axis);
      p->Graphics()->DrawPathEnd(TTMSFNCPDFGraphicsLibPathDrawingMode::dmPathStroke);

      // A smooth trend line: two cubic Bezier segments that share an end point.
      p->Graphics()->Stroke->Color = gcSeagreen;
      p->Graphics()->Stroke->Width = 2;
      p->Graphics()->DrawPathBegin();
      p->Graphics()->DrawPathMoveToPoint(PointF(40, 200));
      p->Graphics()->DrawPathAddCurveToPoint(PointF(100, 120), PointF(160, 120),
        PointF(220, 170));
      p->Graphics()->DrawPathAddCurveToPoint(PointF(280, 220), PointF(340, 90),
        PointF(400, 110));
      p->Graphics()->DrawPathEnd(TTMSFNCPDFGraphicsLibPathDrawingMode::dmPathStroke);

      // A closed shape from a quadratic curve and the closing line, filled only.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
      p->Graphics()->Fill->Color = gcLightsteelblue;
      p->Graphics()->DrawPathBegin();
      p->Graphics()->DrawPathMoveToPoint(PointF(460, 200));
      p->Graphics()->DrawPathAddQuadCurveToPoint(PointF(500, 80), PointF(540, 200));
      p->Graphics()->DrawPathClose();
      p->Graphics()->DrawPathEnd(TTMSFNCPDFGraphicsLibPathDrawingMode::dmPathFill);
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

Path drawing modes

DrawPathEnd takes the mode that decides whether the path is filled, stroked, or both. Unlike DrawRectangle, it does not look at Fill.Kind or Stroke.Kind: it paints exactly what the mode says. Ending a path with a fill mode while Fill.Kind is gfkNone therefore still fills it — with whatever fill colour the page state last had, black by default. Pass dmPathStroke for outlines and dmPathFill for flat shapes rather than relying on the default.

Mode Effect
dmPathFill Fill, non-zero winding rule
dmPathEOFill Fill, even-odd rule
dmPathStroke Outline only
dmPathEOStroke Close the path, then outline it
dmPathFillStroke Fill, then outline (the default)
dmPathEOFillStroke Fill with the even-odd rule, then outline

The winding rule only matters for a path that crosses itself or contains another sub-path: under non-zero winding the centre of the star above is filled. How the even-odd variants render depends on the output engine, so for a hole that must look the same everywhere, paint the inner shape over the fill in the background colour instead.

Gradient-filled paths

A path can be filled with a gradient rather than a solid colour, which is what produces a banded header or a shaded chart area without drawing dozens of adjacent rectangles. Set Fill.Kind := gfkGradient, Fill.Color, Fill.ColorTo, and Fill.Orientation before DrawPathBegin, then end the path with a fill mode: the gradient spans the bounds of the path, as in the star sample above.

DrawPathEndLinearGradient(StartPoint, EndPoint) is the alternative ending when the gradient should span an explicit extent rather than the path bounds. It only fills — no outline is drawn.

Graphics state and clipping

Clipping and transforms change how everything after them is drawn, so both are used inside a saved graphics state that is restored when the effect should end.

Saving and restoring state

DrawSaveState pushes the current graphics state — including the clip region and the transformation matrix — onto a stack; DrawRestoreState pops it. Calls must be balanced: every save needs exactly one restore, and a try / finally around the drawing keeps them balanced when an exception is raised.

Clipping paths

A clip restricts all subsequent drawing to a shape. Use it to crop an image to a frame, to keep a hatch pattern or an overflowing chart inside its panel, or to mask any area a rectangle cannot express on its own. The sequence is: save the state, add the clip shape with path segments, call DrawPathBeginClip to activate it, start a fresh path with DrawPathBegin, draw, and restore the state.

Clipping drawing to a panelPascalC++
procedure TForm1.DrawHatchedPanel(const AFileName: string);
var
  p: TTMSFNCPDFLib;
  Panel: TRectF;
  x, h: Single;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;
      Panel := RectF(40, 40, 300, 160);
      h := Panel.Bottom - Panel.Top;

      { Save first: a clip can only be removed again by restoring the state
        that was saved before it was set. }
      p.Graphics.DrawSaveState;
      try
        { Add the clip shape, activate it, then start a fresh path - the same
          sequence the graphics engine uses for its own ClipRect. }
        p.Graphics.DrawPathAddRectangle(Panel);
        p.Graphics.DrawPathBeginClip;
        p.Graphics.DrawPathBegin;

        { Diagonal hatching that deliberately runs past the panel on both
          sides; the clip trims every line at the panel edge. }
        p.Graphics.Stroke.Kind := gskSolid;
        p.Graphics.Stroke.Color := gcLightsteelblue;
        p.Graphics.Stroke.Width := 2;
        x := Panel.Left - h;
        while x < Panel.Right do
        begin
          p.Graphics.DrawLine(PointF(x, Panel.Bottom), PointF(x + h, Panel.Top));
          x := x + 10;
        end;
      finally
        p.Graphics.DrawRestoreState;
      end;

      { Back outside the saved state: the border and caption are not clipped. }
      p.Graphics.Fill.Kind := gfkNone;
      p.Graphics.Stroke.Color := gcSteelblue;
      p.Graphics.Stroke.Width := 1;
      p.Graphics.DrawRectangle(Panel);
      p.Graphics.Font.Size := 9;
      p.Graphics.DrawText('Reserved for office use', PointF(Panel.Left, Panel.Bottom + 6));
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::DrawHatchedPanel(const String AFileName)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();
      TRectF Panel = RectF(40, 40, 300, 160);
      float h = Panel.Bottom - Panel.Top;

      // Save first: a clip can only be removed again by restoring the state
      // that was saved before it was set.
      p->Graphics()->DrawSaveState();
      try
      {
        // Add the clip shape, activate it, then start a fresh path - the same
        // sequence the graphics engine uses for its own ClipRect.
        p->Graphics()->DrawPathAddRectangle(Panel);
        p->Graphics()->DrawPathBeginClip();
        p->Graphics()->DrawPathBegin();

        // Diagonal hatching that deliberately runs past the panel on both
        // sides; the clip trims every line at the panel edge.
        p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
        p->Graphics()->Stroke->Color = gcLightsteelblue;
        p->Graphics()->Stroke->Width = 2;
        float x = Panel.Left - h;
        while (x < Panel.Right)
        {
          p->Graphics()->DrawLine(PointF(x, Panel.Bottom), PointF(x + h, Panel.Top));
          x = x + 10;
        }
      }
      __finally
      {
        p->Graphics()->DrawRestoreState();
      }

      // Back outside the saved state: the border and caption are not clipped.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkNone;
      p->Graphics()->Stroke->Color = gcSteelblue;
      p->Graphics()->Stroke->Width = 1;
      p->Graphics()->DrawRectangle(Panel);
      p->Graphics()->Font->Size = 9;
      p->Graphics()->DrawText("Reserved for office use", PointF(Panel.Left, Panel.Bottom + 6));
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

The clip lasts until DrawRestoreState pops the state saved before it — PDF has no other way to widen a clip once set. Anything that must not be clipped, such as the panel border in the sample, is drawn after the restore.

Transforms

DrawSetTransform applies a matrix — translation, rotation, scaling — to subsequent drawing, either as six coefficients (m11, m12, m21, m22, dx, dy) or as a TTMSFNCGraphicsMatrix. It is combined with the current matrix rather than replacing it, so wrap it in DrawSaveState / DrawRestoreState to undo it.

The matrix is applied in the output engine's own PDF coordinate space, not in the top-left page coordinates the drawing calls use; on the default engine that space has its origin at the bottom-left with the y axis pointing up, so a translation or rotation behaves mirrored vertically compared with what you might expect. For rotated text, such as a watermark or a rotated column header, the angle parameter of the graphics engine's DrawText handles that conversion for you and is the simpler route.

Clearing a region

DrawClear(Rect) clears a rectangle, but what "clear" means depends on the output engine. When you need a guaranteed white patch — for example to blank out an area before stamping new content on it — draw a white-filled rectangle with no stroke instead.

Images

Draw images with DrawImage (an in-memory bitmap), DrawImageFromFile, or DrawImageWithName. The last looks the image up by name in the BitmapContainer assigned to the document, which is convenient when the same set of logos and icons is used across many documents. On the default output engine, identical image data drawn more than once is embedded only once, whichever of the three calls you use.

Every overload returns the rectangle the image actually occupies — the reliable anchor for a caption or for whatever is drawn underneath it:

Drawing an image from a container, a bitmap and a filePascalC++
procedure TForm1.DrawImageSources(const AFileName, AImageFile: string);
var
  p: TTMSFNCPDFLib;
  Container: TTMSFNCBitmapContainer;
  Bmp: TTMSFNCBitmap;
  r: TRectF;
begin
  Container := TTMSFNCBitmapContainer.Create;
  try
    Container.AddFromFile(AImageFile, 'Logo');
    Bmp := TTMSFNCBitmap.Create;
    try
      Bmp.LoadFromFile(AImageFile);
      p := TTMSFNCPDFLib.Create;
      try
        p.Header := '';
        p.Footer := '';
        { Named images are looked up in the container assigned here. }
        p.BitmapContainer := Container;
        p.BeginDocument(AFileName);
        try
          p.NewPage;
          p.Graphics.Font.Size := 9;

          { Every overload returns the rectangle the image occupies, which is
            the reliable anchor for a caption underneath it. }
          r := p.Graphics.DrawImageWithName('Logo', PointF(40, 60));
          p.Graphics.DrawText('Rendering from image container', PointF(r.Left, r.Bottom + 8));

          r := p.Graphics.DrawImage(Bmp, PointF(230, 60));
          p.Graphics.DrawText('Rendering from memory image', PointF(r.Left, r.Bottom + 8));

          r := p.Graphics.DrawImageFromFile(AImageFile, PointF(420, 60));
          p.Graphics.DrawText('Rendering from file', PointF(r.Left, r.Bottom + 8));
        finally
          p.EndDocument;
        end;
      finally
        { Free the document before the container it references. }
        p.Free;
      end;
    finally
      Bmp.Free;
    end;
  finally
    Container.Free;
  end;
end;
void __fastcall TForm1::DrawImageSources(const String AFileName, const String AImageFile)
{
  TTMSFNCBitmapContainer *Container = new TTMSFNCBitmapContainer(nullptr);
  try
  {
    Container->AddFromFile(AImageFile, "Logo");
    TTMSFNCBitmap *Bmp = new TTMSFNCBitmap();
    try
    {
      Bmp->LoadFromFile(AImageFile);
      TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
      try
      {
        p->Header = "";
        p->Footer = "";
        // Named images are looked up in the container assigned here.
        p->BitmapContainer = Container;
        p->BeginDocument(AFileName);
        try
        {
          p->NewPage();
          p->Graphics()->Font->Size = 9;

          // Every overload returns the rectangle the image occupies, which is
          // the reliable anchor for a caption underneath it.
          TRectF r = p->Graphics()->DrawImageWithName("Logo", PointF(40, 60));
          p->Graphics()->DrawText("Rendering from image container", PointF(r.Left, r.Bottom + 8));

          r = p->Graphics()->DrawImage(Bmp, PointF(230, 60));
          p->Graphics()->DrawText("Rendering from memory image", PointF(r.Left, r.Bottom + 8));

          r = p->Graphics()->DrawImageFromFile(AImageFile, PointF(420, 60));
          p->Graphics()->DrawText("Rendering from file", PointF(r.Left, r.Bottom + 8));
        }
        __finally
        {
          p->EndDocument();
        }
      }
      __finally
      {
        // Free the document before the container it references.
        delete p;
      }
    }
    __finally
    {
      delete Bmp;
    }
  }
  __finally
  {
    delete Container;
  }
}

Images drawn on a PDF page

Fitting an image into a rectangle

The point overloads draw an image at its own size. The rectangle overloads fit it into a box, which is what a layout usually needs, controlled by three flags:

Parameter Default Effect
Stretch True Scale the image to the rectangle
AspectRatio True Keep the image's proportions while scaling
Center True Centre the image in the rectangle; False pins it to the top-left

With AspectRatio on, the drawn image is smaller than the box in one direction, so place captions from the returned rectangle rather than from the box.

Background colour, format, and quality

Every draw call has an overload with an ABackgroundColor parameter, and all of them accept an ImageType (itOriginal, itPNG, itJPG) and a Quality between 0.0 and 1.0. On the default output engine, images are embedded as JPEG at the given quality and transparent pixels are painted in the background colour, which is white when you do not pass one. A transparent logo on a coloured band therefore needs the band colour as its background, and lowering Quality is the lever for a smaller file. An SVG image loaded into a TTMSFNCBitmap is drawn as vector graphics on the default engine (except in TMS WEB Core).

Fitting images and choosing their background colourPascalC++
procedure TForm1.BuildProductSheet(const AFileName, ALogoFile, APhotoFile: string);
var
  p: TTMSFNCPDFLib;
  Box, Drawn: TRectF;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { A coloured header band across the (Letter-sized, 612 pt wide) page. }
      p.Graphics.Fill.Kind := gfkSolid;
      p.Graphics.Fill.Color := gcSteelblue;
      p.Graphics.Stroke.Kind := gskNone;
      p.Graphics.DrawRectangle(RectF(0, 0, 612, 80));

      { Transparent pixels are painted in the background colour, which is
        white unless you pass one. Passing the band colour keeps the logo's
        edges clean on the band. Center = False pins it to the top-left. }
      p.Graphics.DrawImageFromFile(ALogoFile, gcSteelblue, RectF(40, 16, 200, 64),
        True, True, itOriginal, 1.0, False);

      { Fit the photo inside a box: stretch to the box, keep the aspect ratio,
        centre it, and embed it at 80% JPEG quality to keep the file small. }
      Box := RectF(40, 110, 300, 330);
      Drawn := p.Graphics.DrawImageFromFile(APhotoFile, Box, True, True, itJPG, 0.8, True);

      { Caption under the image as actually drawn, not under the box - with
        the aspect ratio kept the two differ in one direction. }
      p.Graphics.Font.Size := 9;
      p.Graphics.Font.Color := gcDimgray;
      p.Graphics.DrawText('Figure 1 - Model 200 in brushed steel',
        PointF(Drawn.Left, Drawn.Bottom + 6));
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::BuildProductSheet(const String AFileName, const String ALogoFile, const String APhotoFile)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // A coloured header band across the (Letter-sized, 612 pt wide) page.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
      p->Graphics()->Fill->Color = gcSteelblue;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
      p->Graphics()->DrawRectangle(RectF(0, 0, 612, 80));

      // Transparent pixels are painted in the background colour, which is
      // white unless you pass one. Passing the band colour keeps the logo
      // edges clean on the band. Center = false pins it to the top-left.
      p->Graphics()->DrawImageFromFile(ALogoFile, gcSteelblue, RectF(40, 16, 200, 64),
        true, true, TTMSFNCPDFGraphicsLibImageType::itOriginal, 1.0, false);

      // Fit the photo inside a box: stretch to the box, keep the aspect ratio,
      // centre it, and embed it at 80% JPEG quality to keep the file small.
      TRectF Box = RectF(40, 110, 300, 330);
      TRectF Drawn = p->Graphics()->DrawImageFromFile(APhotoFile, Box, true, true,
        TTMSFNCPDFGraphicsLibImageType::itJPG, 0.8, true);

      // Caption under the image as actually drawn, not under the box - with
      // the aspect ratio kept the two differ in one direction.
      p->Graphics()->Font->Size = 9;
      p->Graphics()->Font->Color = gcDimgray;
      p->Graphics()->DrawText("Figure 1 - Model 200 in brushed steel",
        PointF(Drawn.Left, Drawn.Bottom + 6));
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

AddURL and AddGoTo add clickable link annotations — a web address, or a jump to another page of the same document. Use them for a clickable table of contents, a "back to contents" link on every page, or a verification URL on a certificate.

Method Target
AddURL(AText, AURL, ARect) Opens AURL in the reader's browser
AddGoTo(AText, APageIndex, AOptions, ARect) Jumps to the zero-based page APageIndex; AOptions is a PDF destination option such as /Fit

Both draw AText inside ARect with Graphics.URLFont (honouring Graphics.Alignment) and make the drawn text clickable. AddGoTo with empty text draws nothing and makes the whole rectangle clickable, which turns ordinary text or an image into a link. Target pages are resolved when the document is written, so a contents page may link to pages you have not added yet. The page-index variant of AddGoTo is listed as Windows only in the 1.0.3.0 release notes.

Web links and links to other pagesPascalC++
procedure TForm1.BuildLinkedManual(const AFileName: string);
const
  Chapters: array[0..1] of string = ('1. Installation', '2. Licensing');
var
  p: TTMSFNCPDFLib;
  I: Integer;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;
      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 18;
      p.Graphics.DrawText('Contents', PointF(40, 40));

      { Link text is drawn with URLFont, not Font. }
      p.Graphics.URLFont.Name := 'Segoe UI';
      p.Graphics.URLFont.Size := 11;
      p.Graphics.URLFont.Color := gcNavy;
      p.Graphics.URLFont.Style := [TFontStyle.fsUnderline];

      { Page indexes are zero-based and resolved when the document is written,
        so an entry may point at a page that has not been added yet. '/Fit'
        asks the reader to show the whole target page. }
      for I := 0 to High(Chapters) do
        p.Graphics.AddGoTo(Chapters[I], I + 1, '/Fit',
          RectF(40, 84 + I * 22, 400, 100 + I * 22));

      p.Graphics.AddURL('Online documentation',
        'https://doc.tmssoftware.com/fnc/core/pdflib/', RectF(40, 150, 400, 166));

      for I := 0 to High(Chapters) do
      begin
        p.NewPage;
        p.Graphics.Font.Size := 18;
        p.Graphics.DrawText(Chapters[I], PointF(40, 40));

        { With empty text, AddGoTo draws nothing and only makes the rectangle
          clickable - here, over text that was drawn normally. }
        p.Graphics.Font.Size := 10;
        p.Graphics.DrawText('Back to contents', PointF(40, 740));
        p.Graphics.AddGoTo('', 0, '/Fit', RectF(40, 740, 140, 754));
      end;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::BuildLinkedManual(const String AFileName)
{
  const String Chapters[2] = {"1. Installation", "2. Licensing"};

  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();
      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 18;
      p->Graphics()->DrawText("Contents", PointF(40, 40));

      // Link text is drawn with URLFont, not Font.
      p->Graphics()->URLFont->Name = "Segoe UI";
      p->Graphics()->URLFont->Size = 11;
      p->Graphics()->URLFont->Color = gcNavy;
      p->Graphics()->URLFont->Style = TFontStyles() << TFontStyle::fsUnderline;

      // Page indexes are zero-based and resolved when the document is written,
      // so an entry may point at a page that has not been added yet. "/Fit"
      // asks the reader to show the whole target page.
      for (int I = 0; I < 2; I++)
        p->Graphics()->AddGoTo(Chapters[I], I + 1, "/Fit",
          RectF(40, 84 + I * 22, 400, 100 + I * 22));

      p->Graphics()->AddURL("Online documentation",
        "https://doc.tmssoftware.com/fnc/core/pdflib/", RectF(40, 150, 400, 166));

      for (int I = 0; I < 2; I++)
      {
        p->NewPage();
        p->Graphics()->Font->Size = 18;
        p->Graphics()->DrawText(Chapters[I], PointF(40, 40));

        // With empty text, AddGoTo draws nothing and only makes the rectangle
        // clickable - here, over text that was drawn normally.
        p->Graphics()->Font->Size = 10;
        p->Graphics()->DrawText("Back to contents", PointF(40, 740));
        p->Graphics()->AddGoTo("", 0, "/Fit", RectF(40, 740, 140, 754));
      }
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

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:

Graphics EnginePascalC++
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;
TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
TTMSFNCGraphicsPDFEngine *g = new TTMSFNCGraphicsPDFEngine(p);
TTMSFNCGraphicsPath *pth = new TTMSFNCGraphicsPath();
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,
    TTMSFNCGraphicsTextAlign::gtaCenter, TTMSFNCGraphicsTextAlign::gtaCenter,
    TTMSFNCGraphicsTextTrimming::gttNone, -45);
  p->EndDocument(true);
}
__finally
{
  delete pth;
  delete g;
  delete p;
}

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.

Rounded rectangles and arcs

The graphics interface also has an extended part, ITMSFNCCustomPDFGraphicsExLib, with rounded rectangles, arc segments, drop shadows, and radial gradients. Those calls are implemented by the native engines on macOS, iOS, and Android only; the default engine used on Windows, Linux, and in TMS WEB Core ignores them, so the document generates but the shape is simply missing.

For a rounded rectangle or an arc that appears on every platform, draw it through the graphics engine: its DrawRoundRectangle and DrawArc build the shape from ordinary path segments, and DrawRoundRectangle can round a chosen subset of corners.

Rounded rectangles through the graphics enginePascalC++
procedure TForm1.DrawRoundedCards(const AFileName: string);
var
  p: TTMSFNCPDFLib;
  g: TTMSFNCGraphicsPDFEngine;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;
      g := TTMSFNCGraphicsPDFEngine.Create(p);
      try
        g.Fill.Kind := gfkSolid;
        g.Fill.Color := gcAliceblue;
        g.Stroke.Kind := gskSolid;
        g.Stroke.Color := gcSteelblue;
        g.Stroke.Width := 1.5;

        { All four corners rounded with a 12 pt radius. }
        g.DrawRoundRectangle(RectF(40, 40, 290, 160), 12);

        { Only the top corners rounded: a tab header. }
        g.Fill.Color := gcSteelblue;
        g.DrawRoundRectangle(RectF(320, 40, 570, 76), 12, [gcTopLeft, gcTopRight]);

        g.Font.Color := gcWhite;
        g.DrawText(RectF(332, 40, 570, 76), 'Order summary', False, gtaLeading, gtaCenter);
      finally
        g.Free;
      end;
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::DrawRoundedCards(const String AFileName)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();
      TTMSFNCGraphicsPDFEngine *g = new TTMSFNCGraphicsPDFEngine(p);
      try
      {
        g->Fill->Kind = TTMSFNCGraphicsFillKind::gfkSolid;
        g->Fill->Color = gcAliceblue;
        g->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
        g->Stroke->Color = gcSteelblue;
        g->Stroke->Width = 1.5;

        // All four corners rounded with a 12 pt radius.
        g->DrawRoundRectangle(RectF(40, 40, 290, 160), 12);

        // Only the top corners rounded: a tab header.
        g->Fill->Color = gcSteelblue;
        g->DrawRoundRectangle(RectF(320, 40, 570, 76), 12,
          TTMSFNCGraphicsCorners() << TTMSFNCGraphicsCorner::gcTopLeft << TTMSFNCGraphicsCorner::gcTopRight);

        g->Font->Color = gcWhite;
        g->DrawText(RectF(332, 40, 570, 76), "Order summary", false,
          TTMSFNCGraphicsTextAlign::gtaLeading, TTMSFNCGraphicsTextAlign::gtaCenter);
      }
      __finally
      {
        delete g;
      }
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

Common mistakes

  • Colours set after DrawPathBegin. The path is configured when it begins; set Fill and Stroke first.
  • A fill mode on a path with the fill "off". DrawPathEnd paints exactly the mode passed and ignores Fill.Kind; end outline-only paths with dmPathStroke.
  • A clip that never ends. Only DrawRestoreState removes a clip; always pair it with a DrawSaveState made before the clip.
  • A transform that moves content the wrong way. DrawSetTransform works in the engine's native PDF space and accumulates; use the graphics engine's text angle for rotated text.
  • A rounded rectangle, arc, or shadow that is missing on Windows. The extended graphics calls are native-engine only; use the graphics engine instead.
  • A white box around a transparent logo. Pass the colour behind the image as ABackgroundColor.
  • A named image that silently does not appear. DrawImageWithName draws nothing when no BitmapContainer is assigned or the name is not in it.
  • Text hidden behind a shape. Drawing order is painter's order — draw fills first, text last.

Putting it together: a certificate page

A gradient pennant built as a custom path, a portrait photo centre-cropped with a clip and framed afterwards, and a verification web link — four sections of this page in one layout:

A certificate page combining paths, clipping, images and linksPascalC++
procedure TForm1.BuildCertificate(const AFileName, APhotoFile: string);
var
  p: TTMSFNCPDFLib;
  Photo: TRectF;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.Header := '';
    p.Footer := '';
    p.BeginDocument(AFileName);
    try
      p.NewPage;

      { Custom path with a gradient fill: a pennant-shaped banner. The fill is
        configured before DrawPathBegin, and dmPathFill paints no outline. }
      p.Graphics.Fill.Kind := gfkGradient;
      p.Graphics.Fill.Color := gcMidnightblue;
      p.Graphics.Fill.ColorTo := gcCornflowerblue;
      p.Graphics.Fill.Orientation := gfoHorizontal;
      p.Graphics.Stroke.Kind := gskNone;
      p.Graphics.DrawPathBegin;
      p.Graphics.DrawPathMoveToPoint(PointF(40, 40));
      p.Graphics.DrawPathAddLineToPoint(PointF(572, 40));
      p.Graphics.DrawPathAddLineToPoint(PointF(540, 80));
      p.Graphics.DrawPathAddLineToPoint(PointF(572, 120));
      p.Graphics.DrawPathAddLineToPoint(PointF(40, 120));
      p.Graphics.DrawPathClose;
      p.Graphics.DrawPathEnd(dmPathFill);

      p.Graphics.Font.Name := 'Segoe UI';
      p.Graphics.Font.Size := 22;
      p.Graphics.Font.Color := gcWhite;
      p.Graphics.DrawText('Certificate of completion', PointF(60, 66));

      { Clipping + image fitting: draw the photo larger than the square with
        its aspect ratio kept, then let the clip trim it to the square - a
        centre crop, so a portrait never spills into the text column. }
      Photo := RectF(40, 150, 200, 310);
      p.Graphics.DrawSaveState;
      try
        p.Graphics.DrawPathAddRectangle(Photo);
        p.Graphics.DrawPathBeginClip;
        p.Graphics.DrawPathBegin;
        p.Graphics.DrawImageFromFile(APhotoFile,
          RectF(Photo.Left - 40, Photo.Top - 40, Photo.Right + 40, Photo.Bottom + 40),
          True, True, itJPG, 0.85, True);
      finally
        p.Graphics.DrawRestoreState;
      end;

      { The frame is drawn after the restore, so it is not clipped. }
      p.Graphics.Fill.Kind := gfkNone;
      p.Graphics.Stroke.Kind := gskSolid;
      p.Graphics.Stroke.Color := gcMidnightblue;
      p.Graphics.Stroke.Width := 2;
      p.Graphics.DrawRectangle(Photo);

      p.Graphics.Font.Size := 12;
      p.Graphics.Font.Color := gcBlack;
      p.Graphics.DrawText('Awarded to Alice Moreau', PointF(230, 170));
      p.Graphics.DrawText('for completing Advanced Report Design, 12 hours', PointF(230, 192));

      { A web link: the text is drawn with URLFont and the measured text
        becomes the clickable area. }
      p.Graphics.URLFont.Size := 10;
      p.Graphics.URLFont.Color := gcNavy;
      p.Graphics.AddURL('Verify this certificate online',
        'https://www.example.com/verify/2026-0142', RectF(230, 280, 560, 296));
    finally
      p.EndDocument;
    end;
  finally
    p.Free;
  end;
end;
void __fastcall TForm1::BuildCertificate(const String AFileName, const String APhotoFile)
{
  TTMSFNCPDFLib *p = new TTMSFNCPDFLib();
  try
  {
    p->Header = "";
    p->Footer = "";
    p->BeginDocument(AFileName);
    try
    {
      p->NewPage();

      // Custom path with a gradient fill: a pennant-shaped banner. The fill is
      // configured before DrawPathBegin, and dmPathFill paints no outline.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkGradient;
      p->Graphics()->Fill->Color = gcMidnightblue;
      p->Graphics()->Fill->ColorTo = gcCornflowerblue;
      p->Graphics()->Fill->Orientation = TTMSFNCGraphicsFillOrientation::gfoHorizontal;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskNone;
      p->Graphics()->DrawPathBegin();
      p->Graphics()->DrawPathMoveToPoint(PointF(40, 40));
      p->Graphics()->DrawPathAddLineToPoint(PointF(572, 40));
      p->Graphics()->DrawPathAddLineToPoint(PointF(540, 80));
      p->Graphics()->DrawPathAddLineToPoint(PointF(572, 120));
      p->Graphics()->DrawPathAddLineToPoint(PointF(40, 120));
      p->Graphics()->DrawPathClose();
      p->Graphics()->DrawPathEnd(TTMSFNCPDFGraphicsLibPathDrawingMode::dmPathFill);

      p->Graphics()->Font->Name = "Segoe UI";
      p->Graphics()->Font->Size = 22;
      p->Graphics()->Font->Color = gcWhite;
      p->Graphics()->DrawText("Certificate of completion", PointF(60, 66));

      // Clipping + image fitting: draw the photo larger than the square with
      // its aspect ratio kept, then let the clip trim it to the square - a
      // centre crop, so a portrait never spills into the text column.
      TRectF Photo = RectF(40, 150, 200, 310);
      p->Graphics()->DrawSaveState();
      try
      {
        p->Graphics()->DrawPathAddRectangle(Photo);
        p->Graphics()->DrawPathBeginClip();
        p->Graphics()->DrawPathBegin();
        p->Graphics()->DrawImageFromFile(APhotoFile,
          RectF(Photo.Left - 40, Photo.Top - 40, Photo.Right + 40, Photo.Bottom + 40),
          true, true, TTMSFNCPDFGraphicsLibImageType::itJPG, 0.85, true);
      }
      __finally
      {
        p->Graphics()->DrawRestoreState();
      }

      // The frame is drawn after the restore, so it is not clipped.
      p->Graphics()->Fill->Kind = TTMSFNCGraphicsFillKind::gfkNone;
      p->Graphics()->Stroke->Kind = TTMSFNCGraphicsStrokeKind::gskSolid;
      p->Graphics()->Stroke->Color = gcMidnightblue;
      p->Graphics()->Stroke->Width = 2;
      p->Graphics()->DrawRectangle(Photo);

      p->Graphics()->Font->Size = 12;
      p->Graphics()->Font->Color = gcBlack;
      p->Graphics()->DrawText("Awarded to Alice Moreau", PointF(230, 170));
      p->Graphics()->DrawText("for completing Advanced Report Design, 12 hours", PointF(230, 192));

      // A web link: the text is drawn with URLFont and the measured text
      // becomes the clickable area.
      p->Graphics()->URLFont->Size = 10;
      p->Graphics()->URLFont->Color = gcNavy;
      p->Graphics()->AddURL("Verify this certificate online",
        "https://www.example.com/verify/2026-0142", RectF(230, 280, 560, 296));
    }
    __finally
    {
      p->EndDocument();
    }
  }
  __finally
  {
    delete p;
  }
}

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:

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