Cet exemple construit un dossier PDF avec un CSV, des notes et un export JSON associé au CSV. La page du contenant explique ses relations ; les fichiers sont réellement incorporés et peuvent être extraits par un lecteur compatible.
Exécuter embedded-files.php :
php examples/embedded-files.php /tmp/embedded-files.pdf
Fournir les octets, puis décrire le fichier
EmbeddedFileStream reçoit les octets originaux et un nom de sous-type MIME.
Son dictionnaire Params calcule Size et le checksum MD5 sur ces octets,
avant les filtres et le chiffrement. Ce checksum est une donnée du format ;
il ne certifie ni l'origine du fichier ni la confiance à lui accorder.
$csv = new EmbeddedFileStream(
"article,montant\nA,12.50\nB,7.25\n",
new NameType('text/csv'),
filters: new FlateDecode(),
);
FileSpecification décrit ensuite le nom F. EmbeddedFileBindings fournit
les références indirectes de EF. Si EF.UF est choisi, il faut aussi fournir
le nom Unicode UF avec FileSpecificationName. Plusieurs noms peuvent
pointer vers le même flux. Les noms restent des valeurs PDF ; le core n'ouvre
aucun chemin pour en deviner le contenu.
Les informations facultatives restent des objets séparés : description,
identifiants, dates, caractère volatile et informations Mac. MacFileCode
interprète explicitement quatre octets ASCII comme un entier de poids fort
en premier. Le fork de ressources est lui aussi un flux fourni par l'application.
Relier un export à son fichier principal
RelatedFiles construit les couples nom/flux de RF.F. L'exemple y place
total.json. La clé F doit déjà être présente dans EF, et chaque valeur
de RF contient des couples complets dont les flux sont indirects.
EmbeddedFiles construit le vrai NameTree de Names.EmbeddedFiles.
Ses clés Mesures et Notes désignent des spécifications de fichiers,
elles-mêmes indirectes. Ces clés sont distinctes des noms mesures.csv
et notes.txt. L'ordre des clés et les limites des branches sont contrôlés
sur le graphe effectif, y compris après une mise à jour incrémentale.
Construire les métadonnées de la collection
CollectionSchema associe des noms de champs à des CollectionField.
L'exemple distingue un texte, un nombre et une date. Un CollectionItem
est construit avec ce schéma ; le nom d'un champ inconnu ou un texte placé
dans le champ numérique est refusé. FileSpecificationOptions inscrit
l'item sous CI avec une référence indirecte.
CollectionSubitem sépare la donnée D du préfixe de présentation P.
Pour les mesures, D vaut 19.75 et P vaut EUR. Le préfixe n'est pas
ajouté au nombre. Pour les notes, le préfixe existe sans D : la norme
autorise cette absence et aucun montant n'est inventé.
CollectionSort nomme ses champs dans S. Un booléen A s'applique à tous
les champs ; un tableau fournit les directions une à une. Une direction
manquante vaut true et les directions supplémentaires sont ignorées.
Ici, Amount descend et Title prend la direction ascendante par défaut.
Collection.D vaut Mesures, une clé du véritable arbre des fichiers
incorporés. Si cette clé manque, la sélection initiale revient au contenant,
comme le prévoit la norme. Le mode de vue et le schéma décrivent une interface
que le lecteur PDF peut présenter ; la page dessinée reste consultable même
dans un lecteur qui n'affiche pas cette interface.
Déclarer un flux dont les données sont externes
ExternalStreamObject traite l'autre cas de la table 5 : F désigne un fichier
externe, FFilter et FDecodeParms décrivent son décodage. Le core ne charge
pas ce fichier et n'exécute aucun encodeur de ExternalStreamFilters.
Les octets locaux entre stream et endstream sont ignorés, mais leur longueur
reste exacte. Ils restent en clair sous chiffrement PDF ; les chaînes du
dictionnaire, dont le nom de fichier, suivent le chiffrement normal.
Ce mécanisme est couvert par les tests ; l'exemple consultable emploie des fichiers incorporés pour que ses données accompagnent le PDF téléchargé.
Vérifications et références
Le corpus indépendant vérifie 90 historiques, soit 180 PDF, avec trois formats
xref, des objets comprimés, la linéarisation et les chiffrements R2 à R5.
Les contenus extraits, Size, le MD5, les forks Mac, les liens EF/RF/CI,
les limites de l'arbre et les mises à jour de valeurs sont vérifiés avec
pypdf, hashlib et qpdf. Les pages et le rendu de l'exemple sont aussi inspectés.
Sources conservées : table 5, page 28, spécifications et flux de fichiers, pages 107–115, collections, pages 378–381.
Parcours des tutoriels · Couverture vérifiée
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : 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;
