This example builds a PDF portfolio with a CSV, notes and a JSON export related to the CSV. The container page explains their relationships; files are actually embedded and can be extracted by a compatible reader.

Run embedded-files.php:

php examples/embedded-files.php /tmp/embedded-files.pdf

Supply bytes, then describe the file

EmbeddedFileStream receives original bytes and a MIME subtype name. Its Params dictionary calculates Size and the MD5 checksum from those bytes before filters and encryption. This checksum is format data; it certifies neither the file's origin nor its trustworthiness.

$csv = new EmbeddedFileStream(
    "article,montant\nA,12.50\nB,7.25\n",
    new NameType('text/csv'),
    filters: new FlateDecode(),
);

FileSpecification then describes name F. EmbeddedFileBindings supplies indirect references for EF. If EF.UF is chosen, also supply Unicode name UF with FileSpecificationName. Several names may point to the same stream. Names remain PDF values; the core opens no paths to guess their content.

Optional information remains in separate objects: description, identifiers, dates, volatility and Mac information. MacFileCode explicitly interprets four ASCII bytes as a most-significant-byte-first integer. The resource fork is also a stream supplied by the application.

Connect an export to its primary file

RelatedFiles constructs the name/stream pairs in RF.F. The example places total.json there. Key F must already be present in EF, and every RF value contains complete pairs whose streams are indirect.

EmbeddedFiles builds the actual NameTree in Names.EmbeddedFiles. Its keys Mesures and Notes identify file specifications that are themselves indirect. Those keys are distinct from filenames mesures.csv and notes.txt. Key order and branch limits are checked against the effective graph, including after an incremental update.

Build collection metadata

CollectionSchema associates field names with CollectionField objects. The example distinguishes text, number and date. A CollectionItem is built with that schema; unknown field names or text in the numeric field are rejected. FileSpecificationOptions records the item under CI through an indirect reference.

CollectionSubitem separates data D from display prefix P. For measurements, D is 19.75 and P is EUR. The prefix is not added to the number. For notes, the prefix exists without D: the specification permits this absence, and no amount is invented.

CollectionSort names its fields in S. Boolean A applies to all fields; an array supplies individual directions. Missing directions default to true and extra directions are ignored. Here, Amount descends while Title takes the default ascending direction.

Collection.D is Mesures, a key in the actual embedded-file tree. If this key is absent, initial selection falls back to the container, as the specification requires. View mode and schema describe an interface the PDF reader may present; the drawn page remains available even in a reader without that interface.

Declare a stream with external data

ExternalStreamObject handles the other case in Table 5: F designates an external file, and FFilter and FDecodeParms describe its decoding. The core does not load this file or execute an ExternalStreamFilters encoder. Local bytes between stream and endstream are ignored, but their length remains exact. They remain cleartext under PDF encryption; dictionary strings, including the filename, follow normal encryption.

Tests cover this mechanism; the viewable example uses embedded files so its data travel with the downloaded PDF.

Verification and references

The independent corpus verifies 90 histories, or 180 PDFs, with three xref formats, compressed objects, linearization and R2 to R5 encryption. Extracted content, Size, MD5, Mac forks, EF/RF/CI links, tree limits and value updates are checked using pypdf, hashlib and qpdf. The example's pages and rendering are inspected too.

Preserved sources: Table 5, page 28, file specifications and streams, pages 107–115, collections, pages 378–381.

Tutorial path · Verified coverage

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 embedded-files: 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: embedded-files.php
<?php

declare(strict_types=1);

use PDF\Document\Collections as C;
use PDF\Document\Files as F;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\FileStructure;
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\NameDictionary;
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\BooleanType;
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';

// Bytes are supplied explicitly. The core opens no external files.
// Params Size and CheckSum are derived before Flate and encryption.
$created = new Date(new DateTimeImmutable('2026-10-13T09:00:00+02:00'));
$csv = new F\EmbeddedFileStream("article,montant\nA,12.50\nB,7.25\n", new NameType('text/csv'), $created, $created, filters: new FlateDecode());
$json = new F\EmbeddedFileStream('{"total":19.75}', new NameType('application/json'), filters: new FlateDecode());
$notes = new F\EmbeddedFileStream("Les montants sont exprimes en euros.\n", new NameType('text/plain'), filters: new FlateDecode());

// A collection names and types its fields. A prefix is presentation data;
// it is never concatenated to the number used for sorting.
$schema = new C\CollectionSchema(
    new C\CollectionSchemaField(new NameType('Title'), new C\CollectionField(C\CollectionFieldType::Text, new TextString('Document'), new IntegerType(0))),
    new C\CollectionSchemaField(new NameType('Amount'), new C\CollectionField(C\CollectionFieldType::Number, new TextString('Montant'), new IntegerType(1))),
    new C\CollectionSchemaField(new NameType('Created'), new C\CollectionField(C\CollectionFieldType::Date, new TextString('Date'), new IntegerType(2))),
);
$measureItem = new C\CollectionItem(
    $schema,
    new C\CollectionItemValue(new NameType('Title'), new TextString('Mesures')),
    new C\CollectionItemValue(new NameType('Amount'), new C\CollectionSubitem(new NumericType(19.75), new TextString('EUR '))),
    new C\CollectionItemValue(new NameType('Created'), $created),
);
$noteItem = new C\CollectionItem(
    $schema,
    new C\CollectionItemValue(new NameType('Title'), new TextString('Notes')),
    new C\CollectionItemValue(new NameType('Amount'), new C\CollectionSubitem(prefix: new TextString('Aucun montant'))),
);

// EF.F and EF.UF target the same stream with their corresponding names.
// RF.F adds a JSON export related to the CSV without declaring it the initial document.
$ef = new F\EmbeddedFileBindings(new F\EmbeddedFileBinding(F\EmbeddedFileKey::File, $csv), new F\EmbeddedFileBinding(F\EmbeddedFileKey::UnicodeFile, $csv));
$related = new F\RelatedFileBindings($ef, new F\RelatedFileBinding(
    F\EmbeddedFileKey::File,
    new F\RelatedFiles(new F\RelatedFile(new LiteralString('total.json'), $json)),
));
$measureFile = new F\FileSpecification(new LiteralString('mesures.csv'), new F\FileSpecificationOptions(
    embedded: $ef,
    related: $related,
    description: new TextString('Mesures et export associe'),
    collectionItem: $measureItem,
    additionalNames: new F\FileSpecificationName(F\EmbeddedFileKey::UnicodeFile, new TextString('mesures.csv')),
));
$noteFile = new F\FileSpecification(new LiteralString('notes.txt'), new F\FileSpecificationOptions(
    embedded: new F\EmbeddedFileBindings(new F\EmbeddedFileBinding(F\EmbeddedFileKey::File, $notes)),
    description: new TextString('Notes du dossier'),
    collectionItem: $noteItem,
));
$attachments = new F\EmbeddedFiles(new F\EmbeddedFileAttachment(new LiteralString('Mesures'), $measureFile), new F\EmbeddedFileAttachment(new LiteralString('Notes'), $noteFile));

// S contains two fields; A supplies only one direction. Amount therefore
// descends and Title takes default true. D is a Names.EmbeddedFiles key.
$sort = new C\CollectionSort($schema, new C\CollectionSortFields(new NameType('Amount'), new NameType('Title')), new C\CollectionSortDirections(new BooleanType(false)));
$collection = new C\Collection($schema, C\CollectionView::Details, new LiteralString('Mesures'), $sort);

// This page is the container. Readers manage attachment and collection
// interfaces; the text below makes the portfolio inspectable everywhere.
$n = static fn (int|float $value): NumericType => new NumericType($value);
$resources = new ResourceDictionary(new Entry(new NameType('Font'), new DictionaryType(new Entry(new NameType('F1'), new IndirectReference(new Type1Font(StandardFont::Helvetica))))));
$operators = [];
$lines = [
    [28, 'Fichiers incorpores et collections'],
    [13, 'Un contenant PDF, deux pieces jointes, un export JSON associe.'],
    [18, 'Mesures / mesures.csv'],
    [12, 'CSV : articles A et B, total 19.75.'],
    [12, 'CI.Amount = 19.75 ; le prefixe EUR ne change pas la valeur.'],
    [12, 'RF.F : total.json contient le meme total.'],
    [18, 'Notes / notes.txt'],
    [12, 'Le sous-element affiche un prefixe, sans inventer de montant.'],
    [18, 'Relations natives'],
    [12, 'Names.EmbeddedFiles trie les cles et reference les Filespec.'],
    [12, 'EF reference les flux ; CI reference les metadonnees typees.'],
    [12, 'Collection.D = Mesures ; Schema et Sort decrivent la presentation.'],
    [18, 'Octets et controles'],
    [12, 'Params.Size compte les octets originaux, avant compression.'],
    [12, 'Params.CheckSum est leur MD5 de format, sans preuve de confiance.'],
    [12, 'Flate compresse ensuite chaque flux incorpore.'],
    [12, 'Aucun chemin, lien distant ou programme joint n est execute.'],
];
$y = 736;
foreach ($lines as [$size, $line]) {
    array_push($operators, new Op\SetNonstrokingRGBColor($n(0.1), $n(0.2), $n(0.28)), new Op\BeginTextObject(), new Op\SetTextFontAndSize(new NameType('F1'), $n($size)), new Op\MoveTextPosition($n(42), $n($y)), new Op\ShowText(new LiteralString($line)), new Op\EndTextObject());
    $y -= $size >= 18 ? 45 : 31;
}
$page = new PageObject(new Rectangle($n(0), $n(0), $n(612), $n(792)), $resources, new StreamObject(new ContentStreams(...$operators)));
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page), $collection->catalogEntry(), new Entry(new NameType('Names'), new NameDictionary($attachments->nameDictionaryEntry())));
$file = new FileStructure(new FileHeader(), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/output/embedded-files.pdf';
$directory = dirname($path);
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) {
    throw new RuntimeException('Cannot create the example output directory.');
}
$file->writeTo($path);
echo $path, PHP_EOL;

Guide source at the documented commit ↗