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

تحويل HTML إلى PDF باستخدام محرك Artisan Chrome

يعرض جسر ⁨Artisan⁩ محتوى ⁨HTML⁩ عبر عملية ⁨Chrome⁩ بلا واجهة رسومية، ثم يستورد النتيجة إلى مستند ⁨NextPDF⁩ ككائن ⁨Form XObject⁩ متجهي. يبقى النص قابلاً للتحديد والبحث بدلاً من تحويله إلى صورة نقطية. أرفِق ChromeRendererConfig، واستدعِ writeHtmlChrome() على مستند، أو استخدم ChromeHtmlRenderer مباشرة، واترك تخطيط الصفحة لـ ⁨Chrome⁩. يغطي هذا الدليل استدعاء العرض وعزل الشبكة وضبط حجم الصفحة وارتفاع المحتوى ودورة حياة محرك العرض طويلة الأمد داخل عامل واحد.

ابدأ بالمتطلبات المسبقة:

  • نواة ⁨NextPDF⁩ وnextpdf/artisan مثبَّتان.
  • ملف تنفيذي لـ ⁨Chrome⁩ أو ⁨Chromium⁩ مثبَّت، ويمكن لمستخدم العامل تشغيله بلا واجهة رسومية. تحقَّق من ذلك باستخدام chromium --headless --dump-dom about:blank قبل أن تبدأ. تغطي صفحة إعداد محرك عرض ⁨Chrome⁩ المرتبطة ضمن “انظر أيضاً” تجهيز الملف التنفيذي وقرار استخدام الحاوية المعزولة.

يفترض هذا الدليل العملي أنك تستطيع تشغيل عملية ⁨Chrome⁩ بالقرب من التطبيق. للحصول على أول مثال قابل للتشغيل، اقرأ دليل البدء السريع لـ ⁨Artisan⁩.

ثبِّت الجسر بجانب النواة.

Terminal window
composer require nextpdf/artisan

ثبِّت إصدار ⁨Chrome⁩ أو ⁨Chromium⁩ يمكن لمستخدم العامل تشغيله. على ⁨Debian⁩ أو ⁨Ubuntu⁩، استخدم حزمة التوزيعة.

Terminal window
apt-get install -y chromium

تأكَّد من أن الملف التنفيذي يعمل بلا واجهة رسومية بصفة مستخدم العامل.

Terminal window
chromium --headless --dump-dom about:blank

رمز الخروج 0 مع نموذج كائنات مستند (⁨DOM⁩) فارغ يعني أن الملف التنفيذي ومكتباته المشتركة موجودة. رمز الخروج غير الصفري هو الفشل نفسه الذي يُظهره الجسر على هيئة ChromeRenderException. أصلِحه هنا أولاً.

writeHtmlChrome() هي طريقة في Document ضمن نواة ⁨NextPDF⁩. تتحقق من صحة المُدخَل، وتحلّ محرك عرض ⁨Artisan⁩، وترسل ⁨HTML⁩ إلى ⁨Chrome⁩ عبر بروتوكول ⁨Chrome DevTools⁩ (⁨CDP⁩)، وتحلّل ملف ⁨PDF⁩ المُعاد، وتُضمِّن الصفحة 0 ككائن ⁨Form XObject⁩ عند موضع المؤشر الحالي. يعمل ⁨Chrome⁩ كعملية فرعية لعامل ⁨PHP⁩. يقود الجسر ⁨Chrome⁩ عبر ⁨CDP⁩ بدلاً من الاتصال بعملية ⁨Chrome⁩ منفصلة عبر منفذ تصحيح، لذلك لا توجد أي نقطة وصول شبكية يجب كشفها أو التحقق من هويتها.

يعرض الجسر في وضع شبكة يحظر كل شيء افتراضياً. يستخدم كل عرض سياسة أمان محتوى (⁨Content-Security-Policy⁩) تمنع كل أصول الموارد (default-src 'none') ولا تسمح إلا بالصور المضمَّنة (img-src data:). كما يحظر الجسر كل عنوان ⁨URL⁩ لمورد فرعي على مستوى نقل ⁨CDP⁩ باستخدام Network.setBlockedURLs(['*']). ونتيجة لذلك، لا تُحمَّل أي صورة أو ورقة أنماط أو خط أو سكربت أو ⁨iframe⁩ بعيد في ⁨HTML⁩ لديك. ضمِّن كل أصل على هيئة معرِّف موارد موحَّد (⁨URI⁩) من نوع data:. بهذه الطريقة يعالج الجسر خطر تزوير الطلب من جانب الخادم (⁨SSRF⁩) عندما يعرض ⁨HTML⁩ قد لا يكون موثوقاً، وينطبق ذلك بغض النظر عن أي إعداد.

يدعم نموذج حجم الصفحة وضعين. عندما تزوّد كلاً من العرض والارتفاع، بوحدات نقاط ⁨PDF⁩، يطبع ⁨Chrome⁩ بحجم الورق هذا بالضبط. عند حذف الارتفاع أو ضبطه على null، يقيس الجسر ارتفاع المحتوى المعروض في ⁨Chrome⁩، ويحوّله إلى نقاط، ويضيف هامش أمان صغيراً لإعادة التدفق يقارب 14.4 نقطة. يمنع ذلك printToPDF من التدفق إلى صفحة ثانية يقتطعها المستورِد الذي لا يتعامل إلا مع الصفحة 0.

// On a NextPDF core Document (the HasTextOutput concern):
writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static
// The standalone renderer:
new ChromeHtmlRenderer(ChromeRendererConfig $config, ?LoggerInterface $logger = null)
ChromeHtmlRenderer::render(string $html, float $widthPt, float $heightPt = 0.0): ChromeRenderResult
ChromeHtmlRenderer::close(): void
// The configuration value object (final readonly):
new ChromeRendererConfig(
?string $chromeBinaryPath = null,
int $renderTimeout = 30,
string $defaultCss = '',
int $maxHtmlSize = 5_000_000,
bool $noSandbox = false,
)
ChromeRendererConfig::fromArray(array $config): self

ChromeRendererConfig هي واجهة الإعداد الوحيدة. وهي غير قابلة للتغيير، لذا أنشئ نسخة جديدة لتغيير أي قيمة. ChromeRenderResult::getPdfData() تُرجع بايتات ⁨PDF⁩. تسرد صفحة إعداد ⁨Artisan⁩ المرتبطة ضمن “انظر أيضاً” مرجع الخيارات الكامل وأعلام تشغيل ⁨Chrome⁩ الثابتة.

أرفِق الإعداد بمستند، واعرض ⁨HTML⁩ موثوقاً به، ثم احفظ.

render-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Artisan\ChromeRendererConfig;
use NextPDF\Core\Document;
$config = new ChromeRendererConfig(
chromeBinaryPath: '/usr/bin/chromium',
);
$document = Document::createStandalone();
$document->setChromeRendererConfig($config);
$document->addPage();
$document->writeHtmlChrome('
<div style="display: flex; gap: 20px; font-family: sans-serif;">
<div style="flex: 1; background: #f0f0f0; padding: 24px;">
<h2>Revenue</h2>
<p style="font-size: 2em; color: #2563eb;">$124,500</p>
</div>
<div style="flex: 1; background: #f0f0f0; padding: 24px;">
<h2>Orders</h2>
<p style="font-size: 2em; color: #16a34a;">1,847</p>
</div>
</div>
');
$document->save('/tmp/report.pdf');

يتولى ⁨Chrome⁩ تخطيط ⁨flex⁩، وتبقى الأرقام قابلة للتحديد في الناتج لأن الصفحة مُضمَّنة ككائن ⁨Form XObject⁩ متجهي، لا كصورة نقطية. لملاءمة صفحة ⁨A4⁩ ثابتة، مرِّر العرض والارتفاع بالنقاط.

explicit A4 page size
$document->writeHtmlChrome($html, width: 595.28, height: 841.89);

في بيئة الإنتاج، أنشئ محرك عرض واحداً لكل عامل، واحقن مسجِّلاً متوافقاً مع ⁨PSR-3⁩، والتقط كل نوع من نوعَي الاستثناء المختلفين على حدة، وحرِّر عملية ⁨Chrome⁩ بشكل حتمي عند الإيقاف.

ReportRenderer.php
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;
use NextPDF\Artisan\ChromeRendererConfig;
use NextPDF\Artisan\Exception\ChromeNotAvailableException;
use NextPDF\Artisan\Exception\ChromeRenderException;
use Psr\Log\LoggerInterface;
final class ReportRenderer
{
private ChromeHtmlRenderer $renderer;
public function __construct(LoggerInterface $logger)
{
$config = ChromeRendererConfig::fromArray([
'chrome_binary' => getenv('CHROME_BINARY') ?: null,
'render_timeout' => 45,
'max_html_size' => 2_000_000,
'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'),
]);
$this->renderer = new ChromeHtmlRenderer($config, $logger);
}
public function render(string $html, float $widthPt, float $heightPt = 0.0): string
{
try {
return $this->renderer->render($html, $widthPt, $heightPt)->getPdfData();
} catch (ChromeNotAvailableException $exception) {
// Deployment fault: the Chrome runtime is missing. Page on-call.
throw $exception;
} catch (ChromeRenderException $exception) {
// Render-time fault: timeout, crash, or empty output. Retryable once.
throw $exception;
}
}
public function shutdown(): void
{
$this->renderer->close();
}
}

أنشئ محرك العرض مرة واحدة، ثم أعد استخدامه. يحافظ تجمُّع المتصفح الداخلي على عملية ⁨Chrome⁩ واحدة قيد التشغيل، ويعيد تشغيلها كل 100 عملية عرض للحد من نمو الذاكرة. تفصل كتلتا الالتقاط بين عطل في النشر، مثل وقت تشغيل مفقود، وعطل في وقت العرض يمكنك إعادة محاولته مرة واحدة. لا تترك أي كتلة التقاط فارغة. استدعِ shutdown() عند إيقاف العامل لتحرير عملية ⁨Chrome⁩ بدلاً من انتظار المُتلِف.

أنشئ الإعداد من مصفوفة إعدادات إطار العمل لاستخدام مفاتيح ⁨snake-case⁩، وثبِّت chromeBinaryPath في بيئة الإنتاج ليكون الملف التنفيذي حتمياً.

  • ⁨HTML⁩ الفارغ لا يفعل شيئاً. writeHtmlChrome('') تُعيد المستند دون تغيير.
  • لا توجد صفحة بعد. إذا لم يكن للمستند أي صفحة، تضيف writeHtmlChrome() واحدة قبل العرض.
  • الأصول البعيدة لا تُحمَّل — بحكم التصميم. <img src="https://..."> تُعرَض فارغة. ضمِّن كل أصل على هيئة معرِّف موارد موحَّد (⁨URI⁩) من نوع data:. هذه هي وضعية عزل الشبكة، وليست عيباً.
  • لا تُستورَد إلا الصفحة 0. يضيف الارتفاع التلقائي للملاءمة هامش إعادة التدفق بحيث تُنتَج صفحة واحدة. مع ارتفاع صريح، لا يُضاف أي هامش ويطابق الناتج حجم الورق المطلوب بالضبط، لذا حدِّد الارتفاع ليلائم محتواك.
  • الجسر مفقود. إذا لم يكن nextpdf/artisan مثبَّتاً، تثير النواة استثناء إعداد بدلاً من خطأ فادح. إذا كانت مكتبة chrome-php/chrome غائبة، يثير الجسر ChromeNotAvailableException مصحوباً بأمر التثبيت.
  • defaultCss و</style>. يُزال أي تسلسل </style> في defaultCss قبل حقنه دفاعاً ضد كسر الأنماط. خطِّط لذلك إن كنت تُنشئ ⁨CSS⁩ من قوالب.

يتحمل العرض الأول تكلفة بدء تشغيل ⁨Chrome⁩ والتخطيط. تعيد عمليات العرض اللاحقة استخدام عملية ⁨Chrome⁩ الحية، فنادراً ما تتحمل تكلفة بدء التشغيل. أنشئ محرك عرض واحداً لكل عامل وأعد استخدامه. لا تنشئ واحداً لكل طلب. توقَّع ارتفاعاً في زمن الاستجابة عند عملية العرض رقم 100، حين يعيد الجسر تشغيل عملية ⁨Chrome⁩ للحد من الذاكرة. احسب حساب ذلك في أهداف زمن الاستجابة بدلاً من معاملته كحادثة. اقرِن renderTimeout بميزانية زمنية أعلى للطلب على امتداد السلسلة في أي مسار يمكن بلوغه عبر مُدخَل غير موثوق.

  • عزل الشبكة هو الضابط الأساسي. لا يسمح الجسر بأي جلب لمورد فرعي صادر إطلاقاً: سياسة أمان المحتوى default-src 'none' إضافة إلى حظر كل عنوان ⁨URL⁩ على مستوى نقل ⁨CDP⁩. وهو لا يطبّق قائمة سماح بالنطاقات لأنه لا يحتاج إليها. ضمِّن الأصول على هيئة معرِّفات موارد موحَّدة (⁨URI⁩) من نوع data:.
  • يُقيَّد المُدخَل قبل الاتصال بـ ⁨Chrome⁩. يرفض الجسر ⁨HTML⁩ الذي يتجاوز maxHtmlSize (5 ⁨MB⁩ افتراضياً)، ومعرِّف موارد موحَّداً (⁨URI⁩) من نوع ⁨base64⁩ إذا كان مفرط الحجم (حماية من قنبلة فك الضغط)، وأي وسم <meta http-equiv="refresh"> (قد يدفع إلى التنقل نحو نقطة وصول داخلية). أبقِ maxHtmlSize على القيمة الافتراضية ما لم يتطلب حِمل عمل معروف قيمة أكبر. إن رفعها يوسِّع سطح استنزاف الموارد.
  • حاوية ⁨Chrome⁩ المعزولة (⁨sandbox⁩) ضابط منفصل. يؤدي ضبط noSandbox: true إلى تشغيل ⁨Chrome⁩ مع --no-sandbox، ما يزيل عزل عملية ⁨Chrome⁩. هذا يقلل الاحتواء فعلياً، وليس مجرد عَلَم شكلي. أبقِه false خارج الحاويات. عندما يتعذر تهيئة حاوية النظام المعزولة، شغِّل ⁨Chrome⁩ بصفة مستخدم غير جذري في حاوية مقيَّدة، وتعامل مع هذا النشر على أنه يتطلب ثقة أعلى بالمُدخَل.
  • السجلات تحمل البيانات الوصفية فقط. احقن مسجِّلاً متوافقاً مع ⁨PSR-3⁩. يسجِّل الجسر أطوال البايتات والأبعاد وأحداث دورة الحياة، ولا يسجِّل أبداً ⁨HTML⁩ أو بايتات ⁨PDF⁩ أو النص المُستخرَج.
  • لا تكشف أبداً منفذ تصحيح ⁨Chrome⁩ عن بُعد. لا يستخدم الجسر أي منفذ، ومنفذ ⁨CDP⁩ المفتوح هو قناة تحكم غير مُتحقَّق من هويتها.

يوجد نموذج التهديد الكامل، بما في ذلك دفاع ⁨SSRF⁩ وحدود الحاوية المعزولة الصريحة وفهرس أنماط الفشل، في صفحة أمان وعمليات ⁨Artisan⁩ المرتبطة ضمن “انظر أيضاً”. توثّق تلك الصفحة بنود ⁨OWASP⁩ و⁨CWE⁩ و⁨NIST⁩ ذات الصلة.

لا يقدّم هذا الدليل أي ادعاء معياري بشأن المواصفات بذاته. تربط صفحة أمان وعمليات ⁨Artisan⁩ الأصلية ضوابط الشبكة والعزل واستنزاف الموارد في الجسر بمعيار ⁨OWASP ASVS⁩، وبأبرز 25 من ⁨CWE⁩ (⁨SSRF⁩ / استهلاك موارد غير منضبط)، وبـ ⁨NIST SP 800-53 SC-7⁩. تعيد صفحة دليل الطبخ هذه ذكر طريقة الاستخدام وتُحيل تلك الاستشهادات المعيارية إلى تلك الصفحة. لا ينفّذ الجسر أي عملية تشفيرية؛ فالتوقيع والتشفير من شؤون النواة أو الإصدار التجاري ولا يتأثران بـ ⁨Artisan⁩.