Перейти к содержимому

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 (переносимой коллекции или портфолио).

КлассКлючевые методыРоль
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()Неизменяемый узел части документа (@since 1.12.0)
DPartRootisEmpty(), write()Корень дерева DPart, который сериализует Writer (@since 1.12.0)
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()Слияние нескольких PDF с перенумерацией объектов (@since 1.9.0)
PdfSplittersplit(), splitEvery(), extractPages()Разделение по диапазонам страниц на SplitDocument (@since 1.9.0)
PageRangecontains(int $page), count(), toArray()Объект-значение диапазона страниц с нумерацией от единицы
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()Объекты результата компоновки
VendorExtensionRegistryрегистрация расширенийРеестр расширений разработчика (@since 2.2.0)
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()Неизменяемый построитель словаря расширений (@since 2.0.0)
CollectionDictionarytoPdfDictionary()Запись каталога переносимой коллекции (@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/.