Lewati ke konten

Menambahkan tanda air teks dan gambar, atau latar belakang, ke halaman

Anda dapat menambahkan tanda “DRAFT” atau “CONFIDENTIAL” di setiap halaman, atau menempatkan logo samar di belakang konten. Resep ini menunjukkan cara menambahkan keduanya ke halaman core NextPDF melalui permukaan dokumen publik: setAlpha() untuk transparansi, startTransform() / rotate() / stopTransform() untuk stempel diagonal, text() untuk tanda teks, dan image() untuk latar belakang raster.

Tanda air dan latar belakang berbeda dalam satu pilihan: urutan penggambaran.

  • Latar belakang: gambar terlebih dahulu, lalu tulis konten halaman Anda di atasnya. Tanda berada di belakang teks.
  • Tanda air hamparan: tulis konten halaman Anda terlebih dahulu, lalu gambar tandanya di atasnya. Tanda berada di lapisan atas.

NextPDF menggambar konten sesuai urutan pemanggilan Anda, sehingga urutan panggilan menentukan urutan lapisan. Tidak ada “mode latar belakang” tersendiri. Anda memilih lapisan dengan menentukan kapan konten digambar.

Prasyarat: instalasi core (composer require nextpdf/core:^3), dan, untuk latar belakang gambar, berkas raster yang dapat dibaca (PNG, JPEG, atau WebP) di disk. Seluruh alur berjalan di dalam proses, tanpa peramban headless atau panggilan jaringan.

Terminal window
composer require nextpdf/core:^3

Setiap tanda yang Anda tambahkan adalah konten halaman biasa yang digambar melalui graphics state. Tiga bagian dari permukaan publik bekerja sama untuk menghasilkan tanda air:

  1. Transparansi. setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) mengatur opasitas isian untuk semua yang Anda gambar setelahnya, dari 0.0 (tidak terlihat) hingga 1.0 (buram). Tanda air biasanya paling efektif pada 0.1 hingga 0.3, sehingga konten di bawahnya tetap dapat dibaca. Mode pembauran berasal dari enum NextPDF\Graphics\BlendMode. Sebagai contoh, BlendMode::Multiply menggelapkan area tempat tanda bertumpang tindih dengan konten.

  2. Rotasi. Stempel diagonal adalah teks yang diputar di sekitar titik poros. startTransform() menyimpan graphics state, rotate(float $angle, float $x, float $y) memutar sistem koordinat berlawanan arah jarum jam dengan poros pada ($x, $y), dan stopTransform() memulihkan state yang tersimpan. Membungkus tanda dalam blok transform mencegah rotasi dan alpha memengaruhi bagian halaman lainnya.

  3. Tanda itu sendiri. text(float $x, float $y, string $text) menulis string pada posisi absolut dengan fon, warna, dan alpha saat ini. image(string $file, ?float $x, ?float $y, ?float $width, ?float $height) menempatkan gambar raster: dasar untuk tanda air gambar atau latar belakang satu halaman penuh.

Graphics state dipulihkan dengan bersih karena startTransform() dan stopTransform() mengapit perubahannya. Nilai setAlpha() bertahan hingga Anda mengaturnya lagi. Jika konten berikutnya harus sepenuhnya buram, atur ulang opasitas ke 1.0 setelah tanda. Pola yang lebih aman, seperti ditunjukkan di bawah ini, menggambar tanda di dalam blok transform-nya sendiri dan mengatur alpha konten halaman secara eksplisit.

Paket ini juga menyertakan objek nilai NextPDF\Graphics\Watermark dan NextPDF\Graphics\WatermarkPosition. Watermark adalah wadah konfigurasi yang tidak dapat diubah untuk teks, ukuran fon, sudut, warna, flag hamparan, dan praatur posisi, seperti WatermarkPosition::Diagonal. Objek-objek ini memodelkan parameter tanda air. Resep ini menggambar tanda dengan metode-metode penggambaran halaman di atas, sehingga keluarannya langsung masuk ke aliran konten halaman.

Semua metode di bawah ini bersifat publik pada NextPDF\Core\Document dan mengembalikan static, sehingga Anda dapat merantainya.

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: mengatur opasitas isian (0.0-1.0) dan mode pembauran untuk konten berikutnya.
  • startTransform(): static: menyimpan graphics state (memancarkan q).
  • rotate(float $angle, float $x = 0, float $y = 0): static: memutar sistem koordinat sebesar $angle derajat berlawanan arah jarum jam dengan poros pada ($x, $y).
  • stopTransform(): static: memulihkan state yang disimpan oleh startTransform() (memancarkan Q), sekaligus membatalkan rotasi dan perubahan alpha.
  • setFont(string $family, string $style = '', float $size = 12.0): static: memilih fon untuk tanda. Keluarga Base-14 helvetica selalu tersedia dan tidak memerlukan berkas fon.
  • setTextColor(int $r, int $g = -1, int $b = -1): static: mengatur warna tanda dalam komponen merah, hijau, biru (atau satu nilai skala abu-abu).
  • text(float $x, float $y, string $text): static: menulis tanda pada posisi absolut.
  • image(string $file, ?float $x = null, ?float $y = null, ?float $width = null, ?float $height = null): static: menempatkan gambar raster, dasar untuk tanda air gambar atau latar belakang satu halaman penuh.
  • getPageWidth(): float / getPageHeight(): float: membaca ukuran halaman saat ini dalam poin sehingga Anda dapat memusatkan tanda.

Tipe-tipe pendukung berada di bawah NextPDF\Graphics: enum BlendMode, objek nilai Color, dan pasangan konfigurasi Watermark / WatermarkPosition.

Contoh ini menulis satu halaman, menggambar stempel diagonal “DRAFT” yang samar di atas konten, dan menyimpan berkasnya. Contoh ini mengabaikan penanganan kesalahan untuk menunjukkan bentuk pemanggilannya. Contoh produksi di bawah ini menambahkan seluruh pengaman.

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

Program mandiri ini menggambar tanda air teks diagonal di atas konten yang dihasilkan. Ketika Anda memberikan path gambar melalui variabel lingkungan NEXTPDF_WATERMARK_IMAGE, program ini menempatkan gambar tersebut sebagai latar belakang samar yang terpusat pada halaman kedua. Program ini memvalidasi path gambar sebelum digunakan, menangkap eksepsi NextPDF yang paling spesifik, dan menulis hasilnya ke path yang dikendalikan server. Ganti konten dalam memori dengan konten Anda sendiri, lalu teruskan keluarannya ke lapisan respons atau penyimpanan Anda.

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

Keluaran standar (STDOUT) yang diharapkan (ukuran byte bergantung pada build dan apakah Anda memberikan gambar):

Wrote <n>-byte PDF to <path>
  • Urutan lapisan adalah urutan panggilan. Latar belakang adalah konten yang digambar sebelum konten halaman Anda. Tanda air hamparan adalah konten yang digambar setelahnya. Tidak ada flag yang mengatur ulang urutan lapisan; cukup pindahkan panggilannya.
  • Alpha bertahan hingga diatur ulang. setAlpha() mengubah state untuk semua yang digambar setelahnya. Apit tanda dalam startTransform() / stopTransform(), yang memulihkan alpha sebelumnya, atau panggil setAlpha(1.0) sebelum konten buram. Contoh produksi melakukan keduanya.
  • Seimbangkan setiap blok transform. Setiap startTransform() membutuhkan stopTransform() yang sepadan. Blok yang tidak seimbang membuat rotasi atau alpha tetap diterapkan pada konten berikutnya, dan stopTransform() yang hilang menciptakan ketidakseimbangan graphics state yang ditolak oleh penulis saat tahap keluaran.
  • rotate() berporos dalam koordinat pengguna. Poros ($x, $y) dinyatakan dalam satuan pengguna yang diukur dari sudut kiri atas halaman, dalam kerangka yang sama dengan text(). Untuk diagonal yang melintasi pusat, gunakan pusat halaman (getPageWidth() / 2, getPageHeight() / 2).
  • Teks yang diputar membutuhkan offset lebar manual. text() menempatkan titik asal string; ia tidak memusatkannya untuk Anda. Kurangi kira-kira setengah dari perkiraan lebar teks dari poros X agar tanda yang diputar melintang di pusat, seperti yang dilakukan helper.
  • Gambar diskalakan ke kotak yang Anda berikan. image() meregangkan raster ke width dan height yang Anda berikan. Untuk latar belakang satu halaman penuh, berikan lebar dan tinggi halaman; untuk logo di sudut, berikan ukuran aslinya. Dimensi nol atau negatif memunculkan PageLayoutException.
  • image() menolak URL dan byte NUL. Path scheme:// atau byte NUL dalam $file memunculkan PageLayoutException sebelum dekode apa pun. Berikan hanya path lokal yang sudah divalidasi.
  • Tanda adalah konten yang terlihat. Tanda air yang digambar dengan cara ini adalah konten halaman nyata, bukan anotasi tersembunyi. Siapa pun yang memiliki berkas tersebut dapat membacanya. Ini adalah isyarat visual, bukan kontrol akses.

Tanda air teks menggunakan sedikit operator aliran konten per halaman dan menambahkan waktu atau memori yang dapat diabaikan. Tanda air atau latar belakang gambar memerlukan satu dekode raster ditambah byte gambar yang tertanam dalam keluaran. Menggunakan ulang gambar yang sama di seluruh halaman akan menggunakan XObject yang sudah didekode melalui cache gambar, sehingga Anda membayar biaya dekode hanya sekali. Sesuaikan ukuran gambar latar belakang dengan kotak tampilannya sebelum menanamkannya. Foto 4000 px yang diskalakan ke halaman letter membawa byte yang tidak pernah dilihat pembaca. Tanda air teks satu halaman yang umum tetap jauh di bawah anggaran waktu 500 ms dan puncak 32 MB. Latar belakang gambar mengikuti ukuran raster sumber yang sudah didekode.

Alur berjalan di dalam proses. Tidak ada byte dokumen yang meninggalkan host, dan tidak ada panggilan jaringan yang dilakukan. Perlakukan setiap path gambar yang berasal dari luar kode Anda sebagai masukan yang tidak tepercaya.

  • Validasi path gambar sebelum digunakan. Tolak byte NUL, selesaikan path dengan realpath(), dan pastikan is_file() dan is_readable() sebelum Anda memanggil image(), persis seperti yang dilakukan contoh produksi. Ini memblokir path traversal serta menolak direktori dan tautan rusak sejak dini.
  • Jangan pernah menyisipkan bidang permintaan ke dalam sebuah path. Turunkan path gambar dan path keluaran dari nilai yang dikendalikan server, bukan dari parameter permintaan. Ini mencegah Anda membaca atau menulis berkas di luar direktori yang dimaksud.
  • Perlakukan gambar yang tidak tepercaya sebagai masukan berbahaya. Raster yang cacat memunculkan ImageProcessingException alih-alih merusak dokumen, dan loader membatasi dimensi gambar untuk menahan masukan bom dekompresi. Tangkap eksepsi tersebut dan tolak unggahannya. Jangan mencoba ulang secara membabi buta.
  • Tanda air bukan tempat menyimpan rahasia. Tanda adalah konten yang terlihat. Jangan menyandikan kredensial, token, atau pengidentifikasi internal dalam tanda air atau latar belakang yang Anda kembalikan ke klien.

Resep ini tidak membuat klaim standar normatif tersendiri. Resep ini menyusun primitif publik alpha, transform, text, dan image. Setiap primitif memancarkan operator aliran konten PDF standar. Graphics state diisolasi dengan operator q / Q yang dipancarkan oleh startTransform() dan stopTransform(), dan transparansi dibawa melalui parameter graphics state ExtGState. Keluarannya baru secara struktural, bukan stabil per byte, sehingga halaman ini mendeklarasikan profil reproduksibilitas structural. Untuk detail tingkat operator tentang permukaan transform dan graphics state, lihat referensi modul Graphics.