Pular para o conteúdo

Adicionar marcas d'água e planos de fundo com texto e imagem às páginas

Você pode adicionar uma marca “DRAFT” ou “CONFIDENTIAL” a cada página ou colocar um logotipo sutil atrás do conteúdo. Esta receita adiciona os dois às páginas do core do NextPDF com a superfície pública do documento: setAlpha() para transparência, startTransform() / rotate() / stopTransform() para um carimbo diagonal, text() para a marca e image() para um plano de fundo raster.

Uma marca d’água e um plano de fundo diferem em uma única escolha: a ordem de pintura.

  • Plano de fundo: pinte primeiro e, depois, escreva o conteúdo da página sobre ele. A marca fica atrás do texto.
  • Marca d’água sobreposta: escreva primeiro o conteúdo da página e, depois, pinte a marca sobre ele. A marca fica por cima.

O NextPDF pinta o conteúdo na ordem em que você o chama; portanto, a ordem das chamadas define a ordem das camadas. Não existe um “modo de plano de fundo” separado. Você escolhe a camada ao decidir quando desenhar.

Pré-requisitos: uma instalação do core (composer require nextpdf/core:^3) e, para um plano de fundo de imagem, um arquivo raster legível (PNG, JPEG ou WebP) em disco. Todo o pipeline é executado no processo, sem navegador headless nem chamada de rede.

Terminal window
composer require nextpdf/core:^3

Cada marca que você adiciona é conteúdo de página comum, desenhado por meio de um estado gráfico. Três partes da superfície pública trabalham juntas para produzir uma marca d’água:

  1. Transparência. setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) define a opacidade de preenchimento de tudo o que você desenhar em seguida, de 0.0 (invisível) a 1.0 (opaco). Uma marca d’água geralmente funciona melhor entre 0.1 e 0.3, para que o conteúdo abaixo continue legível. O modo de mesclagem vem do enum NextPDF\Graphics\BlendMode. Por exemplo, BlendMode::Multiply escurece as áreas em que a marca se sobrepõe ao conteúdo.

  2. Rotação. Um carimbo diagonal é um texto rotacionado em torno de um ponto de pivô. startTransform() salva o estado gráfico, rotate(float $angle, float $x, float $y) gira o sistema de coordenadas no sentido anti-horário em torno de ($x, $y) e stopTransform() restaura o estado salvo. Envolver a marca em um bloco de transform impede que a rotação e o alpha afetem o restante da página.

  3. A própria marca. text(float $x, float $y, string $text) escreve uma string em uma posição absoluta na fonte, cor e alpha atuais. image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) coloca uma imagem raster: a base para uma marca d’água de imagem ou um plano de fundo de página inteira.

O estado gráfico é restaurado de forma limpa porque startTransform() e stopTransform() delimitam a alteração. O valor de setAlpha() persiste até você defini-lo novamente. Se o conteúdo posterior precisar ser totalmente opaco, redefina a opacidade para 1.0 depois da marca. O padrão mais seguro, mostrado abaixo, desenha a marca dentro de seu próprio bloco de transform e define explicitamente o alpha do conteúdo da página.

O pacote também inclui os objetos de valor NextPDF\Graphics\Watermark e NextPDF\Graphics\WatermarkPosition. Watermark é um portador de configuração imutável para texto, tamanho da fonte, ângulo, cor, flag de sobreposição e predefinições de posição, como WatermarkPosition::Diagonal. Esses objetos modelam os parâmetros de uma marca d’água. Esta receita pinta a marca com os métodos de gravação na página apresentados acima, de modo que a saída chega diretamente ao fluxo de conteúdo (stream) da página.

Todos os métodos abaixo são públicos em NextPDF\Core\Document e retornam static, para que você possa encadeá-los.

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: define a opacidade de preenchimento (0.0-1.0) e o modo de mesclagem para o conteúdo posterior.
  • startTransform(): static: salva o estado gráfico (emite q).
  • rotate(float $angle, float $x = 0, float $y = 0): static: gira o sistema de coordenadas $angle graus no sentido anti-horário em torno do pivô ($x, $y).
  • stopTransform(): static: restaura o estado salvo por startTransform() (emite Q), desfazendo a rotação e a alteração de alpha em conjunto.
  • setFont(string $family, string $style = '', float $size = 12.0): static: seleciona a fonte para a marca. A família Base-14 helvetica está sempre disponível e não precisa de um arquivo de fonte.
  • setTextColor(int $r, int $g = -1, int $b = -1): static: define a cor da marca em vermelho, verde e azul (ou um único valor em escala de cinza).
  • text(float $x, float $y, string $text): static: escreve a marca em uma posição absoluta.
  • image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: coloca uma imagem raster, a base para uma marca d’água de imagem ou um plano de fundo de página inteira.
  • getPageWidth(): float / getPageHeight(): float: lê o tamanho atual da página em pontos para que você possa centralizar a marca.

Os tipos de apoio ficam em NextPDF\Graphics: o enum BlendMode, o objeto de valor Color e o par de configuração Watermark / WatermarkPosition.

Este exemplo escreve uma página, pinta um carimbo “DRAFT” diagonal e sutil sobre o conteúdo e salva o arquivo. Ele omite o tratamento de erros para mostrar o formato das chamadas. O exemplo de produção abaixo acrescenta todas as proteções.

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

Este programa autossuficiente pinta uma marca d’água diagonal em texto sobre o conteúdo gerado. Quando você fornece um caminho de imagem por meio da variável de ambiente NEXTPDF_WATERMARK_IMAGE, ele coloca essa imagem como um plano de fundo sutil e centralizado em uma segunda página. Ele valida o caminho da imagem antes de usá-lo, captura as exceções mais específicas do NextPDF e grava o resultado em um caminho controlado pelo servidor. Substitua o conteúdo em memória pelo seu próprio conteúdo e, em seguida, conecte a saída à camada de resposta ou de armazenamento.

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

Saída padrão (STDOUT) esperada (o tamanho em bytes depende do build e de você ter fornecido uma imagem ou não):

Wrote <n>-byte PDF to <path>
  • A ordem das camadas é a ordem das chamadas. Um plano de fundo é conteúdo desenhado antes do conteúdo da página. Uma marca d’água sobreposta é conteúdo desenhado depois dele. Nenhuma flag reordena as camadas; em vez disso, mova a chamada.
  • O alpha persiste até ser redefinido. setAlpha() altera o estado de tudo o que é desenhado em seguida. Você pode delimitar a marca com startTransform() / stopTransform(), que restaura o alpha anterior, ou chamar setAlpha(1.0) antes do conteúdo opaco. O exemplo de produção faz as duas coisas.
  • Equilibre cada bloco de transform. Cada startTransform() precisa de um stopTransform() correspondente. Um bloco desequilibrado deixa a rotação ou o alpha aplicados ao conteúdo posterior, e a ausência de stopTransform() cria um desequilíbrio de estado gráfico que o writer rejeita na saída.
  • rotate() faz o pivô em coordenadas de usuário. O pivô ($x, $y) está em unidades de usuário medidas a partir do canto superior esquerdo da página, no mesmo referencial que text(). Para uma diagonal que passa pelo centro, use o centro da página (getPageWidth() / 2, getPageHeight() / 2).
  • O texto rotacionado precisa de um deslocamento manual de largura. text() posiciona a origem da string; ele não centraliza por você. Subtraia do X do pivô aproximadamente metade da largura estimada do texto para que a marca rotacionada fique centralizada sobre o centro, como o auxiliar faz.
  • As imagens são dimensionadas para a caixa que você passa. image() estica o raster até a width e a height que você informa. Para um plano de fundo de página inteira, passe a largura e a altura da página; para um logotipo no canto, passe seu tamanho natural. Uma dimensão zero ou negativa gera PageLayoutException.
  • image() rejeita URLs e bytes NUL. Um caminho scheme:// ou um byte NUL em $file gera PageLayoutException antes de qualquer decodificação. Passe apenas um caminho local e validado.
  • A marca é conteúdo visível. Uma marca d’água desenhada dessa forma é conteúdo de página real, não uma anotação oculta. Qualquer pessoa com o arquivo pode lê-la. Ela é uma indicação visual, não um controle de acesso.

Uma marca d’água de texto usa um punhado de operadores de fluxo de conteúdo por página e acrescenta tempo ou memória desprezíveis. Uma marca d’água ou plano de fundo de imagem custa uma decodificação raster mais os bytes da imagem incorporada na saída. Reutilizar a mesma imagem em várias páginas reaproveita o XObject decodificado por meio do cache de imagens, de modo que você paga o custo da decodificação apenas uma vez. Dimensione as imagens de plano de fundo para a caixa de exibição antes de incorporá-las. Uma foto de 4000 px redimensionada para uma página carta armazena bytes que o leitor nunca vê. Uma marca d’água de texto típica de uma única página permanece com folga dentro de um orçamento de 500 ms de tempo e 32 MB de pico. Um plano de fundo de imagem acompanha o tamanho decodificado do raster de origem.

O pipeline é executado no processo. Nenhum byte do documento sai do host, e nenhuma chamada de rede é feita. Trate qualquer caminho de imagem que se origine fora do seu código como entrada não confiável.

  • Valide o caminho da imagem antes de usá-lo. Rejeite bytes NUL, resolva o caminho com realpath() e confirme is_file() e is_readable() antes de chamar image(), exatamente como o exemplo de produção faz. Isso bloqueia path traversal e rejeita diretórios e links pendentes logo no início.
  • Nunca interpole um campo de requisição em um caminho. Derive o caminho da imagem e o caminho de saída de valores controlados pelo servidor, não de um parâmetro de requisição. Isso evita que você leia ou grave arquivos fora do diretório previsto.
  • Trate imagens não confiáveis como entrada hostil. Um raster malformado gera ImageProcessingException em vez de corromper o documento, e o carregador limita as dimensões da imagem para resistir a entradas de bomba de descompressão. Capture a exceção e rejeite o upload. Não tente de novo às cegas.
  • Uma marca d’água não é um cofre de segredos. A marca é conteúdo visível. Não codifique credenciais, tokens ou identificadores internos em uma marca d’água ou plano de fundo que você retorna a um cliente.

Esta receita não faz nenhuma reivindicação normativa de padrões por conta própria. Ela compõe as primitivas públicas de alpha, transform, text e image. Cada primitiva emite operadores de fluxo de conteúdo PDF padrão. O estado gráfico é isolado com os operadores q / Q que startTransform() e stopTransform() emitem, e a transparência é transportada por meio de um parâmetro de estado gráfico ExtGState. A saída é estruturalmente nova em vez de estável em bytes; portanto, esta página declara um perfil de reprodutibilidade structural. Para detalhes em nível de operador sobre a superfície de transform e estado gráfico, consulte a referência do módulo Graphics.