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

แยก PDF และดึงช่วงหน้า

คุณมี PDF หนึ่งไฟล์ และต้องการหลายไฟล์ สูตรนี้แกะสลักเอกสารเดียวออกเป็นหลายไฟล์ ด้วยพื้นผิวการแยกของ Core คือ NextPDF\Document\PdfSplitter คุณส่งต้นฉบับเป็น สตริงไบต์ PDF ดิบและอธิบายว่าต้องการหน้าใด ตัวแยกแยกวิเคราะห์ต้นฉบับผ่านกราฟวัตถุ คัดลอกวัตถุที่เข้าถึงได้ของแต่ละช่วงที่ร้องขอลงในเอกสารใหม่ที่ตั้งหมายเลขใหม่ พร้อม page tree และตาราง cross-reference ของตัวเอง แล้วส่งคืน PDF ที่มีโครงสร้างสมบูรณ์ ซึ่งโหลดได้ในโปรแกรมอ่านที่สอดคล้องตามมาตรฐาน

นี่คือกระบวนการตรงข้ามของ สูตรรวมเอกสาร: การรวมประกอบเอกสาร หลายไฟล์เป็นไฟล์เดียว ส่วนการแยกแตกเอกสารหนึ่งไฟล์ออกเป็นหลายไฟล์ พื้นผิวเดียวกัน ครอบคลุมสามงานที่คุณต้องการบ่อยที่สุด

  • แยกตามช่วง — สร้างเอกสารเอาต์พุตหนึ่งไฟล์ต่อหนึ่งช่วงหน้าที่คุณระบุ
  • แยกทุก N หน้า — ตัดไฟล์ยาวออกเป็นเซกเมนต์ขนาดคงที่
  • ดึงช่วง — ดึงช่วงหน้าที่ต่อเนื่องช่วงเดียวออกมาเป็นเอกสารเดียว

การแยกทำงานภายในกระบวนการ โดยไม่มีเบราว์เซอร์แบบ headless หรือการเรียกผ่านเครือข่าย คุณต้องติดตั้ง Core (composer require nextpdf/core:^3) และมี PDF ที่อ่านได้ หนึ่งไฟล์

Terminal window
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 เดียวกันเมื่อข้อกำหนดผิดรูปแบบหรือจำนวนหน้ารวมไม่เป็น จำนวนบวก

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 หรือค่าสิ้นสุดที่อยู่ก่อน ค่าเริ่มต้น raise PageLayoutException จาก constructor ของ PageRange เอง
  • ช่วงที่เกินปลายเป็นข้อผิดพลาด ไม่ใช่การตัดให้พอดี หากค่าสิ้นสุดของช่วงเกิน จำนวนหน้าของต้นฉบับ split() จะ raise PageLayoutException มันไม่เคยตัดช่วง เงียบๆ ให้เหลือถึงหน้าสุดท้าย ตรวจสอบจำนวนหน้าก่อนหากช่วงของคุณมาจากผู้เรียก
  • splitEvery() เก็บส่วนที่เหลือ เซกเมนต์สุดท้ายเก็บหน้าที่เหลืออยู่ ดังนั้น เอกสาร 7 หน้าที่แยกทุก 2 หน้าให้สี่เซกเมนต์: สามเซกเมนต์ละ 2 หน้าและหนึ่ง เซกเมนต์ 1 หน้า $pagesPerSegment ต้องอย่างน้อย 1 มิฉะนั้นคุณจะได้ InvalidArgumentException
  • รายการช่วงที่ว่างเปล่าถูกปฏิเสธ split() ที่มี $ranges === [] raise InvalidArgumentException สร้างอย่างน้อยหนึ่งช่วงก่อนที่คุณจะเรียกมัน
  • ขอบเขต raise แทนการตัดทอน การเกิน maxBytes หรือ maxRanges raise InvalidArgumentException ตัวแยกไม่เคยประมวลผลอินพุตที่เกินขนาดบางส่วน ดังนั้น ปรับทั้งสองขอบเขตให้เข้ากับภาระงานของคุณ
  • ต้นฉบับที่เข้ารหัส ลงนาม และมีฟอร์มจะล้มเหลวแบบปิด ต้นฉบับที่เข้ารหัส (คัดลอกไม่ได้หากไม่มีคีย์) ต้นฉบับที่ลงนามดิจิทัล (การจัดหน้าใหม่จะทำให้ช่วงไบต์ ของลายเซ็นไม่ถูกต้อง) หรือต้นฉบับที่มีฟอร์มแบบโต้ตอบ (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