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

Метаданные: построение пакета XMP и потоковое чтение

Модуль Metadata — слой Extensible Metadata Platform (XMP) в движке. Он создаёт пакет XMP, который файл Portable Document Format (PDF) хранит как поток метаданных. Он читает существующий пакет, не загружая весь документ в память. Он формирует XMP-расширение движка для журнала аудита.

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

PDF хранит метаданные уровня документа как пакет XMP в потоке метаданных, прикреплённом к каталогу документа, как описано в ISO 32000-2 §14.3. Этот модуль отвечает за создание и чтение такого пакета. Его API намеренно невелик и сфокусирован: три класса в пространстве имён NextPDF\Metadata\Xmp.

XmpMetadataBuilder создаёт пакет. Он сериализует набор свойств в корректно сформированный XMP-документ, обёрнутый в стандартные инструкции обработки <?xpacket?>. Он использует канонический глобально уникальный идентификатор (GUID) пакета и метку порядка байтов, заданные спецификацией XMP. Результат — строка байтов, которую Writer встраивает как поток метаданных, то есть представление XMP внутри PDF, описанное в §14.3.

XmpStreamReader читает пакет. Он рассчитан на недоверенный ввод. Перед разбором источник потоково, фрагментами по 64 КБ, записывается во временный файл с ограниченным размером. Во время этой записи средство чтения контролирует общий лимит по байтам. На время разбора загрузчик сущностей libxml устанавливается в null, а затем восстанавливается. При наличии DOCTYPE происходит жёсткий отказ. iterateProperties() возвращает генератор, который выдаёт кортежи (namespaceUri, localName, textContent) для каждого конечного элемента, не строя всё дерево в памяти; в любой момент в анализаторе хранится только текущий элемент и его текстовый узел. Слишком большой пакет вызывает PacketTooLargeException; некорректный Extensible Markup Language (XML), DOCTYPE или ввод не в UTF-8 вызывает InvalidConfigException.

XmpAuditFieldEmitter — расширение, специфичное для движка. Он отрисовывает AuditReport в пользовательское поле XMP в пространстве имён nextpdfAudit, поэтому аудит соответствия документа перемещается вместе с файлом как стандартизированный XMP, а не как отдельный файл. AuditReport, который он отрисовывает, создаётся не эмиттером. Вызывающий код включает обогащение, запуская отрисовку в режиме CssRenderingMode::Audit с переданным auditCollector, настроенным через Config(auditCollector: ...). Сборщиком управляет вызывающая сторона: она наполняет его, а эмиттер отрисовывает всё, что было собрано. Он новее базовой поверхности XMP (@since 5.4.0). Построитель и средство чтения — @since 2.0.0.

КлассКлючевые членыРоль
XmpMetadataBuilderbuild(): string, XPACKET_GUID, XPACKET_BOMСериализует набор свойств в пакет XMP (@since 2.0.0)
XmpStreamReaderiterateProperties(mixed $source, int $byteCap = DEFAULT_BYTE_CAP): \Generator, DEFAULT_BYTE_CAPПотоковое средство чтения XMP с ограничением размера, отклоняющее DOCTYPE (@since 2.0.0)
PacketTooLargeExceptionрасширяет NextPdfExceptionВыбрасывается, когда пакет XMP превышает лимит по байтам (@since 2.0.0)
XmpAuditFieldEmitterrender(?AuditReport $report): string, NAMESPACE_URIОтрисовывает журнал аудита как пользовательское поле XMP (@since 5.4.0)

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

Потоковое извлечение свойств из существующего пакета XMP с явно заданным лимитом в байтах.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Metadata\Xmp\XmpStreamReader;
$reader = new XmpStreamReader();
foreach ($reader->iterateProperties(file_get_contents('/srv/in/xmp.xml'), byteCap: 1_048_576) as [$ns, $name, $value]) {
printf("%s:%s = %s\n", $ns, $name, $value);
}

Защищённое чтение пакета: типизированные ошибки модуля сопоставляются с результатом уровня приложения, а низкоуровневые сбои анализатора не выходят наружу.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Metadata\Xmp\PacketTooLargeException;
use NextPDF\Metadata\Xmp\XmpStreamReader;
use Psr\Log\LoggerInterface;
final readonly class XmpIngestService
{
public function __construct(
private XmpStreamReader $reader,
private LoggerInterface $logger,
) {}
/**
* @param resource|string $source A stream resource or XMP byte string.
*
* @return array<string, string> Flattened "ns:name" => value map.
*/
public function ingest(mixed $source): array
{
$properties = [];
try {
// Cap untrusted XMP at 4 MB regardless of the 1 GiB default.
foreach ($this->reader->iterateProperties($source, byteCap: 4_194_304) as [$ns, $name, $value]) {
$properties["{$ns}:{$name}"] = $value;
}
} catch (PacketTooLargeException $e) {
$this->logger->warning('XMP packet exceeded ingest cap; rejected.', ['error' => $e->getMessage()]);
return [];
} catch (InvalidConfigException $e) {
$this->logger->warning('XMP packet malformed or unsafe; rejected.', ['error' => $e->getMessage()]);
return [];
}
return $properties;
}
}
  • XmpStreamReader сразу отклоняет любой DOCTYPE. Это защита от XML External Entity (XXE), а не косметическая проверка валидации; пакет, которому нужен DOCTYPE, не принимается. Очистите его заранее, на стороне источника.
  • По умолчанию лимит по байтам составляет 1 ГиБ (DEFAULT_BYTE_CAP). Это значение по умолчанию — верхний предел, а не рекомендация. Для недоверенного ввода передавайте жёсткий byteCap.
  • iterateProperties() — это генератор. Пройдите по нему один раз; повторный перебор не воспроизводит данные заново.
  • На время разбора средство чтения устанавливает загрузчик сущностей libxml в null, а затем восстанавливает его. Не запускайте такое чтение одновременно с другим разбором на основе libxml в том же запросе, если тот разбор зависит от загрузчика сущностей.
  • XmpAuditFieldEmitter::render(null) допустим и даёт пустой результат отрисовки; null в качестве AuditReport означает “нет аудита”, а не ошибку.

Построитель работает линейно по количеству свойств. Потребление памяти средством чтения определяется самым длинным отдельным фрагментом текста, а не размером документа, потому что в анализаторе хранится только текущий элемент; крупные пакеты обрабатываются потоком, а не загружаются в память. Эталонная нагрузка по умолчанию укладывается в бюджет 1500 мс по времени и 64 МБ по пиковой памяти. Профиль воспроизводимости — structural: пакет XMP записывает временные метки изменения. Две сборки одних и тех же логических метаданных различаются в этих полях, тогда как их структура идентична.

XmpStreamReader разбирает недоверенный XML и имеет соответствующие защиты. Потоковая разбивка на фрагменты с принудительным лимитом по байтам ограничивает отказ в обслуживании из-за роста потребления памяти. Отклонение DOCTYPE закрывает XXE. LIBXML_NONET блокирует разрешение сетевых сущностей. Ввод не в UTF-8 отвергается. Тем не менее задавайте подходящий для развёртывания byteCap для любого пакета из внешнего источника, а не полагайтесь на гигабайтное значение по умолчанию. Рассматривайте значения свойств XMP как недоверенные строки, когда они снова попадают в приложение. См. модель угроз движка в /modules/core/security/.

Пакет, который создаёт XmpMetadataBuilder, — это представление потока метаданных XMP внутри PDF, определённое в ISO 32000-2 §14.3 (). Сама форма сериализации XMP регулируется спецификацией XMP (ISO 16684-1), которая не входит в проверяемый корпус цитирования. На это требование ссылаются по номеру, без привязки к конкретному фрагменту. Эти факты реализации содержатся в src/Metadata/Xmp/ и проверяются в tests/Unit/Metadata/Xmp/. Сквозное соответствие метаданных профилю (PDF/A, PDF/UA) проверяется наборами oracle и golden, описанными в /modules/core/conformance/.