Agregar marcas de agua de texto e imagen e imágenes de fondo a las páginas
De un vistazo
Sección titulada «De un vistazo»Puede que necesites una marca «DRAFT» o «CONFIDENTIAL» a lo largo de cada página, o un logotipo tenue detrás del contenido. Esta receta superpone ambos elementos en las páginas del núcleo de NextPDF mediante la superficie pública del documento: setAlpha() para la transparencia, startTransform() / rotate() / stopTransform() para un sello diagonal, text() para la marca, e image() para un fondo ráster.
Una marca de agua y un fondo solo se diferencian en una decisión: el orden de pintado.
- Fondo: se pinta primero y luego se escribe el contenido de la página encima. La marca queda detrás del texto.
- Marca de agua superpuesta: se escribe primero el contenido de la página y luego se pinta la marca encima. La marca queda por encima.
NextPDF pinta el contenido en el orden en que se invoca, así que ese orden de llamadas es el orden de las capas. No existe un «modo de fondo» aparte. La capa se elige según cuándo se dibuja.
Requisitos previos: una instalación del núcleo (composer require nextpdf/core:^3) y, para un fondo con imagen, un archivo ráster legible (PNG, JPEG o WebP) en disco. Todo el procesamiento se ejecuta en el proceso, sin navegador headless y sin llamadas de red.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3Panorama conceptual
Sección titulada «Panorama conceptual»Cada marca que se agrega es contenido de página normal dibujado a través de un estado gráfico. Tres piezas de la superficie pública se combinan para producir una marca de agua:
-
Transparencia.
setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal)establece la opacidad de relleno para todo lo que se dibuje después, desde0.0(invisible) hasta1.0(opaco). Una marca de agua suele situarse entre0.1y0.3para que el contenido subyacente siga siendo legible. El modo de fusión proviene de la enumeraciónNextPDF\Graphics\BlendMode. Por ejemplo,BlendMode::Multiplyoscurece las zonas donde la marca se superpone al contenido. -
Rotación. Un sello diagonal es texto girado alrededor de un punto de pivote.
startTransform()guarda el estado gráfico,rotate(float $angle, float $x, float $y)gira el sistema de coordenadas en sentido antihorario alrededor de($x, $y), ystopTransform()restaura el estado guardado. Encerrar la marca en un bloque de transformación impide que la rotación y el alfa se filtren al resto de la página. -
La marca en sí.
text(float $x, float $y, string $text)escribe una cadena en una posición absoluta con la fuente, el color y el alfa actuales.image(string $file, ?float $x, ?float $y, ?float $width, ?float $height)coloca una imagen ráster: la base de una marca de agua de imagen o de un fondo de página completa.
El estado gráfico se restaura correctamente porque startTransform() y stopTransform() delimitan el cambio. El valor de setAlpha() persiste hasta que se vuelve a establecer. Por eso, si el contenido posterior debe ser totalmente opaco, hay que restablecer la opacidad a 1.0 después de la marca. El patrón más seguro, que se muestra a continuación, dibuja la marca dentro de su propio bloque de transformación y establece de forma explícita el alfa del contenido de la página.
El paquete también incluye los objetos de valor NextPDF\Graphics\Watermark y NextPDF\Graphics\WatermarkPosition. Watermark es un contenedor de configuración inmutable: texto, tamaño de fuente, ángulo, color, indicador de superposición y posiciones predefinidas como WatermarkPosition::Diagonal. Modelan los parámetros de una marca de agua. Esta receta pinta la marca con los métodos anteriores de escritura en la página, de modo que la salida llega directamente al flujo de contenido de la página.
Superficie de la API
Sección titulada «Superficie de la API»Todos los métodos siguientes son públicos en NextPDF\Core\Document y devuelven static, por lo que pueden encadenarse.
setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: establece la opacidad de relleno (0.0-1.0) y el modo de fusión para el contenido posterior.startTransform(): static: guarda el estado gráfico (emiteq).rotate(float $angle, float $x = 0, float $y = 0): static: gira el sistema de coordenadas$anglegrados en sentido antihorario alrededor del pivote($x, $y).stopTransform(): static: restaura el estado guardado porstartTransform()(emiteQ), deshaciendo a la vez la rotación y el cambio de alfa.setFont(string $family, string $style = '', float $size = 12.0): static: selecciona la fuente para la marca. La familia Base-14helveticaestá siempre disponible y no necesita ningún archivo de fuente.setTextColor(int $r, int $g = -1, int $b = -1): static: establece el color de la marca en rojo, verde y azul (o en un único valor en escala de grises).text(float $x, float $y, string $text): static: escribe la marca en una posición absoluta.image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: coloca una imagen ráster, la base de una marca de agua de imagen o de un fondo a página completa.getPageWidth(): float/getPageHeight(): float: obtiene el tamaño actual de la página en puntos para poder centrar la marca.
Los tipos auxiliares viven bajo NextPDF\Graphics: la enumeración BlendMode, el objeto de valor Color y el par de configuración Watermark / WatermarkPosition.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»Este ejemplo escribe una página, pinta un sello «DRAFT» diagonal tenue sobre el contenido y guarda el archivo. Omite el manejo de errores para mostrar la estructura de las llamadas. El ejemplo de producción que aparece a continuación agrega todas las protecciones.
<?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());Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»Este programa autónomo pinta una marca de agua de texto diagonal sobre el contenido generado. Luego, cuando se proporciona una ruta de imagen mediante la variable de entorno NEXTPDF_WATERMARK_IMAGE, coloca esa imagen como un fondo tenue y centrado en una segunda página. Valida la ruta de la imagen antes de usarla, captura las excepciones más específicas de NextPDF y escribe el resultado en una ruta controlada por el servidor. El contenido en memoria puede reemplazarse por el propio, y la salida puede conectarse a la capa de respuesta o de almacenamiento.
<?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);Salida esperada en STDOUT (el tamaño en bytes depende de la compilación y de si se proporcionó una imagen):
Wrote <n>-byte PDF to <path>Casos límite y errores comunes
Sección titulada «Casos límite y errores comunes»- El orden de las capas es el orden de las llamadas. Un fondo es contenido dibujado antes del contenido de tu página. Una marca de agua superpuesta es contenido dibujado después. Ningún indicador reordena las capas; en su lugar, mueve la llamada.
- El orden de las capas es el orden de las llamadas. Un fondo es contenido dibujado antes del contenido de la página. Una marca de agua superpuesta es contenido dibujado después. Ningún indicador reordena las capas; en su lugar, hay que mover la llamada.
- El alfa persiste hasta que se restablece.
setAlpha()cambia el estado para todo lo que se dibuja después. O bien se delimita la marca enstartTransform()/stopTransform(), que restaura el alfa anterior, o se llama asetAlpha(1.0)antes del contenido opaco. El ejemplo de producción hace ambas cosas. - Equilibrar cada bloque de transformación. Cada
startTransform()necesita unstopTransform()que lo acompañe. Un bloque desequilibrado deja la rotación o el alfa aplicados al contenido posterior, y unstopTransform()faltante es un desequilibrio del estado gráfico que el escritor rechaza en la salida. rotate()usa coordenadas de usuario como pivote. El pivote($x, $y)está en unidades de usuario medidas desde la esquina superior izquierda de la página, el mismo sistema de referencia quetext(). Para una diagonal que cruce el centro, se usa el centro de la página (getPageWidth() / 2,getPageHeight() / 2).- El texto rotado necesita un desplazamiento de ancho manual.
text()coloca el origen de la cadena; no la centra automáticamente. Hay que restar aproximadamente la mitad del ancho de texto estimado de la X del pivote para que la marca rotada quede a horcajadas sobre el centro, tal como hace la función auxiliar. - Las imágenes se escalan al cuadro que se les pasa.
image()estira el ráster alwidthy alheightque se indican. Para un fondo de página completa, se pasa el ancho y el alto de la página; para un logotipo de esquina, se pasa su tamaño natural. Una dimensión cero o negativa lanzaPageLayoutException. image()rechaza las URL y los bytes NUL. Una rutascheme://o un byte NUL en$filelanzaPageLayoutExceptionantes de cualquier decodificación. Se debe pasar solo una ruta local y validada.- La marca es contenido visible. Una marca de agua dibujada de esta forma es contenido de página real, no una anotación oculta. Cualquiera que tenga el archivo puede leerla. Es una señal visual, no un control de acceso.
Rendimiento
Sección titulada «Rendimiento»Una marca de agua de texto es un puñado de operadores de flujo de contenido por página y añade un consumo insignificante de tiempo y memoria. Una marca de agua o un fondo de imagen requieren una decodificación del ráster más los bytes de la imagen incrustada en la salida. Al reutilizar la misma imagen entre páginas, se reutiliza el XObject decodificado a través de la caché de imágenes, por lo que el costo de decodificación se paga una sola vez. Conviene ajustar el tamaño de las imágenes de fondo a su cuadro de visualización antes de incrustarlas. Una foto de 4000 px dimensionada para una página tamaño carta almacena bytes que el lector nunca ve. Una marca de agua de texto típica de una sola página suele quedar con amplio margen dentro de un presupuesto de 500 ms de reloj y 32 MB de pico. Un fondo de imagen depende del tamaño decodificado del ráster de origen.
Notas de seguridad
Sección titulada «Notas de seguridad»El procesamiento se ejecuta en el proceso. Ningún byte del documento sale del host y no se hace ninguna llamada de red. Hay que tratar cualquier ruta de imagen que se origine fuera del código como entrada no confiable.
- Validar la ruta de la imagen antes de usarla. Rechazar los bytes NUL, resolver la ruta con
realpath()y confirmaris_file()eis_readable()antes de llamar aimage(), exactamente como lo hace el ejemplo de producción. Esto bloquea el path traversal y rechaza de forma temprana los directorios y los enlaces colgantes. - No interpolar nunca un campo de la solicitud en una ruta. Derivar la ruta de la imagen y la ruta de salida de valores controlados por el servidor, no de un parámetro de la solicitud. Esto evita leer o escribir archivos fuera del directorio previsto.
- Tratar las imágenes no confiables como entrada hostil. Un ráster malformado lanza
ImageProcessingExceptionen lugar de corromper el documento, y el cargador limita las dimensiones de la imagen para resistir entradas de tipo bomba de descompresión. Capturar la excepción y rechazar la subida. No reintentar a ciegas. - Una marca de agua no es un almacén de secretos. La marca es contenido visible. No codificar credenciales, tokens ni identificadores internos en una marca de agua o un fondo que se devuelva a un cliente.
Conformidad
Sección titulada «Conformidad»Esta receta no formula por sí misma ninguna afirmación normativa sobre estándares. Compone las primitivas públicas de alfa, transformación, texto e imagen. Cada una de ellas emite operadores estándar de flujo de contenido PDF. El estado gráfico se aísla con los operadores q / Q que emiten startTransform() y stopTransform(), y la transparencia se transmite a través de un parámetro de estado gráfico ExtGState. La salida es estructuralmente nueva, no estable a nivel de bytes, por lo que esta página declara un perfil de reproducibilidad structural. Para consultar el detalle a nivel de operador de la superficie de transformación y de estado gráfico, consulta la referencia del módulo Graphics.
Véase también
Sección titulada «Véase también»- Referencia del módulo Graphics: toda la superficie de trazado, transformación, color y estado gráfico relacionada con estos métodos.
- Incrustar imágenes: carga, dimensionamiento y colocación de imágenes ráster, la base de una marca de agua de imagen o de un fondo.
- Gradientes y transparencia: la superficie de alfa y de modo de fusión explicada en profundidad, incluidos los rellenos translúcidos.
- Transformar el espacio de coordenadas: rotación, escalado y traslado de contenido con bloques de transformación equilibrados.
- Manejo de errores basado en excepciones: la jerarquía de excepciones de NextPDF relacionada con
ImageProcessingException,PageLayoutExceptionyNextPdfException.