تخطَّ إلى المحتوى

إضافة علامات مائية نصية أو صورية وخلفيات إلى الصفحات

يمكنك إضافة علامة “⁨DRAFT⁩” أو “⁨CONFIDENTIAL⁩” إلى كل صفحة، أو وضع شعار باهت خلف المحتوى. توضّح هذه الوصفة كيفية إضافة الخيارين إلى صفحات ⁨NextPDF⁩ الأساسية باستخدام واجهة المستند العامة: setAlpha() للشفافية، وstartTransform() / rotate() / stopTransform() للختم القطري، وtext() للعلامة، وimage() للخلفية النقطية.

يرجع الفرق بين العلامة المائية والخلفية إلى اختيار واحد: ترتيب الرسم.

  • الخلفية: ارسمها أولًا، ثم اكتب محتوى صفحتك فوقها. تظهر العلامة خلف النص.
  • العلامة المائية المتراكبة: اكتب محتوى صفحتك أولًا، ثم ارسم العلامة فوقه. تظهر العلامة في الأعلى.

يرسم ⁨NextPDF⁩ المحتوى بالترتيب الذي تستدعيه به، لذلك يحدد ترتيب الاستدعاءات ترتيب الطبقات. لا يوجد “وضع خلفية” منفصل؛ بل تحدد الطبقة باختيار وقت الرسم.

المتطلبات المسبقة: ثبّت النواة (composer require nextpdf/core:^3)، واستخدم، لإضافة خلفية صورية، ملفًا نقطيًا قابلًا للقراءة (⁨PNG⁩ أو ⁨JPEG⁩ أو ⁨WebP⁩) على القرص. يُنفَّذ المسار كله داخل العملية، من دون متصفح بلا واجهة أو استدعاء عبر الشبكة.

Terminal window
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() الحالة المحفوظة. تمنع إحاطة العلامة بكتلة تحويل تسرّب التدوير وقيمة ⁨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\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)، فيلغي التدوير وتغيير ⁨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) قبل المحتوى المعتم. تستخدم عينة الإنتاج الأسلوبين.
  • وازِن كل كتلة تحويل. يحتاج كل startTransform() إلى stopTransform() مطابق. تترك الكتلة غير المتوازنة التدوير أو قيمة ⁨alpha⁩ مطبَّقة على المحتوى اللاحق، ويُنشئ غياب 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 ⁨ms⁩ وميزانية ذروة قدرها 32 ⁨MB⁩ بهامش جيد. تتبع خلفية الصورة الحجم المفكوك ترميزه للصورة النقطية المصدر.

يعمل المسار داخل العملية. لا تغادر أي بايتات المستند المضيف، ولا يُجرى أي استدعاء عبر الشبكة. عامِل أي مسار صورة يأتي من خارج شفرتك بوصفه إدخالًا غير موثوق.

  • تحقق من مسار الصورة قبل الاستخدام. ارفض بايتات ⁨NUL⁩، وحُل المسار باستخدام realpath()، وأكِّد is_file() وis_readable() قبل أن تستدعي image()، تمامًا كما تفعل عينة الإنتاج. يمنع هذا اجتياز المسارات ويرفض الأدلة والروابط المعلَّقة مبكرًا.
  • لا تُدرج حقل طلب داخل مسار أبدًا. اشتق مسار الصورة ومسار المخرج من قيم يتحكم فيها الخادم، لا من معامل طلب. يمنعك هذا من قراءة أو كتابة ملفات خارج الدليل المقصود.
  • عامِل الصور غير الموثوقة بوصفها إدخالًا عدائيًا. تثير الصورة النقطية المشوَّهة ImageProcessingException بدلًا من إفساد المستند، ويحدد المُحمِّل سقفًا لأبعاد الصورة لمقاومة مدخلات قنابل فك الضغط. التقط الاستثناء وارفض الملف المرفوع. لا تُعد المحاولة دون فحص.
  • العلامة المائية ليست مخزنًا للأسرار. العلامة محتوى مرئي. لا تُرمِّز بيانات الاعتماد أو الرموز المميزة أو المعرِّفات الداخلية في علامة مائية أو خلفية تُرجعها إلى عميل.

لا تقدم هذه الوصفة أي ادعاء معياري خاص بها بشأن المعايير. إنها تركِّب أوليات ⁨alpha⁩ والتحويل والنص والصورة العامة. تُصدر كل أولية معاملات دفق محتوى ⁨PDF⁩ القياسية. تُعزَل الحالة الرسومية بمعاملَي q / Q اللذين يُصدرهما startTransform() وstopTransform()، وتُنقل الشفافية عبر معامل الحالة الرسومية ExtGState. الإخراج جديد بنيويًا وليس ثابت البايتات، لذلك تعلن هذه الصفحة عن ملف تعريف قابلية إعادة الإنتاج structural. للحصول على تفاصيل على مستوى المعاملات حول واجهة التحويل والحالة الرسومية، راجع مرجع وحدة ⁨Graphics⁩.