為頁面加入文字與影像浮水印或背景
重點摘要
標題為「重點摘要」的區段當你想在每一頁印上「DRAFT」或「CONFIDENTIAL」標記,或在內容後方放一個淡淡的標誌時,這則 recipe(範例)會透過公開的文件介面,把兩者都疊加到 NextPDF core 頁面上:用 setAlpha() 控制透明度、用 startTransform() / rotate() / stopTransform() 製作對角戳記、用 text() 繪出標記,並用 image() 放上點陣背景。
浮水印與背景只差在一個決定:繪製順序。
- 背景:先繪製,再把頁面內容寫在它上面。標記位於文字後方。
- 疊加浮水印:先寫頁面內容,再把標記繪製在它上面。標記位於最上層。
NextPDF 會依你呼叫的順序繪製內容,因此呼叫順序就是圖層順序。沒有獨立的「背景模式」;你是靠選擇繪製時機來決定圖層。
先決條件:已安裝 core(composer require nextpdf/core:^3),若要製作影像背景,磁碟上還需要一個可讀取的點陣檔(PNG、JPEG 或 WebP)。整個流程都在行程內執行,不需要 headless 瀏覽器,也不會發出網路呼叫。
composer require nextpdf/core:^3概念總覽
標題為「概念總覽」的區段你加入的每個標記,都是透過圖形狀態繪製的一般頁面內容。公開介面中有三個部分會一起組成浮水印:
-
透明度。
setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal)會替後續繪製的所有內容設定填色不透明度,範圍從0.0(看不見)到1.0(不透明)。浮水印通常設定在0.1到0.3,讓下方內容仍然清楚可讀。混合模式來自NextPDF\Graphics\BlendMode列舉。例如,BlendMode::Multiply會在標記與內容重疊處加深顏色。 -
旋轉。 對角戳記就是以某個樞紐點旋轉的文字。
startTransform()會儲存圖形狀態,rotate(float $angle, float $x, float $y)會讓座標系統繞著($x, $y)逆時針旋轉,而stopTransform()會還原先前儲存的狀態。將標記包在一個 transform 區塊中,可以避免旋轉與 alpha 外溢到頁面的其他部分。 -
標記本身。
text(float $x, float $y, string $text)會以目前的字型、顏色與 alpha,在絕對位置寫出一個字串。image(string $file, ?float $x, ?float $y, ?float $width, ?float $height)會放置一張點陣影像:這是影像浮水印或整頁背景的基礎構件。
由於 startTransform() 與 stopTransform() 會把變更夾在中間,圖形狀態可以乾淨還原。setAlpha() 的值會持續生效,直到你再次設定為止。因此,若之後的內容必須完全不透明,就在標記之後把不透明度重設為 1.0。下方示範的較安全做法,是把標記畫在它自己的 transform 區塊內,並明確設定頁面內容的 alpha。
這個套件也提供 NextPDF\Graphics\Watermark 與 NextPDF\Graphics\WatermarkPosition 這兩個值物件。Watermark 是不可變的設定容器:包含文字、字型大小、角度、顏色、疊加旗標,以及像 WatermarkPosition::Diagonal 之類的位置預設值。它們用來描述浮水印參數。這則 recipe 使用上述會直接寫入頁面的方法繪製標記,因此輸出會直接進入頁面內容串流。
API 介面
標題為「API 介面」的區段以下方法都是 NextPDF\Core\Document 的 public 方法,並回傳 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),一併復原旋轉與 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 這組設定配對。
程式碼範例 — 快速開始
標題為「程式碼範例 — 快速開始」的區段這段程式會產生一頁、在內容上印上一個淡淡的對角「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>邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 圖層順序就是呼叫順序。 背景是在頁面內容之前繪製的內容;疊加浮水印則是在頁面內容之後才繪製的內容。沒有任何旗標可以重排圖層;要改順序就移動呼叫的位置。
- Alpha 會持續到重設為止。
setAlpha()會改變之後所有繪製內容的狀態。你可以把標記夾在startTransform()/stopTransform()之間以還原先前的 alpha,或在不透明內容之前呼叫setAlpha(1.0)。正式版範例同時示範兩種做法。 - 每個 transform 區塊都要成對。 每個
startTransform()都需要一個對應的stopTransform()。未成對的區塊會讓旋轉或 alpha 套用到之後的內容,而缺少stopTransform()會造成圖形狀態不平衡,寫入器會在輸出時拒絕。 rotate()以使用者座標為樞紐旋轉。 樞紐($x, $y)以使用者單位表示,從頁面左上角起算,與text()使用同一個座標系統。若要做穿過中心的對角線,請使用頁面中心(getPageWidth() / 2,getPageHeight() / 2)。- 旋轉的文字需要手動補上寬度位移。
text()只放置字串的起點,並不會替你置中。請從樞紐 X 減去估計文字寬度的一半左右,讓旋轉後的標記橫跨中心,就像這個輔助函式的做法。 - 影像會縮放到你傳入的方框。
image()會把點陣影像拉伸到你提供的width與height。若要製作整頁背景,請傳入頁面的寬度與高度;若要做角落標誌,請傳入它的原始尺寸。傳入零或負值尺寸會引發PageLayoutException。 image()會拒絕 URL 與 NUL 位元組。scheme://路徑或$file中的 NUL 位元組,會在任何解碼之前就引發PageLayoutException。請只傳入本機、已驗證的路徑。- 標記是可見的內容。 以這種方式繪製的浮水印是真正的頁面內容,而不是隱藏的註解。任何拿到檔案的人都能讀到它。它是一種視覺提示,而不是存取控制。
文字浮水印每頁只會多出少數幾個內容串流運算子,增加的時間與記憶體用量可以忽略不計。影像浮水印或背景的成本,是將點陣解碼一次,再加上輸出中嵌入的影像位元組。跨多頁重複使用同一張影像時,會透過影像快取重用已解碼的 XObject,因此解碼成本只會付一次。請在嵌入之前,先把背景影像縮放到它的顯示方框大小。把一張 4000 px 的相片縮放進一頁 letter 大小的頁面,會儲存讀者永遠看不到的位元組。一個典型的單頁文字浮水印,落在 500 ms 的時間與 32 MB 的尖峰記憶體預算之內,仍有充裕空間。影像背景的成本則隨來源點陣的解碼後大小而定。
安全性須知
標題為「安全性須知」的區段這個流程在行程內執行。沒有任何文件位元組會離開主機,也不會發出任何網路呼叫。請把任何源自你程式碼以外的影像路徑,都當成不可信的輸入。
- 在使用前驗證影像路徑。 請拒絕 NUL 位元組,用
realpath()正規化路徑,並確認is_file()與is_readable(),再呼叫image(),就像正式版範例所做的那樣。這能阻擋路徑穿越,並及早拒絕目錄與失效的連結。 - 絕不要把請求欄位插入路徑。 請從伺服器控制的值衍生影像路徑與輸出路徑,不要從請求參數取得。這能避免讀取或寫入預期目錄以外的檔案。
- 把不可信的影像當成惡意輸入看待。 格式錯誤的點陣會引發
ImageProcessingException,而不會損毀文件,而且載入器會限制影像尺寸,以抵禦解壓縮炸彈式的輸入。請攔截這個例外並拒絕該上傳。不要盲目重試。 - 浮水印不是機密儲存區。 標記是可見的內容。不要把憑證、權杖或內部識別碼編碼進你要回傳給用戶端的浮水印或背景裡。
符合性
標題為「符合性」的區段這則 recipe 本身不主張任何規範性標準。 它組合了公開的 alpha、transform、text 與 image 基本元件。 它們每一個都會輸出標準的 PDF 內容串流運算子。 圖形狀態是用 q / Q 運算子隔離的,這兩個運算子正是 startTransform() 與 stopTransform() 所輸出的,而透明度則是透過 ExtGState 圖形狀態參數帶入。 由於輸出在結構上是全新的,而非位元組穩定的,因此這個頁面宣告為 structural 可重現性設定檔。 關於 transform 與圖形狀態介面在運算子層級的細節,請參閱 Graphics 模組參考一節。
另請參閱
標題為「另請參閱」的區段- Graphics 模組參考:這些方法背後完整的 path、transform、color 與圖形狀態介面。
- 嵌入影像:載入、調整大小並放置點陣影像;這是影像浮水印或背景的基本構件。
- 漸層與透明度:深入說明 alpha 與混合模式介面,包含半透明填色。
- 轉換座標空間:以成對的 transform 區塊旋轉、縮放與平移內容。
- 例外感知的錯誤處理:位於
ImageProcessingException、PageLayoutException與NextPdfException背後的 NextPDF 例外階層架構。