Cette leçon reprend une page de texte et change la manière dont ses objets sont rangés dans le fichier. Elle montre la différence entre compresser le contenu d'un flux et regrouper des dictionnaires dans un flux d'objets.
Prérequis : avoir lu le premier document. Le dégradé de la leçon précédente est indépendant de ce sujet.
Exécuter l'exemple
php examples/object-stream.php
Ouvrir examples/object-stream.pdf. La page affiche « An explicit object stream and cross-reference stream. ». Le résultat visuel reste une page ordinaire ; le conteneur d'objets se constate en examinant la structure du PDF.

Le code commenté précise chaque étape. Il utilise une police standard et un flux de texte comme le premier tutoriel.
1. Compresser le flux de contenu
Le StreamObject reçoit un FlateDecode en plus de son contenu. Cette instance encode les octets et conduit à l'entrée /Filter /FlateDecode. Le nom est celui de l'opération que le lecteur utilisera pour décoder le flux.
/Length décrit les octets après compression. On ne fournit donc pas à la main une longueur calculée sur les instructions originales. Cette compression concerne ce flux précis, pas toutes les structures du document.
2. Former un conteneur d'objets
$objects = (new ObjectStream(
new ObjectStreamEntry(new ObjectNumber(40), $catalog),
new ObjectStreamEntry(new ObjectNumber(41), $pages),
new ObjectStreamEntry(new ObjectNumber(42), $page),
new ObjectStreamEntry(new ObjectNumber(43), $font),
))->withFilters(new FlateDecode());
Un ObjectStream est un flux /Type /ObjStm. Les quatre entrées choisissent les objets qui seront rangés à l'intérieur. Les numéros 40 à 43 sont explicites pour rendre l'exemple inspectable ; le writer les réserve et attribue les autres numéros sans collision.
Le conteneur porte /N, le nombre de membres, et /First, l'offset du début de leurs données après l'index interne, dans les octets décodés du conteneur. À l'inverse, /Length mesure les octets encodés écrits dans le fichier. withFilters() retourne un nouveau conteneur ; le script conserve cette valeur. Son FlateDecode compresse l'index et les objets regroupés. Il s'agit d'une seconde compression indépendante de celle du contenu de page.
Les membres ont une génération zéro et ne peuvent pas être eux-mêmes des flux. Le flux de contenu demeure donc un objet indirect hors de ce conteneur. Le catalogue et la page conservent les mêmes références entre eux, quelle que soit leur représentation physique.
3. Utiliser un flux de références croisées
CrossReferenceFormat::Stream demande un flux /Type /XRef à la place de la table textuelle xref. Les entrées de références croisées décrivent notamment :
| Type | Emplacement d'un objet |
|---|---|
| 0 | Objet libre. |
| 1 | Objet indirect autonome, identifié par son offset dans le fichier. |
| 2 | Membre compressé, identifié par le numéro du conteneur et son index à l'intérieur. |
Un membre de /ObjStm nécessite cette représentation de type 2. Le writer refuse donc un flux d'objets associé à CrossReferenceFormat::Table. /Root continue à référencer le catalogue, même lorsque ce dernier est compressé.
Les flux d'objets et de références croisées existent depuis PDF 1.5. L'exemple utilise un header 1.7. Une table traditionnelle et un flux de références croisées sont deux représentations du fichier ; ce choix ne réalise ni linéarisation ni mise à jour incrémentale.
Examiner le fichier
Avec qpdf installé :
qpdf --check examples/object-stream.pdf
qpdf --show-xref examples/object-stream.pdf
qpdf --qdf --object-streams=disable examples/object-stream.pdf objets-decompresses.pdf
--show-xref permet de repérer les objets 40 à 43 dans le conteneur. La dernière commande réécrit le document avec des objets autonomes et des flux décompressés pour faciliter leur lecture. Les numéros peuvent changer lors de cette réécriture.
Variantes
- Retirer le filtre du flux de contenu permet de lire directement ses instructions dans le fichier, tout en conservant le conteneur d'objets compressé.
- Retirer
->withFilters(new FlateDecode())laisse l'index et les membres du conteneur non compressés, tout en conservant leur regroupement dans/ObjStm. - Pour revenir au fichier du premier tutoriel, utiliser
FileBody($catalog), retirer le conteneur et choisirCrossReferenceFormat::Table.
Dans la norme : filtres (§7.4), flux d'objets (§7.5.7), flux de références croisées et types d'entrées (§7.5.8).
Précédent : dégradé et fonction · Index des tutoriels · Recette : tableau
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é : object-stream.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\CrossReferenceFormat;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\FileStructure\ObjectStream;
use PDF\Syntax\FileStructure\ObjectStreamEntry;
use PDF\Syntax\Filters\FlateDecode;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectObject\ObjectNumber;
use PDF\Syntax\Object\IndirectReference;
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';
// Tutorial 08: docs/tutorials/08-object-stream.md.
// Keep the first tutorial's graph and change its file representation:
// some dictionaries are stored inside an /ObjStm.
// 1. Prepare the font and page resources, as in tutorial 01.
$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(new Font(new DictionaryType(
new Entry(new NameType('F1'), new IndirectReference($font)),
)));
// 2. Build text and compress its bytes with FlateDecode.
// The filter is named for the operation readers apply when decoding.
// It does not compress other file objects: each stream has its own filters.
$contents = new StreamObject(new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(18)),
new MoveTextPosition(new NumericType(72), new NumericType(720)),
new ShowText(new LiteralString('An explicit object stream and cross-reference stream.')),
new EndTextObject(),
), null, new FlateDecode());
// 3. Assemble the page, its page tree and catalog.
$page = new PageObject(
new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)),
$resources,
$contents,
);
$pages = (new PageTreeNode())->addKid($page);
$catalog = new DocumentCatalog($pages);
// 4. Choose objects that will be members of the /ObjStm stream.
// Numbers 40 to 43 are explicitly reserved; the writer avoids collisions
// when assigning other numbers. Every member has generation zero.
// $contents, itself a stream, remains an indirect object outside the container.
// The second FlateDecode compresses the whole container independently of content.
$objects = (new ObjectStream(
new ObjectStreamEntry(new ObjectNumber(40), $catalog),
new ObjectStreamEntry(new ObjectNumber(41), $pages),
new ObjectStreamEntry(new ObjectNumber(42), $page),
new ObjectStreamEntry(new ObjectNumber(43), $font),
))->withFilters(new FlateDecode());
// 5. Select a cross-reference stream (/Type /XRef).
// Its type 2 entries identify the container and compressed-object indices.
// /Root still targets the catalog even when it is inside /ObjStm.
// Both representations have existed since PDF 1.5; our header is 1.7.
$file = new FileStructure(
new FileHeader('1.7'),
new FileBody($objects),
new IndirectReference($catalog),
null,
CrossReferenceFormat::Stream,
);
// 6. Write the PDF. Page rendering depends on content and resources,
// while /ObjStm and /XRef organize objects within the file's bytes.
$path = $argv[1] ?? __DIR__ . '/object-stream.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;
