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

ContentStream: генератор потоков содержимого PDF

Модуль ContentStream генерирует операторы маркированного содержимого Portable Document Format (PDF). Он открывает и закрывает структурные теги и артефакты, отслеживает глубину вложенности и возвращает буфер с операторами.

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

ContentStreamBuilder — единственный класс модуля. Он формирует слой маркированного содержимого в потоке содержимого страницы. Поток содержимого кодирует содержимое страницы как последовательность операторов — ISO 32000-2 §8. Затем построитель генерирует вокруг этого содержимого операторы маркированного содержимого.

append() добавляет необработанные байты операторов дословно. Построитель не экранирует эти входные данные. За их корректность отвечаете вы. Используйте эту границу, когда HTML-конвейеру и модулю Graphics нужно чередовать собственные операторы.

beginTag() открывает последовательность со структурным тегом. Он генерирует оператор BDC со списком свойств MCID согласно ISO 32000-2 §14.6. endTag() генерирует соответствующий оператор EMC. Построитель подсчитывает глубину вложенности. Если вызвать endTag(), когда открытых последовательностей нет, он выбросит PageLayoutException вместо записи несбалансированного EMC.

beginArtifact() открывает последовательность артефакта. Используйте артефакты для элементов оформления макета: колонтитулов, номеров страниц и линеек. Они должны оставаться вне дерева структуры согласно ISO 32000-2 §14.8.2.2. Подтип — это одно из четырёх значений ISO: Pagination, Layout, Page или Background. Лучше использовать типизированное перечисление ArtifactSubtype. Строковая перегрузка проверяется по перечислению, поэтому при нестандартном значении вызов сразу завершается ошибкой.

relabelTag() перезаписывает ранее сгенерированный тег на месте. finish() возвращает полный буфер и выбрасывает исключение, если маркированное содержимое несбалансированно. drain() возвращает накопленный буфер без проверки баланса для инкрементальной потоковой передачи. peek() возвращает буфер, не потребляя его. reset() очищает состояние.

МетодСигнатураНазначение
append()append(string $raw): voidДобавляет необработанные байты операторов дословно (без экранирования)
beginTag()beginTag(string $structType, int $mcid): voidОткрывает последовательность BDC со структурным тегом
endTag()endTag(): voidЗакрывает самую внутреннюю последовательность оператором EMC
beginArtifact()beginArtifact(ArtifactSubtype|string $type): voidОткрывает последовательность артефакта
endArtifact()endArtifact(): voidЗакрывает самый внутренний артефакт
getMarkedContentDepth()getMarkedContentDepth(): intВозвращает текущую глубину вложенности
relabelTag()relabelTag(string $old, string $new, int $mcid): voidПерезаписывает сгенерированный тег на месте
finish()finish(): stringВозвращает полный буфер; выбрасывает исключение при несбалансированности
drain()drain(): stringВозвращает буфер без проверки баланса
peek()peek(): stringВозвращает буфер, не потребляя его
reset()reset(): voidОчищает всё состояние

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

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\ContentStream\ContentStreamBuilder;
$builder = new ContentStreamBuilder();
$builder->beginTag('P', mcid: 0);
$builder->append("BT /F1 12 Tf 72 720 Td (Hello) Tj ET\n");
$builder->endTag();
$pageContent = $builder->finish();

Используйте этот шаблон, чтобы заключить абзац в структурный тег, а нижний колонтитул — в артефакт. Шаблон передаёт буфер инкрементально с помощью drain().

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Accessibility\ArtifactSubtype;
use NextPDF\ContentStream\ContentStreamBuilder;
$builder = new ContentStreamBuilder();
$builder->beginTag('H1', mcid: 0);
$builder->append($titleOperators);
$builder->endTag();
$builder->beginArtifact(ArtifactSubtype::Pagination);
$builder->append($footerOperators);
$builder->endArtifact();
if ($builder->getMarkedContentDepth() !== 0) {
throw new RuntimeException('Unbalanced marked content before flush.');
}
$chunk = $builder->drain();
  • append() не экранирует входные данные. Передавайте только корректные байты операторов. Построитель доверяет вызывающему коду.
  • endTag() и endArtifact() выбрасывают исключение, если открытых последовательностей нет. Никогда не закрывайте последовательность, которую не открывали.
  • finish() проверяет баланс и выбрасывает исключение, когда глубина не равна нулю. drain() не проверяет. Используйте drain() только для инкрементальной потоковой передачи.
  • Счётчик глубины не различает теги и артефакты. EMC закрывает самую внутреннюю последовательность любого вида. Вкладывайте последовательности в строгом порядке.
  • Строковая перегрузка beginArtifact() проверяется по перечислению. При нестандартном подтипе вызов завершается ошибкой, а не формирует ошибочные выходные данные.
  • relabelTag() перезаписывает сгенерированный тег. Используйте тот же mcid, с которым он был сгенерирован.

Каждая операция добавляет строку со сложностью O(1), кроме relabelTag(), где перезапись выполняется со сложностью O(buffer). Модуль хранит один строковый буфер и один целочисленный счётчик глубины. Он не выполняет синтаксический разбор и выделяет память только под буфер. Бюджет эталонной нагрузки — 1500 мс реального времени и 64 МБ пиковой памяти. Модуль остаётся значительно ниже этих значений.

append() — это граница доверия. Построитель записывает байты дословно, поэтому вышестоящий код должен экранировать любую строку до того, как она попадёт в оператор строкового литерала. Канонический механизм экранирования — PdfStringEscaper::escapeLiteral() (ADR-015). Никогда не передавайте неэкранированный пользовательский текст через append(). Проверки баланса в endTag(), endArtifact() и finish() не дают некорректному дереву маркированного содержимого попасть в Writer. Модель угроз документа см. в /modules/core/security/.

Модуль генерирует структуры операторов маркированного содержимого, соответствующие ISO 32000-2: пары BDC/EMC со списком свойств MCID согласно §14.6 и последовательности артефактов согласно §14.8.2.2. Это факты реализации. Их подтверждают src/ContentStream/ContentStreamBuilder.php, перечисление src/Accessibility/ArtifactSubtype.php, tests/Unit/ContentStream/ContentStreamBuilderMarkedContentBalanceCoverageTest, а также ContentStreamBuilderRelabelTagInvariantTest. Это не заявление о сквозном соответствии PDF/UA-2 или PDF 2.0. Внешний оракул проверяет структуру тегированного PDF, где используются эти операторы: tests/Integration/Accessibility/VeraPdfUa2GoldenTest проверяет сгенерированную фикстуру с помощью veraPDF на соответствие профилю PDF/UA-2. Этот тест-оракул пропускается при отсутствии бинарного файла veraPDF, поэтому он работает как опциональный контрольный барьер. Указывайте, что этот модуль “создаёт структуры маркированного содержимого; соответствие PDF/UA-2 проверяется veraPDF”, вместо безоговорочного утверждения о соответствии.