메타데이터: XMP 패킷 생성 및 스트리밍 읽기
한눈에 보기
섹션 제목: “한눈에 보기”메타데이터 모듈은 엔진의 XMP 계층입니다. PDF가 메타데이터 스트림으로 담는 XMP 패킷을 생성합니다. 문서 전체를 메모리에 로드하지 않고 기존 패킷을 읽습니다. 엔진의 감사 추적용 XMP 확장을 내보냅니다.
composer require nextpdf/core:^3개념 개요
섹션 제목: “개념 개요”PDF는 문서 카탈로그에 첨부된 메타데이터 스트림 안에 XMP 패킷으로 문서 수준 메타데이터를 담습니다 — ISO 32000-2 §14.3. 이 모듈은 해당 패킷의 생성과 소비를 담당합니다. 의도적으로 작고 집중된 표면을 제공합니다: NextPDF\Metadata\Xmp 아래의 세 클래스입니다.
XmpMetadataBuilder가 패킷을 생성합니다. 속성 집합을 표준 <?xpacket?> 처리 명령으로 감싼 올바른 형식의 XMP 문서로 직렬화합니다. XMP 사양이 고정한 정규 패킷 GUID와 바이트 순서 표시를 사용합니다. 출력은 Writer가 메타데이터 스트림으로 임베드하는 바이트 문자열입니다 — §14.3이 설명하는 PDF 내 XMP 표현입니다.
XmpStreamReader는 패킷을 소비합니다. 적대적인 입력을 염두에 두고 설계되었습니다. 소스는 파싱하기 전에 64 KB 청크 단위로, 경계가 지정된 임시 파일에 스트리밍됩니다. 해당 쓰기 중에는 집계 바이트 상한이 적용됩니다. libxml 엔터티 로더는 파싱 중 null로 설정되었다가 이후 복원됩니다. DOCTYPE은 강력한 거부를 유발합니다. iterateProperties()를 노출합니다. 이는 전체 트리를 구체화하지 않고 각 리프 요소에 대해 (namespaceUri, localName, textContent) 튜플을 산출하는 제너레이터입니다 — 어느 순간에도 현재 요소와 그 텍스트 노드만 파서에 유지됩니다. 크기를 초과한 패킷은 PacketTooLargeException을 발생시키며, 잘못된 형식의 XML, DOCTYPE, 또는 UTF-8이 아닌 입력은 InvalidConfigException을 발생시킵니다.
XmpAuditFieldEmitter는 엔진별 확장입니다. AuditReport를 nextpdfAudit 네임스페이스 아래의 사용자 정의 XMP 필드로 렌더링하므로, 문서의 적합성 감사가 사이드카가 아니라 표준 준수 XMP로서 파일과 함께 이동합니다. 이것이 렌더링하는 AuditReport는 이미터 자체에서 생성되지 않습니다. 보강은 호출자가 제공한 auditCollector와 함께 CssRenderingMode::Audit 아래에서 렌더링이 실행될 때 활성화되며, 이는 Config(auditCollector: ...)를 통해 구성됩니다. 컬렉터는 호출자 주도입니다 — 호출자가 여기에 데이터를 공급하고, 이미터는 컬렉터가 수집한 내용을 렌더링합니다. 코어 XMP 표면보다 도입 시점이 더 늦습니다 (@since 5.4.0). 빌더와 리더는 @since 2.0.0입니다.
API 표면
섹션 제목: “API 표면”| 클래스 | 주요 멤버 | 역할 |
|---|---|---|
XmpMetadataBuilder | build(): string, XPACKET_GUID, XPACKET_BOM | 속성 집합을 XMP 패킷으로 직렬화합니다 (@since 2.0.0) |
XmpStreamReader | iterateProperties(mixed $source, int $byteCap = DEFAULT_BYTE_CAP): \Generator, DEFAULT_BYTE_CAP | 경계가 지정되고 스트리밍 방식으로 동작하며 DOCTYPE을 거부하는 XMP 리더 (@since 2.0.0) |
PacketTooLargeException | NextPdfException을 확장합니다 | XMP 패킷이 바이트 상한을 초과할 때 발생합니다 (@since 2.0.0) |
XmpAuditFieldEmitter | render(?AuditReport $report): string, NAMESPACE_URI | 감사 추적을 사용자 정의 XMP 필드로 렌더링합니다 (@since 5.4.0) |
전체 PHPDoc 표는 composer docs:generate-api-php -- --module=Metadata를 실행하면 볼 수 있습니다.
코드 샘플 — 빠른 시작
섹션 제목: “코드 샘플 — 빠른 시작”명시적인 바이트 상한 내에서 기존 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을 즉시 거부합니다. 이는 검증 편의가 아니라 XXE 방어입니다 — DOCTYPE이 필요한 패킷은 허용되지 않습니다. 업스트림에서 정제해야 합니다.- 바이트 상한은 기본적으로 1 GiB입니다 (
DEFAULT_BYTE_CAP). 그 기본값은 권장 사항이 아니라 상한선입니다. 신뢰할 수 없는 입력에는 더 엄격한byteCap을 전달해야 합니다. iterateProperties()는 제너레이터입니다. 한 번만 소비해야 합니다. 두 번 반복해도 재생되지 않습니다.- 리더는 파싱을 위해 libxml 엔터티 로더를 null로 설정했다가 복원합니다. 같은 요청 내에서 엔터티 로더에 의존하는 다른 libxml 기반 파싱과 동시에 실행하면 안 됩니다.
XmpAuditFieldEmitter::render(null)은 유효하며 빈 렌더링을 산출합니다. nullAuditReport는 오류가 아니라 “감사 없음”입니다.
빌더는 속성 수에 대해 선형으로 동작합니다. 파서에는 현재 요소만 유지되므로, 리더의 메모리는 문서 크기가 아니라 가장 긴 단일 텍스트 런에 의해 좌우됩니다 — 큰 패킷은 로드되지 않고 스트리밍됩니다. 기본 참조 워크로드는 1500 ms 벽시계 시간 / 64 MB 피크 예산 안에 들어갑니다. 재현성 프로필은 structural입니다: XMP 패킷은 수정 타임스탬프를 기록합니다. 동일한 논리적 메타데이터를 두 번 빌드하면 구조는 같지만 해당 필드는 달라집니다.
보안 참고 사항
섹션 제목: “보안 참고 사항”XmpStreamReader는 신뢰할 수 없는 XML의 파서이며, 그에 맞게 강화되어 있습니다. 강제 바이트 상한을 적용한 스트리밍 청크 처리는 메모리 증폭형 서비스 거부를 제한합니다. XXE를 차단하기 위해 DOCTYPE은 거부됩니다. LIBXML_NONET은 네트워크 엔터티 해석을 차단합니다. UTF-8이 아닌 입력은 거부됩니다. 외부에서 가져온 패킷에는 기가바이트 단위의 기본값에 의존하지 말고, 배포 환경에 맞는 byteCap을 설정해야 합니다. XMP 속성 값이 애플리케이션으로 다시 들어올 때는 신뢰할 수 없는 문자열로 취급해야 합니다. 엔진 위협 모델은 /modules/core/security/에서 참고할 수 있습니다.
적합성
섹션 제목: “적합성”XmpMetadataBuilder가 생성하는 패킷은 ISO 32000-2 §14.3 ()에 정의된 PDF 내 XMP 메타데이터 스트림 표현입니다. XMP 직렬화 형식 자체는 XMP 사양(ISO 16684-1)의 규율을 받으며, 이는 검증 가능한 인용 코퍼스에 포함되어 있지 않습니다. 해당 요구 사항은 청크에 고정하지 않고 번호로 참조합니다. 이것들은 src/Metadata/Xmp/가 생성하고 tests/Unit/Metadata/Xmp/가 검증하는 구현 사실입니다. 프로필(PDF/A, PDF/UA)에 대한 종단 간 메타데이터 적합성은 /modules/core/conformance/에 설명된 오라클 및 골든 스위트로 검증됩니다.
함께 보기
섹션 제목: “함께 보기”- Document 모듈 — DPM이 짝을 이루는 DPart 트리입니다.
- Audit 모듈 — 이미터가 렌더링하는
AuditReport를 생성합니다. - Writer 모듈 — 패킷을 메타데이터 스트림으로 임베드합니다.
- 적합성 개요
- 엔진 보안 모델