The complete script produces a PDF 2.0 file with a bilingual title, an author's declaration, an embedded report and separate metadata for the document, one document part and one drawing sequence. The coloured rectangle uses black point compensation. These values are written into the corresponding PDF objects; they are more than explanatory text on the page.

Run the example

From a core 0.7.1 checkout with its dependencies installed:

php examples/pdf20-metadata-declarations.php metadata.pdf

The example supplies Courier through explicit metrics: FirstChar/LastChar, ASCII widths and a FontDescriptor. PDF 2.0 no longer permits omitting these entries for the fourteen standard fonts. This construction does not embed a font program; profiles requiring embedding need an explicitly embedded font.

PHP 8.3 or later is sufficient. This script uses native core objects and needs no image or font converter. The preview and download below come from the actual generated file.

Construct an XMP packet with explicit values

XmpPacket serializes RDF properties into a UTF-8 XML packet. XmpName supplies the prefix, namespace URI and local name separately; XmlText checks XML characters and escapes text. The UTF-8 BOM in the XMP marker does not turn the packet into a PDF text string: it is an XML stream's content.

$title = new XmpLanguageAlternative(
    new XmpName('dc', new MetadataUri('http://purl.org/dc/elements/1.1/'), 'title'),
    new XmlText('PDF 2.0 metadata and document parts'),
    new XmpLocalizedText('fr', new XmlText('Métadonnées PDF 2.0 et parties de document')),
);
$metadata = new MetadataStream(new XmpPacket($title, $review));
$catalogMetadata = new MetadataEntry($metadata);

XmpLanguageAlternative writes an rdf:Alt whose first value is x-default. Language choices are unique even when their casing differs. MetadataStream provides /Type /Metadata and /Subtype /XML; MetadataEntry then creates its indirect reference. XML metadata streams have existed since PDF 1.4; this example explicitly selects PDF 2.0 for its other mechanisms. Conflicting properties or namespace prefixes are rejected.

Declare a document property without inventing a certification

A Declaration contains the URI identifying the claimed specification or profile. Declarations writes the unordered pdfd:declarations array in the exact http://pdfa.org/declarations/ namespace. Each declaration can contain an unordered ClaimData array: claimant, explicit date, credentials and report URL.

$review = new Declarations(new Declaration(
    new MetadataUri('https://example.org/pdf/reviewed'),
    new ClaimData(
        new XmlText('PDFToolkit example'),
        new ClaimDate('2026-10-14'),
        claimReport: new ClaimReport('#ef=review.txt'),
    ),
));

The example.org identifier describes a demonstration workflow. Having that declaration does not prove PDF/A, PDF/UA or WTPDF conformance, and the core adds none of those claims. DeclarationsPdfaExtensionSchema lets the caller supply the vocabulary description required in a PDF/A context explicitly; that description alone does not make a document PDF/A conforming.

The #ef=review.txt fragment identifies an embedded report by name. The example supplies its bytes, MIME type and source date, adds it to Names.EmbeddedFiles and associates it using AFRelationship /Supplement. Readers that expose attachments can therefore discover the report. URIs and filenames cause no external loading.

Give each metadata stream its owner

The catalog declaration applies to the document; it does not extend to embedded or referenced files. The document part's XMP stream describes that part, while another stream describes only the rectangle's drawing sequence. The three streams retain distinct identities and vocabularies.

The drawing sequence uses a MetadataPropertyList under Resources.Properties.ColourInfo, followed by /Sample /ColourInfo BDC ... EMC. The named list keeps its indirect XML stream reference outside content-stream syntax. A Metadata property written directly inside a content operand is rejected. References are resolved against the actual owner's resources, including when the content belongs to an already emitted revision.

MetadataResourceLocation also expresses the application note's locations: embedded font program rather than font descriptor, tiling-pattern stream, shading dictionary rather than its pattern wrapper, structure element, annotation, optional-content group or 3D stream. The application chooses the location because it knows which information resource the metadata describes.

Separate DPM, document parts and graphics state

The DocumentPart contains a DocumentPartMetadata dictionary with Job (Proof 42) and an XMP stream under Metadata. DPM and XMP are separate mechanisms. DPM holds typed application data; its keys and level names follow XML Name syntax. Its values are text/date strings, arrays, dictionaries, booleans and numbers.

DocumentPartRoot links the catalog to the node. The page refers to its part through DocumentPartEntry; setPageRange() then sets that same page as the range. The hierarchy builder manages parent links. Graph validation checks reciprocal references, page order, nonoverlapping ranges, page coverage and declared levels. DPM is not implicitly inherited from a parent.

Finally, UseBlackPointCompensation(BlackPointCompensation::On) writes UseBlackPtComp /ON into a GraphicsStateParameters resource selected by gs. The other two exact values are /OFF and /Default. The rectangle uses a CalRGB colour space; compensation participates in CIE colour conversions performed by a PDF 2.0 processor. Its appearance depends on the output profile and viewer, and /Default leaves the choice to them.

Inspect the evidence and sources

tools/validate_pdf20_metadata.py checks XML with an independent parser and uses pypdf to inspect RDF arrays, AF and DPart identities, MIME types and source dates, BPC state and content operators. qpdf checks the file. Native tests cover invalid XML characters, impossible dates, missing properties and inconsistent incremental replacements. Rendering checks the visible page separately.

The packet constructor does not parse user-supplied XML. An application that manually constructs a raw XML stream remains responsible for its content; the graph validates its references and PDF type. This lesson is not a general validator for every ISO 32000-2 rule.

Preserved sources: PDF Declarations, pages 7–10, black point compensation, page 4, associated files, pages 7 and 12–13 and metadata locations, pages 4–10. Document parts are defined in ISO 32000-2, physical pages 850–853. The bundle comes from the PDF Association's sponsored access.

Tutorial path · PDF 2.0 guide

View the PDF result

This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.

PDF result pdf20-metadata-declarations: page 1 of 1
Page 1 of 1
Use the browser PDF viewer

This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.

Complete source code

Code from roukmoute/pdf · View the source at the documented commit ↗

Read the commented script: pdf20-metadata-declarations.php
<?php

declare(strict_types=1);

use PDF\Document\Files as Files;
use PDF\Document\Metadata as Metadata;
use PDF\Document\Metadata\Declarations as Declarations;
use PDF\Document\Parts as Parts;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources;
use PDF\FileStructure;
use PDF\Graphics\ColourSpaces\CalRGB;
use PDF\Graphics\ColourSpaces\WhitePoint;
use PDF\Graphics\GraphicsState\BlackPointCompensation;
use PDF\Graphics\GraphicsState\GraphicsStateParameters;
use PDF\Graphics\GraphicsState\UseBlackPointCompensation;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators as Op;
use PDF\Syntax\DataStructure\Date;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Filters\FlateDecode;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;

require dirname(__DIR__) . '/vendor/autoload.php';

// PDF Declarations are explicit author assertions. This demonstration identifier
// asserts a local review workflow; it does not claim PDF/A, PDF/UA or WTPDF conformance.
$review = new Declarations\Declarations(new Declarations\Declaration(
    new Metadata\MetadataUri('https://example.org/pdf/reviewed'),
    new Declarations\ClaimData(new Metadata\XmlText('PDFToolkit example'), new Declarations\ClaimDate('2026-10-14'), claimReport: new Declarations\ClaimReport('#ef=review.txt')),
));
$title = new Metadata\XmpLanguageAlternative(new Metadata\XmpName('dc', new Metadata\MetadataUri('http://purl.org/dc/elements/1.1/'), 'title'), new Metadata\XmlText('PDF 2.0 metadata and document parts'), new Metadata\XmpLocalizedText('fr', new Metadata\XmlText('Métadonnées PDF 2.0 et parties de document')));
$documentMetadata = new Metadata\MetadataStream(new Metadata\XmpPacket($title, $review));
// Object-level declarations remain separate: the catalog claim does not extend
// to this embedded report. Names/EmbeddedFiles makes the report easy to discover.
$report = new Files\EmbeddedFileStream("Example review report. This is not a certification.\n", new NameType('text/plain'), modificationDate: new Date(new DateTimeImmutable('2026-10-14T00:00:00Z')), filters: new FlateDecode());
$reportFile = new Files\FileSpecification(new LiteralString('review.txt'), new Files\FileSpecificationOptions(embedded: new Files\EmbeddedFileBindings(new Files\EmbeddedFileBinding(Files\EmbeddedFileKey::File, $report))), new Files\AssociatedFileRelationship(Files\AFRelationship::Supplement));
$attachments = new Files\EmbeddedFiles(new Files\EmbeddedFileAttachment(new LiteralString('review.txt'), $reportFile));
$associated = new Files\AssociatedFiles(new IndirectReference($reportFile));

// DPM is a typed PDF dictionary describing a workflow. Metadata is a different,
// XMP stream entry. Both may coexist on the same document-part node.
$part = new Parts\DocumentPart(new Parts\DocumentPartMetadata(new Entry(new NameType('Job'), new TextString('Proof 42'))), new Metadata\MetadataEntry(new Metadata\MetadataStream(new Metadata\XmpPacket(new Metadata\XmpTextProperty(new Metadata\XmpName('job', new Metadata\MetadataUri('urn:example:job'), 'description'), new Metadata\XmlText('One-page proof'))))), new Files\AssociatedFilesEntry($associated));
$partRoot = new Parts\DocumentPartRoot($part, new IntegerType(0), new NameType('Proof'));
$sequenceMetadata = new Metadata\MetadataStream(new Metadata\XmpPacket(new Metadata\XmpTextProperty(new Metadata\XmpName('sample', new Metadata\MetadataUri('urn:example:sample'), 'purpose'), new Metadata\XmlText('Calibrated colour demonstration'))));
$state = new GraphicsStateParameters(new UseBlackPointCompensation(BlackPointCompensation::On));
// PDF 2.0 requires explicit metrics and a descriptor even for standard fonts.
$font = require __DIR__ . '/fixtures/pdf20-courier-font.php';
$resources = new ResourceDictionary(
    new Resources\Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))),
    new Resources\ExtGState(new DictionaryType(new Entry(new NameType('BpcOn'), new IndirectReference($state)))),
    new Resources\ColorSpace(new DictionaryType(new Entry(new NameType('Calibrated'), new CalRGB(new WhitePoint(new NumericType(0.9505), new NumericType(1), new NumericType(1.089)))))),
    new Resources\Properties(new DictionaryType(new Entry(new NameType('ColourInfo'), new Metadata\MetadataPropertyList($sequenceMetadata)))),
);
$operators = [];
foreach (['PDF 2.0: metadata and document parts', 'Catalog XMP: a bilingual title and explicit review declaration.', 'A named property list attaches XMP to this colour sample.', 'The page belongs to one DPart with separate DPM and XMP.', 'The review report is an associated file and a named attachment.', 'BPC is ON for the calibrated colour conversion; its display', 'depends on the PDF processor and its output colour profile.'] as $index => $line) {
    $operators[] = new Op\BeginTextObject();
    $operators[] = new Op\SetTextFontAndSize(new NameType('F1'), new NumericType($index === 0 ? 20 : 11));
    $operators[] = new Op\MoveTextPosition(new NumericType(48), new NumericType(780 - 30 * $index));
    $operators[] = new Op\ShowText(new LiteralString($line));
    $operators[] = new Op\EndTextObject();
}
$operators = [...$operators, new Op\SaveGraphicsState(), new Op\BeginMarkedContentWithProperties(new NameType('Sample'), new NameType('ColourInfo')), new Op\SetGraphicsStateParameters(new NameType('BpcOn')), new Op\SetNonstrokingColorSpace(new NameType('Calibrated')), new Op\SetNonstrokingColor(new NumericType(0.1), new NumericType(0.3), new NumericType(0.6)), new Op\AppendRectangle(new NumericType(48), new NumericType(440), new NumericType(496), new NumericType(100)), new Op\FillPathNonzero(), new Op\EndMarkedContent(), new Op\RestoreGraphicsState()];
$page = new PageObject(new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)), $resources, new StreamObject(new ContentStreams(...$operators), filters: new FlateDecode()), new Parts\DocumentPartEntry($part));
$pages = (new PageTreeNode())->addKid($page);
$part->setPageRange($page);
$catalog = new DocumentCatalog($pages, new Metadata\MetadataEntry($documentMetadata), $partRoot->catalogEntry(), new Files\AssociatedFilesEntry($associated), new Entry(new NameType('Names'), new DictionaryType($attachments->nameDictionaryEntry())));
$output = $argv[1] ?? __DIR__ . '/pdf20-metadata-declarations.pdf';
(new FileStructure(new FileHeader('2.0'), new FileBody($catalog)))->writeTo($output);
echo $output . PHP_EOL;

Guide source at the documented commit ↗