แยก PDF และดึงช่วงหน้า
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”คุณมี PDF หนึ่งไฟล์ และต้องการหลายไฟล์ สูตรนี้แกะสลักเอกสารเดียวออกเป็นหลายไฟล์
ด้วยพื้นผิวการแยกของ Core คือ NextPDF\Document\PdfSplitter คุณส่งต้นฉบับเป็น
สตริงไบต์ PDF ดิบและอธิบายว่าต้องการหน้าใด ตัวแยกแยกวิเคราะห์ต้นฉบับผ่านกราฟวัตถุ
คัดลอกวัตถุที่เข้าถึงได้ของแต่ละช่วงที่ร้องขอลงในเอกสารใหม่ที่ตั้งหมายเลขใหม่ พร้อม
page tree และตาราง cross-reference ของตัวเอง แล้วส่งคืน PDF ที่มีโครงสร้างสมบูรณ์
ซึ่งโหลดได้ในโปรแกรมอ่านที่สอดคล้องตามมาตรฐาน
นี่คือกระบวนการตรงข้ามของ สูตรรวมเอกสาร: การรวมประกอบเอกสาร หลายไฟล์เป็นไฟล์เดียว ส่วนการแยกแตกเอกสารหนึ่งไฟล์ออกเป็นหลายไฟล์ พื้นผิวเดียวกัน ครอบคลุมสามงานที่คุณต้องการบ่อยที่สุด
- แยกตามช่วง — สร้างเอกสารเอาต์พุตหนึ่งไฟล์ต่อหนึ่งช่วงหน้าที่คุณระบุ
- แยกทุก N หน้า — ตัดไฟล์ยาวออกเป็นเซกเมนต์ขนาดคงที่
- ดึงช่วง — ดึงช่วงหน้าที่ต่อเนื่องช่วงเดียวออกมาเป็นเอกสารเดียว
การแยกทำงานภายในกระบวนการ โดยไม่มีเบราว์เซอร์แบบ headless หรือการเรียกผ่านเครือข่าย
คุณต้องติดตั้ง Core (composer require nextpdf/core:^3) และมี PDF ที่อ่านได้
หนึ่งไฟล์
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”composer require nextpdf/core:^3ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”PDF ระบุตำแหน่งหน้าของมันผ่าน page tree ที่มีรากเป็นโหนด /Pages และเข้าถึงทุก
indirect object ผ่านข้อมูล cross-reference (ตารางหรือสตรีม) คุณไม่สามารถดึงหน้า
โดยการตัดไบต์ได้ เพราะหน้าเดียวอ้างอิงฟอนต์ รูปภาพ และ resource dictionary ที่ใช้
ร่วมกันซึ่งอยู่ที่อื่นในไฟล์ และ offset ของ cross-reference จะไม่ถูกต้องอีกต่อไป
PdfSplitter ทำงานจริง สำหรับแต่ละช่วงมันเดินกราฟวัตถุจากวัตถุหน้าที่ร้องขอ
รวบรวม closure ของวัตถุที่เข้าถึงได้ ตั้งหมายเลขวัตถุเหล่านั้นใหม่ลงในพื้นที่
แอดเดรสใหม่ สร้างเอกสารที่มี page tree เดียวขึ้นใหม่ และส่งออกตาราง cross-reference
จริงตามโครงสร้าง PDF 2.0
(ISO 32000-2:2020, ตาราง cross-reference §7.5.4, page tree §7.7.3) เอาต์พุต
แต่ละไฟล์เป็นเอกสารที่สมบูรณ์ในตัว ไม่ใช่ชิ้นส่วน
หมายเลขหน้านับจาก 1 และนับรวมปลายทั้งสอง ช่วงคือ value object
NextPDF\Document\PageRange: new PageRange(2, 5) หมายถึงหน้า 2 ถึง 5
constructor ตรวจสอบ invariant ของตัวเอง — มันปฏิเสธค่าเริ่มต้นที่ต่ำกว่า 1 หรือ
ค่าสิ้นสุดที่อยู่ก่อนค่าเริ่มต้น โดยการ raise
NextPDF\Exception\PageLayoutException — ดังนั้นช่วงที่เป็นไปไม่ได้จะล้มเหลวที่
การสร้าง ไม่ใช่ลึกเข้าไปในตัวแยก PageRange::parse() และ PageRange::all()
raise PageLayoutException เดียวกันเมื่อข้อกำหนดผิดรูปแบบหรือจำนวนหน้ารวมไม่เป็น
จำนวนบวก
พื้นผิว API
หัวข้อที่มีชื่อว่า “พื้นผิว API”new NextPDF\Document\PdfSplitter() เปิดเผยสามเมธอด ทุกเมธอดรับต้นฉบับเป็นสตริง
ไบต์ PDF ดิบ ไม่ใช่พาธ
split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResultสร้างเอกสารเอาต์พุตหนึ่งไฟล์ต่อPageRangeใน$rangesตามลำดับ พารามิเตอร์ ขอบเขตทั้งสองจำกัดขนาดอินพุตและจำนวนช่วงsplitEvery(string $pdfData, int $pagesPerSegment): SplitResultตัดเอกสารเป็น เซกเมนต์ขนาดคงที่ละ$pagesPerSegmentหน้า เซกเมนต์สุดท้ายเก็บส่วนที่เหลือextractPages(string $pdfData, PageRange $range): SplitDocumentดึงช่วงเดียว และคืนเอกสารนั้นเพียงไฟล์เดียวโดยตรง
split() และ splitEvery() คืน NextPDF\Document\SplitResult ซึ่งเป็นวัตถุ
readonly ที่นำพา $documents (รายการเซกเมนต์), $totalPages
(หน้าในต้นฉบับ) และ $sourceSize มันมี count(), document(int $index)
เพื่อดึงเซกเมนต์ด้วยดัชนีฐานศูนย์ และ totalOutputSize()
แต่ละเซกเมนต์และค่าที่คืนจาก extractPages() เป็น
NextPDF\Document\SplitDocument: วัตถุ readonly ที่เปิดเผย $pdfData
(ไบต์ของเซกเมนต์), $range, $pageCount, $sizeBytes และตัวช่วย isValid()
isValid() เป็นการตรวจสอบความสมเหตุสมผลของส่วนหัว %PDF แบบแคบ — มันคืน true
เมื่อไบต์ของเซกเมนต์เริ่มต้นด้วย %PDF — ไม่ใช่การตรวจสอบโครงสร้างเอกสารหรือ
ความสอดคล้อง มันยืนยันว่าตัวแยกสร้าง PDF ออกมา ไม่ใช่ว่าไฟล์สอดคล้องตามมาตรฐาน
อย่างสมบูรณ์
คุณสร้าง PageRange โดยตรงด้วย new PageRange($start, $end) หรือแยกวิเคราะห์
ข้อกำหนดที่มนุษย์อ่านได้ด้วย PageRange::parse('1-3,5,7-10') ซึ่งคืน
list<PageRange> ที่พร้อมส่งให้ split() PageRange::all($totalPages) คืนช่วง
เดียวที่ครอบคลุมทั้งเอกสาร
ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว”ตัวอย่างนี้อ่านไฟล์หนึ่งไฟล์และแยกเป็นสองเอกสาร: หน้า 1 ถึง 3 และหน้า 4 ถึง 6 มันละการจัดการข้อผิดพลาดออกเพื่อแสดงรูปแบบการเรียก ตัวอย่างสำหรับการใช้งานจริง ด้านล่างเพิ่มการป้องกันแบบเต็ม
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->split( file_get_contents(__DIR__ . '/report.pdf'), [ new PageRange(1, 3), new PageRange(4, 6), ],);
foreach ($result->documents as $i => $segment) { file_put_contents(__DIR__ . sprintf('/part-%d.pdf', $i + 1), $segment->pdfData);}
printf("Split %d-page source into %d document(s).\n", $result->totalPages, $result->count());ตัวอย่างโค้ด — สำหรับการใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — สำหรับการใช้งานจริง”โปรแกรมที่สมบูรณ์ในตัวนี้สร้างเอกสารหลายหน้าขนาดเล็กไฟล์หนึ่งในหน่วยความจำ จึงรัน
ได้โดยไม่ต้องมีไฟล์ภายนอก มันสาธิตการดำเนินการทั้งสามแบบ — แยกตามช่วง แยกทุก N
หน้า และดึงช่วงเดียว มันตรวจสอบและเขียนเซกเมนต์ตามช่วงและส่วนท้ายที่ดึงออกมา และ
รายงานผลลัพธ์ตามขนาดเป็นจำนวนนับ เพื่อให้คุณเห็นรูปแบบการเรียกแต่ละแบบโดยไม่มี
ลูปเขียนที่เกือบเหมือนกันสามลูป มันจับข้อยกเว้นที่พื้นผิวการแยก raise และ rethrow
แต่ละตัวพร้อมบริบทแทนที่จะกลืนมันไว้ แทนที่ต้นฉบับในหน่วยความจำด้วยการอ่านด้วย
file_get_contents() ของคุณเองหรือการดึงจาก object storage
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use InvalidArgumentException;use NextPDF\Core\Document;use NextPDF\Document\Merge\UnsupportedSourceDocumentException;use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;use NextPDF\Document\SplitDocument;use NextPDF\Exception\PageLayoutException;
/** * Build a tiny labelled multi-page PDF so the program is self-contained. * * In your own code, replace this with a read of the PDF you want to split, * for example file_get_contents($path). */function buildSample(int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle('Split sample');
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('Source page %d', $page), newLine: true); }
return $doc->getPdfData();}
$source = buildSample(7);
$splitter = new PdfSplitter();
try { // 1. Split into named ranges: one output per PageRange, in order. $byRange = $splitter->split( $source, PageRange::parse('1-3,4-6'), maxBytes: 50_000_000, maxRanges: 100, );
// 2. Split every 2 pages: segments of [1-2], [3-4], [5-6], [7] (remainder). $bySize = $splitter->splitEvery($source, 2);
// 3. Extract a single range as one document. $tail = $splitter->extractPages($source, new PageRange(7, 7));} catch (InvalidArgumentException $e) { // Raised on an oversized input, an empty range list, or too many ranges. throw new RuntimeException('Split rejected its input: ' . $e->getMessage(), previous: $e);} catch (PageLayoutException $e) { // Raised when a range exceeds the source page count, and also by the // PageRange constructor / PageRange::parse() on an invalid or malformed range. throw new RuntimeException( sprintf('Range out of bounds (page %d): %s', $e->getPageNumber(), $e->getConstraint()), previous: $e, );} catch (UnsupportedSourceDocumentException $e) { // Raised fail-closed on an encrypted, signed, or form-bearing source. throw new RuntimeException('Source cannot be split: ' . $e->getMessage(), previous: $e);}
printf( "Source has %d page(s). By-range produced %d doc(s); by-size produced %d doc(s).\n", $byRange->totalPages, $byRange->count(), $bySize->count(),);
foreach ($byRange->documents as $i => $segment) { emitSegment(sprintf('range-%d', $i + 1), $segment);}
emitSegment('tail', $tail);
/** * Validate a segment and write it to the cookbook side-channel directory, * or to the script directory by default. */function emitSegment(string $name, SplitDocument $segment): void{ if (!$segment->isValid()) { throw new RuntimeException(sprintf('Segment "%s" failed its %%PDF header check.', $name)); }
$dir = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $dir = $dir !== false && $dir !== '' ? $dir : __DIR__; $path = sprintf('%s/%s.pdf', rtrim($dir, '/'), $name);
if (file_put_contents($path, $segment->pdfData) === false) { throw new RuntimeException(sprintf('Could not write segment to "%s".', $path)); }
printf("Wrote %s: pages %d-%d, %d bytes.\n", $name, $segment->range->start, $segment->range->end, $segment->sizeBytes);}เอาต์พุตมาตรฐานที่คาดหวัง (ขนาดไบต์ขึ้นอยู่กับ build)
Source has 7 page(s). By-range produced 2 doc(s); by-size produced 4 doc(s).Wrote range-1: pages 1-3, <n> bytes.Wrote range-2: pages 4-6, <n> bytes.Wrote tail: pages 7-7, <n> bytes.กรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- ต้นฉบับเป็นไบต์ ไม่ใช่พาธ ทุกเมธอดรับสตริง PDF ดิบ อ่านไฟล์ด้วย
file_get_contents()ก่อน หรือดึงไบต์จาก object storage การส่งพาธทำให้ต้นฉบับ แยกวิเคราะห์ล้มเหลว - หมายเลขหน้านับจาก 1 และนับรวมปลายทั้งสอง
new PageRange(1, 3)ครอบคลุม หน้า 1, 2 และ 3 — สามหน้า ค่าเริ่มต้นที่ต่ำกว่า 1 หรือค่าสิ้นสุดที่อยู่ก่อน ค่าเริ่มต้น raisePageLayoutExceptionจาก constructor ของPageRangeเอง - ช่วงที่เกินปลายเป็นข้อผิดพลาด ไม่ใช่การตัดให้พอดี หากค่าสิ้นสุดของช่วงเกิน
จำนวนหน้าของต้นฉบับ
split()จะ raisePageLayoutExceptionมันไม่เคยตัดช่วง เงียบๆ ให้เหลือถึงหน้าสุดท้าย ตรวจสอบจำนวนหน้าก่อนหากช่วงของคุณมาจากผู้เรียก splitEvery()เก็บส่วนที่เหลือ เซกเมนต์สุดท้ายเก็บหน้าที่เหลืออยู่ ดังนั้น เอกสาร 7 หน้าที่แยกทุก 2 หน้าให้สี่เซกเมนต์: สามเซกเมนต์ละ 2 หน้าและหนึ่ง เซกเมนต์ 1 หน้า$pagesPerSegmentต้องอย่างน้อย 1 มิฉะนั้นคุณจะได้InvalidArgumentException- รายการช่วงที่ว่างเปล่าถูกปฏิเสธ
split()ที่มี$ranges === []raiseInvalidArgumentExceptionสร้างอย่างน้อยหนึ่งช่วงก่อนที่คุณจะเรียกมัน - ขอบเขต raise แทนการตัดทอน การเกิน
maxBytesหรือmaxRangesraiseInvalidArgumentExceptionตัวแยกไม่เคยประมวลผลอินพุตที่เกินขนาดบางส่วน ดังนั้น ปรับทั้งสองขอบเขตให้เข้ากับภาระงานของคุณ - ต้นฉบับที่เข้ารหัส ลงนาม และมีฟอร์มจะล้มเหลวแบบปิด ต้นฉบับที่เข้ารหัส
(คัดลอกไม่ได้หากไม่มีคีย์) ต้นฉบับที่ลงนามดิจิทัล (การจัดหน้าใหม่จะทำให้ช่วงไบต์
ของลายเซ็นไม่ถูกต้อง) หรือต้นฉบับที่มีฟอร์มแบบโต้ตอบ (widget ของฟิลด์อาจอยู่บน
หน้าที่ถูกตัดทิ้งและกลายเป็นหน้ากำพร้า) raise
UnsupportedSourceDocumentExceptionตัวแยกปฏิเสธแทนที่จะส่งออกเอกสารที่เสียหาย หรือถูกบุกรุก การแยกเอกสารฟอร์มเป็นข้อจำกัดที่ทราบของรุ่นนี้ UnsupportedSourceDocumentExceptionอยู่ภายใต้เนมสเปซMergeชื่อแบบ fully-qualified ของมันคือNextPDF\Document\Merge\UnsupportedSourceDocumentExceptionพาธMergeบนหน้าการแยกนี้ไม่ใช่ข้อผิดพลาดจากการ copy/paste มันคือข้อยกเว้นการปฏิเสธ ต้นฉบับที่ใช้ร่วมกันเพียงตัวเดียวซึ่งทั้งพื้นผิวการรวมและการแยก raise เมื่อ ต้นฉบับไม่สามารถคัดลอกได้อย่างปลอดภัย import มันจากเนมสเปซนั้น- เอาต์พุตสดใหม่เชิงโครงสร้าง ไม่เสถียรในระดับไบต์ แต่ละเซกเมนต์เป็นเอกสารใหม่
พร้อม catalog, page tree และ trailer ของตัวเอง การรันสองครั้งบนอินพุตเดียวกัน
เท่ากันเชิงโครงสร้าง แต่ไม่รับประกันว่าจะเหมือนกันในระดับไบต์ — จึงเป็น
reproducibility profile แบบ
structural
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”การแยกเป็นเชิงเส้นตามจำนวนหน้าที่คัดลอกข้ามทุกช่วง การแยกวิเคราะห์ต้นฉบับและการ
คัดลอก closure ของวัตถุในแต่ละช่วง ไม่ใช่งานบันทึกข้อมูลของตัวแยกเอง คือสิ่งที่ครอง
งานส่วนใหญ่ ต้นฉบับถูกเก็บในหน่วยความจำเป็นสตริง และไบต์ของแต่ละเซกเมนต์ถูกเก็บ
ไว้จนกว่าคุณจะเขียนมัน ดังนั้นหน่วยความจำสูงสุดติดตามขนาดต้นฉบับบวกกับช่วงที่ใหญ่
ที่สุดที่คุณสร้าง ตัวป้องกัน maxBytes ทำให้ด้านต้นฉบับของจุดสูงสุดนั้นมีขอบเขต
สำหรับไปป์ไลน์ปริมาณสูง ให้ตั้ง maxBytes และ maxRanges เป็นค่าที่เล็กที่สุด
เท่าที่ภาระงานของคุณต้องการ เพื่อให้อินพุตที่ผิดรูปแบบหรือเกินขนาดล้มเหลวเร็ว
แทนที่จะใช้หน่วยความจำจนหมด
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”การแยกทำงานภายในกระบวนการ ไม่มีไบต์เอกสารใดออกจากโฮสต์ และไม่มีการเรียกผ่าน เครือข่าย จัดการกับ PDF ต้นฉบับทุกไฟล์เป็นอินพุตที่ไม่น่าเชื่อถือ
- รักษาขอบเขตให้รัดกุม
maxBytesและmaxRangesเป็นแนวป้องกันแรกของคุณ ต่ออินพุตที่เป็น denial-of-service สำหรับพื้นผิวใดก็ตามที่รับการอัปโหลด ให้ตั้ง เป็นเพดานจริงของคุณ ไม่ใช่ค่าเริ่มต้นที่เผื่อไว้มาก - คัดแยกก่อนที่คุณจะแยก ต้นฉบับที่เข้ารหัสหรือลงนามจะล้มเหลวแบบปิด แต่คุณ ตรวจพบเงื่อนไขเหล่านั้นได้เร็วกว่า รันอินพุตที่ไม่น่าเชื่อถือผ่านตัวตรวจสอบของ Core ก่อน ดู แยกวิเคราะห์และตรวจสอบ PDF สำหรับการสแกนแบบมีขอบเขตที่ตั้งธงการเข้ารหัส ลายเซ็น และเครื่องหมายความเสี่ยง ก่อนการประมวลผลที่หนักกว่า
- อย่าแทรกอินพุตของผู้ใช้ลงในพาธ สูตรนี้เขียนไปยังไดเรกทอรีที่กำหนดไว้ตายตัว หรือ side-channel ของ cookbook สร้างพาธเอาต์พุตและชื่อเซกเมนต์จากค่าที่เซิร์ฟเวอร์ ควบคุม ไม่ใช่จากฟิลด์ของคำขอ เพื่อหลีกเลี่ยง path traversal
- ไม่มีความลับในเอาต์พุต อย่าเขียนไฟล์เซกเมนต์ไปยังตำแหน่งหรือด้วยชื่อที่ เปิดเผยตัวระบุภายในแก่ไคลเอนต์ที่ไม่ควรเห็น
ความสอดคล้องตามมาตรฐาน
หัวข้อที่มีชื่อว่า “ความสอดคล้องตามมาตรฐาน”สูตรนี้ไม่อ้างเรื่องมาตรฐานเชิงบรรทัดฐานใดๆ ด้วยตัวเอง มันแตกเอกสารหนึ่งไฟล์ผ่าน
พื้นผิวการแยกของ Core และตรวจสอบความสมเหตุสมผลของแต่ละเซกเมนต์ด้วยการตรวจสอบส่วนหัว
%PDF ของ SplitDocument::isValid() — การตรวจสอบการมีอยู่ว่าตัวแยกส่งออก PDF
ไม่ใช่การตรวจสอบความสอดคล้องหรือโครงสร้างเอกสาร โครงสร้าง page tree และ
cross-reference ที่ PdfSplitter สร้างขึ้นใหม่สำหรับแต่ละเซกเมนต์คือโครงสร้าง
PDF 2.0 ที่อธิบายไว้ในเอกสารอ้างอิง /modules/core/document/
(ISO 32000-2:2020, ตาราง cross-reference §7.5.4, page tree §7.7.3) สำหรับการอ่าน
เชิงโครงสร้างของเอกสารอินพุตหรือเอาต์พุตใดก็ตาม รวมถึงเวอร์ชัน จำนวนหน้า การเข้ารหัส
และธงลายเซ็น ให้ใช้ตัวตรวจสอบของ Core ที่บันทึกไว้ใน
แยกวิเคราะห์และตรวจสอบ PDF
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- เอกสารอ้างอิงโมดูล Document — พื้นผิวการแยก การรวม และส่วนของเอกสารแบบเต็ม
- รวม PDF ภายนอก — สูตรตรงข้าม: ประกอบเอกสารหลายไฟล์เป็นไฟล์เดียว
- แยกวิเคราะห์และตรวจสอบ PDF — คัดแยกอินพุตที่ไม่น่าเชื่อถือก่อนที่คุณจะแยกมัน
- การจัดการข้อผิดพลาดที่ตระหนักถึงข้อยกเว้น
— ลำดับชั้นข้อยกเว้นของ NextPDF เบื้องหลัง
PageLayoutExceptionและUnsupportedSourceDocumentException