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

Writer: сериализатор PDF 2.0 и xref

Модуль Writer сериализует документ в байтовое представление Portable Document Format (PDF). Он выбирает стратегию версии, записывает граф объектов и формирует структуру перекрёстных ссылок и трейлер.

Окно терминала
composer require nextpdf/core:^3

Используйте PdfWriter как точку входа. Передайте объект-значение DocumentData методу write(). Метод возвращает готовый PDF в виде байтовой строки. Writer собирает граф объектов, присваивает номера объектов, фиксирует байтовые смещения и записывает структуру перекрёстных ссылок в последнюю очередь.

При каждом вызове Writer использует одну стратегию сериализации. Интерфейс PdfSerializationStrategy задаёт четыре метода: writeHeader(), getCatalogVersion(), writeXrefAndTrailer() и usesXrefStream(). Реализаций три. Pdf20StreamStrategy записывает заголовок %PDF-2.0, задаёт версию каталога /2.0 и формирует поток перекрёстных ссылок. Pdf17TableStrategy записывает %PDF-1.7 и классическую таблицу перекрёстных ссылок. Pdf14TableStrategy записывает %PDF-1.4 и таблицу перекрёстных ссылок. PdfWriter выбирает стратегию через match по DocumentData::$outputProfile. По умолчанию используется Pdf20StreamStrategy.

Перечисление PdfOutputProfile содержит три целевые версии: Pdf20, Pdf17 и Pdf14. Оно предоставляет headerVersion(), catalogVersion(), allowsObjectStreams() и usesXrefStream(). Режим архивного соответствия переопределяет выбранный профиль ещё до выбора стратегии. Pdf14FeatureGuard отклоняет возможности PDF 2.0, если выбран профиль Pdf14.

Поток перекрёстных ссылок сопоставляет каждый номер объекта с его байтовым смещением согласно ISO 32000-2 §7. Инкрементные обновления добавляют новые объекты в конец файла согласно ISO 32000-2 §7.5.6. Writer экранирует каждую литеральную строку единым каноническим способом, через PdfStringEscaper::escapeLiteral(), в соответствии с нормативной таблицей экранирования ISO 32000-2 §7.3.4.2 (ADR-015).

Writer поддерживает детерминированный вывод. setDeterministicMode() фиксирует идентификаторы объектов и порядок ключей словаря. setReproducibleClock() фиксирует временную метку документа. Когда включены обе фиксации, фиксированный вход даёт побайтово идентичный вывод. Метод writeChunked() возвращает генератор, который выдаёт PDF фрагментами фиксированного размера. Streaming/StreamingPdfWriter записывает по одной странице за раз в поток, предоставленный вызывающей стороной, для документов, превышающих бюджет памяти.

Linearizer переписывает готовый PDF в линеаризованную структуру. Он размещает первую страницу в начале, чтобы средство просмотра могло показать её до завершения полной загрузки. shadowValidate() проверяет результат перезаписи, не изменяя входные данные.

Внимание. PdfWriter.php и Linearizer.php критически важны для байтовых смещений и графа объектов (опасные зоны манифеста). Не меняйте нумерацию объектов или вычисление смещений xref без эталонного набора тестов Writer.

КлассОсновные методыРоль
PdfWriterwrite(DocumentData): string, writeChunked(DocumentData, int): Generator, setDeterministicMode(), setReproducibleClock(), setOutputColorProfile(), getLastXrefOffset(), getFileId()Основной сериализатор
PdfSerializationStrategy (интерфейс)writeHeader(), getCatalogVersion(), writeXrefAndTrailer(), usesXrefStream()Контракт стратегии версии
Pdf20StreamStrategywriteHeader()%PDF-2.0, getCatalogVersion()/2.0, usesXrefStream()trueСтратегия xref-потока PDF 2.0
Pdf17TableStrategywriteHeader()%PDF-1.7, таблица xrefСтратегия таблицы xref PDF 1.7
Pdf14TableStrategywriteHeader()%PDF-1.4, таблица xrefСтратегия таблицы xref PDF 1.4
PdfOutputProfile (перечисление)Pdf20, Pdf17, Pdf14; headerVersion(), catalogVersion(), allowsObjectStreams()Селектор целевой версии
PdfXrefWritergenerateFileId(), finalizeTrailerAndXref()Идентификатор файла + завершение trailer/xref
Linearizerlinearize(string): string, shadowValidate(string): arrayПерезапись для быстрого веб-просмотра
Streaming\StreamingPdfWriteropen(), newPage(), close()Однопроходный потоковый writer

Запустите composer docs:generate-api-php -- --module=Writer, чтобы сформировать полную таблицу PHPDoc.

Источник: examples/02-pdf-factory.php.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Writer\PdfWriter;
$writer = new PdfWriter();
$pdfBytes = $writer->write($documentData);
file_put_contents('out.pdf', $pdfBytes);

По умолчанию используется профиль PDF 2.0. Вывод начинается с %PDF-2.0 и заканчивается потоком перекрёстных ссылок.

В этом примере включается детерминированный режим, задаются фиксированные часы для побайтово идентичного вывода, а результат затем передаётся потоком фрагментами фиксированного размера.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use DateTimeImmutable;
use NextPDF\Writer\PdfWriter;
use NextPDF\Writer\ReproducibleClock;
$pinned = new DateTimeImmutable('2026-01-01T00:00:00Z');
$writer = new PdfWriter();
$writer->setDeterministicMode($pinned, 'nextpdf-fixed-file-id');
$writer->setReproducibleClock(new ReproducibleClock($pinned));
$out = fopen('php://output', 'wb');
foreach ($writer->writeChunked($documentData, chunkSize: 65536) as $chunk) {
fwrite($out, $chunk);
}
fclose($out);
  • В рамках одного вызова write() выполняется только одна стратегия. При каждом вызове writer заново выбирает стратегию на основе профиля. Предыдущий вызов не переносит свою версию в следующий.
  • Режим архивного соответствия переопределяет запрошенный профиль. Сборка PDF/A-3 принудительно использует PDF 1.7. Сборка PDF/A-4 принудительно использует PDF 2.0.
  • Для побайтово идентичного вывода требуются обе фиксации. Задайте детерминированный режим и воспроизводимые часы. Одной фиксации недостаточно.
  • writeChunked() возвращает генератор. Обработайте его полностью. Частичное чтение даёт усечённый и недействительный PDF.
  • Linearizer переписывает смещения перекрёстных ссылок. Если конвейер не допускает неудачной перезаписи, сначала запустите shadowValidate().
  • Pdf14TableStrategy объявлен как final readonly. Путь PDF 1.4 отклоняет возможности PDF 2.0 через Pdf14FeatureGuard; он не переводит их на более низкий уровень.

Сериализация линейна относительно числа объектов и общего размера в байтах. Поток перекрёстных ссылок добавляет один проход по таблице объектов. writeChunked() держит собранный документ в памяти, но выдаёт его ограниченными порциями, поэтому пиковое потребление памяти равно размеру документа плюс один фрагмент. Streaming\StreamingPdfWriter не держит документ целиком; используйте его для входных данных, превышающих бюджет памяти. Бюджет эталонной нагрузки составляет 1500 мс общего времени и 64 МБ пикового потребления памяти. Линеаризация добавляет ещё один полный проход и проход измерения. Учитывайте это в бюджете явно.

Writer сериализует доверенный граф объектов из памяти. Основная граница угроз проходит по его входным данным. Каждая литеральная строка проходит через канонический PdfStringEscaper::escapeLiteral() (ADR-015), поэтому встроенные управляющие байты не выходят за пределы строкового токена. Шифрование подключено через PdfEncryptionWriter и запись трейлера /Encrypt. Шифрование с открытым ключом явно отклоняется исключением, а не понижается без уведомления. Детерминированный режим и режим воспроизводимых часов устраняют из вывода побочные каналы по временным меткам и порядку. Модель угроз документа и границы доверия шифрования см. в /modules/core/security/.

Writer формирует файловые структуры PDF 2.0: заголовок %PDF-2.0, версию каталога /2.0, поток перекрёстных ссылок и экранирование литеральных строк согласно таблице экранирования ISO 32000-2 §7.3.4.2. Это факты реализации. Их подтверждают src/Writer/Pdf20StreamStrategy.php, src/Writer/PdfSerializationStrategy.php и выбор стратегии в src/Writer/PdfWriter.php. Такое поведение проверяется набором tests/Unit/Writer/ (192 теста, включая наборы Pdf20StreamStrategy, PdfXrefWriter и Linearizer*) и эталоном tests/Golden/PdfWriter/PdfWriterGoldenBaselineSmokeTest.

Это не заявление о полном соответствии PDF 2.0. Полное соответствие ISO 32000-2 — свойство готового документа, проверенного внешним оракулом, а не самого сериализатора. Сквозное соответствие утверждается только там, где его подтверждает оракул: tests/Integration/Accessibility/VeraPdfUa2GoldenTest проверяет сгенерированную фикстуру через veraPDF на соответствие PDF/UA-2, а tests/Standards/Profile/PdfRConformanceTest охватывает профиль PDF/R. Эталонный тест veraPDF пропускается, когда двоичный файл veraPDF отсутствует на исполнителе, поэтому это опциональный оракул-шлюз, а не безусловная проверка. Чтобы запустить его, задайте VERAPDF_BINARY. Выбор архивного профиля (PDF/A-3 → PDF 1.7, PDF/A-4 → PDF 2.0) определяется ADR-011 и режимом соответствия и проверяется наборами соответствия в /modules/core/conformance/. За пределами этих подтверждённых оракулом профилей указывайте, что Writer “формирует структуры PDF 2.0; соответствие проверяется veraPDF для профиля PDF/UA-2”, а не утверждайте безоговорочное соответствие.