コンテンツにスキップ

ページにテキストや画像の透かし・背景を追加する

各ページに「DRAFT」や「CONFIDENTIAL」のマークを入れたい場合や、コンテンツの背後に薄いロゴを入れたい場合を想定します。このレシピでは、公開されているドキュメント機能を使い、その両方を NextPDF core のページに重ねます。透明度には setAlpha()、斜めのスタンプには startTransform() / rotate() / stopTransform()、マーク自体には text()、ラスター背景には image() を使います。

透かしと背景の違いは、描画順序という 1 つの判断に尽きます。

  • 背景:先に描画し、その上にページコンテンツを書き込みます。マークはテキストの背後に配置されます。
  • オーバーレイ透かし:先にページコンテンツを書き込み、その上にマークを描画します。マークは最前面に配置されます。

NextPDF は呼び出した順序でコンテンツを描画するため、呼び出し順序がそのままレイヤーの順序になります。独立した「背景モード」はありません。いつ描画するかを選ぶことで、レイヤーを選択します。

前提条件:core のインストール(composer require nextpdf/core:^3)、および画像背景を使う場合はディスク上の読み取り可能なラスターファイル(PNG、JPEG、WebP)。パイプライン全体はプロセス内で実行され、ヘッドレスブラウザもネットワーク呼び出しも使いません。

Terminal window
composer require nextpdf/core:^3

追加するマークはすべて、グラフィックスステートを通じて描画される通常のページコンテンツです。公開機能の 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() は保存したステートを復元します。マークを変換ブロックで囲むことで、回転や alpha の影響がページの他の部分に及ぶのを防ぎます。

  3. マーク自体。 text(float $x, float $y, string $text) は、現在のフォント、色、alpha で、絶対位置に文字列を書き込みます。image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) はラスター画像を配置します。これは画像の透かしやページ全体の背景の基本要素です。

startTransform()stopTransform() で変更範囲を囲むため、グラフィックスステートは確実に復元されます。setAlpha() の値は、再度設定するまで維持されます。そのため、後続のコンテンツを完全に不透明にする必要がある場合は、マークの後で不透明度を 1.0 にリセットします。以下に示すより安全なパターンでは、マークを専用の変換ブロック内に描画し、ページコンテンツの alpha を明示的に設定します。

このパッケージには、値オブジェクト NextPDF\Graphics\WatermarkNextPDF\Graphics\WatermarkPosition も同梱されています。Watermark は不変の設定ホルダーで、テキスト、フォントサイズ、角度、色、オーバーレイフラグ、および WatermarkPosition::Diagonal などの位置プリセットを保持します。これらは透かしのパラメーターをモデル化します。このレシピでは、上記のページへ直接描画するメソッドでマークを描画するため、出力はページコンテンツストリームに直接到達します。

以下のメソッドはすべて NextPDF\Core\Document の public メソッドです。static を返すため、チェーンできます。

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: 以降のコンテンツの塗りの不透明度(0.01.0)とブレンドモードを設定します。
  • startTransform(): static: グラフィックスステートを保存します(q を出力します)。
  • rotate(float $angle, float $x = 0, float $y = 0): static: 座標系を $angle 度だけ反時計回りに、ピボット ($x, $y) を中心に回転させます。
  • stopTransform(): static: startTransform() で保存したステートを復元し(Q を出力します)、回転と alpha の変更をまとめて取り消します。
  • setFont(string $family, string $style = '', float $size = 12.0): static: マークのフォントを選択します。 Base-14 ファミリーの helvetica は常に利用可能で、フォントファイルは不要です。
  • 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 の設定ペアです。

これは 1 ページを書き込み、コンテンツの上に薄い斜めの「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 環境変数で画像パスが指定された場合は、その画像を 2 ページ目に薄い中央配置の背景として配置します。使用前に画像パスを検証し、最も具体的な 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>
  • レイヤー順序は呼び出し順序です。 背景は、ページコンテンツの前に描画されるコンテンツです。オーバーレイ透かしは、後から描画されるコンテンツです。レイヤーを並べ替えるフラグはありません。代わりに呼び出しの位置を移動します。
  • alpha はリセットするまで維持されます。 setAlpha() は、以降に描画するすべてのもののステートを変更します。マークを startTransform() / stopTransform() で囲んで以前の alpha を復元するか、不透明なコンテンツの前に setAlpha(1.0) を呼び出してください。本番向けサンプルでは、その両方を行っています。
  • すべての変換ブロックのバランスを取ります。 startTransform() ごとに、対応する stopTransform() が必要です。バランスの取れていないブロックは、回転や alpha を後続のコンテンツに適用したままにします。また、stopTransform() が欠けていると、ライターが出力時に拒否するグラフィックスステートの不整合になります。
  • rotate() はユーザー座標でピボットします。 ピボット ($x, $y) は、ページ左上から測ったユーザー単位で表され、text() と同じ座標系です。中心を通る斜め方向に配置するには、ページ中心(getPageWidth() / 2getPageHeight() / 2)を使います。
  • 回転したテキストには手動の幅オフセットが必要です。 text() は文字列の原点を配置するだけで、中央揃えは行いません。ヘルパーと同じように、回転したマークが中心をまたぐように、推定したテキスト幅のおよそ半分をピボット X から引きます。
  • 画像は渡したボックスに合わせて拡大縮小されます。 image() は、指定した widthheight までラスターを引き伸ばします。ページ全体の背景にはページの幅と高さを渡し、角のロゴにはその元のサイズを渡します。ゼロまたは負の寸法は PageLayoutException を発生させます。
  • image() は URL と NUL バイトを拒否します。 scheme:// パスや $file 内の NUL バイトは、デコード前に PageLayoutException を発生させます。ローカルで検証済みのパスのみを渡してください。
  • マークは可視のコンテンツです。 この方法で描画した透かしは、隠れた注釈ではなく実際のページコンテンツです。ファイルを持っている人は誰でも読むことができます。これはアクセス制御ではなく、視覚的な手がかりです。

テキスト透かしは、ページごとに追加されるコンテンツストリーム演算子がわずかで、時間やメモリへの影響もごくわずかです。画像の透かしや背景には、ラスターのデコード 1 回分と、出力に埋め込まれる画像のバイト分のコストがかかります。複数のページで同じ画像を再利用すると、画像キャッシュを通じてデコード済みの XObject が再利用されるため、デコードのコストは一度だけで済みます。背景画像は、埋め込む前に表示ボックスのサイズに合わせてください。4000 px の写真をレターサイズのページに縮小すると、読み手には決して見えないバイトを保存することになります。一般的な 1 ページのテキスト透かしは、実時間 500 ms およびピーク 32 MB の予算に十分収まります。画像背景は、元のラスターのデコード後のサイズに比例します。

パイプラインはプロセス内で実行されます。ドキュメントのバイトがホストから出ることはなく、ネットワーク呼び出しも行われません。コードの外部に由来する画像パスは、信頼できない入力として扱ってください。

  • 使用前に画像パスを検証してください。 NUL バイトを拒否し、realpath() でパスを resolve(解決)し、is_file()is_readable() を確認してから、本番向けサンプルとまったく同じように image() を呼び出してください。これにより、パストラバーサルを防ぎ、ディレクトリや無効なリンクを早い段階で拒否します。
  • リクエストのフィールドをパスに展開しないでください。 画像パスと出力パスは、リクエストパラメーターではなく、サーバー側で管理する値から導出してください。これにより、意図したディレクトリの外でファイルを読み書きすることを防げます。
  • 信頼できない画像は敵対的な入力として扱ってください。 不正な形式のラスターは、ドキュメントを破損させるのではなく ImageProcessingException を発生させます。また、ローダーは解凍爆弾の入力に対抗するために画像の寸法に上限を設けます。例外をキャッチして、アップロードを拒否してください。むやみに再試行しないでください。
  • 透かしは機密情報の保管場所ではありません。 マークは可視のコンテンツです。クライアントに返す透かしや背景に、資格情報、トークン、内部識別子をエンコードしないでください。

このレシピ自体は、規範的な標準への主張を一切行いません。公開されている alpha、transform、text、image のプリミティブを組み合わせています。これらはそれぞれ、標準的な PDF コンテンツストリーム演算子を出力します。グラフィックスステートは、q / Q 演算子で分離されます。これらは startTransform()stopTransform() が出力するもので、透明度は ExtGState グラフィックスステートパラメーターを通じて伝えられます。出力はバイト単位で安定しているのではなく構造的に新しいため、このページは structural 再現性プロファイルを宣言します。transform およびグラフィックスステート機能の演算子レベルの詳細については、Graphics モジュールリファレンス を参照してください。