Skip to main content
Table of Contents

Page overlays and underlays

Draw on a page of an existing document using the same graphics API you use to generate one — above the original content, or beneath it.

Stamping an approval mark, adding a page footer to a supplier's invoice, putting a "DRAFT" watermark behind a contract, marking up a scan: all of these mean drawing on a page whose content you did not create and do not want to disturb. BeginPageEdit opens a drawing surface over one page of an opened document, Graphics gives you the full PDF drawing API on it — text, HTML text, shapes, paths, images, gradients — and EndPageEdit merges the result into the page. The original content is never rewritten: your drawing is merged in as a separate layer, either on top of or underneath what was already there. Use this rather than regenerating the page whenever you only need to add something; regenerate only when you own the content and are producing it from data anyway.

The edit lifecycle

A page edit is a small transaction inside the document transaction. It begins, you draw, and it either merges or is discarded — and until it merges, the page is unchanged.

Stamp a page with an overlayPascal
procedure TForm1.StampApproved(const AFileName: string; APageIndex: Integer);
var
  p: TTMSFNCPDFLib;
  Info: TTMSFNCPDFPageInfo;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.OpenDocument(AFileName);
    try
      Info := p.GetDocumentPageInfo(APageIndex);

      { The edit surface is the VISIBLE page box, so (0,0) is its top-left
        corner and Info.Width / Info.Height are its extents in points. }
      p.BeginPageEdit(APageIndex, pelOverlay);
      try
        p.Graphics.Stroke.Color := gcGreen;
        p.Graphics.Stroke.Width := 2;
        p.Graphics.Fill.Kind := gfkNone;
        p.Graphics.DrawRectangle(RectF(Info.Width - 220, 40, Info.Width - 40, 100));

        p.Graphics.Font.Name := 'Segoe UI';
        p.Graphics.Font.Size := 14;
        p.Graphics.Font.Color := gcGreen;
        p.Graphics.DrawText('APPROVED', PointF(Info.Width - 205, 55));

        p.Graphics.Font.Size := 9;
        p.Graphics.DrawText(DateToStr(Date), PointF(Info.Width - 205, 78));

        { EndPageEdit merges the drawing into the page. Until it runs, nothing
          has changed. }
        p.EndPageEdit;
      except
        p.CancelPageEdit;
        raise;
      end;

      p.SaveDocument(AFileName);
    finally
      p.CloseDocument;
    end;
  finally
    p.Free;
  end;
end;
Call Effect
BeginPageEdit(PageIndex, Layer) Opens a drawing surface for one zero-based page of a document opened with OpenDocument. Layer defaults to pelOverlay.
Graphics While an edit is active, returns the canvas of the edit surface instead of the generation canvas.
EndPageEdit Merges the drawing into the page.
CancelPageEdit Discards the drawing; the page stays exactly as it was.
IsPageEditing True between begin and end/cancel.

Only one page edit can be active at a time, and structural operations (DeletePage, MergeDocument, SaveDocument, …) raise ETMSFNCPDFDocument instead of running while one is open. Two calls are the exception: CloseDocument and OpenDocument cancel an active edit silently, so an unfinished edit is discarded rather than reported. Pair every BeginPageEdit with an EndPageEdit or a CancelPageEdit, and put the cancel on the exception path as the snippets here do.

An edit that draws nothing is still valid: EndPageEdit merges an empty layer and leaves the page exactly as it was. A loop that stamps only the pages matching some condition therefore does not have to decide whether to open the edit before it knows what it will draw — opening one and ending it unused is safe.

The edit surface and its coordinate space

Getting a stamp to land in the right corner of every page is the part that usually goes wrong, because a document that arrives from elsewhere is rarely one uniform page size. The edit surface removes the problem by measuring in the page's own visible geometry.

The surface is created at the visible size of the page it targets: the crop box with UserUnit scaling and rotation already applied — the same Width and Height that GetDocumentPageInfo reports. So (0, 0) is the top-left corner of the page as a reader sees it, and you position against Info.Width / Info.Height rather than against a page-size constant.

This is what makes stamping a mixed document reliable: a portrait A4 page, a rotated landscape scan and an oversized drawing each get a correctly sized surface, and the same layout code lands in the right place on all three.

The surface also inherits EmbedFonts, EmbedFontType, BitmapContainer and the font fall-back list from the TTMSFNCPDFLib instance, so fonts and named bitmaps behave exactly as they do when you generate a document. Stamping every page of a long document does not multiply its size for that reason: on save, objects whose contents are byte-identical are shared, so one embedded font or logo is written once no matter how many overlays referenced it.

What the surface does not inherit is the automatic page furniture. Header, Footer and automatic page numbering are deliberately switched off on it — a document you did not generate has its own margins, and an automatic header would land on top of them. Draw your own footer instead, as the combined example below does.

Overlay or underlay

TTMSFNCPDFPageEditLayer decides which side of the original content your drawing lands on. It is the difference between a stamp and a watermark.

Value Draws Use for
pelOverlay Above the original content Stamps, annotations, footers, redaction boxes, page numbers
pelUnderlay Below the original content Watermarks, background tints, letterhead, pre-printed forms

An underlay is usually the right choice for anything that covers a large area, because the original text keeps drawing over it and stays perfectly readable without any transparency work:

Put a watermark under the contentPascal
procedure TForm1.WatermarkDocument(const AFileName, AText: string);
var
  p: TTMSFNCPDFLib;
  Info: TTMSFNCPDFPageInfo;
  I: Integer;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.OpenDocument(AFileName);
    try
      for I := 0 to p.GetPageCount - 1 do
      begin
        Info := p.GetDocumentPageInfo(I);

        { pelUnderlay draws BELOW the original content, so the watermark never
          hides the text - which is what makes it readable without transparency. }
        p.BeginPageEdit(I, pelUnderlay);
        try
          p.Graphics.Font.Name := 'Segoe UI';
          p.Graphics.Font.Size := 64;
          p.Graphics.Font.Color := gcGainsboro;
          p.Graphics.Alignment := gtaCenter;
          p.Graphics.DrawText(AText,
            RectF(0, Info.Height / 2 - 50, Info.Width, Info.Height / 2 + 50));
          p.EndPageEdit;
        except
          p.CancelPageEdit;
          raise;
        end;
      end;

      p.SaveDocument(AFileName);
    finally
      p.CloseDocument;
    end;
  finally
    p.Free;
  end;
end;

To put the same watermark on top, switch the layer to pelOverlay — and expect to have to lighten the colour so it does not swallow the text underneath.

Not everything the drawing API produces is drawing. AddURL writes visible text and a link annotation over it, and an annotation is page furniture rather than page content — so it has to be transferred separately when the edit merges. It is: annotations created on the edit surface are carried onto the page with their rectangles put through the same transform as the drawing, so a link stays over its own text on a rotated page, a page with a CropBox offset and a page with a UserUnit scale alike. That makes stamping the straightforward way to turn part of a flat, incoming document into something clickable.

Two details of AddURL are easy to miss. It draws its text with URLFont, not Font: for the duration of the call it assigns URLFont into Font and restores Font afterwards, so styling a link through Font has no effect. And the annotation covers the text that was actually drawn inside the rectangle you pass — positioned in it according to Alignment — rather than the whole rectangle.

Add a clickable link with an overlayPascal
procedure TForm1.StampOrderLink(const AFileName, AURL: string);
var
  p: TTMSFNCPDFLib;
  Info: TTMSFNCPDFPageInfo;
  LinkRect: TRectF;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.OpenDocument(AFileName);
    try
      Info := p.GetDocumentPageInfo(0);

      p.BeginPageEdit(0, pelOverlay);
      try
        { AddURL draws its text with URLFont, not Font: it assigns URLFont into
          Font for the duration of the call and restores Font afterwards. Style
          the link through URLFont, which starts out blue and underlined. }
        p.Graphics.URLFont.Name := 'Segoe UI';
        p.Graphics.URLFont.Size := 9;
        p.Graphics.URLFont.Color := gcBlue;

        { AddURL draws the text and adds a link annotation over the text it
          actually drew inside ARect. The annotation is carried onto the page
          when the edit is merged, with its rectangle transformed exactly like
          the drawing. }
        LinkRect := RectF(40, Info.Height - 60, 300, Info.Height - 40);
        p.Graphics.AddURL('View this order online', AURL, LinkRect);

        p.EndPageEdit;
      except
        p.CancelPageEdit;
        raise;
      end;

      p.SaveDocument(AFileName);
    finally
      p.CloseDocument;
    end;
  finally
    p.Free;
  end;
end;

The result is the same on every platform. Some graphics backends cannot emit an annotation while they are drawing, so the link is recorded as the page is drawn and written once its content exists. That is what makes AddURL inside a page edit behave identically on Windows, Linux, macOS, iOS and Android.

One class of annotation is deliberately dropped: anything that points inside the edit surface's own page tree. The surface is a single-page document, so an internal destination or a GoTo action — what either AddGoTo overload produces — would resolve against a page that does not exist in the document being stamped, and a link to nowhere is worse than no link. External AddURL links transfer; AddGoTo links do not. To cross-link the pages of an assembled document, add the links while generating the document you merge in, then merge it: a merge clones each page with its annotations intact. See Rearranging pages.

Abandoning an edit

Not every edit should go through. CancelPageEdit throws the drawing away and leaves the page untouched, which makes it the natural response to "nothing to stamp after all" as well as to an exception.

Abandon an edit that turns out to be unnecessaryPascal
procedure TForm1.MarkOverdue(APDF: TTMSFNCPDFLib; APageIndex: Integer;
  const ANotice: string);
begin
  APDF.BeginPageEdit(APageIndex, pelOverlay);
  try
    if ANotice = '' then
    begin
      { CancelPageEdit discards the drawing and leaves the page exactly as it
        was. The document stays open and can be edited or saved afterwards. }
      APDF.CancelPageEdit;
      Exit;
    end;

    APDF.Graphics.Font.Size := 11;
    APDF.Graphics.Font.Color := gcRed;
    APDF.Graphics.DrawText(ANotice, PointF(40, 40));
    APDF.EndPageEdit;
  except
    { A structural mutation raises while an edit is active, so always leave the
      page-edit state clean before letting the exception travel. }
    if APDF.IsPageEditing then
      APDF.CancelPageEdit;
    raise;
  end;
end;

By the time an exception reaches the handler the edit may already have merged, so the handler checks IsPageEditing before cancelling. CancelPageEdit is a no-op when no edit is active, so the guard is not strictly required — it makes the intent explicit in a handler that covers both the drawing and everything after EndPageEdit.

Putting it together

Branding a document that arrives from elsewhere usually needs both layers: a watermark underneath and a footer on top, sized per page and applied across the whole document. This combines the lifecycle, the coordinate space and both layer modes in one pass, and saves under a new name.

Watermark and number an existing documentPascal
procedure TForm1.BrandExistingDocument(const ASourceFile, ATargetFile,
  AWatermark: string);
var
  p: TTMSFNCPDFLib;
  Info: TTMSFNCPDFPageInfo;
  I, PageCount: Integer;
begin
  p := TTMSFNCPDFLib.Create;
  try
    p.OpenDocument(ASourceFile);
    try
      PageCount := p.GetPageCount;
      for I := 0 to PageCount - 1 do
      begin
        { Each page can have its own size and rotation, so read the geometry
          per page instead of assuming the size of page 1. }
        Info := p.GetDocumentPageInfo(I);

        { Pass 1 - the watermark goes underneath the original content. }
        p.BeginPageEdit(I, pelUnderlay);
        try
          p.Graphics.Font.Name := 'Segoe UI';
          p.Graphics.Font.Size := 56;
          p.Graphics.Font.Color := gcGainsboro;
          p.Graphics.Alignment := gtaCenter;
          p.Graphics.DrawText(AWatermark,
            RectF(0, Info.Height / 2 - 40, Info.Width, Info.Height / 2 + 40));
          p.EndPageEdit;
        except
          p.CancelPageEdit;
          raise;
        end;

        { Pass 2 - the footer goes on top, so it stays legible over content. }
        p.BeginPageEdit(I, pelOverlay);
        try
          p.Graphics.Stroke.Color := gcSilver;
          p.Graphics.Stroke.Width := 1;
          p.Graphics.DrawLine(PointF(40, Info.Height - 46),
            PointF(Info.Width - 40, Info.Height - 46));

          p.Graphics.Font.Name := 'Segoe UI';
          p.Graphics.Font.Size := 9;
          p.Graphics.Font.Color := gcGray;
          p.Graphics.Alignment := gtaCenter;
          p.Graphics.DrawText(Format('Page %d of %d', [I + 1, PageCount]),
            RectF(0, Info.Height - 40, Info.Width, Info.Height - 20));
          p.EndPageEdit;
        except
          p.CancelPageEdit;
          raise;
        end;
      end;

      p.SaveDocument(ATargetFile);
    finally
      p.CloseDocument;
    end;
  finally
    p.Free;
  end;
end;

Common mistakes

  • Drawing before BeginPageEdit. Outside an edit, Graphics addresses the generation canvas, so the drawing silently goes nowhere near the opened document.
  • Leaving an edit open. The next structural call — including SaveDocument — raises ETMSFNCPDFDocument until you end or cancel.
  • Expecting CloseDocument to complain about it. It does not: CloseDocument and OpenDocument cancel an active edit silently. An edit left open when a finally closes the document is discarded without a word, so the page comes out unstamped rather than raising.
  • Styling a link through Font. AddURL draws with URLFont and puts Font back when it returns, so set the name, size and colour on URLFont.
  • Expecting an internal link to survive. An AddGoTo link made on the edit surface targets that surface's own single page, so it is dropped when the edit merges. Use AddURL for external targets.
  • Reaching for a page edit when you need a new page. BeginPageEdit draws on a page that already exists; NewPage on an opened document raises rather than appending one. Generate the page in its own document and bring it in with MergeDocument — see Editing existing documents.
  • Nesting edits. Finish the page you are on before beginning the next; two concurrent edits are not supported.
  • Laying out against a fixed page size. Use GetDocumentPageInfo per page; A4 is an assumption, not a guarantee.
  • Expecting an automatic header or footer. They are switched off on the edit surface by design — draw them yourself.
  • Overlaying a large opaque shape. Use pelUnderlay, or the content you were trying to annotate disappears.

See also