Technical Article

Page Stitching and Watermark Templates in Delphi PDFs

Page stitching composes the content of existing pages onto a shared canvas, which is a different operation from merging files. losLab PDF Library implements Form XObject page stitching and composable watermark templates for Delphi: StitchPageOverlay, StitchPagesSideBySide, and StitchPagesVertically place captured pages at any scale with full vector fidelity, and TPDFlibCompositeWatermark stacks text and image watermarks across a page range in one call. If what you actually need is to append whole pages from several documents into one file, that is the merge family, covered in the article on fast byte-level PDF merging; this article is about combining page content within one coordinate space

The requests that lead here are mundane and stubborn. A school wants two exam pages imposed on one landscape sheet to halve paper use. A localisation reviewer wants the English page and its translation side by side on a single page, scrolled together. A legal team wants every draft stamped with three layers at once: a logo in the corner, a diagonal CONFIDENTIAL across the middle, and a small footer line, on pages 1 through 3 only. All three are the same underlying problem, namely placing already-typeset page content somewhere else at some other size without degrading it to a bitmap

Why does concatenating PDF content streams break?

Naive stitching, copying the source page's /Contents stream into the target page, fails for three structural reasons. First, coordinates collide: both content streams assume their own origin at the bottom-left of their own MediaBox, so the pasted content lands on top of the existing content at the wrong place. Second, resource names collide: each page resolves operators like /F1 Tf and /Im0 Do against its own /Resources dictionary, and two pages routinely bind the same name to different fonts or images, so one of them silently renders with the wrong resource. Third, a raw stream copy gives you no scaling at all; the best you can do is translate

Form XObjects solve all three at once, and that is why losLab PDF Library builds its stitching on them. ISO 32000-1 §8.10 defines a form XObject as a self-contained content stream with its own /Resources dictionary and its own coordinate space, placed into a page by invoking its name under whatever current transformation matrix is active. CapturePage wraps an entire existing page, content plus resources, into a /Subtype /Form XObject, and the stitch functions then position it with a cm matrix: scale, translate, rotate, at will. Because the source stays vector, text remains text and paths remain paths at any zoom level, the same property that makes vector graphics and shading patterns resolution-independent. Nothing is rasterised, and the isolated resource dictionary means the captured page cannot fight with the target page over the name /F1

How do you combine two PDF pages onto one page?

StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) is the direct answer: it captures both source pages, creates a new wider page, and places the two captures left and right with a gutter between them, each scaled to fit TargetHeight. The function returns the new page number (1-based) or 0 on failure, and both source pages are captured and hidden as part of the operation. The two-pages-per-sheet exam case is a three-line job

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

For vertical composition, StitchPagesVertically(PageRanges, Gap, TargetWidth) stacks a whole page range into one tall, poster-style page. PageRanges takes the standard range syntax such as '1-3,5'. Each source page is scaled so its width matches TargetWidth, so the height contribution of each page is its own height times its own scale factor, and the output page height is the sum of those plus one Gap between each pair. This is the shape you want for continuous-scroll review documents or long receipts assembled from letter-size sources

How do you overlay one page onto another?

StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) paints one existing page onto another existing page, rather than creating a new one. The source page is captured as a form XObject and drawn onto the target at the rectangle you give, with Opacity from 0 to 1 (1 is opaque). Width and height are free parameters, so the placement stretches if your ratio differs from the source page's own aspect ratio; pass a proportional pair when you want an undistorted thumbnail of the source. A typical use is a document whose approval block is maintained as its own page and stamped onto the cover

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

What happens to page numbers after a capture?

CapturePage consumes the source page: its content is moved into the XObject and the now-empty page is hidden, so every page numbered above the source immediately shifts down by one. This is the single most important semantic in the whole stitching API. losLab PDF Library handles the arithmetic inside each stitch call, StitchPageOverlay adjusts the target index when the target sits above the source, and StitchPagesSideBySide captures the higher-numbered page first so the second capture's index is still valid, but your own surrounding code must account for the shift between calls. Two rules keep multi-stitch code correct. Record any page sizes or page numbers you will need before the first capture, because they change afterward. And when you capture several pages yourself in a loop, iterate in descending page order, so that hiding a later page never disturbs the index of an earlier one you have not captured yet

How do you apply a multi-layer watermark to a page range?

The watermark template layer in PDFlibWatermark.pas turns "define once, apply to many pages" into objects. TPDFlibWatermarkBase carries what every watermark shares: a nine-point Position (wpTopLeft through wpBottomRight, plus wpCustom which uses raw X/Y coordinates), an Angle in degrees, an Opacity from 0 to 1, and a PageRange string where empty means all pages. TPDFlibTextWatermark adds text, a font ID (defaulting to Helvetica), size, and an RGB colour; TPDFlibImageWatermark adds an image ID from a previously added image plus a placement width and height. TPDFlibCompositeWatermark holds a list of sub-watermarks and applies them recursively, and TPDFlib.ApplyWatermark is the one-call entry point that honors the template's own page range and returns the number of pages stamped. The three-layer legal stamp looks like this

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

Two composition details are worth spelling out. Range inheritance is one-way and non-destructive: a child with an empty PageRange temporarily inherits the composite's range during Apply, while a child that sets its own range keeps it, so you can pin the footer to every page while the diagonal stamp covers only the draft section. And ownership follows the composite: TPDFlibCompositeWatermark.Destroy frees its children, so you free the composite once and nothing else, as in the example above

Boundaries worth knowing

Two honest limits apply, and both follow from how the mechanism works. First, a capture wraps the page's content stream and resources only. Annotations, links, form fields, and comments live as separate objects in the page dictionary, not in the content stream, so they do not travel into the XObject; a stitched layout carries the visible page content but not the interactive layer of its sources. Second, the nine-point text positioning is an approximation: without a full layout engine the library cannot know the exact rendered width of a string, so it estimates the text box at 40% of the page width when resolving positions like wpCenter. That is accurate enough for centring a short label, but for pixel-exact placement of long strings, switch to wpCustom and compute the coordinates yourself

Scale is not a practical constraint here, since capture and stitch manipulate object references rather than re-encoding page content; if your sources are hundreds of megabytes, the same discipline described in the guide to large-file merge and split with direct access applies before you stitch. The stitching functions and the watermark template classes shown here ship in the current losLab PDF Library for Delphi, C# and VB.NET, alongside the lower-level CapturePage, DrawCapturedPage, and DrawCapturedPageMatrix primitives when you need a layout the three prebuilt stitch calls do not cover