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

เรนเดอร์ HTML เป็น PDF ด้วยตัวเรนเดอร์ Chrome ของ Artisan

บริดจ์ Artisan เรนเดอร์ HTML ด้วยกระบวนการ Chrome แบบ headless แล้วนำผลลัพธ์เข้าเอกสาร NextPDF ในรูปแบบ vector Form XObject ข้อความจึงยังเลือกและค้นหาได้ แทนที่จะถูกแปลงเป็นภาพแรสเตอร์ คุณสามารถแนบ ChromeRendererConfig เรียก writeHtmlChrome() บนเอกสาร หรือใช้ ChromeHtmlRenderer โดยตรง แล้วให้ Chrome จัดการเค้าโครง คู่มือนี้ครอบคลุมการเรียกเรนเดอร์ การแยกเครือข่าย การกำหนดขนาดหน้า ความสูงของเนื้อหา และวงจรชีวิตของตัวเรนเดอร์ที่ทำงานยาวนานสำหรับเวิร์กเกอร์

ข้อกำหนดเบื้องต้นโดยสรุป:

  • ติดตั้ง NextPDF core และ nextpdf/artisan แล้ว
  • ติดตั้งไบนารี Chrome หรือ Chromium แล้ว และผู้ใช้ที่รันเวิร์กเกอร์สามารถเรียกใช้แบบ headless ได้ ตรวจสอบด้วย chromium --headless --dump-dom about:blank ก่อนเริ่ม หน้าการตั้งค่าตัวเรนเดอร์ Chrome ที่ลิงก์ไว้ในส่วนดูเพิ่มเติมครอบคลุมการจัดเตรียมไบนารีและการตัดสินใจเรื่องแซนด์บ็อกซ์ของคอนเทนเนอร์

คู่มือนี้ถือว่าคุณสามารถรันกระบวนการ Chrome ใกล้กับแอปพลิเคชันได้ หากต้องการตัวอย่างแรกที่รันได้ โปรดอ่าน Artisan quickstart

ติดตั้งบริดจ์พร้อมกับ core

Terminal window
composer require nextpdf/artisan

ติดตั้งบิลด์ Chrome หรือ Chromium ที่ผู้ใช้ซึ่งรันเวิร์กเกอร์สามารถเรียกใช้ได้ บน Debian หรือ Ubuntu ให้ใช้แพ็กเกจจากดิสทริบิวชัน

Terminal window
apt-get install -y chromium

ยืนยันว่าไบนารีรันแบบ headless ได้ในฐานะผู้ใช้ที่รันเวิร์กเกอร์

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

โค้ดออก 0 พร้อม document object model (DOM) ว่างเปล่าหมายความว่าไบนารีและไลบรารีที่ใช้ร่วมกันมีอยู่ครบ โค้ดออกที่ไม่ใช่ศูนย์คือความล้มเหลวแบบเดียวกับที่บริดจ์จะแสดงเป็น ChromeRenderException ให้แก้ไขจุดนี้ก่อน

writeHtmlChrome() เป็นเมธอดบน Document ของ NextPDF core เมธอดนี้ตรวจสอบความถูกต้องของอินพุต แก้ไขจุดเชื่อมตัวเรนเดอร์ Artisan ส่ง HTML ไปยัง Chrome ผ่าน Chrome DevTools Protocol (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 ของคุณจะไม่โหลด ฝังแอสเซตทุกรายการแบบอินไลน์เป็น data: URI นี่คือวิธีที่บริดจ์จัดการความเสี่ยง server-side request forgery (SSRF) เมื่อเรนเดอร์ HTML ที่อาจไม่น่าเชื่อถือ และมีผลโดยไม่คำนึงถึงการกำหนดค่า

โมเดลขนาดหน้ามีสองโหมด เมื่อคุณระบุทั้งความกว้างและความสูงเป็นหน่วย point ของ PDF Chrome จะพิมพ์ตามขนาดกระดาษนั้นพอดี เมื่อละความสูงไว้หรือเป็น null บริดจ์จะวัดความสูงของเนื้อหาที่เรนเดอร์ใน Chrome แปลงเป็น point และเพิ่มบัฟเฟอร์นิรภัยเล็กน้อยสำหรับการ reflow ประมาณ 14.4 point วิธีนี้ช่วยป้องกันไม่ให้ 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 และตัวเลขในผลลัพธ์ยังเลือกได้ เพราะหน้าถูกฝังเป็น vector Form XObject ไม่ใช่ภาพแรสเตอร์ หากต้องการให้พอดีกับหน้า A4 แบบกำหนดตายตัว ให้ส่งความกว้างและความสูงเป็นหน่วย point

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

สำหรับการใช้งานจริง ให้สร้างตัวเรนเดอร์หนึ่งตัวต่อหนึ่งเวิร์กเกอร์ ฉีด PSR-3 logger จับข้อยกเว้นทั้งสองประเภทแยกกัน และปล่อยทรัพยากรกระบวนการ 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 การเรนเดอร์เพื่อจำกัดการเพิ่มขึ้นของหน่วยความจำ บล็อก catch ทั้งสองแยกความผิดพลาดด้านการดีพลอย เช่น รันไทม์ที่ขาดหายไป ออกจากความผิดพลาดขณะเรนเดอร์ที่ลองใหม่ได้หนึ่งครั้ง บล็อก catch ทั้งสองไม่ปล่อยว่างไว้ ให้เรียก shutdown() เมื่อเวิร์กเกอร์ปิดการทำงานเพื่อปล่อยกระบวนการ Chrome แทนที่จะรอ destructor

สร้างการกำหนดค่าจากอาร์เรย์การกำหนดค่าของเฟรมเวิร์กเพื่อใช้คีย์แบบ snake-case และตรึง chromeBinaryPath ในการใช้งานจริงเพื่อให้เลือกไบนารีได้แน่นอน

  • HTML ที่ว่างเปล่าไม่มีผลใดๆ writeHtmlChrome('') คืนค่าเอกสารโดยไม่เปลี่ยนแปลง
  • ยังไม่มีหน้า หากเอกสารไม่มีหน้า writeHtmlChrome() จะเพิ่มหน้าหนึ่งก่อนเรนเดอร์
  • แอสเซตจากระยะไกลไม่โหลด — โดยการออกแบบ <img src="https://..."> จะเรนเดอร์ออกมาเป็นพื้นที่ว่าง ฝังแอสเซตทุกรายการแบบอินไลน์เป็น data: URI นี่คือนโยบายการแยกเครือข่าย ไม่ใช่ข้อบกพร่อง
  • นำเข้าเฉพาะหน้า 0 เท่านั้น ความสูงแบบพอดีอัตโนมัติจะเพิ่มบัฟเฟอร์การ reflow เพื่อให้ได้หน้าเดียว เมื่อระบุความสูงอย่างชัดเจน จะไม่มีการเพิ่มบัฟเฟอร์และผลลัพธ์จะตรงกับขนาดกระดาษที่ร้องขอพอดี ดังนั้นควรกำหนดความสูงให้พอดีกับเนื้อหาของคุณ
  • บริดจ์ขาดหายไป หากไม่ได้ติดตั้ง nextpdf/artisan core จะยกข้อยกเว้นเกี่ยวกับเค้าโครงแทนข้อผิดพลาดร้ายแรง หากไม่มีไลบรารี chrome-php/chrome บริดจ์จะยก ChromeNotAvailableException พร้อมคำสั่งติดตั้ง
  • defaultCss และ </style> ลำดับ </style> ใดๆ ใน defaultCss จะถูกตัดออกก่อนการแทรกเพื่อป้องกันไม่ให้หลุดออกจากสไตล์ เตรียมรองรับกรณีนี้หากคุณใช้เทมเพลต CSS

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

  • การแยกเครือข่ายเป็นมาตรการควบคุมหลัก บริดจ์ไม่อนุญาตให้ดึงซับรีซอร์สขาออกใดๆเลย: CSP default-src 'none' ร่วมกับการบล็อก URL ทุกรายการที่ระดับการขนส่งของ CDP บริดจ์ไม่ได้ใช้รายการอนุญาตโดเมนเพราะไม่จำเป็นต้องมี ฝังแอสเซตแบบอินไลน์เป็น data: URI
  • มีการจำกัดอินพุตก่อนที่จะติดต่อ Chrome บริดจ์ปฏิเสธ HTML ที่เกิน maxHtmlSize (ค่าเริ่มต้น 5 MB) data URI แบบ base64 ที่ใหญ่เกินไป (ตัวป้องกัน decompression bomb) และแท็ก <meta http-equiv="refresh"> ใดๆ (ซึ่งอาจสั่งให้นำทางไปยังปลายทางภายใน) คง maxHtmlSize ไว้ที่ค่าเริ่มต้น เว้นแต่ภาระงานที่ทราบแน่ชัดจำเป็นต้องใช้มากกว่านั้น การเพิ่มค่านี้จะขยายพื้นที่เสี่ยงต่อการทำให้ทรัพยากรหมดสิ้น
  • แซนด์บ็อกซ์ของ Chrome เป็นมาตรการควบคุมแยกต่างหาก การตั้งค่า noSandbox: true จะเปิด Chrome ด้วย --no-sandbox ซึ่งนำการแยกกระบวนการของ Chrome ออก นั่นคือการลดทอนการกักกันอย่างแท้จริง ไม่ใช่แฟล็กเชิงตกแต่ง ปล่อยไว้เป็น false นอกคอนเทนเนอร์ เมื่อแซนด์บ็อกซ์ของคอนเทนเนอร์ไม่สามารถเริ่มต้นได้ ให้รัน Chrome ในฐานะผู้ใช้ที่ไม่ใช่ root ภายในคอนเทนเนอร์ที่มีข้อจำกัด และถือว่าการดีพลอยนั้นต้องมีระดับความเชื่อถือที่สูงขึ้นต่ออินพุต
  • บันทึกล็อกมีเฉพาะข้อมูลเมตาเท่านั้น ฉีด PSR-3 logger บริดจ์บันทึกความยาวไบต์ มิติ และเหตุการณ์ในวงจรชีวิต ไม่บันทึก HTML ไบต์ของ PDF หรือข้อความที่สกัดออกมาเด็ดขาด
  • อย่าเปิดเผยพอร์ตดีบักระยะไกลของ Chrome เด็ดขาด บริดจ์ไม่ได้ใช้พอร์ตนั้น และพอร์ต CDP ที่เปิดอยู่คือช่องทางควบคุมที่ไม่มีการยืนยันตัวตน

โมเดลภัยคุกคามฉบับเต็มซึ่งรวมถึงการป้องกัน SSRF ขอบเขตแซนด์บ็อกซ์ที่ชัดเจน และแคตตาล็อกโหมดความล้มเหลว อยู่ในหน้า security-and-operations ของ Artisan ที่ลิงก์ไว้ในส่วนดูเพิ่มเติม หน้านั้นระบุข้อกำหนด OWASP, CWE และ NIST ที่เกี่ยวข้อง

คู่มือนี้ไม่ได้กล่าวอ้างมาตรฐานเชิงบรรทัดฐานด้วยตนเอง หน้า security-and-operations ของ Artisan ต้นทางจับคู่มาตรการควบคุมด้านเครือข่าย การแยก และการทำให้ทรัพยากรหมดสิ้นของบริดจ์เข้ากับ OWASP ASVS, CWE Top 25 (SSRF / uncontrolled resource consumption) และ NIST SP 800-53 SC-7 หน้าคุกบุ๊กนี้ทบทวนการใช้งานและโยงการอ้างอิงเชิงบรรทัดฐานเหล่านั้นไปยังหน้าดังกล่าว บริดจ์ไม่ได้ทำการดำเนินการเข้ารหัสลับใดๆ การลงนามและการเข้ารหัสลับเป็นเรื่องของ core หรือเอดิชันเชิงพาณิชย์ และไม่ได้รับผลกระทบจาก Artisan