ผสาน PDF ภายนอกหรือเพิ่มหน้าจากเอกสารที่มีอยู่
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”เมื่อมีไฟล์ PDF หลายไฟล์อยู่บนดิสก์และต้องการรวมให้เหลือ PDF เพียงไฟล์เดียว สูตรนี้จะรวมเอกสารที่มีอยู่เข้าด้วยกันแบบครบวงจรผ่านส่วนเชื่อมต่อการผสานของ Core ซึ่งก็คือ NextPDF\Document\PdfMerger โดยส่งสตริงไบต์ PDF แบบ raw เข้าไป ตัวผสานจะกำหนดหมายเลขใหม่ให้ทุกอ็อบเจ็กต์เพื่อหลีกเลี่ยงการชนกัน สร้าง page tree หนึ่งชุดและตาราง cross-reference หนึ่งตาราง แล้วคืนค่า NextPDF\Document\MergeResult ซึ่งสามารถเขียนลงดิสก์หรือสตรีมไปยังไคลเอนต์ได้
ส่วนเชื่อมต่อเดียวกันนี้รองรับงานหลักสามอย่างที่ใช้บ่อยที่สุด:
- ผสาน (Merge) รายการ PDF ที่เรียงลำดับไว้ให้เป็นเอกสารเดียว
- ต่อท้าย (Append) ไฟล์ PDF ที่สองต่อหลัง PDF หลัก
- ต่อหน้า (Prepend) หน้าเอกสารโดยจัดเอกสารใหม่ไว้เป็นลำดับแรกในลำดับอินพุต
การผสานทำงานภายในกระบวนการโดยไม่ใช้ headless browser หรือการเรียกผ่านเครือข่าย ต้องติดตั้ง Core (composer require nextpdf/core:^3) และมีไฟล์ PDF ที่อ่านได้ตั้งแต่สองไฟล์ขึ้นไป
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”composer require nextpdf/core:^3ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”PDF จัดระเบียบหน้าเอกสารไว้ใน page tree ที่มีโหนดรากเป็น /Pages และระบุตำแหน่งของแต่ละ indirect object ผ่านตาราง cross-reference เมื่อรวมเอกสารต้นทางสองฉบับเข้าด้วยกัน หมายเลขอ็อบเจ็กต์ของทั้งสองฉบับจะทับซ้อนกัน แทบทุกครั้งทั้งสองไฟล์จะมีอ็อบเจ็กต์ 1 0 obj โหนด /Catalog และโหนด /Pages หากเพียงนำไบต์มาต่อกัน จะได้ไฟล์ที่เสียหายเพราะการอ้างอิงต่างๆ ไม่ชี้ไปยังอ็อบเจ็กต์ที่ระบุไว้อีกต่อไป
PdfMerger จัดการการทับซ้อนนี้ ตัวผสานจะดึงอ็อบเจ็กต์ของหน้าออกมาจากแต่ละอินพุต กำหนดหมายเลขใหม่ให้ทุกอ็อบเจ็กต์อยู่ใน address space เดียว เขียนการอ้างอิง /Parent ของแต่ละหน้าใหม่ให้ชี้ไปยังโหนด /Pages ที่ผสานแล้วเพียงโหนดเดียว แล้วสร้าง catalog, page tree และ trailer อย่างละหนึ่งชุด ผลลัพธ์จึงเป็นเอกสารที่สร้างโครงสร้างขึ้นใหม่ทั้งหมด ไม่ใช่ไฟล์ที่นำมาต่อกันแบบเย็บรวม
กฎการเรียงลำดับเรียบง่าย: หน้าเอกสารจะปรากฏตามลำดับเดียวกับไฟล์ต้นทางในรายการอินพุต หากต้องการต่อท้าย ให้จัดเอกสารหลักไว้เป็นลำดับแรก หากต้องการต่อหน้า ให้จัดเอกสารใหม่ไว้เป็นลำดับแรก ไม่มีเมธอด prepend แยกต่างหากเพราะลำดับอินพุตเป็นตัวควบคุมเพียงอย่างเดียวที่จำเป็น
ส่วนเชื่อมต่อ API
หัวข้อที่มีชื่อว่า “ส่วนเชื่อมต่อ API”new NextPDF\Document\PdfMerger() มีเมธอดให้ใช้สองรายการ
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResultรวมรายการสตริงไบต์ PDF แบบ raw ที่เรียงลำดับไว้ พารามิเตอร์สำหรับกำหนดขอบเขตสองตัวจะจำกัดจำนวนไฟล์และขนาดอินพุตรวม ทั้งสองตัวมีค่าเริ่มต้นที่ปลอดภัยสำหรับการใช้งานจริง ควรปรับให้รัดกุมยิ่งขึ้นตามภาระงานแต่ละประเภทappend(string $basePdf, string $appendPdf): MergeResultเป็นตัวห่อหุ้มเพื่อความสะดวกที่รวมเอกสารสองฉบับตามลำดับนั้นโดยตรง มีค่าเทียบเท่ากับmerge([$basePdf, $appendPdf])ทุกประการ
ทั้งสองเมธอดคืนค่า NextPDF\Document\MergeResult ซึ่งเป็นอ็อบเจ็กต์แบบ readonly ที่บรรจุ $pdfData (ไบต์ที่ผสานแล้ว) $totalPages $sourceCount $mergedSize และตัวช่วย isValid() ที่ตรวจว่าผลลัพธ์ขึ้นต้นด้วยส่วนหัว %PDF
อินพุตเป็นสตริงไบต์แบบ raw ไม่ใช่พาธของไฟล์ ให้อ่านไฟล์เองด้วย file_get_contents() หรือดึงไบต์มาจาก object storage วิธีนี้ทำให้ตัวผสานไม่ผูกติดกับสมมติฐานเกี่ยวกับระบบไฟล์ และช่วยให้ผสานเอกสารที่ไม่เคยแตะดิสก์ได้
หากต้องการนำเข้าหน้าเดียวจาก PDF ภายนอกในรูปของ Form XObject ที่นำกลับมาใช้ซ้ำได้ เช่น เพื่อประทับหน้าหัวจดหมายไว้ด้านหลังเนื้อหาที่สร้างขึ้น ให้ใช้สัญญา (contract) ของตัวนำเข้าข้ามแพ็กเกจ NextPDF\Contracts\ImportedFormObjectInterface ที่ตัวนำเข้าอย่าง nextpdf/artisan นำไปใช้งาน สำหรับการประกอบเอกสารทั้งฉบับและหน้าทั้งหน้า ให้ใช้ส่วนเชื่อมต่อ PdfMerger ที่ระบุไว้ในที่นี้
ตัวอย่างโค้ด — เริ่มต้นใช้งานอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นใช้งานอย่างรวดเร็ว”ตัวอย่างนี้อ่านไฟล์สองไฟล์แล้วเขียนผลลัพธ์ที่ผสานแล้วออกมา ตัวอย่างนี้เว้นการจัดการข้อผิดพลาดไว้เพื่อแสดงรูปแบบการเรียกใช้ ตัวอย่างสำหรับการใช้งานจริงด้านล่างเพิ่มมาตรการป้องกันไว้ครบถ้วน
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([ file_get_contents(__DIR__ . '/cover.pdf'), file_get_contents(__DIR__ . '/body.pdf'), file_get_contents(__DIR__ . '/appendix.pdf'),]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);ตัวอย่างโค้ด — สำหรับการใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — สำหรับการใช้งานจริง”โปรแกรมที่ทำงานได้ครบในตัวเองนี้สร้างเอกสารขนาดเล็กสองฉบับในหน่วยความจำ จึงทำงานได้โดยไม่ต้องใช้ไฟล์ภายนอก โปรแกรมจะผสานเอกสารเหล่านั้น ตรวจสอบความถูกต้องของผลลัพธ์ แล้วเขียนผลลัพธ์ออกมา โปรแกรมจะดักจับ exception สองตัวที่ส่วนเชื่อมต่อการผสานยกขึ้น แล้วส่งต่อ exception แต่ละตัวพร้อมบริบทแทนที่จะกลืนไว้ ให้แทนที่อินพุตในหน่วยความจำด้วยการอ่าน file_get_contents() ของคุณเองหรือการดึงจาก object storage และเชื่อมผลลัพธ์เข้ากับเลเยอร์การตอบสนองหรือการจัดเก็บของคุณ
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\MergeResult;use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;
/** * Build a tiny labelled PDF so the program is self-contained. * * In your own code, replace calls to this helper with reads of the external * PDFs you want to combine, for example file_get_contents($path). */function buildSample(string $label, int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true); }
return $doc->getPdfData();}
// Validate the input set before touching the merger. An empty set is a// configuration error, not an empty success./** @var list<string> $sources Raw PDF byte strings, in output order. */$sources = [ buildSample('Cover', 1), // first in the list -> first in the output (prepend position) buildSample('Body', 2), buildSample('Appendix', 1), // last in the list -> appended after the body];
if ($sources === []) { throw new RuntimeException('No source PDFs supplied to merge.');}
$merger = new PdfMerger();
try { // Bound the merge deliberately: at most 50 files, 100 MB total input. $result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { // Raised when the list is empty or an input does not begin with %PDF. throw new RuntimeException( sprintf('Merge rejected an input: %s', $e->getConstraint()), previous: $e, );} catch (WriterException $e) { // Raised when the total input size exceeds the configured byte cap. throw new RuntimeException( sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()), previous: $e, );}
if (!$result->isValid()) { throw new RuntimeException('Merged output failed its structural header check.');}
emitResult($result);
/** * Write the merged document to the cookbook side-channel, or to a default file. */function emitResult(MergeResult $result): void{ printf( "Merged %d source(s) into %d page(s), %d bytes.\n", $result->sourceCount, $result->totalPages, $result->mergedSize, );
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) { throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path)); }}เอาต์พุตมาตรฐานที่คาดไว้ (จำนวนหน้ารวมคือผลรวมของจำนวนหน้าจากแต่ละแหล่งที่มา และขนาดไบต์ขึ้นอยู่กับการ build):
Merged 3 source(s) into 4 page(s), <n> bytes.กรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- อินพุตเป็นไบต์ ไม่ใช่พาธ
merge()รับสตริง PDF แบบ raw ให้อ่านไฟล์ด้วยfile_get_contents()ก่อน การส่งสตริงที่เป็นพาธเข้าไปจะทำให้อินพุตไม่ผ่านการตรวจสอบส่วนหัว%PDFและยกข้อยกเว้นPageLayoutException - ลำดับคือลำดับของเอาต์พุต หน้าเอกสารจะปรากฏตามลำดับที่ไฟล์ต้นทางปรากฏในรายการ ไม่มีเมธอด prepend: หากต้องการต่อหน้าให้จัดเอกสารใหม่ไว้เป็นลำดับแรก หรือไว้ลำดับสุดท้ายหากต้องการต่อท้าย
- รายการว่างเป็นข้อผิดพลาด
$pdfFilesที่ว่างเปล่าจะยกข้อยกเว้นPageLayoutExceptionไม่ใช่คืนผลลัพธ์เปล่า ตรวจสอบความถูกต้องของชุดข้อมูลก่อนเรียกใช้ตัวผสาน - อินพุตทุกตัวได้รับการตรวจสอบความถูกต้องตั้งแต่ต้น แต่ละรายการต้องไม่ว่างและขึ้นต้นด้วย
%PDFอินพุตตัวแรกที่ไม่ผ่านจะยกข้อยกเว้นPageLayoutExceptionพร้อมข้อจำกัดที่ถูกละเมิด และไม่มีการผสานใดๆ เกิดขึ้น - ขอบเขตจะยกข้อยกเว้นแทนที่จะตัดทอน การเกิน
maxFilesจะยกข้อยกเว้นผ่านตัวป้องกันทรัพยากรภายใน และการเกินmaxTotalBytesจะยกข้อยกเว้นWriterExceptionตัวผสานจะไม่ทิ้งไฟล์หรือตัดไบต์ทิ้งไปอย่างเงียบๆ ดังนั้นจึงควรปรับขอบเขตทั้งสองให้เหมาะกับภาระงานของคุณ - ผลลัพธ์มีโครงสร้างขึ้นใหม่ ไม่เสถียรในระดับไบต์ เอกสารที่ผสานแล้วมี catalog, page tree และ trailer ชุดใหม่ การรันสองครั้งกับอินพุตเดียวกันจะมีโครงสร้างเหมือนกัน แต่ไม่รับประกันว่าจะเหมือนกันในระดับไบต์ ด้วยเหตุนี้สูตรนี้จึงประกาศโปรไฟล์การทำซ้ำแบบ
structuralเอาไว้ - คำอธิบายประกอบระดับหน้าและทรัพยากรที่ใช้ร่วมกัน การผสานจะประกอบอ็อบเจ็กต์ของหน้าเอกสารให้อยู่ในทรีเดียว โครงสร้างระดับเอกสารที่อยู่นอกอ็อบเจ็กต์ของหน้าในไฟล์ต้นทางจะไม่ถูกนำติดมาด้วย เมื่อต้องการนำเข้าหน้าเดียวในรูปของกราฟิกที่นำกลับมาใช้ซ้ำได้พร้อมทรัพยากรของหน้านั้น ให้ใช้เส้นทาง
ImportedFormObjectInterfaceผ่านตัวนำเข้าอย่างnextpdf/artisanเป็นต้น
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”การผสานใช้เวลาเชิงเส้นตามจำนวนหน้ารวม การแจงอ็อบเจ็กต์และการกำหนดหมายเลขใหม่เป็นงานหลัก ไม่ใช่การจัดการระเบียนภายในของตัวผสานเอง หน่วยความจำสูงสุดแปรผันตามจำนวนไบต์อินพุตรวม เพราะแหล่งที่มาทุกแหล่งถูกเก็บไว้ในหน่วยความจำในรูปของสตริงระหว่างที่ประกอบเอาต์พุต ตัวป้องกัน maxTotalBytes ช่วยจำกัดขอบเขตของจุดสูงสุดดังกล่าว สำหรับไปป์ไลน์ที่มีปริมาณงานสูง ให้ตั้งค่า maxFiles และ maxTotalBytes ให้เป็นค่าน้อยที่สุดเท่าที่ภาระงานของคุณจำเป็น เพื่อให้ชุดงานที่ผิดรูปแบบหรือมีขนาดใหญ่เกินไปล้มเหลวอย่างรวดเร็วแทนที่จะใช้หน่วยความจำจนหมด โดยทั่วไปการผสานขนาดเล็กจะอยู่ภายในงบเวลา wall 1500 ms และหน่วยความจำสูงสุด 64 MB
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”การผสานทำงานภายในกระบวนการ ไบต์ของเอกสารไม่ออกไปนอกโฮสต์ และไม่มีการเรียกผ่านเครือข่าย ให้ถือว่า PDF ภายนอกทุกไฟล์เป็นอินพุตที่ไม่น่าเชื่อถือ:
- รักษาขอบเขตให้รัดกุม
maxFilesและmaxTotalBytesเป็นแนวป้องกันด่านแรกของคุณต่ออินพุตที่ก่อ denial-of-service สำหรับส่วนเชื่อมต่อใดก็ตามที่รับการอัปโหลด ให้ตั้งค่าทั้งสองเป็นเพดานจริงของระบบ ไม่ใช่ค่าเริ่มต้นที่ผ่อนปรน - ตรวจสอบความถูกต้องก่อนจะเชื่อถือ การผสานที่สำเร็จหมายความว่าไบต์ถูกนำมารวมกันแล้ว ไม่ได้หมายความว่าอินพุตปลอดภัย ให้ส่งอินพุตที่ไม่น่าเชื่อถือผ่าน Core inspector ก่อน ดู การแจงและตรวจสอบ PDF สำหรับการสแกนคัดกรองแบบมีขอบเขตที่ตั้งค่าสถานะการเข้ารหัสลับ ลายเซ็น และเครื่องหมายความเสี่ยงก่อนการประมวลผลที่หนักกว่า
- ห้ามนำอินพุตของผู้ใช้มาแทรกในพาธ สูตรนี้เขียนไปยังพาธคงที่หรือ side-channel ของ cookbook ให้กำหนดพาธเอาต์พุตจากค่าที่เซิร์ฟเวอร์ควบคุม ไม่ใช่จากฟิลด์ในคำขอ เพื่อหลีกเลี่ยงการข้ามพาธ (path traversal)
- ห้ามมีข้อมูลลับในเอกสาร อย่าฝังข้อมูลรับรอง โทเค็น หรือตัวระบุภายในไว้ในเอกสารที่ผสานแล้วซึ่งส่งคืนไปยังไคลเอนต์
ความสอดคล้อง
หัวข้อที่มีชื่อว่า “ความสอดคล้อง”สูตรนี้ไม่ได้กล่าวอ้างความสอดคล้องเชิงบรรทัดฐานกับมาตรฐานใดด้วยตัวเอง สูตรนี้ประกอบเอกสารที่มีอยู่ผ่านส่วนเชื่อมต่อการผสานของ Core และตรวจสอบความถูกต้องของผลลัพธ์ด้วยการตรวจสอบส่วนหัว MergeResult::isValid() แบบจำลอง page tree ที่ PdfMerger สร้างขึ้นใหม่คือโครงสร้าง page tree ของ PDF 2.0 ที่อธิบายไว้ในเอกสารอ้างอิง /modules/core/document/ สำหรับการอ่านโครงสร้างของเอกสารอินพุตหรือเอาต์พุตใดๆ รวมถึงเวอร์ชัน จำนวนหน้า การเข้ารหัสลับ และแฟล็กลายเซ็น ให้ใช้ Core inspector ที่ระบุไว้ใน การแจงและตรวจสอบ PDF เพื่อจุดประสงค์ดังกล่าว
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- เอกสารอ้างอิงโมดูล Document — ส่วนเชื่อมต่อการแยก การผสาน และส่วนประกอบของเอกสารอย่างครบถ้วน
- การแจงและตรวจสอบ PDF — คัดกรองอินพุตที่ไม่น่าเชื่อถือก่อนนำมาผสาน
- การจัดการข้อผิดพลาดที่ตระหนักถึง exception — ลำดับชั้น exception ของ NextPDF ที่รองรับ
PageLayoutExceptionและWriterException - สร้างเอกสารหลายหน้า — เขียนหน้าเอกสารที่จะนำมารวมกันในภายหลัง