Объединение внешних PDF и добавление страниц из существующих документов
Коротко о главном
Заголовок раздела «Коротко о главном»У вас есть несколько файлов PDF на диске, а нужен один PDF. Этот рецепт показывает, как полностью объединить существующие документы с помощью интерфейса объединения из Core — NextPDF\Document\PdfMerger. Вы передаёте строки с необработанными байтами PDF. Модуль объединения перенумеровывает каждый объект, чтобы избежать конфликтов, строит единое дерево страниц и единую таблицу перекрёстных ссылок и возвращает NextPDF\Document\MergeResult, который можно записать на диск или передать клиенту потоком.
Тот же интерфейс охватывает три задачи, которые нужны чаще всего:
- Объединить упорядоченный список PDF в один документ.
- Добавить второй PDF после базового PDF.
- Добавить в начало страницы, поместив новый документ первым в порядке входных данных.
Объединение выполняется внутри процесса, без headless-браузера и сетевых вызовов. Вам потребуется установленный Core (composer require nextpdf/core:^3) и два или более доступных для чтения файла PDF.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Концептуальный обзор
Заголовок раздела «Концептуальный обзор»PDF организует страницы в дерево страниц с корневым узлом /Pages и находит каждый косвенный объект через таблицу перекрёстных ссылок. При объединении двух исходных документов номера их объектов пересекаются. Оба файла почти всегда содержат объект 1 0 obj, узел /Catalog и узел /Pages. Если просто склеить байты, получится повреждённый файл: ссылки больше не указывают на объекты, которые должны идентифицировать.
PdfMerger устраняет это пересечение. Он извлекает объекты страниц из каждого входного документа, перенумеровывает каждый объект в едином адресном пространстве, переписывает ссылку /Parent каждой страницы так, чтобы она указывала на общий объединённый узел /Pages, и выдаёт один каталог, одно дерево страниц и один трейлер. Результат — структурно новый документ, а не склеенная конкатенация.
Правило упорядочивания простое: страницы располагаются в том же порядке, что и исходные файлы во входном списке. Чтобы добавить в конец, поместите базовый документ первым. Чтобы добавить в начало, поместите новый документ первым. Отдельного метода для добавления в начало нет: порядок входных данных — единственный управляющий параметр, который здесь нужен.
Поверхность API
Заголовок раздела «Поверхность API»new NextPDF\Document\PdfMerger() предоставляет два метода.
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResultобъединяет упорядоченный список строк с необработанными байтами PDF. Два ограничивающих параметра задают пределы для количества файлов и общего объёма входных данных. Значения по умолчанию безопасны для эксплуатации; ужесточайте их под каждую рабочую нагрузку.append(string $basePdf, string $appendPdf): MergeResult— удобная обёртка, которая объединяет ровно два документа по порядку. Она эквивалентнаmerge([$basePdf, $appendPdf]).
Оба метода возвращают NextPDF\Document\MergeResult — readonly-объект с $pdfData (объединёнными байтами), $totalPages, $sourceCount, $mergedSize и вспомогательным методом isValid(), который подтверждает, что результат начинается с заголовка %PDF.
Входные данные — это строки с необработанными байтами, а не пути к файлам. Прочитайте файл самостоятельно с помощью file_get_contents() или получите байты из объектного хранилища. Так модуль объединения не делает предположений о файловой системе и может объединять документы, которые ни разу не попадают на диск.
Если нужно импортировать одну страницу из внешнего PDF как переиспользуемый Form XObject, например чтобы поместить страницу с фирменным бланком позади сгенерированного содержимого, используйте межпакетный контракт импортёра NextPDF\Contracts\ImportedFormObjectInterface, который реализуют импортёры, такие как nextpdf/artisan. Для компоновки целых документов и целых страниц используйте интерфейс PdfMerger, описанный здесь.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Этот пример читает файлы и записывает объединённый результат. Обработка ошибок здесь опущена, чтобы показать форму вызова; в эксплуатационном примере ниже добавлены все проверки.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([ file_get_contents(__DIR__ . '/cover.pdf'), file_get_contents(__DIR__ . '/body.pdf'), file_get_contents(__DIR__ . '/appendix.pdf'),]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);Пример кода — эксплуатация
Заголовок раздела «Пример кода — эксплуатация»Эта самодостаточная программа создаёт два небольших документа в памяти, поэтому работает без внешних файлов. Она объединяет их, проверяет результат и записывает вывод. Программа перехватывает два исключения, которые выбрасывает интерфейс объединения, и повторно выбрасывает каждое с контекстом, не поглощая его. Замените входные данные из памяти собственным чтением через file_get_contents() или выборками из объектного хранилища и подключите вывод к слою ответа или хранения.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\MergeResult;use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;
/** * Build a tiny labelled PDF so the program is self-contained. * * In your own code, replace calls to this helper with reads of the external * PDFs you want to combine, for example file_get_contents($path). */function buildSample(string $label, int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true); }
return $doc->getPdfData();}
// Validate the input set before touching the merger. An empty set is a// configuration error, not an empty success./** @var list<string> $sources Raw PDF byte strings, in output order. */$sources = [ buildSample('Cover', 1), // first in the list -> first in the output (prepend position) buildSample('Body', 2), buildSample('Appendix', 1), // last in the list -> appended after the body];
if ($sources === []) { throw new RuntimeException('No source PDFs supplied to merge.');}
$merger = new PdfMerger();
try { // Bound the merge deliberately: at most 50 files, 100 MB total input. $result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { // Raised when the list is empty or an input does not begin with %PDF. throw new RuntimeException( sprintf('Merge rejected an input: %s', $e->getConstraint()), previous: $e, );} catch (WriterException $e) { // Raised when the total input size exceeds the configured byte cap. throw new RuntimeException( sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()), previous: $e, );}
if (!$result->isValid()) { throw new RuntimeException('Merged output failed its structural header check.');}
emitResult($result);
/** * Write the merged document to the cookbook side-channel, or to a default file. */function emitResult(MergeResult $result): void{ printf( "Merged %d source(s) into %d page(s), %d bytes.\n", $result->sourceCount, $result->totalPages, $result->mergedSize, );
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) { throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path)); }}Ожидаемый стандартный вывод (общее число страниц равно сумме числа страниц исходных файлов, а размер в байтах зависит от сборки):
Merged 3 source(s) into 4 page(s), <n> bytes.Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Входные данные — это байты, а не пути.
merge()принимает строки с необработанными байтами PDF. Сначала прочитайте файл с помощьюfile_get_contents(). Если передать строку с путём, входные данные не пройдут проверку заголовка%PDFи вызовутPageLayoutException. - Порядок входа — это порядок вывода. Страницы располагаются в том порядке, в каком их исходные файлы перечислены в списке. Метода для добавления в начало нет: поместите новый документ первым, чтобы добавить в начало, или последним, чтобы добавить в конец.
- Пустой список — это ошибка. Пустой
$pdfFilesвызываетPageLayoutException, а не пустой результат. Проверьте набор, прежде чем вызывать модуль объединения. - Каждый входной документ проверяется заранее. Каждая запись должна быть непустой и начинаться с
%PDF. Первый входной документ, не прошедший проверку, вызываетPageLayoutExceptionс нарушенным ограничением, и ничего не объединяется. - Превышение ограничений вызывает исключение, а не усечение. Превышение
maxFilesвызывает исключение через внутренний охранник ресурсов, а превышениеmaxTotalBytesвызываетWriterException. Модуль объединения никогда не отбрасывает файлы и не обрезает байты молча, поэтому настройте оба ограничения под вашу рабочую нагрузку. - Результат структурно новый, а не побайтово стабильный. Объединённый документ содержит новый каталог, дерево страниц и трейлер. Два запуска на одних и тех же входных данных структурно равны, но побайтовая идентичность не гарантируется. Именно поэтому в этом рецепте объявлен профиль воспроизводимости
structural. - Аннотации на уровне страниц и общие ресурсы. Объединение компонует объекты страниц в одно дерево. Структуры уровня документа, находящиеся за пределами объектов страниц в исходном файле, не переносятся. Если нужно импортировать одну страницу как переиспользуемую графику с её ресурсами, используйте путь через
ImportedFormObjectInterfaceс импортёром, таким какnextpdf/artisan.
Производительность
Заголовок раздела «Производительность»Объединение линейно зависит от общего числа страниц. Основную часть работы составляют разбор и перенумерация объектов, а не собственный учёт модуля объединения. Пиковое потребление памяти соответствует общему объёму входных байтов, поскольку во время сборки результата каждый исходный файл хранится в памяти как строка. Охранник maxTotalBytes удерживает этот пик в заданных пределах. Для высоконагруженных конвейеров установите maxFiles и maxTotalBytes в минимальные значения, необходимые вашей рабочей нагрузке, чтобы некорректный или слишком большой пакет быстро завершался с ошибкой, а не исчерпывал память. Типичное небольшое объединение укладывается в бюджет 1500 мс по времени выполнения и 64 МБ по пиковой памяти.
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»Объединение выполняется внутри процесса; ни один байт документа не покидает хост, сетевые вызовы не выполняются. Считайте каждый внешний PDF недоверенными входными данными:
- Держите ограничения жёсткими.
maxFilesиmaxTotalBytes— ваш первый рубеж защиты от входных данных, направленных на отказ в обслуживании. Для любого интерфейса, принимающего загрузки, задавайте их по вашему реальному потолку, а не по щедрым значениям по умолчанию. - Проверяйте, прежде чем доверять. Успешное объединение означает, что байты были скомбинированы, но не что входные данные безопасны. Сначала пропустите недоверенные входные данные через инспектор Core. См. Разбор и проверка PDF, где описано ограниченное сканирование для предварительной оценки: оно отмечает шифрование, подписи и маркеры рисков до более тяжёлой обработки.
- Никогда не подставляйте пользовательский ввод в путь. Этот рецепт записывает по фиксированному пути или в служебный канал cookbook. Формируйте выходные пути из контролируемых сервером значений, но никогда из поля запроса, чтобы избежать обхода каталогов.
- Никаких секретов в документе. Не встраивайте учётные данные, токены или внутренние идентификаторы в объединённый документ, который вы возвращаете клиенту.
Соответствие
Заголовок раздела «Соответствие»Этот рецепт сам по себе не заявляет о соответствии нормативным стандартам. Он компонует существующие документы через интерфейс объединения из Core и проверяет результат проверкой заголовка MergeResult::isValid(). Модель дерева страниц, которую перестраивает PdfMerger, — это структура дерева страниц PDF 2.0, описанная в справочнике /modules/core/document/. Для структурного чтения любого входного или выходного документа, включая версию, число страниц, флаги шифрования и подписи, используйте инспектор Core, описанный в Разбор и проверка PDF.
См. также
Заголовок раздела «См. также»- Справочник по модулю Document — полный интерфейс для разделения, объединения и работы с частями документа.
- Разбор и проверка PDF — выполните предварительную оценку недоверенных входных данных, прежде чем объединять их.
- Обработка ошибок с учётом исключений — иерархия исключений NextPDF, стоящая за
PageLayoutExceptionиWriterException. - Создание многостраничного документа — подготовьте страницы, которые затем объедините.