ข้ามไปยังเนื้อหา

เพิ่มลายน้ำข้อความ ลายน้ำรูปภาพ หรือพื้นหลังลงในหน้า

คุณสามารถเพิ่มเครื่องหมาย “DRAFT” หรือ “CONFIDENTIAL” พาดทับทุกหน้า หรือวางโลโก้แบบจางไว้ด้านหลังเนื้อหาได้ สูตรนี้เพิ่มทั้งสองอย่างให้กับหน้าใน NextPDF core ผ่าน public document surface ได้แก่ setAlpha() สำหรับความโปร่งใส startTransform() / rotate() / stopTransform() สำหรับตราประทับแนวทแยง text() สำหรับเครื่องหมาย และ image() สำหรับพื้นหลังแบบ raster

ลายน้ำกับพื้นหลังต่างกันที่ตัวเลือกเดียว นั่นคือลำดับการวาด

  • พื้นหลัง: วาดก่อน แล้วจึงเขียนเนื้อหาของหน้าทับลงไป เครื่องหมายจะอยู่ด้านหลังข้อความ
  • ลายน้ำแบบซ้อนทับ: เขียนเนื้อหาของหน้าก่อน แล้วจึงวาดเครื่องหมายทับลงไป เครื่องหมายจะอยู่ด้านบนสุด

NextPDF วาดเนื้อหาตามลำดับการเรียกใช้ของคุณ ดังนั้นลำดับการเรียกใช้จึงกำหนดลำดับเลเยอร์ ไม่มี “background mode” แยกต่างหาก คุณกำหนดเลเยอร์ได้จากจังหวะที่เลือกวาด

ข้อกำหนดเบื้องต้น: ต้องมีการติดตั้ง core (composer require nextpdf/core:^3) ที่ใช้งานได้ และสำหรับพื้นหลังรูปภาพ ต้องมีไฟล์ raster ที่อ่านได้ (PNG, JPEG หรือ WebP) บนดิสก์ ไปป์ไลน์ทั้งหมดทำงานภายในโพรเซส โดยไม่ใช้ headless browser หรือการเรียกผ่านเครือข่าย

Terminal window
composer require nextpdf/core:^3

เครื่องหมายแต่ละรายการที่คุณเพิ่มคือเนื้อหาของหน้าตามปกติ ซึ่งวาดผ่าน graphics state public surface ส่วนประกอบสามส่วนทำงานร่วมกันเพื่อสร้างลายน้ำ:

  1. ความโปร่งใส setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal) กำหนดความทึบของการเติมสีให้กับทุกสิ่งที่คุณวาดหลังจากนั้น ตั้งแต่ 0.0 (มองไม่เห็น) ไปจนถึง 1.0 (ทึบแสง) โดยทั่วไปลายน้ำจะทำงานได้ดีที่สุดที่ 0.1 ถึง 0.3 เพื่อให้เนื้อหาด้านล่างยังคงอ่านได้ blend mode มาจาก enum NextPDF\Graphics\BlendMode ตัวอย่างเช่น BlendMode::Multiply จะทำให้บริเวณที่เครื่องหมายซ้อนทับกับเนื้อหามืดลง

  2. การหมุน ตราประทับแนวทแยงคือข้อความที่หมุนรอบจุดหมุน startTransform() บันทึก graphics state 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) วางรูปภาพแบบ raster ซึ่งเป็นพื้นฐานของลายน้ำรูปภาพหรือพื้นหลังแบบเต็มหน้า

graphics state กู้คืนได้อย่างเรียบร้อยเพราะ startTransform() และ stopTransform() คร่อมการเปลี่ยนแปลงไว้ ค่า setAlpha() จะคงอยู่จนกว่าคุณจะกำหนดอีกครั้ง หากเนื้อหาที่ตามมาต้องทึบแสงเต็มที่ ให้รีเซ็ตความทึบเป็น 1.0 หลังจากวาดเครื่องหมาย รูปแบบที่ปลอดภัยกว่าซึ่งแสดงด้านล่างจะวาดเครื่องหมายไว้ภายในบล็อกการแปลงของตัวเอง และกำหนด alpha ของเนื้อหาหน้าอย่างชัดเจน

แพ็กเกจนี้ยังมี value object NextPDF\Graphics\Watermark และ NextPDF\Graphics\WatermarkPosition ด้วย Watermark เป็นตัวเก็บการตั้งค่าแบบไม่เปลี่ยนรูปสำหรับข้อความ ขนาดฟอนต์ มุม สี แฟล็กการซ้อนทับ และค่าตำแหน่งที่เตรียมไว้ เช่น WatermarkPosition::Diagonal อ็อบเจกต์เหล่านี้จำลองพารามิเตอร์ของลายน้ำ สูตรนี้วาดเครื่องหมายด้วยเมธอดข้างต้นซึ่งเขียนลงหน้าโดยตรง ดังนั้นผลลัพธ์จึงไปถึงสตรีมเนื้อหาของหน้าโดยตรง

เมธอดทั้งหมดด้านล่างนี้เป็น public บน NextPDF\Core\Document และคืนค่า static จึงสามารถเรียกต่อกันเป็นลูกโซ่ได้

  • setAlpha(float $alpha, BlendMode $mode = BlendMode::Normal): static: กำหนดความทึบของการเติมสี (0.0-1.0) และ blend mode สำหรับเนื้อหาที่ตามมา
  • startTransform(): static: บันทึก graphics state (ปล่อย 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: วางรูปภาพแบบ raster ซึ่งเป็นพื้นฐานของลายน้ำรูปภาพหรือพื้นหลังแบบเต็มหน้า
  • getPageWidth(): float / getPageHeight(): float: อ่านขนาดหน้าปัจจุบันเป็นพอยต์ เพื่อให้คุณจัดเครื่องหมายไว้กึ่งกลางได้

ชนิดที่รองรับอยู่ภายใต้ NextPDF\Graphics ได้แก่ enum BlendMode value object 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 โปรแกรมจะวางรูปภาพนั้นเป็นพื้นหลังแบบจางที่จัดกึ่งกลางบนหน้าที่สอง โปรแกรมตรวจสอบความถูกต้องของพาทรูปภาพก่อนใช้งาน จับ exception ของ 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() จะสร้างความไม่สมดุลของ graphics state ที่ตัวเขียนปฏิเสธในขั้นตอนเอาต์พุต
  • rotate() หมุนรอบจุดในพิกัดผู้ใช้ จุดหมุน ($x, $y) อยู่ในหน่วยผู้ใช้ที่วัดจากมุมบนซ้ายของหน้า ในกรอบเดียวกับ text() สำหรับเส้นทแยงที่ผ่านกึ่งกลาง ให้ใช้จุดกึ่งกลางหน้า (getPageWidth() / 2, getPageHeight() / 2)
  • ข้อความที่หมุนต้องชดเชยความกว้างด้วยตนเอง text() วางจุดเริ่มต้นของสตริง ไม่ได้จัดกึ่งกลางให้คุณ ลบความกว้างข้อความโดยประมาณราวครึ่งหนึ่งออกจาก X ของจุดหมุน เพื่อให้เครื่องหมายที่หมุนคร่อมกึ่งกลาง ตามที่ฟังก์ชันตัวช่วยทำ
  • รูปภาพปรับขนาดตามกล่องที่คุณส่งให้ image() ยืดรูปภาพ raster ให้พอดีกับ width และ height ที่คุณกำหนด สำหรับพื้นหลังแบบเต็มหน้า ให้ส่งความกว้างและความสูงของหน้า สำหรับโลโก้ที่มุม ให้ส่งขนาดตามธรรมชาติของโลโก้ ขนาดที่เป็นศูนย์หรือค่าลบจะทำให้เกิด PageLayoutException
  • image() ปฏิเสธ URL และไบต์ NUL พาทแบบ scheme:// หรือไบต์ NUL ใน $file จะทำให้เกิด PageLayoutException ก่อนการถอดรหัสใด ๆ ให้ส่งเฉพาะพาทในเครื่องที่ผ่านการตรวจสอบแล้วเท่านั้น
  • เครื่องหมายเป็นเนื้อหาที่มองเห็นได้ ลายน้ำที่วาดด้วยวิธีนี้เป็นเนื้อหาของหน้าจริง ไม่ใช่คำอธิบายประกอบที่ซ่อนอยู่ ผู้ที่มีไฟล์นี้สามารถอ่านได้ เครื่องหมายนี้เป็นเพียงสัญญาณทางสายตา ไม่ใช่การควบคุมการเข้าถึง

ลายน้ำข้อความใช้ตัวดำเนินการของสตรีมเนื้อหาเพียงไม่กี่ตัวต่อหน้า และเพิ่มเวลาหรือหน่วยความจำเพียงเล็กน้อยจนแทบไม่มีนัยสำคัญ ลายน้ำหรือพื้นหลังแบบรูปภาพมีต้นทุนจากการถอดรหัส raster หนึ่งครั้งบวกกับไบต์รูปภาพที่ฝังอยู่ในเอาต์พุต การใช้รูปภาพเดียวกันซ้ำในหลายหน้าจะนำ XObject ที่ถอดรหัสแล้วกลับมาใช้ผ่านแคชรูปภาพ คุณจึงเสียต้นทุนการถอดรหัสเพียงครั้งเดียว ปรับขนาดรูปภาพพื้นหลังให้พอดีกับกล่องแสดงผลก่อนฝัง ภาพถ่ายขนาด 4000 px ที่ย่อลงบนหน้ากระดาษ letter จะยังเก็บไบต์ที่ผู้อ่านไม่มีวันเห็นไว้ ลายน้ำข้อความหน้าเดียวโดยทั่วไปอยู่ในกรอบงบประมาณเวลารวม 500 ms และหน่วยความจำสูงสุด 32 MB ได้อย่างเหลือเฟือ พื้นหลังแบบรูปภาพจะแปรผันตามขนาดที่ถอดรหัสแล้วของ raster ต้นทาง

ไปป์ไลน์ทำงานภายในโพรเซส ไม่มีไบต์ของเอกสารออกจากโฮสต์ และไม่มีการเรียกผ่านเครือข่าย ให้ถือว่าพาทรูปภาพใด ๆ ที่มาจากภายนอกโค้ดของคุณเป็นอินพุตที่ไม่น่าเชื่อถือ

  • ตรวจสอบความถูกต้องของพาทรูปภาพก่อนใช้งาน ปฏิเสธไบต์ NUL แปลงพาทเป็นพาทสมบูรณ์ด้วย realpath() และยืนยัน is_file() และ is_readable() ก่อนที่คุณจะเรียก image() เช่นเดียวกับที่ตัวอย่างสำหรับใช้งานจริงทำ วิธีนี้ปิดกั้น path traversal และปฏิเสธไดเรกทอรีและลิงก์ที่ปลายทางหายไปตั้งแต่ต้น
  • อย่านำฟิลด์จากคำขอมาแทรกลงในพาทเด็ดขาด ให้ได้พาทรูปภาพและพาทเอาต์พุตมาจากค่าที่ควบคุมโดยเซิร์ฟเวอร์ ไม่ใช่จากพารามิเตอร์ของคำขอ วิธีนี้ป้องกันไม่ให้คุณอ่านหรือเขียนไฟล์นอกไดเรกทอรีที่ตั้งใจไว้
  • ให้ถือว่ารูปภาพที่ไม่น่าเชื่อถือเป็นอินพุตที่เป็นอันตราย raster ที่ผิดรูปแบบจะทำให้เกิด ImageProcessingException แทนที่จะทำให้เอกสารเสียหาย และตัวโหลดจำกัดขนาดรูปภาพเพื่อต้านอินพุตแบบ decompression bomb จับ exception และปฏิเสธการอัปโหลด อย่าลองใหม่โดยไม่พิจารณา
  • ลายน้ำไม่ใช่ที่เก็บความลับ เครื่องหมายเป็นเนื้อหาที่มองเห็นได้ อย่าเข้ารหัสข้อมูลรับรอง โทเค็น หรือตัวระบุภายในไว้ในลายน้ำหรือพื้นหลังที่คุณส่งกลับไปยังไคลเอนต์

สูตรนี้ไม่ได้อ้างความสอดคล้องตามมาตรฐานในเชิงบรรทัดฐานด้วยตัวเอง สูตรนี้ประกอบขึ้นจาก primitive สาธารณะสำหรับ alpha การแปลง ข้อความ และรูปภาพ primitive แต่ละตัวปล่อยตัวดำเนินการของสตรีมเนื้อหา PDF มาตรฐาน graphics state ถูกแยกออกด้วยตัวดำเนินการ q / Q ที่ startTransform() และ stopTransform() ปล่อยออกมา และความโปร่งใสถูกส่งผ่านพารามิเตอร์ graphics state แบบ ExtGState เอาต์พุตเน้นความสามารถในการทำซ้ำเชิงโครงสร้างมากกว่าความเสถียรระดับไบต์ ดังนั้นหน้านี้จึงประกาศโปรไฟล์ความสามารถในการทำซ้ำแบบ structural สำหรับรายละเอียดระดับตัวดำเนินการเกี่ยวกับพื้นผิวการแปลงและ graphics state โปรดดู การอ้างอิงโมดูล Graphics