Salta ai contenuti

Aggiungere filigrane di testo o immagine e sfondi alle pagine

Si desidera un contrassegno «DRAFT» o «CONFIDENTIAL» su ogni pagina, oppure un logo sfumato dietro al contenuto. Questa ricetta mostra come sovrapporre entrambi alle pagine di NextPDF core tramite la superficie pubblica del documento: setAlpha() per la trasparenza, startTransform() / rotate() / stopTransform() per un timbro diagonale, text() per il contrassegno e image() per uno sfondo raster.

Una filigrana e uno sfondo differiscono per una sola scelta: l’ordine di disegno.

  • Sfondo: disegnarlo per primo, poi scrivere sopra il contenuto della pagina. Il contrassegno resta dietro il testo.
  • Filigrana in sovrimpressione: scrivere prima il contenuto della pagina, poi disegnare il contrassegno sopra di esso. Il contrassegno resta in primo piano.

NextPDF disegna il contenuto nell’ordine in cui vengono richiamati i metodi, quindi l’ordine delle chiamate è l’ordine dei livelli. Non esiste una «modalità sfondo» separata. Si decide il livello scegliendo quando disegnare.

Prerequisiti: un’installazione di NextPDF core (composer require nextpdf/core:^3) e, per uno sfondo immagine, un file raster leggibile (PNG, JPEG o WebP) su disco. L’intera pipeline viene eseguita nel processo, senza browser headless e senza chiamate di rete.

Terminal window
composer require nextpdf/core:^3

Ogni contrassegno aggiunto è semplice contenuto di pagina disegnato tramite uno stato grafico. Tre elementi della superficie pubblica si combinano per produrre una filigrana:

  1. Trasparenza. setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) imposta l’opacità di riempimento per tutto ciò che viene disegnato in seguito, da 0.0 (invisibile) a 1.0 (opaco). Per una filigrana si usa di solito un valore tra 0.1 e 0.3, in modo che il contenuto sottostante resti leggibile. La modalità di fusione proviene dall’enum NextPDF\Graphics\BlendMode. Per esempio, BlendMode::Multiply scurisce i punti in cui il contrassegno si sovrappone al contenuto.

  2. Rotazione. Un timbro diagonale è testo ruotato attorno a un punto pivot. startTransform() salva lo stato grafico, rotate(float $angle, float $x, float $y) ruota il sistema di coordinate in senso antiorario attorno a ($x, $y) e stopTransform() ripristina lo stato salvato. Racchiudere il contrassegno in un blocco di trasformazione impedisce che la rotazione e l’alpha si propaghino al resto della pagina.

  3. Il contrassegno vero e proprio. text(float $x, float $y, string $text) scrive una stringa in una posizione assoluta con il font, il colore e l’alpha correnti. image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) colloca un’immagine raster: l’elemento costitutivo di una filigrana immagine o di uno sfondo a pagina intera.

Lo stato grafico viene ripristinato in modo pulito perché startTransform() e stopTransform() racchiudono la modifica. Il valore di setAlpha() persiste finché non viene impostato di nuovo. Quindi, se il contenuto successivo deve essere completamente opaco, reimpostare l’opacità a 1.0 dopo il contrassegno. Il pattern più sicuro, mostrato di seguito, disegna il contrassegno all’interno del proprio blocco di trasformazione e imposta esplicitamente l’alpha del contenuto della pagina.

Il pacchetto include inoltre i value object NextPDF\Graphics\Watermark e NextPDF\Graphics\WatermarkPosition. Watermark è un contenitore di configurazione immutabile: testo, dimensione del font, angolo, colore, flag di sovrimpressione e preset di posizione come WatermarkPosition::Diagonal. Questi oggetti modellano i parametri di una filigrana. Questa ricetta disegna il contrassegno con i metodi di commit della pagina descritti sopra, in modo che l’output arrivi direttamente nel flusso di contenuto della pagina.

Tutti i metodi seguenti sono pubblici in NextPDF\Core\Document e restituiscono static, quindi sono concatenabili.

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: imposta l’opacità di riempimento (0.0-1.0) e la modalità di fusione per il contenuto successivo.
  • startTransform(): static: salva lo stato grafico (emette q).
  • rotate(float $angle, float $x = 0, float $y = 0): static: ruota il sistema di coordinate di $angle gradi in senso antiorario attorno al pivot ($x, $y).
  • stopTransform(): static: ripristina lo stato salvato da startTransform() (emette Q), annullando insieme la rotazione e la modifica dell’alpha.
  • setFont(string $family, string $style = '', float $size = 12.0): static: seleziona il font per il contrassegno. La famiglia Base-14 helvetica è sempre disponibile e non richiede alcun file di font.
  • setTextColor(int $r, int $g = -1, int $b = -1): static: imposta il colore del contrassegno come componenti rosso, verde e blu (oppure con un singolo valore in scala di grigi).
  • text(float $x, float $y, string $text): static: scrive il contrassegno in una posizione assoluta.
  • image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: colloca un’immagine raster, la base di una filigrana immagine o di uno sfondo a pagina intera.
  • getPageWidth(): float / getPageHeight(): float: leggono le dimensioni correnti della pagina in punti, così da poter centrare il contrassegno.

I tipi di supporto si trovano sotto NextPDF\Graphics: l’enum BlendMode, il value object Color e la coppia di configurazione Watermark / WatermarkPosition.

Questo esempio scrive una pagina, disegna un timbro diagonale «DRAFT» sfumato sopra il contenuto e salva il file. Tralascia la gestione degli errori per mostrare la forma delle chiamate. L’esempio di produzione più avanti aggiunge tutte le protezioni.

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

Questo programma autonomo disegna una filigrana di testo diagonale sopra il contenuto generato. Quando poi viene fornito un percorso immagine tramite la variabile d’ambiente NEXTPDF_WATERMARK_IMAGE, colloca l’immagine come sfondo sfumato e centrato su una seconda pagina. Convalida il percorso dell’immagine prima dell’uso, intercetta le eccezioni NextPDF più specifiche e scrive il risultato in un percorso controllato dal server. Sostituire il contenuto in memoria con il proprio e collegare l’output al proprio livello di risposta o archiviazione.

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

Output STDOUT atteso (la dimensione in byte dipende dalla build e dal fatto che sia stata fornita un’immagine):

Wrote <n>-byte PDF to <path>
  • L’ordine dei livelli è l’ordine delle chiamate. Uno sfondo è contenuto disegnato prima del contenuto della pagina. Una filigrana in sovrimpressione è contenuto disegnato dopo. Nessun flag riordina i livelli; spostare invece la chiamata interessata.
  • L’alpha persiste finché non viene reimpostato. setAlpha() modifica lo stato per tutto ciò che viene disegnato in seguito. È possibile racchiudere il contrassegno in startTransform() / stopTransform(), che ripristina l’alpha precedente, oppure richiamare setAlpha(1.0) prima del contenuto opaco. L’esempio di produzione fa entrambe le cose.
  • Bilanciare ogni blocco di trasformazione. Ogni startTransform() richiede un corrispondente stopTransform(). Un blocco non bilanciato lascia la rotazione o l’alpha applicati al contenuto successivo, e uno stopTransform() mancante è uno squilibrio dello stato grafico che il writer rifiuta in fase di output.
  • rotate() ruota in coordinate utente. Il pivot ($x, $y) è in unità utente misurate dall’angolo superiore sinistro della pagina, lo stesso riferimento di text(). Per una diagonale che passa per il centro, usare il centro della pagina (getPageWidth() / 2, getPageHeight() / 2).
  • Il testo ruotato richiede un offset manuale per la larghezza. text() colloca l’origine della stringa; non la centra automaticamente. Sottrarre circa metà della larghezza stimata del testo dalla X del pivot, in modo che il contrassegno ruotato si disponga a cavallo del centro, come fa la funzione di supporto.
  • Le immagini si ridimensionano al riquadro passato. image() deforma il raster fino alla width e all’height indicate. Per uno sfondo a pagina intera, passare la larghezza e l’altezza della pagina; per un logo d’angolo, passare le sue dimensioni naturali. Una dimensione pari a zero o negativa solleva PageLayoutException.
  • image() rifiuta URL e byte NUL. Un percorso scheme:// o un byte NUL in $file solleva PageLayoutException prima di qualsiasi decodifica. Passare solo un percorso locale e convalidato.
  • Il contrassegno è contenuto visibile. Una filigrana disegnata in questo modo è contenuto di pagina reale, non un’annotazione nascosta. Chiunque disponga del file può leggerla. È un segnale visivo, non un controllo degli accessi.

Una filigrana di testo è una manciata di operatori del flusso di contenuto per pagina e incide in modo trascurabile su tempo e memoria. Una filigrana o uno sfondo immagine richiede una decodifica del raster, oltre ai byte dell’immagine incorporata nell’output. Quando la stessa immagine viene riutilizzata su più pagine, l’XObject decodificato viene riutilizzato tramite la cache delle immagini, quindi il costo di decodifica si paga una sola volta. Dimensionare le immagini di sfondo al loro riquadro di visualizzazione prima dell’incorporamento. Una foto da 4000 px ridimensionata in una pagina Letter memorizza byte che il visualizzatore non mostrerà mai. Una tipica filigrana di testo a pagina singola rientra ampiamente in un budget di 500 ms di tempo e 32 MB di picco. Uno sfondo immagine dipende dalla dimensione decodificata del raster di origine.

La pipeline viene eseguita nel processo. Nessun byte del documento lascia l’host e non viene effettuata alcuna chiamata di rete. Trattare come input non attendibile qualsiasi percorso immagine che provenga dall’esterno del proprio codice.

  • Convalidare il percorso dell’immagine prima dell’uso. Rifiutare i byte NUL, risolvere il percorso con realpath() e verificare is_file() e is_readable() prima di richiamare image(), esattamente come fa l’esempio di produzione. Questo blocca il path traversal e rifiuta in anticipo directory e link pendenti.
  • Non interpolare mai un campo di una richiesta in un percorso. Derivare il percorso dell’immagine e il percorso di output da valori controllati dal server, non da un parametro della richiesta. Questo impedisce di leggere o scrivere file al di fuori della directory prevista.
  • Trattare le immagini non attendibili come input ostile. Un raster malformato solleva ImageProcessingException invece di corrompere il documento, e il loader limita le dimensioni dell’immagine per resistere a input di tipo decompression bomb. Intercettare l’eccezione e rifiutare il caricamento. Non riprovare alla cieca.
  • Una filigrana non è un contenitore di segreti. Il contrassegno è contenuto visibile. Non codificare credenziali, token o identificatori interni in una filigrana o in uno sfondo che si restituisce a un client.

Questa ricetta non formula alcuna dichiarazione normativa di conformità agli standard. Compone le primitive pubbliche di alpha, trasformazioni, testo e immagini. Ognuna di queste emette operatori standard del flusso di contenuto PDF. Lo stato grafico è isolato con gli operatori q / Q che startTransform() e stopTransform() emettono, e la trasparenza è veicolata tramite un parametro di stato grafico ExtGState. L’output è strutturalmente nuovo anziché stabile a livello di byte, quindi questa pagina dichiara un profilo di riproducibilità structural. Per i dettagli a livello di operatori della superficie di trasformazioni e stato grafico, vedere il riferimento del modulo Graphics.