Tách một PDF và trích xuất các dải trang
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”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.
Cài đặt
Phần tiêu đề “Cài đặt”composer require nextpdf/core:^3Tổng quan về khái niệm
Phần tiêu đề “Tổng quan về khái niệm”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() và
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.
Giao diện API
Phần tiêu đề “Giao diện API”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): SplitResulttạo một tài liệu đầu ra cho mỗiPageRangetrong$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): SplitResultchặt tài liệu thành các đoạn có kích thước cố định gồm$pagesPerSegmenttrang mỗi đoạn; đoạn cuối giữ phần dư.extractPages(string $pdfData, PageRange $range): SplitDocumenttrích xuất một dải duy nhất và trả về trực tiếp tài liệu đó.
split() và 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 mã — bắt đầu nhanh
Phần tiêu đề “Mẫu mã — bắt đầu nhanh”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());Mẫu mã — môi trường thực tế
Phần tiêu đề “Mẫu mã — môi trường thực tế”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.Trường hợp biên & điểm cần lưu ý
Phần tiêu đề “Trường hợp biên & điểm cần lưu ý”- 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émPageLayoutExceptiontừ chính hàm khởi tạoPageRange. - 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émPageLayoutException; 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.$pagesPerSegmentphải ít nhất là 1, nếu không bạn nhận mộtInvalidArgumentException.- Một danh sách dải rỗng bị từ chối.
split()với$ranges === []némInvalidArgumentException. 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á
maxByteshoặcmaxRangesnémInvalidArgumentException. 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. UnsupportedSourceDocumentExceptionnằm dưới namespaceMerge. Tên đầy đủ của nó làNextPDF\Document\Merge\UnsupportedSourceDocumentException. Đường dẫnMergeđó 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.
Hiệu năng
Phần tiêu đề “Hiệu năng”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 maxBytes và maxRanges 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ớ.
Lưu ý về bảo mật
Phần tiêu đề “Lưu ý về bảo mật”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.
maxBytesvàmaxRangeslà 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.
Tuân thủ
Phần tiêu đề “Tuân thủ”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.
Xem thêm
Phần tiêu đề “Xem thêm”- Tham chiếu module Document — bề mặt tách, gộp, và phần tài liệu đầy đủ.
- Gộp các PDF bên ngoài — công thức nghịch đảo: kết hợp nhiều tài liệu thành một.
- Phân tích và kiểm tra một PDF — phân loại các đầu vào không đáng tin trước khi bạn tách chúng.
- Xử lý lỗi nhận biết ngoại lệ
— phân cấp ngoại lệ NextPDF đứng sau
PageLayoutExceptionvàUnsupportedSourceDocumentException.