Bỏ qua để đến nội dung

Document: DPart, tách / gộp và phần mở rộng của nhà cung cấp

Module Document làm việc với toàn bộ tệp Portable Document Format (PDF), không phải với nội dung trang. Module này dựng cây phân cấp Document Part để các quy trình làm việc chịu sự quản lý gắn metadata. Module này tách một PDF thành các phần theo dải trang, gộp nhiều PDF thành một, và đăng ký các phần mở rộng dành cho nhà phát triển trong document catalog.

Terminal window
composer require nextpdf/core:^3

Module này nằm ở lớp phía trên nội dung trang. Trong khi Graphics và Content phát ra các toán tử, Document làm việc ở cấp cấu trúc: cây trang, document catalog và cây Document Part.

Một Document Part (DPart) là một phân vùng logic của một PDF. ISO 32000-2 định nghĩa cây phân cấp DPart, trong đó các nút mang Document Part Metadata (DPM). Một quy trình làm việc chịu sự quản lý, chẳng hạn như quy trình dược phẩm, pháp lý hoặc lưu trữ, có thể gắn metadata với một dải trang con thay vì toàn bộ tệp — §14.12. DPart là một nút bất biến readonly: nút lá tham chiếu đến một dãy chỉ mục trang liên tiếp, còn nút trung gian nhóm các nút DPart con thành một cây. DPartRoot là gốc của cây mà Writer tuần tự hóa. Các mục /Start/End của nút lá là tham chiếu gián tiếp đến đối tượng trang, không phải số nguyên chỉ mục trang — §14.12. DPart::resolveWithPageObjects() phân giải các mục đó dựa trên ánh xạ chỉ-mục-trang→số-đối-tượng do writer cung cấp và trả về dạng tham chiếu /Start (và tùy chọn /End). Phương thức này chỉ quay về dạng số nguyên trong các luồng kiểm thử khi không có ánh xạ.

PdfMergerPdfSplitter là bề mặt soạn tài liệu. PdfMerger kết hợp các đối tượng trang từ nhiều PDF đầu vào, đánh số lại đối tượng để tránh xung đột, rồi dựng lại một cây trang và một bảng tham chiếu chéo duy nhất. Cây trang mà nó tạo ra là một nút Pages cân bằng với KidsCount, cùng mô hình thuộc tính kế thừa mà PDF định nghĩa cho các nút cây trang — §7.7.3. PdfSplitter làm điều ngược lại: trích các dải trang thành các đối tượng SplitDocument độc lập. PageRange là value object được cả hai lớp sử dụng. Nó đánh chỉ mục từ 1, kiểm tra hợp lệ các giới hạn của chính nó, và trả lời contains(), count()toArray().

VendorExtensionRegistry, ExtensionsDictionaryDeveloperExtensionEntry mô hình hóa từ điển phần mở rộng dành cho nhà phát triển trong document catalog. Engine dùng từ điển đó để khai báo cấp phần mở rộng của nhà cung cấp nằm ngoài đặc tả cơ sở. Sổ đăng ký từ chối việc đăng ký lại cùng một tiền tố nhà cung cấp nếu gây xung đột, bằng VendorExtensionRegistryConflictException. CollectionDictionaryCollectionSort mô hình hóa mục catalog collection của PDF (portable collection hoặc portfolio).

LớpPhương thức chínhVai trò
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()Nút Document Part bất biến (@since 1.12.0)
DPartRootisEmpty(), write()Gốc cây DPart mà Writer tuần tự hóa (@since 1.12.0)
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()Gộp nhiều PDF kèm đánh số lại đối tượng (@since 1.9.0)
PdfSplittersplit(), splitEvery(), extractPages()Tách theo dải trang thành SplitDocument (@since 1.9.0)
PageRangecontains(int $page), count(), toArray()Value object dải trang đánh chỉ mục từ 1
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()Các đối tượng kết quả soạn tài liệu
VendorExtensionRegistryđăng ký phần mở rộngSổ đăng ký phần mở rộng dành cho nhà phát triển (@since 2.2.0)
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()Trình dựng từ điển phần mở rộng bất biến (@since 2.0.0)
CollectionDictionarytoPdfDictionary()Mục catalog portable-collection (@since 2.0.0)

Chạy composer docs:generate-api-php -- --module=Document để tạo bảng PHPDoc đầy đủ.

Tách một PDF thành các tài liệu một trang, rồi kiểm tra kết quả.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) {
$segment = $result->document($index);
file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);
}
$singlePage = $splitter->extractPages(
file_get_contents('/srv/in/report.pdf'),
new PageRange(2, 4),
);

Gộp nhiều PDF trong một ngân sách đầu vào rõ ràng, rồi kiểm tra tính hợp lệ của kết quả trước khi ghi đầu ra đã kết hợp.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;
use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */
$sources = array_map(
static fn (string $path): string => file_get_contents($path),
glob('/srv/batch/*.pdf') ?: [],
);
$merger = new PdfMerger();
try {
// Bound the merge: at most 50 files, 100 MB total.
$merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);
} catch (PageLayoutException $e) {
throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);
}
if (!$merged->isValid()) {
throw new \RuntimeException('Merged document failed structural validation.');
}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);

Trường hợp đặc biệt & điểm cần lưu ý

Phần tiêu đề “Trường hợp đặc biệt & điểm cần lưu ý”
  • PdfMerger::merge()PdfSplitter::split() áp đặt giới hạn đầu vào thông qua ResourceGuard. Đầu vào có quá nhiều tệp hoặc quá nhiều byte sẽ phát ra exception thay vì âm thầm cắt bớt. Hãy đặt maxFiles / maxTotalBytes có chủ đích theo khối lượng công việc của bạn.
  • Danh sách tệp rỗng hoặc danh sách dải rỗng sẽ phát ra PageLayoutException. Hãy xem đây là lỗi cấu hình, chứ không phải kết quả rỗng.
  • PageRange đánh chỉ mục từ 1 và bao gồm cả hai đầu mút. DPart lá có danh sách pages là các chỉ mục trang đánh từ 0. Hai mức trừu tượng này dùng cơ sở chỉ mục khác nhau. Hãy chuyển đổi tường minh khi bạn chuyển qua lại giữa chúng.
  • DPartreadonly. Để dựng một cây khác, hãy tạo nút mới thay vì thay đổi một nút sẵn có. resolveWithPageObjects() chỉ trả về dạng dự phòng chỉ-mục-số-nguyên khi ánh xạ đối tượng trang rỗng. Đừng dựa vào luồng đó cho đầu ra production.
  • VendorExtensionRegistry phát ra VendorExtensionRegistryConflictException khi gặp tiền tố nhà cung cấp trùng lặp. Đăng ký mỗi tiền tố một lần.

Tách và gộp tăng tuyến tính theo số trang, và chủ yếu bị chi phối bởi quá trình phân tích cú pháp và đánh số lại đối tượng, không phải bởi việc quản lý nội bộ của module. Khối lượng công việc tham chiếu mặc định nằm trong ngân sách 1500 ms thời gian thực / 64 MB đỉnh. Các phép gộp lớn bị ràng buộc chủ yếu bởi tổng số byte đầu vào. Bộ bảo vệ maxTotalBytes giữ cho bộ nhớ đỉnh nằm trong giới hạn. Hồ sơ khả tái lập là structural: một PDF đã gộp hoặc đã tách mang một trailer mới và /ID mới, nên hai lần chạy tương đương về cấu trúc nhưng không giống nhau từng byte.

PdfMerger::merge()PdfSplitter::split() xử lý các byte PDF không đáng tin cậy. Trước khi phân tích cú pháp, cả hai đều cho đầu vào đi qua ResourceGuard::assertSize() / assertCount(), nhằm giới hạn các kiểu tấn công từ chối dịch vụ bằng khuếch đại giải nén hoặc khuếch đại số lượng đối tượng. Hãy đặt chặt chẽ các đối số maxFiles, maxTotalBytesmaxBytes cho lần triển khai thay vì dựa vào các giá trị mặc định. Hãy coi mọi PDF đầu vào là thù địch. Khi nguồn do người dùng cung cấp, hãy chạy việc soạn theo lô trong một worker bị giới hạn. Xem mô hình mối đe dọa của engine trong /modules/core/security/ để biết ranh giới tin cậy.

Cây DPart mà module này dựng tuân theo mô hình Document Part trong ISO 32000-2 §14.12, với các mục /Start/End của nút lá được phát ra dưới dạng tham chiếu gián tiếp đến các đối tượng trang theo cùng điều khoản đó. Đầu ra đã gộp dùng cấu trúc nút cây trang được định nghĩa trong §7.7.3. Đây là các sự kiện triển khai do src/Document/ tạo ra và được kiểm thử bởi tests/Unit/Document/ (DPartTest, DPartRootTest, DPartPageRefTest, DocumentPdfMergerDeepTest, DocumentPageRangeParseDeepTest). Chúng không phải là tuyên bố về việc tuân thủ PDF 2.0 toàn trình. Mức tuân thủ của toàn tài liệu được kiểm tra hợp lệ bằng các bộ oracle và golden được mô tả trong /modules/core/conformance/.