This lesson takes a text page and changes how its objects are stored in the file. It demonstrates the difference between compressing a stream's content and grouping dictionaries inside an object stream.
Prerequisite: read the first document. The previous lesson's gradient is independent of this topic.
Run the example
php examples/object-stream.php
Open examples/object-stream.pdf. The page displays “An explicit object stream and cross-reference stream.” Visually it remains an ordinary page; the object container becomes apparent when inspecting the PDF's structure.

The commented code explains each step. It uses a standard font and text stream like the first tutorial.
1. Compress the content stream
StreamObject receives a FlateDecode in addition to its content. This instance encodes the bytes and produces the /Filter /FlateDecode entry. The name describes the operation the reader will use to decode the stream.
/Length describes bytes after compression. Do not manually provide a length calculated from the original instructions. This compression concerns that specific stream, not every document structure.
2. Form an object container
$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());
An ObjectStream is a stream with /Type /ObjStm. The four entries select the objects stored inside. Numbers 40 to 43 are explicit to make the example inspectable; the writer reserves them and assigns other numbers without collisions.
The container carries /N, the member count, and /First, the offset at which their data start after the internal index, in the container's decoded bytes. In contrast, /Length measures the encoded bytes written to the file. withFilters() returns a new container; the script retains that value. Its FlateDecode compresses the index and grouped objects. This is a second compression, independent of page-content compression.
Members have generation zero and cannot themselves be streams. The content stream therefore remains an indirect object outside the container. The catalog and page retain their references to each other regardless of their physical representation.
3. Use a cross-reference stream
CrossReferenceFormat::Stream requests a /Type /XRef stream instead of a textual xref table. Cross-reference entries describe, among other things:
| Type | Object location |
|---|---|
| 0 | Free object. |
| 1 | Standalone indirect object, identified by its file offset. |
| 2 | Compressed member, identified by its container number and index inside it. |
An /ObjStm member requires this type 2 representation. The writer therefore rejects an object stream combined with CrossReferenceFormat::Table. /Root continues to reference the catalog even when the catalog is compressed.
Object and cross-reference streams have existed since PDF 1.5. The example uses a 1.7 header. A traditional table and a cross-reference stream are two file representations; this choice performs neither linearization nor incremental updating.
Inspect the file
With qpdf installed:
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 lets you locate objects 40 to 43 in the container. The last command rewrites the document with standalone objects and decompressed streams to make them easier to read. Object numbers may change during rewriting.
Variations
- Remove the content-stream filter to read its instructions directly in the file while retaining the compressed object container.
- Remove
->withFilters(new FlateDecode())to leave the container's index and members uncompressed while retaining their grouping in/ObjStm. - To return to the first tutorial's file, use
FileBody($catalog), remove the container and chooseCrossReferenceFormat::Table.
In the specification: filters (§7.4), object streams (§7.5.7), cross-reference streams and entry types (§7.5.8).
Previous: gradient and function · Tutorial index · Recipe: table
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.
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: 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;
