Zum Inhalt springen

Text- und Bildwasserzeichen oder Hintergründe zu Seiten hinzufügen

Sie möchten auf jeder Seite eine Markierung wie „DRAFT“ oder „CONFIDENTIAL“ einfügen oder ein blasses Logo hinter dem Inhalt platzieren. Dieses Recipe setzt beides auf NextPDF-Core-Seiten um – mit der öffentlichen Dokumentoberfläche: setAlpha() für Transparenz, startTransform() / rotate() / stopTransform() für einen Diagonalstempel, text() für die Markierung und image() für einen Rasterhintergrund.

Ein Wasserzeichen und ein Hintergrund unterscheiden sich nur durch eine Entscheidung: die Zeichenreihenfolge.

  • Hintergrund: zuerst zeichnen, dann Ihren Seiteninhalt darüber schreiben. Die Markierung liegt hinter dem Text.
  • Overlay-Wasserzeichen: zuerst Ihren Seiteninhalt schreiben, dann die Markierung darüber zeichnen. Die Markierung liegt obenauf.

NextPDF zeichnet Inhalt in der Reihenfolge, in der Sie ihn aufrufen – Ihre Aufrufreihenfolge ist also die Ebenenreihenfolge. Es gibt keinen separaten „Background-Modus“. Sie wählen die Ebene, indem Sie festlegen, wann Sie zeichnen.

Voraussetzungen: eine Core-Installation (composer require nextpdf/core:^3) und – für einen Bildhintergrund – eine lesbare Rasterdatei (PNG, JPEG oder WebP) auf der Festplatte. Die gesamte Pipeline läuft im Prozess – ohne Headless-Browser und ohne Netzwerkaufruf.

Terminal-Fenster
composer require nextpdf/core:^3

Jede Markierung, die Sie hinzufügen, ist normaler Seiteninhalt, der über einen Grafikzustand gezeichnet wird. Drei Teile der öffentlichen Oberfläche greifen zusammen, um ein Wasserzeichen zu erzeugen:

  1. Transparenz. setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) setzt die Fülldeckkraft für alles, was Sie danach zeichnen – von 0.0 (unsichtbar) bis 1.0 (deckend). Ein Wasserzeichen liegt üblicherweise bei 0.1 bis 0.3, damit der Inhalt darunter lesbar bleibt. Der Blend-Modus stammt aus dem NextPDF\Graphics\BlendMode-Enum. Zum Beispiel verdunkelt BlendMode::Multiply dort, wo die Markierung Inhalt überlappt.

  2. Rotation. Ein Diagonalstempel ist Text, der um einen Drehpunkt gedreht wird. startTransform() sichert den Grafikzustand, rotate(float $angle, float $x, float $y) dreht das Koordinatensystem gegen den Uhrzeigersinn um ($x, $y), und stopTransform() stellt den gesicherten Zustand wieder her. Wenn Sie die Markierung in einen Transform-Block einschließen, verhindern Sie, dass Rotation und Alpha in den Rest der Seite überlaufen.

  3. Die Markierung selbst. text(float $x, float $y, string $text) schreibt eine Zeichenkette an einer absoluten Position in der aktuellen Schrift, Farbe und Alpha. image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) platziert ein Rasterbild: die Grundlage eines Bildwasserzeichens oder eines seitenfüllenden Hintergrunds.

Der Grafikzustand wird sauber wiederhergestellt, weil startTransform() und stopTransform() die Änderung einklammern. Der setAlpha()-Wert bleibt erhalten, bis Sie ihn erneut setzen. Wenn späterer Inhalt also vollständig deckend sein muss, setzen Sie die Deckkraft nach der Markierung wieder auf 1.0. Das unten gezeigte, sicherere Muster zeichnet die Markierung in einem eigenen Transform-Block und setzt das Alpha des Seiteninhalts explizit.

Das Paket liefert außerdem die Value Objects NextPDF\Graphics\Watermark und NextPDF\Graphics\WatermarkPosition. Watermark ist ein unveränderliches Konfigurationsobjekt: Text, Schriftgröße, Winkel, Farbe, Overlay-Flag und Positionsvorgaben wie WatermarkPosition::Diagonal. Sie modellieren die Parameter eines Wasserzeichens. Dieses Recipe zeichnet die Markierung mit den oben genannten Methoden zum Schreiben auf Seiten, sodass die Ausgabe direkt in den Content-Stream der Seite gelangt.

Alle folgenden Methoden sind auf NextPDF\Core\Document public und geben static zurück, sodass sie sich verketten lassen.

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: setzt die Fülldeckkraft (0.0-1.0) und den Blend-Modus für nachfolgenden Inhalt.
  • startTransform(): static: sichert den Grafikzustand (gibt q aus).
  • rotate(float $angle, float $x = 0, float $y = 0): static: dreht das Koordinatensystem um $angle Grad gegen den Uhrzeigersinn um den Drehpunkt ($x, $y).
  • stopTransform(): static: stellt den von startTransform() gesicherten Zustand wieder her (gibt Q aus) und macht Rotation und Alpha-Änderung zusammen rückgängig.
  • setFont(string $family, string $style = '', float $size = 12.0): static: wählt die Schrift für die Markierung. Die Base-14-Familie helvetica ist immer verfügbar und braucht keine Schriftdatei.
  • setTextColor(int $r, int $g = -1, int $b = -1): static: setzt die Markierungsfarbe in Rot, Grün und Blau (oder einen einzelnen Graustufenwert).
  • text(float $x, float $y, string $text): static: schreibt die Markierung an einer absoluten Position.
  • image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: platziert ein Rasterbild, die Grundlage eines Bildwasserzeichens oder eines seitenfüllenden Hintergrunds.
  • getPageWidth(): float / getPageHeight(): float: lesen die aktuelle Seitengröße in Punkten, damit Sie die Markierung zentrieren können.

Unterstützende Typen liegen unter NextPDF\Graphics: das BlendMode-Enum, das Color-Value-Object und das Konfigurationspaar Watermark / WatermarkPosition.

Dieses Beispiel schreibt eine Seite, zeichnet einen blassen diagonalen „DRAFT“-Stempel über den Inhalt und speichert die Datei. Es verzichtet auf Fehlerbehandlung, um die Aufrufform zu zeigen. Das Produktionsbeispiel unten ergänzt die vollständigen Schutzmaßnahmen.

<?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());

Dieses eigenständige Programm zeichnet ein diagonales Textwasserzeichen über generierten Inhalt. Wenn zusätzlich ein Bildpfad über die Umgebungsvariable NEXTPDF_WATERMARK_IMAGE bereitgestellt wird, platziert es dieses Bild als blassen, zentrierten Hintergrund auf einer zweiten Seite. Es validiert den Bildpfad vor der Verwendung, fängt die spezifischsten NextPDF-Exceptions ab und schreibt das Ergebnis an einen serverseitig gesteuerten Pfad. Ersetzen Sie den In-Memory-Inhalt durch Ihren eigenen und binden Sie die Ausgabe an Ihre Response- oder Storage-Schicht an.

<?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);

Erwartete Ausgabe auf STDOUT (die Byte-Größe hängt vom Build ab und davon, ob Sie ein Bild bereitgestellt haben):

Wrote <n>-byte PDF to <path>
  • Die Ebenenreihenfolge ist die Aufrufreihenfolge. Ein Hintergrund ist Inhalt, der vor Ihrem Seiteninhalt gezeichnet wird. Ein Overlay-Wasserzeichen ist Inhalt, der danach gezeichnet wird. Kein Flag ordnet die Ebenen um; verschieben Sie stattdessen den Aufruf.
  • Alpha bleibt erhalten, bis es zurückgesetzt wird. setAlpha() ändert den Zustand für alles, was danach gezeichnet wird. Schließen Sie die Markierung entweder in startTransform() / stopTransform() ein, wodurch das vorherige Alpha wiederhergestellt wird, oder rufen Sie setAlpha(1.0) vor deckendem Inhalt auf. Das Produktionsbeispiel macht beides.
  • Halten Sie jeden Transform-Block ausgewogen. Jedes startTransform() braucht ein passendes stopTransform(). Ein unausgewogener Block bewirkt, dass Rotation oder Alpha auf späteren Inhalt angewendet bleiben, und ein fehlendes stopTransform() ist ein unausgewogener Grafikzustand, den der Writer bei der Ausgabe ablehnt.
  • rotate() dreht in Benutzerkoordinaten. Der Drehpunkt ($x, $y) ist in Benutzereinheiten angegeben, gemessen von der oberen linken Ecke der Seite – im selben Bezugsrahmen wie text(). Für eine durch die Mitte verlaufende Diagonale verwenden Sie den Seitenmittelpunkt (getPageWidth() / 2, getPageHeight() / 2).
  • Gedrehter Text braucht einen manuellen Breiten-Versatz. text() platziert den Ursprung der Zeichenkette; eine automatische Zentrierung erfolgt nicht. Ziehen Sie etwa die Hälfte der geschätzten Textbreite vom Drehpunkt-X ab, damit die gedrehte Markierung die Mitte umspannt – wie im Helper gezeigt.
  • Bilder skalieren auf die Box, die Sie übergeben. image() streckt das Raster auf die width und height, die Sie angeben. Für einen seitenfüllenden Hintergrund übergeben Sie die Seitenbreite und -höhe; für ein Ecklogo übergeben Sie seine natürliche Größe. Eine Dimension von null oder negativem Wert löst PageLayoutException aus.
  • image() lehnt URLs und NUL-Bytes ab. Ein scheme://-Pfad oder ein NUL-Byte in $file löst PageLayoutException aus, noch vor jeder Dekodierung. Übergeben Sie nur einen lokalen, validierten Pfad.
  • Die Markierung ist sichtbarer Inhalt. Ein so gezeichnetes Wasserzeichen ist echter Seiteninhalt, keine versteckte Annotation. Jeder, der die Datei hat, kann sie lesen. Sie ist ein visueller Hinweis, keine Zugriffskontrolle.

Ein Textwasserzeichen besteht pro Seite aus einer Handvoll Content-Stream-Operatoren und beansprucht vernachlässigbar wenig Zeit und Speicher. Ein Bildwasserzeichen oder Bildhintergrund kostet eine Dekodierung des Rasters zuzüglich der eingebetteten Bild-Bytes in der Ausgabe. Wenn Sie dasselbe Bild über mehrere Seiten hinweg wiederverwenden, wird das dekodierte XObject über den Bild-Cache wiederverwendet, sodass Sie die Dekodierungskosten nur einmal zahlen. Dimensionieren Sie Hintergrundbilder vor dem Einbetten auf ihre Anzeigebox. Ein 4000-px-Foto, das in eine Letter-Seite skaliert wird, enthält Bytes, die beim Lesen nie sichtbar werden. Ein typisches einseitiges Textwasserzeichen liegt deutlich innerhalb eines Budgets von 500 ms Laufzeit und 32 MB Spitzenspeicher. Ein Bildhintergrund richtet sich nach der dekodierten Größe des Quellrasters.

Die Pipeline läuft im Prozess. Keine Dokument-Bytes verlassen den Host, und es wird kein Netzwerkaufruf gemacht. Behandeln Sie jeden Bildpfad, der von außerhalb Ihres Codes stammt, als nicht vertrauenswürdige Eingabe.

  • Validieren Sie den Bildpfad vor der Verwendung. Weisen Sie NUL-Bytes ab, lösen Sie den Pfad mit realpath() auf und bestätigen Sie is_file() und is_readable(), bevor Sie image() aufrufen – genau so, wie es das Produktionsbeispiel macht. Das blockiert Path Traversal und weist Verzeichnisse sowie ins Leere zeigende Links frühzeitig ab.
  • Interpolieren Sie niemals ein Request-Feld in einen Pfad. Leiten Sie den Bildpfad und den Ausgabepfad aus servergesteuerten Werten ab, nicht aus einem Request-Parameter. Das verhindert, dass Sie Dateien außerhalb des vorgesehenen Verzeichnisses lesen oder schreiben.
  • Behandeln Sie nicht vertrauenswürdige Bilder als feindliche Eingabe. Ein fehlerhaftes Raster löst ImageProcessingException aus, statt das Dokument zu beschädigen, und der Loader begrenzt die Bildabmessungen, um Decompression-Bomb-Eingaben standzuhalten. Fangen Sie die Exception ab und weisen Sie den Upload ab. Wiederholen Sie den Vorgang nicht blind.
  • Ein Wasserzeichen ist kein Geheimnisspeicher. Die Markierung ist sichtbarer Inhalt. Kodieren Sie keine Anmeldedaten, Tokens oder internen Bezeichner in einem Wasserzeichen oder Hintergrund, den Sie an einen Client zurückgeben.

Dieses Recipe erhebt selbst keinen normativen Standardanspruch. Es setzt sich aus den öffentlichen Alpha-, Transform-, Text- und Bildprimitiven zusammen. Jedes davon gibt Standard-PDF-Content-Stream-Operatoren aus. Der Grafikzustand wird mit den Operatoren q / Q isoliert, die startTransform() und stopTransform() ausgeben, und Transparenz wird über einen ExtGState-Grafikzustandsparameter getragen. Die Ausgabe ist strukturell frisch statt bytestabil; daher deklariert diese Seite ein structural-Reproduzierbarkeitsprofil. Für Details auf Operator-Ebene der Transform- und Grafikzustandsoberfläche siehe die Referenz zum Graphics-Modul.