Add text and image watermarks or backgrounds to pages
At a glance
Section titled “At a glance”You can add a “DRAFT” or “CONFIDENTIAL” mark across each page, or place a faint
logo behind the content. This recipe adds both to NextPDF core pages with the
public document surface: setAlpha() for transparency, startTransform() /
rotate() / stopTransform() for a diagonal stamp, text() for the mark, and
image() for a raster background.
A watermark and a background differ in one choice: paint order.
- Background: paint first, then write your page content over it. The mark sits behind the text.
- Overlay watermark: write your page content first, then paint the mark over it. The mark sits on top.
NextPDF paints content in the order you call it, so your call order sets the layer order. There is no separate “background mode”. You choose the layer by choosing when to draw.
Prerequisites: a core install (composer require nextpdf/core:^3), and, for an
image background, a readable raster file (PNG, JPEG, or WebP) on disk. The whole
pipeline runs in process, without a headless browser or a network call.
Install
Section titled “Install”composer require nextpdf/core:^3Conceptual overview
Section titled “Conceptual overview”Every mark you add is regular page content drawn through a graphics state. Three parts of the public surface work together to produce a watermark:
-
Transparency.
setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal)sets the fill opacity for everything you draw afterward, from0.0(invisible) to1.0(opaque). A watermark usually works best at0.1to0.3, so the content underneath stays readable. The blend mode comes from theNextPDF\Graphics\BlendModeenum. For example,BlendMode::Multiplydarkens areas where the mark overlaps content. -
Rotation. A diagonal stamp is text rotated around a pivot point.
startTransform()saves the graphics state,rotate(float $angle, float $x, float $y)turns the coordinate system counter-clockwise about($x, $y), andstopTransform()restores the saved state. Wrapping the mark in a transform block keeps the rotation and alpha from affecting the rest of the page. -
The mark itself.
text(float $x, float $y, string $text)writes a string at an absolute position in the current font, color, and alpha.image(string $file, ?float $x, ?float $y, ?float $width, ?float $height)places a raster image: the base for an image watermark or a full-page background.
The graphics state restores cleanly because startTransform() and
stopTransform() bracket the change. The setAlpha() value persists until you
set it again. If later content must be fully opaque, reset opacity to 1.0
after the mark. The safer pattern, shown below, draws the mark inside its own
transform block and sets the page content’s alpha explicitly.
The package also includes the value objects NextPDF\Graphics\Watermark and
NextPDF\Graphics\WatermarkPosition. Watermark is an immutable configuration
holder for text, font size, angle, color, overlay flag, and position presets,
such as WatermarkPosition::Diagonal. These objects model a watermark’s
parameters. This recipe paints the mark with the page-committing methods above,
so the output reaches the page content stream directly.
API surface
Section titled “API surface”All methods below are public on NextPDF\Core\Document and return static, so
you can chain them.
setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: set the fill opacity (0.0-1.0) and blend mode for later content.startTransform(): static: save the graphics state (emitsq).rotate(float $angle, float $x = 0, float $y = 0): static: rotate the coordinate system$angledegrees counter-clockwise about pivot($x, $y).stopTransform(): static: restore the state saved bystartTransform()(emitsQ), undoing the rotation and alpha change together.setFont(string $family, string $style = '', float $size = 12.0): static: select the font for the mark. The Base-14 familyhelveticais always available and does not need a font file.setTextColor(int $r, int $g = -1, int $b = -1): static: set the mark color in red, green, blue (or a single grayscale value).text(float $x, float $y, string $text): static: write the mark at an absolute position.image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: place a raster image, the base for an image watermark or a full-page background.getPageWidth(): float/getPageHeight(): float: read the current page size in points so you can center the mark.
Supporting types live under NextPDF\Graphics: the BlendMode enum, the
Color value object, and the Watermark / WatermarkPosition configuration
pair.
Code sample — Quick start
Section titled “Code sample — Quick start”This sample writes one page, paints a faint diagonal “DRAFT” stamp over the content, and saves the file. It omits error handling to show the call shape. The production sample below adds the full guards.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->addPage();
// Page content first, so the watermark lands on top of it.$doc->setFont('helvetica', '', 12);$doc->text(20.0, 40.0, 'Quarterly report: internal review copy.');
// Watermark second: a translucent, rotated stamp through the page center.$pivotX = $doc->getPageWidth() / 2.0;$pivotY = $doc->getPageHeight() / 2.0;
$doc->startTransform();$doc->setAlpha(0.15);$doc->setTextColor(150, 150, 150);$doc->setFont('helvetica', 'B', 72);$doc->rotate(45.0, $pivotX, $pivotY);$doc->text($pivotX - 110.0, $pivotY, 'DRAFT');$doc->stopTransform();
file_put_contents(__DIR__ . '/watermarked.pdf', $doc->getPdfData());Code sample — Production
Section titled “Code sample — Production”This self-contained program paints a diagonal text watermark over generated
content. When you supply an image path through the NEXTPDF_WATERMARK_IMAGE
environment variable, it places that image as a faint, centered background on a
second page. It validates the image path before use, catches the most specific
NextPDF exceptions, and writes the result to a server-controlled path. Replace
the in-memory content with your own, then connect the output to your response or
storage layer.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\ImageProcessingException;use NextPDF\Exception\NextPdfException;use NextPDF\Exception\PageLayoutException;
/** * Paint a translucent, rotated text stamp across the current page. * * The mark is bracketed in a transform block so the rotation and the alpha * change are undone together and never leak into later content. * * @param non-empty-string $mark The watermark text (for example "CONFIDENTIAL") */function paintTextWatermark(Document $doc, string $mark): void{ $pivotX = $doc->getPageWidth() / 2.0; $pivotY = $doc->getPageHeight() / 2.0;
// Estimate the mark width so the rotated text sits centered on the pivot. // Helvetica averages ~0.5 em per glyph; half the width offsets the origin. $fontSize = 64.0; $halfWidth = (\strlen($mark) * $fontSize * 0.5) / 2.0;
$doc->startTransform(); $doc->setAlpha(0.12); $doc->setTextColor(120, 120, 120); $doc->setFont('helvetica', 'B', $fontSize); $doc->rotate(45.0, $pivotX, $pivotY); $doc->text($pivotX - $halfWidth, $pivotY, $mark); $doc->stopTransform();}
/** * Place a raster image as a faint, full-page background behind later content. * * The image is drawn first and at low opacity; page content written after this * call sits over it. The path is validated by the caller before it arrives. * * @param non-empty-string $imagePath A readable raster image (PNG, JPEG, WebP) * * @throws ImageProcessingException If the file is missing, unreadable, or corrupt. * @throws PageLayoutException If the placement coordinates are rejected. */function paintImageBackground(Document $doc, string $imagePath): void{ $doc->startTransform(); $doc->setAlpha(0.08); // Cover the full page: origin at the top-left, sized to the page box. $doc->image( file: $imagePath, x: 0.0, y: 0.0, width: $doc->getPageWidth(), height: $doc->getPageHeight(), ); $doc->stopTransform();}
$doc = Document::createStandalone();$doc->setTitle('Watermark and background sample');
// Page 1: content first, then an overlay text watermark on top.$doc->addPage();$doc->setAlpha(1.0);$doc->setTextColor(0, 0, 0);$doc->setFont('helvetica', '', 12);$doc->text(20.0, 40.0, 'Quarterly report: internal review copy.');
try { paintTextWatermark($doc, 'CONFIDENTIAL');} catch (PageLayoutException $e) { // Raised if a coordinate or page state is rejected while placing the mark. throw new RuntimeException( sprintf('Watermark placement failed: %s', $e->getConstraint()), previous: $e, );}
// Page 2: an optional image background, then content over it.$imagePath = getenv('NEXTPDF_WATERMARK_IMAGE');
if ($imagePath !== false && $imagePath !== '') { // Validate the path before touching the image loader: reject NUL bytes, // require a real readable file, and resolve it to defeat path traversal. if (str_contains($imagePath, "\0")) { throw new RuntimeException('Image path must not contain NUL bytes.'); }
$resolved = realpath($imagePath);
if ($resolved === false || !is_file($resolved) || !is_readable($resolved)) { throw new RuntimeException( sprintf('Background image "%s" is not a readable file.', $imagePath), ); }
$doc->addPage();
try { paintImageBackground($doc, $resolved); } catch (ImageProcessingException $e) { // Raised when the file cannot be decoded as a supported raster format. throw new RuntimeException( sprintf( 'Background image rejected (%s, op "%s").', $e->getFormat(), $e->getOperation(), ), previous: $e, ); } catch (PageLayoutException $e) { throw new RuntimeException( sprintf('Background placement failed: %s', $e->getConstraint()), previous: $e, ); }
$doc->setAlpha(1.0); $doc->setTextColor(0, 0, 0); $doc->setFont('helvetica', '', 12); $doc->text(20.0, 40.0, 'Page two over a faint background.');}
try { $pdf = $doc->getPdfData();} catch (NextPdfException $e) { // Base of the NextPDF exception hierarchy: any output-stage failure. throw new RuntimeException( sprintf('Document output failed: %s', $e->getMessage()), previous: $e, );}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$path = $out !== false && $out !== '' ? $out : __DIR__ . '/watermarked.pdf';
if (file_put_contents($path, $pdf) === false) { throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));}
printf("Wrote %d-byte PDF to %s\n", strlen($pdf), $path);Expected standard output (STDOUT) (the byte size depends on the build and on whether you supplied an image):
Wrote <n>-byte PDF to <path>Edge cases & gotchas
Section titled “Edge cases & gotchas”- Layer order is call order. A background is content drawn before your page content. An overlay watermark is content drawn after it. No flag reorders the layers; move the call instead.
- Alpha persists until reset.
setAlpha()changes the state for everything drawn afterward. Either bracket the mark instartTransform()/stopTransform(), which restores the prior alpha, or callsetAlpha(1.0)before opaque content. The production sample does both. - Balance every transform block. Each
startTransform()needs a matchingstopTransform(). An unbalanced block leaves the rotation or alpha applied to later content, and a missingstopTransform()creates a graphics-state imbalance that the writer rejects at output. rotate()pivots in user coordinates. The pivot($x, $y)is in user units measured from the page top-left, in the same frame astext(). For a through-center diagonal, use the page center (getPageWidth() / 2,getPageHeight() / 2).- Rotated text needs a manual width offset.
text()places the string origin; it does not center for you. Subtract roughly half the estimated text width from the pivot X so the rotated mark straddles the center, as the helper does. - Images scale to the box you pass.
image()stretches the raster to thewidthandheightyou give. For a full-page background, pass the page width and height; for a corner logo, pass its natural size. A zero or negative dimension raisesPageLayoutException. image()rejects URLs and NUL bytes. Ascheme://path or a NUL byte in$fileraisesPageLayoutExceptionbefore any decode. Pass only a local, validated path.- The mark is visible content. A watermark drawn this way is real page content, not a hidden annotation. Anyone with the file can read it. It is a visual cue, not an access control.
Performance
Section titled “Performance”A text watermark uses a handful of content-stream operators per page and adds negligible time or memory. An image watermark or background costs one raster decode plus the embedded image bytes in the output. Reusing the same image across pages reuses the decoded XObject through the image cache, so you pay the decode cost once. Size background images to their display box before embedding. A 4000 px photo scaled into a letter page stores bytes the reader never sees. A typical single-page text watermark stays well inside a 500 ms wall and 32 MB peak budget. An image background tracks the source raster’s decoded size.
Security notes
Section titled “Security notes”The pipeline runs in process. No document bytes leave the host, and no network call is made. Treat any image path that originates outside your code as untrusted input.
- Validate the image path before use. Reject NUL bytes, resolve the path
with
realpath(), and confirmis_file()andis_readable()before you callimage(), exactly as the production sample does. This blocks path traversal and rejects directories and dangling links early. - Never interpolate a request field into a path. Derive the image path and the output path from server-controlled values, not from a request parameter. This keeps you from reading or writing files outside the intended directory.
- Treat untrusted images as hostile input. A malformed raster raises
ImageProcessingExceptionrather than corrupting the document, and the loader caps image dimensions to resist decompression-bomb inputs. Catch the exception and reject the upload. Do not retry blindly. - A watermark is not a secret store. The mark is visible content. Do not encode credentials, tokens, or internal identifiers in a watermark or background that you return to a client.
Conformance
Section titled “Conformance”This recipe makes no normative standards claim of its own. It composes the
public alpha, transform, text, and image primitives. Each primitive emits
standard PDF content-stream operators. The graphics state is isolated with the
q / Q operators that startTransform() and stopTransform() emit, and
transparency is carried through an ExtGState graphics-state parameter. The
output is structurally fresh rather than byte-stable, so this page declares a
structural reproducibility profile. For operator-level detail about the
transform and graphics-state surface, see the
Graphics module reference.
See also
Section titled “See also”- Graphics module reference: the full path, transform, color, and graphics-state surface behind these methods.
- Embed images: load, size, and place raster images, the base for an image watermark or background.
- Gradients and transparency: the alpha and blend-mode surface in depth, including translucent fills.
- Transform the coordinate space: rotate, scale, and translate content with balanced transform blocks.
- Exception-aware error handling:
the NextPDF exception hierarchy behind
ImageProcessingException,PageLayoutException, andNextPdfException.