Document: DParts, разделение и слияние, расширения поставщиков
Модуль Document работает с файлами Portable Document Format (PDF) целиком, а не с содержимым страниц. Он строит иерархию частей документа, которую регулируемые рабочие процессы используют для привязки метаданных. Модуль разделяет PDF на сегменты по диапазонам страниц, объединяет несколько PDF в один файл и регистрирует расширения разработчика в каталоге документа.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Обзор концепции
Заголовок раздела «Обзор концепции»Этот модуль находится выше уровня содержимого страниц. Там, где Graphics и Content выдают операторы, Document работает со структурой: деревьями страниц, каталогом документа и деревом частей документа.
Часть документа (DPart) — логический раздел PDF. ISO 32000-2 определяет иерархию DPart, узлы которой содержат метаданные частей документа (DPM). Регулируемый рабочий процесс, например фармацевтический, юридический или архивный, может привязать метаданные к поддиапазону страниц, а не ко всему файлу — §14.12. DPart — неизменяемый readonly-узел: лист ссылается на непрерывный диапазон индексов страниц, а промежуточный узел группирует дочерние узлы DPart в дерево. DPartRoot — корень дерева, который сериализует Writer. Записи /Start и /End листового узла — косвенные ссылки на объекты страниц, а не целочисленные индексы страниц — §14.12. DPart::resolveWithPageObjects() разрешает эти записи по карте «индекс страницы→номер объекта», которую предоставляет Writer, и возвращает форму ссылки /Start (и, при наличии, /End). Целочисленную форму он использует только на тестовых путях, где карта недоступна.
PdfMerger и PdfSplitter служат интерфейсом компоновки документа. PdfMerger объединяет объекты страниц из нескольких входных PDF, перенумеровывает объекты, чтобы избежать коллизий, и заново строит единое дерево страниц и таблицу перекрёстных ссылок. Создаваемое им дерево страниц представляет собой сбалансированный узел Pages с Kids и Count, а также наследуемой моделью атрибутов, которую PDF определяет для узлов дерева страниц — §7.7.3. PdfSplitter выполняет обратное: извлекает диапазоны страниц в самостоятельные объекты SplitDocument. PageRange — объект-значение, который используют оба класса. Нумерация в нём начинается с единицы; он проверяет свои границы и отвечает на contains(), count() и toArray().
VendorExtensionRegistry, ExtensionsDictionary и DeveloperExtensionEntry моделируют словарь расширений разработчика в каталоге документа. Движок использует этот словарь, чтобы объявить уровень расширения поставщика сверх базовой спецификации. Реестр отклоняет конфликтующую повторную регистрацию того же префикса поставщика и выбрасывает VendorExtensionRegistryConflictException. CollectionDictionary и CollectionSort моделируют запись каталога коллекции PDF (переносимой коллекции или портфолио).
Поверхность API
Заголовок раздела «Поверхность API»| Класс | Ключевые методы | Роль |
|---|---|---|
DPart | isLeaf(), hasMetadata(), resolveWithPageObjects(), write() | Неизменяемый узел части документа (@since 1.12.0) |
DPartRoot | isEmpty(), write() | Корень дерева DPart, который сериализует Writer (@since 1.12.0) |
PdfMerger | merge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append() | Слияние нескольких PDF с перенумерацией объектов (@since 1.9.0) |
PdfSplitter | split(), splitEvery(), extractPages() | Разделение по диапазонам страниц на SplitDocument (@since 1.9.0) |
PageRange | contains(int $page), count(), toArray() | Объект-значение диапазона страниц с нумерацией от единицы |
MergeResult / SplitResult | isValid(), count(), document(), totalOutputSize() | Объекты результата компоновки |
VendorExtensionRegistry | регистрация расширений | Реестр расширений разработчика (@since 2.2.0) |
ExtensionsDictionary | withEntry(), entries(), isEmpty(), toPdfDictionary() | Неизменяемый построитель словаря расширений (@since 2.0.0) |
CollectionDictionary | toPdfDictionary() | Запись каталога переносимой коллекции (@since 2.0.0) |
Выполните composer docs:generate-api-php -- --module=Document, чтобы сгенерировать полную таблицу PHPDoc.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Разделите PDF на документы по одной странице, затем проверьте результат.
<?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),);Пример кода — продакшн
Заголовок раздела «Пример кода — продакшн»Объедините несколько PDF с явным бюджетом входных данных, затем проверьте результат перед записью объединённого вывода.
<?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);Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»PdfMerger::merge()иPdfSplitter::split()накладывают ограничения на входные данные черезResourceGuard. Если входных файлов или байтов слишком много, возникает исключение вместо тихого усечения. ЗадавайтеmaxFiles/maxTotalBytesосознанно, под вашу рабочую нагрузку.- Если список файлов или диапазонов пуст, возникает
PageLayoutException. Рассматривайте это как ошибку конфигурации, а не как пустой результат. PageRangeначинается с единицы и включает обе границы. Листовой узелDPartхранит списокpagesс индексами страниц, нумеруемыми от нуля. Эти две абстракции используют разные базы индексации. При переходе между ними выполняйте явное преобразование.DPartявляетсяreadonly. Чтобы построить новое дерево, создавайте новые узлы, а не изменяйте существующий.resolveWithPageObjects()возвращает резервную форму с целочисленными индексами только тогда, когда карта объектов страниц пуста. Не полагайтесь на этот путь в продакшн-выводе.VendorExtensionRegistryвыбрасываетVendorExtensionRegistryConflictExceptionпри дублировании префикса поставщика. Регистрируйте каждый префикс один раз.
Производительность
Заголовок раздела «Производительность»Разделение и слияние масштабируются линейно по числу страниц, а основная нагрузка приходится на разбор и перенумерацию объектов, а не на собственный учёт модуля. Эталонная рабочая нагрузка по умолчанию укладывается в бюджет 1500 мс по времени / 64 МБ по пику памяти. Крупные слияния ограничиваются главным образом суммарным числом входных байтов. Ограничитель maxTotalBytes удерживает пиковое потребление памяти в заданных пределах. Профиль воспроизводимости — structural: объединённый или разделённый PDF получает новый трейлер и /ID, поэтому два прогона структурно равны, но не идентичны побайтово.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»PdfMerger::merge() и PdfSplitter::split() обрабатывают недоверенные байты PDF. Перед разбором оба пропускают входные данные через ResourceGuard::assertSize() / assertCount(), что снижает риск отказа в обслуживании из-за усиления распаковки или числа объектов. Держите аргументы maxFiles, maxTotalBytes и maxBytes жёсткими для конкретного развёртывания, а не полагайтесь на значения по умолчанию. Рассматривайте каждый входной PDF как враждебный. Если источники предоставляет пользователь, выполняйте пакетную компоновку в ограниченном рабочем процессе. Описание границы доверия см. в модели угроз движка в /modules/core/security/.
Соответствие стандартам
Заголовок раздела «Соответствие стандартам»Дерево DPart, которое строит этот модуль, следует модели частей документа в ISO 32000-2 §14.12; записи /Start и /End листового узла выдаются как косвенные ссылки на объекты страниц согласно тому же пункту. Объединённый вывод использует структуру узла дерева страниц, определённую в §7.7.3. Это факты реализации, заданные в src/Document/ и проверяемые в tests/Unit/Document/ (DPartTest, DPartRootTest, DPartPageRefTest, DocumentPdfMergerDeepTest, DocumentPageRangeParseDeepTest). Они не являются заявлением о сквозном соответствии PDF 2.0. Соответствие на уровне всего документа проверяется наборами oracle и golden, описанными в /modules/core/conformance/.
См. также
Заголовок раздела «См. также»- Модуль Core
- Модуль Writer — сериализует дерево DPart и дерево страниц.
- Модуль Metadata — Extensible Metadata Platform (XMP), совместимая с DPM.
- Модуль Navigation
- Обзор соответствия стандартам
- Модель безопасности движка