Перейти к содержимому

Добавление текстовых и графических водяных знаков или фонов на страницы

Вы можете добавить пометку “DRAFT” или “CONFIDENTIAL” на каждую страницу либо разместить бледный логотип за содержимым. В этом рецепте показано, как добавить и то, и другое на страницы NextPDF core с помощью публичных средств документа: setAlpha() для прозрачности, startTransform() / rotate() / stopTransform() для диагонального штампа, text() для самой пометки и image() для растрового фона.

Разница между водяным знаком и фоном сводится к порядку отрисовки.

  • Фон: сначала отрисуйте его, затем выведите содержимое страницы поверх него. Пометка окажется позади текста.
  • Наложенный водяной знак: сначала выведите содержимое страницы, затем отрисуйте пометку поверх него. Пометка окажется сверху.

NextPDF отрисовывает содержимое в том порядке, в котором вы его вызываете, поэтому порядок вызовов задаёт порядок слоёв. Отдельного “режима фона” не существует. Вы выбираете слой через момент отрисовки.

Предварительные требования: установленный core (composer require nextpdf/core:^3) и — для графического фона — доступный для чтения растровый файл (PNG, JPEG или WebP) на диске. Весь конвейер выполняется в самом процессе, без headless-браузера и сетевых вызовов.

Окно терминала
composer require nextpdf/core:^3

Каждая добавляемая пометка — обычное содержимое страницы, выводимое с учётом графического состояния. В создании водяного знака участвуют три группы публичных средств:

  1. Прозрачность. setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) задаёт непрозрачность заливки для всего, что будет отрисовано после вызова, от 0.0 (невидимо) до 1.0 (полностью непрозрачно). Водяной знак обычно лучше всего выглядит в диапазоне от 0.1 до 0.3, чтобы содержимое под ним оставалось читаемым. Режим наложения берётся из перечисления NextPDF\Graphics\BlendMode. Например, BlendMode::Multiply затемняет области, где пометка перекрывает содержимое.

  2. Поворот. Диагональный штамп — это текст, повёрнутый вокруг опорной точки. startTransform() сохраняет графическое состояние, rotate(float $angle, float $x, float $y) поворачивает систему координат против часовой стрелки вокруг ($x, $y), а stopTransform() восстанавливает сохранённое состояние. Если поместить пометку в блок преобразования, поворот и прозрачность не затронут остальную часть страницы.

  3. Сама пометка. text(float $x, float $y, string $text) выводит строку в абсолютной позиции в текущем шрифте, цвете и с текущей прозрачностью. image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) размещает растровое изображение — основу для графического водяного знака или полностраничного фона.

Графическое состояние полностью восстанавливается, потому что startTransform() и stopTransform() обрамляют изменение. Значение setAlpha() сохраняется до тех пор, пока вы не зададите его снова. Если последующее содержимое должно быть полностью непрозрачным, после пометки сбросьте непрозрачность до 1.0. Более безопасный подход, показанный ниже, отрисовывает пометку внутри собственного блока преобразования и явно задаёт прозрачность содержимого страницы.

Пакет также включает объекты-значения NextPDF\Graphics\Watermark и NextPDF\Graphics\WatermarkPosition. Watermark — это неизменяемый контейнер конфигурации для текста, размера шрифта, угла, цвета, флага наложения и предустановок положения, таких как WatermarkPosition::Diagonal. Эти объекты моделируют параметры водяного знака. Этот рецепт отрисовывает пометку с помощью приведённых выше методов, которые фиксируют содержимое на странице, поэтому результат попадает прямо в поток содержимого страницы.

Все перечисленные ниже методы публичны в NextPDF\Core\Document и возвращают static, поэтому их можно объединять в цепочку.

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: задаёт непрозрачность заливки (0.0-1.0) и режим наложения для последующего содержимого.
  • startTransform(): static: сохраняет графическое состояние (выдаёт q).
  • rotate(float $angle, float $x = 0, float $y = 0): static: поворачивает систему координат на $angle градусов против часовой стрелки вокруг опорной точки ($x, $y).
  • stopTransform(): static: восстанавливает состояние, сохранённое startTransform() (выдаёт Q), одновременно отменяя поворот и изменение прозрачности.
  • setFont(string $family, string $style = '', float $size = 12.0): static: выбирает шрифт для пометки. Гарнитура helvetica из набора Base-14 доступна всегда и не требует файла шрифта.
  • setTextColor(int $r, int $g = -1, int $b = -1): static: задаёт цвет пометки в красном, зелёном, синем (или одно значение оттенка серого).
  • text(float $x, float $y, string $text): static: выводит пометку в абсолютной позиции.
  • image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: размещает растровое изображение — основу для графического водяного знака или полностраничного фона.
  • getPageWidth(): float / getPageHeight(): float: считывают текущий размер страницы в пунктах, чтобы можно было отцентрировать пометку.

Вспомогательные типы находятся в NextPDF\Graphics: перечисление BlendMode, объект-значение Color и пара типов конфигурации Watermark / WatermarkPosition.

Этот пример создаёт одну страницу, отрисовывает бледный диагональный штамп “DRAFT” поверх содержимого и сохраняет файл. Обработка ошибок опущена, чтобы показать форму вызовов. В рабочем примере ниже проверки приведены полностью.

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

Эта самодостаточная программа отрисовывает диагональный текстовый водяной знак поверх сгенерированного содержимого. Если вы передадите путь к изображению через переменную окружения NEXTPDF_WATERMARK_IMAGE, программа разместит это изображение как бледный, отцентрированный фон на второй странице. Она проверяет путь к изображению перед использованием, перехватывает наиболее конкретные исключения NextPDF и записывает результат по контролируемому сервером пути. Замените сгенерированное содержимое своим, затем подключите вывод к своему ответу или слою хранения.

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

Ожидаемый вывод в STDOUT (размер в байтах зависит от сборки и от того, передали ли вы изображение):

Wrote <n>-byte PDF to <path>
  • Порядок слоёв — это порядок вызовов. Фон — это содержимое, отрисованное до содержимого вашей страницы. Наложенный водяной знак — это содержимое, отрисованное после него. Ни один флаг не меняет порядок слоёв — вместо этого переместите вызов.
  • Прозрачность сохраняется до сброса. setAlpha() изменяет состояние для всего, что отрисовывается после него. Либо заключите пометку в startTransform() / stopTransform(), что восстановит прежнюю прозрачность, либо вызовите setAlpha(1.0) перед непрозрачным содержимым. Рабочий пример делает и то, и другое.
  • Соблюдайте баланс каждого блока преобразования. Каждому startTransform() нужен парный stopTransform(). Несбалансированный блок оставляет поворот или прозрачность применёнными к последующему содержимому, а отсутствующий stopTransform() создаёт нарушение баланса графического состояния, которое модуль записи отклоняет на выводе.
  • rotate() вращает вокруг точки в пользовательских координатах. Опорная точка ($x, $y) задаётся в пользовательских единицах, отсчитываемых от верхнего левого угла страницы, в той же системе, что и text(). Для диагонали, проходящей через центр, используйте центр страницы (getPageWidth() / 2, getPageHeight() / 2).
  • Повёрнутый текст требует вручную учесть ширину. text() размещает начало строки; автоматического центрирования нет. Вычтите примерно половину оценочной ширины текста из координаты X опорной точки, чтобы повёрнутая пометка располагалась по обе стороны от центра, как это делает вспомогательная функция.
  • Изображения масштабируются под переданную область. image() растягивает растр под заданные вами width и height. Для полностраничного фона передавайте ширину и высоту страницы; для углового логотипа — его естественный размер. Нулевое или отрицательное измерение вызывает PageLayoutException.
  • image() отклоняет URL-адреса и байты NUL. Путь вида scheme:// или байт NUL в $file вызывает PageLayoutException ещё до декодирования. Передавайте только локальный, проверенный путь.
  • Пометка — это видимое содержимое. Отрисованный таким способом водяной знак — это настоящее содержимое страницы, а не скрытая аннотация. Любой, у кого есть файл, может её прочитать. Это визуальный признак, а не средство контроля доступа.

Текстовый водяной знак добавляет несколько операторов потока содержимого на страницу и почти не расходует время и память. Графический водяной знак или фон требует одного декодирования растра и добавляет байты встроенного изображения к выводу. Повторное использование одного изображения на разных страницах переиспользует декодированный XObject через кэш изображений, поэтому декодирование выполняется один раз. Приводите размер фоновых изображений к их области отображения перед встраиванием. Фотография в 4000 px, масштабированная под страницу формата letter, всё равно хранит байты, которые читатель никогда не увидит. Типичный одностраничный текстовый водяной знак с большим запасом укладывается в бюджет 500 мс и 32 МБ пиковой памяти. Графический фон зависит от декодированного размера исходного растра.

Конвейер выполняется в самом процессе. Ни один байт документа не покидает хост, и сетевые вызовы не выполняются. Любой путь к изображению, поступающий извне вашего кода, рассматривайте как недоверенный ввод.

  • Проверяйте путь к изображению перед использованием. Отклоняйте байты NUL, получайте абсолютный путь с помощью realpath() и подтверждайте is_file() и is_readable(), прежде чем вызывать image(), точно так же, как это делает рабочий пример. Это блокирует обход каталогов и заранее отклоняет каталоги и битые ссылки.
  • Никогда не подставляйте поле запроса в путь. Формируйте путь к изображению и путь вывода из контролируемых сервером значений, а не из параметра запроса. Это не даёт читать или записывать файлы за пределами предполагаемого каталога.
  • Рассматривайте недоверенные изображения как враждебный ввод. Некорректный растр вызывает ImageProcessingException, а не повреждает документ, а загрузчик ограничивает размеры изображения, чтобы противостоять входным данным типа “бомба распаковки”. Перехватите исключение и отклоните загрузку. Не повторяйте попытку вслепую.
  • Водяной знак — не хранилище секретов. Пометка — это видимое содержимое. Не кодируйте учётные данные, токены или внутренние идентификаторы в водяном знаке или фоне, который вы возвращаете клиенту.

Этот рецепт сам по себе не делает нормативных заявлений о соответствии стандартам. Он комбинирует публичные примитивы alpha, transform, text и image. Каждый примитив выдаёт стандартные операторы потока содержимого PDF. Графическое состояние изолируется операторами q / Q, которые выдают startTransform() и stopTransform(), а прозрачность передаётся через параметр графического состояния ExtGState. Результат воспроизводим на уровне структуры, а не байтов, поэтому на этой странице объявлен профиль воспроизводимости structural. Подробные сведения об операторах для средств преобразования и графического состояния см. в справочнике по модулю Graphics.