Bỏ qua để đến nội dung
getnextpdf.com

Tách một PDF và trích xuất các dải trang

Bạn có một PDF, và bạn cần nhiều PDF. Công thức này khắc một tài liệu duy nhất thành nhiều tệp bằng bề mặt tách của Core, NextPDF\Document\PdfSplitter. Bạn truyền nguồn dưới dạng một chuỗi byte PDF thô và mô tả những trang bạn muốn. Bộ tách phân tích nguồn qua đồ thị đối tượng, sao chép các đối tượng có thể tới được của mỗi dải được yêu cầu vào một tài liệu mới, được đánh số lại với cây trang và bảng tham chiếu chéo riêng, rồi trả về các PDF hoàn chỉnh về cấu trúc, tải được trong một trình đọc tuân thủ.

Đây là phép nghịch đảo của công thức gộp: gộp kết hợp nhiều tài liệu thành một, tách phân rã một tài liệu thành nhiều. Cùng một bề mặt bao quát ba tác vụ bạn cần nhất:

  • Tách theo dải — tạo một tài liệu đầu ra cho mỗi dải trang bạn nêu tên.
  • Tách mỗi N trang — chặt một tệp dài thành các đoạn có kích thước cố định.
  • Trích xuất một dải — kéo một dải trang liên tục duy nhất vào một tài liệu.

Quá trình tách chạy trong tiến trình, không cần trình duyệt headless hay một lệnh gọi mạng. Bạn cần Core đã được cài đặt (composer require nextpdf/core:^3) và một PDF đọc được.

Terminal window
composer require nextpdf/core:^3

Một PDF định vị các trang của nó qua một cây trang gốc tại nút /Pages, và nó tới được mọi đối tượng gián tiếp qua dữ liệu tham chiếu chéo của nó (một bảng hoặc một luồng). Bạn không thể trích xuất các trang bằng cách cắt byte: một trang duy nhất tham chiếu các phông chữ, hình ảnh và từ điển tài nguyên dùng chung nằm ở nơi khác trong tệp, và các offset tham chiếu chéo sẽ không còn hợp lệ.

PdfSplitter làm phần việc thực sự. Với mỗi dải, nó đi qua đồ thị đối tượng từ các đối tượng trang được yêu cầu, thu thập bao đóng đối tượng có thể tới được, đánh số lại các đối tượng đó vào một không gian địa chỉ mới, xây dựng lại một tài liệu có một cây trang, và phát ra một bảng tham chiếu chéo thật theo cấu trúc PDF 2.0 (ISO 32000-2:2020, bảng tham chiếu chéo §7.5.4, cây trang §7.7.3). Mỗi đầu ra là một tài liệu tự chứa, không phải một mảnh.

Số trang bắt đầu từ 1 và bao gồm cả hai đầu. Một dải là một đối tượng giá trị NextPDF\Document\PageRange: new PageRange(2, 5) nghĩa là các trang 2 đến 5. Hàm khởi tạo xác thực bất biến của chính nó — nó từ chối một điểm bắt đầu dưới 1 hoặc một điểm kết thúc trước điểm bắt đầu bằng cách ném NextPDF\Exception\PageLayoutException — nên một dải bất khả thi thất bại tại lúc khởi tạo, không phải sâu bên trong bộ tách. PageRange::parse()PageRange::all() ném cùng PageLayoutException đó trên một đặc tả bị hỏng định dạng hoặc một tổng số trang không dương.

new NextPDF\Document\PdfSplitter() cung cấp ba phương thức. Tất cả nhận nguồn dưới dạng một chuỗi byte PDF thô, không bao giờ là một đường dẫn.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult tạo một tài liệu đầu ra cho mỗi PageRange trong $ranges, theo thứ tự. Hai tham số giới hạn chặn kích thước đầu vào và số lượng dải.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult chặt tài liệu thành các đoạn có kích thước cố định gồm $pagesPerSegment trang mỗi đoạn; đoạn cuối giữ phần dư.
  • extractPages(string $pdfData, PageRange $range): SplitDocument trích xuất một dải duy nhất và trả về trực tiếp tài liệu đó.

split()splitEvery() trả về một NextPDF\Document\SplitResult, một đối tượng readonly mang theo $documents (một danh sách các đoạn), $totalPages (số trang trong nguồn), và $sourceSize. Nó cung cấp count(), document(int $index) để lấy một đoạn theo chỉ số bắt đầu từ 0, và totalOutputSize().

Mỗi đoạn, và giá trị trả về của extractPages(), là một NextPDF\Document\SplitDocument: một đối tượng readonly phơi bày $pdfData (các byte của đoạn), $range, $pageCount, $sizeBytes, và trợ thủ isValid(). isValid() là một kiểm tra hợp lý hẹp về header %PDF — nó trả về true khi các byte của đoạn bắt đầu bằng %PDF — không phải một xác thực về cấu trúc tài liệu hay về tuân thủ; nó xác nhận bộ tách đã tạo ra một PDF, không phải rằng tệp hoàn toàn tuân thủ.

Bạn dựng một PageRange trực tiếp bằng new PageRange($start, $end), hoặc phân tích một đặc tả mà con người đọc được bằng PageRange::parse('1-3,5,7-10'), trả về một list<PageRange> sẵn sàng để truyền cho split(). PageRange::all($totalPages) trả về một dải duy nhất bao toàn bộ tài liệu.

Mẫu này đọc một tệp và tách nó thành hai tài liệu: các trang 1 đến 3, và các trang 4 đến 6. Nó bỏ qua việc xử lý lỗi để cho thấy hình dạng lệnh gọi; mẫu môi trường thực tế bên dưới thêm các bảo vệ đầy đủ.

<?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());

Chương trình tự chứa này dựng một tài liệu nhỏ nhiều trang trong bộ nhớ, nên nó chạy mà không cần một tệp bên ngoài. Nó minh họa cả ba thao tác — tách theo dải, tách mỗi N trang, và trích xuất một dải duy nhất. Nó xác thực và ghi các đoạn theo dải cùng phần đuôi đã trích xuất, và báo cáo kết quả theo kích thước dưới dạng một số đếm, nên bạn thấy mỗi hình dạng lệnh gọi mà không cần ba vòng lặp ghi gần như giống hệt nhau. Nó bắt các ngoại lệ mà bề mặt tách ném ra và ném lại từng cái kèm ngữ cảnh thay vì nuốt nó. Hãy thay nguồn trong bộ nhớ bằng một lệnh đọc file_get_contents() của riêng bạn hoặc một lệnh lấy từ lưu trữ đối tượng.

<?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);
}

Đầu ra chuẩn dự kiến (kích thước byte phụ thuộc vào bản dựng):

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.
  • Nguồn là byte, không phải đường dẫn. Mọi phương thức nhận một chuỗi PDF thô. Hãy đọc tệp bằng file_get_contents() trước, hoặc kéo các byte từ lưu trữ đối tượng. Truyền một đường dẫn khiến nguồn không phân tích được.
  • Số trang bắt đầu từ 1 và bao gồm cả hai đầu. new PageRange(1, 3) bao các trang 1, 2, và 3 — ba trang. Một điểm bắt đầu dưới 1 hoặc một điểm kết thúc trước điểm bắt đầu ném PageLayoutException từ chính hàm khởi tạo PageRange.
  • Một dải vượt quá điểm cuối là một lỗi, không phải một sự kẹp. Nếu điểm kết thúc của một dải vượt quá số trang của nguồn, split() ném PageLayoutException; nó không bao giờ âm thầm cắt dải về trang cuối. Hãy kiểm tra số trang trước nếu các dải của bạn do người gọi cung cấp.
  • splitEvery() giữ phần dư. Đoạn cuối giữ bất kỳ trang nào còn lại, nên một tài liệu 7 trang tách mỗi 2 trang cho ra bốn đoạn: ba đoạn 2 trang và một đoạn 1 trang. $pagesPerSegment phải ít nhất là 1, nếu không bạn nhận một InvalidArgumentException.
  • Một danh sách dải rỗng bị từ chối. split() với $ranges === [] ném InvalidArgumentException. Hãy dựng ít nhất một dải trước khi bạn gọi nó.
  • Các giới hạn ném thay vì cắt bớt. Vượt quá maxBytes hoặc maxRanges ném InvalidArgumentException. Bộ tách không bao giờ xử lý một phần đầu vào quá khổ, nên hãy điều chỉnh cả hai giới hạn cho khối lượng công việc của bạn.
  • Các nguồn đã mã hóa, đã ký, và mang biểu mẫu thất bại đóng. Một nguồn đã mã hóa (nó không thể được sao chép mà không có khóa), một nguồn đã ký số (việc đánh trang lại sẽ làm vô hiệu dải byte của chữ ký), hoặc một nguồn mang một biểu mẫu tương tác (các widget của một trường có thể nằm trên các trang bị bỏ và mồ côi) ném UnsupportedSourceDocumentException. Bộ tách từ chối thay vì phát ra một tài liệu hỏng hoặc bị tổn hại. Tách một tài liệu biểu mẫu là một giới hạn đã biết của bản phát hành này.
  • UnsupportedSourceDocumentException nằm dưới namespace Merge. Tên đầy đủ của nó là NextPDF\Document\Merge\UnsupportedSourceDocumentException. Đường dẫn Merge đó trên một trang tách không phải là lỗi sao chép/dán: nó là ngoại lệ từ chối tài liệu nguồn duy nhất, dùng chung, mà cả bề mặt gộp lẫn tách đều ném khi một nguồn không thể được sao chép an toàn. Hãy import nó từ namespace đó.
  • Đầu ra mới về cấu trúc, không ổn định về byte. Mỗi đoạn là một tài liệu mới với catalog, cây trang, và trailer riêng. Hai lần chạy trên cùng đầu vào bằng nhau về cấu trúc, nhưng không được đảm bảo giống hệt nhau về byte — vì thế mà có hồ sơ tái lập structural.

Việc tách tuyến tính theo số trang được sao chép qua tất cả các dải. Việc phân tích nguồn và sao chép bao đóng đối tượng của mỗi dải, không phải sổ sách của chính bộ tách, chiếm phần lớn công việc. Nguồn được giữ trong bộ nhớ dưới dạng một chuỗi, và các byte của mỗi đoạn được giữ cho đến khi bạn ghi chúng, nên bộ nhớ đỉnh theo dõi kích thước nguồn cộng với dải lớn nhất bạn tạo ra. Bảo vệ maxBytes giữ cho phía nguồn của đỉnh đó được giới hạn. Với các pipeline khối lượng lớn, hãy đặt maxBytesmaxRanges thành các giá trị nhỏ nhất mà khối lượng công việc của bạn cần, để một đầu vào bị hỏng định dạng hoặc quá khổ thất bại nhanh thay vì làm cạn bộ nhớ.

Việc tách chạy trong tiến trình; không byte tài liệu nào rời khỏi host, và không có lệnh gọi mạng nào được thực hiện. Hãy coi mọi PDF nguồn là đầu vào không đáng tin:

  • Giữ các giới hạn chặt. maxBytesmaxRanges là tuyến phòng thủ đầu tiên của bạn chống lại đầu vào gây từ chối dịch vụ. Với bất kỳ bề mặt nào nhận tải lên, hãy đặt chúng thành trần thực của bạn, không phải các mặc định hào phóng.
  • Phân loại trước khi bạn tách. Một nguồn đã mã hóa hoặc đã ký thất bại đóng, nhưng bạn có thể phát hiện những điều kiện đó sớm hơn. Hãy chạy các đầu vào không đáng tin qua bộ kiểm tra Core trước. Xem Phân tích và kiểm tra một PDF để có một lần quét giới hạn báo hiệu mã hóa, chữ ký, và các dấu hiệu rủi ro trước khi xử lý nặng hơn.
  • Không bao giờ chèn đầu vào người dùng vào một đường dẫn. Công thức này ghi vào một thư mục cố định hoặc kênh phụ của cookbook. Hãy lấy các đường dẫn đầu ra và tên đoạn từ các giá trị do máy chủ kiểm soát, không bao giờ từ một trường yêu cầu, để tránh truyền tải đường dẫn.
  • Không bí mật trong đầu ra. Đừng ghi các tệp đoạn vào một vị trí, hoặc với một tên, làm lộ các định danh nội bộ cho một client không nên thấy chúng.

Công thức này không đưa ra tuyên bố tiêu chuẩn quy phạm nào của riêng nó. Nó phân rã một tài liệu qua bề mặt tách của Core và kiểm tra hợp lý mỗi đoạn bằng kiểm tra header %PDF của SplitDocument::isValid() — một kiểm tra sự hiện diện rằng bộ tách đã phát ra một PDF, không phải một xác thực về tuân thủ hay về cấu trúc tài liệu. Các cấu trúc cây trang và tham chiếu chéo mà PdfSplitter xây dựng lại cho mỗi đoạn là các cấu trúc PDF 2.0 được mô tả trong tham chiếu /modules/core/document/ (ISO 32000-2:2020, bảng tham chiếu chéo §7.5.4, cây trang §7.7.3). Để đọc về cấu trúc của bất kỳ tài liệu đầu vào hay đầu ra nào, bao gồm phiên bản, số trang, mã hóa, và các cờ chữ ký, hãy dùng bộ kiểm tra Core được tài liệu hóa trong Phân tích và kiểm tra một PDF.