An incremental update appends objects and a new cross-reference section to the file. Previously written bytes remain intact. This example creates a document, replaces its content stream twice and preserves the original font, page and catalog.
Run the example
php examples/incremental-update.php incremental-update.pdf
The complete script produces a page displaying “Revision 2 : contenu mis a jour.” The file also contains bytes from revisions 0 and 1, but the reader uses the latest xref records to resolve the page's stream.

1. Freeze an initial revision
FileStructure::toRevision() passes the graph to FileWriter and receives a
FileRevision. This object contains serialized bytes, the last xref position,
/Size, catalog, additional trailer entries and document identities. It does
not read an existing PDF file.
$initial = $file->toRevision();
$identifier = $initial->binding($initialContents)->identifier;
An object number belongs to that revision. binding() retrieves its identifier
from an instance or explicit ObjectIdentifier. reference() returns an ordinary
IndirectReference to the effective value, so dictionary type constraints remain available.
2. Select the replacement
A new StreamObject instance represents new content. IndirectObject preserves
the old stream's number and generation:
$update = new IncrementalUpdate(
$initial,
new FileBody(new IndirectObject($newContents, $identifier)),
);
$next = $update->toRevision();
FileBody selects objects to write. A reference to an object in an earlier
revision produces only n g R: it does not traverse or rewrite that graph.
A historical instance explicitly placed in FileBody, however, is rewritten
under its existing identifier. Changing a builder after the snapshot does not
change already frozen bytes.
Historical aliases are retained during replacement. An old PHP reference to
the initial stream remains a reference to the same PDF number, even after
several updates; reference() now returns the new effective value. Newly
referenced objects receive numbers starting at the previous /Size, without
reusing old gaps, and begin with generation zero. A replacement preserves the
value's PDF family and its /Type if defined: historical references cannot
silently turn a stream into a number or a page tree into another dictionary.
3. Link cross-references
IncrementalWriter starts with $initial's bytes. It measures new offsets from
the beginning of the complete file. The new /Prev entry targets exactly the
previous revision's xref section; startxref targets the newly appended section.
The xref format is inherited from the initial revision. This example uses xref
streams; traditional tables are supported too. A new ObjectStream can contain
new or replaced objects with generation zero. An old container is not replaced:
a new one is constructed. A previously compressed member can also become a
standalone object under the same number.
CrossReferenceSection distinguishes the complete initial index from partial
incremental sections. Unused new numbers are represented by free entries and
linked to the existing free list. CrossReferenceLimits bounds cumulative
capacity to 100,000 entries by default; a writer can receive an explicitly different limit.
4. Inherit the trailer
All additional entries from the previous trailer are carried forward, including
private extensions. Entries supplied by the new revision replace them individually.
In the example, /Info targets a new title while the catalog stays unchanged.
Writers manage /Size, /Root, /Prev and xref structures. Original /ID[0]
is preserved. If /ID exists and is not explicitly replaced, its second identifier
is renewed using a deterministic fingerprint of revision content. This fingerprint
is a file identifier, not a signature. An explicit /ID must preserve the first identifier.
Try it
- Use
CrossReferenceFormat::Tableto obtain textualxrefsections. - Add a third update and count
startxrefand/Prevoccurrences. - Replace only
/Infoto change metadata without rewriting page content. - Keep an old stream reference and use it in a new value to observe its number being preserved.
Verification and limitations
qpdf --check incremental-update.pdf
qpdf --show-xref incremental-update.pdf
Supported revisions come from FileRevision objects produced by the library.
The optional pdftoolkit/pdf-reader module can already inspect external PDFs,
as lesson 12 demonstrates. Importing their objects into
the writer, remapping them and converting them into editable revisions remain
to be built. Object deletion
and reuse, xref-format changes, hybrid references, encrypted files, signatures
and linearization require their own mechanisms. An incremental update does not
remove old content from the file: it must not be used to erase confidential information.
References: ISO 32000-1:2008, 7.5.4–7.5.8 for xref, trailers, revisions and object containers, and 14.4 for file identifiers.
← 10. JPEG and PNG images · All tutorials
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: incremental-update.php
<?php
declare(strict_types=1);
use PDF\Document\DocumentInformation;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\Information\Title;
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\DataStructure\TextString;
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\IncrementalUpdate;
use PDF\Syntax\Object\ArrayType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectObject;
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\HexadecimalString;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// Tutorial 11: all three stream versions remain in the file's bytes.
// Readers follow the latest xref and display only revision 2 content.
$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(new Font(new DictionaryType(
new Entry(new NameType('F1'), new IndirectReference($font)),
)));
$initialContents = new StreamObject(new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(20)),
new MoveTextPosition(new NumericType(64), new NumericType(700)),
new ShowText(new LiteralString('Revision 0 : document initial.')),
new EndTextObject(),
));
$page = new PageObject(
new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)),
$resources,
$initialContents,
);
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page));
$information = new DocumentInformation(new Title(new TextString('Document initial')));
$initial = (new FileStructure(
new FileHeader('1.7'),
new FileBody($catalog),
trailerEntries: new DictionaryType(
new Entry(new NameType('Info'), new IndirectReference($information)),
new Entry(new NameType('ID'), new ArrayType(
new HexadecimalString('incremental-example-original'),
new HexadecimalString('incremental-example-revision-0'),
)),
),
crossReferenceFormat: CrossReferenceFormat::Stream,
))->toRevision();
// The identifier belongs to the snapshot, not a global counter.
// It is preserved: replacing a live object does not change its generation.
$contentIdentifier = $initial->binding($initialContents)->identifier;
$revision = $initial;
foreach ([1, 2] as $number) {
$contents = new StreamObject(new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(22)),
new MoveTextPosition(new NumericType(64), new NumericType(700)),
new ShowText(new LiteralString('Revision ' . $number . ' : contenu mis a jour.')),
new EndTextObject(),
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
new MoveTextPosition(new NumericType(64), new NumericType(646)),
new ShowText(new LiteralString('La police, la page et le catalogue restent les objets initiaux.')),
new EndTextObject(),
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
new MoveTextPosition(new NumericType(64), new NumericType(614)),
new ShowText(new LiteralString('Les anciens octets sont conserves ; Prev relie les trois xref.')),
new EndTextObject(),
));
// FileBody explicitly selects the stream to replace. A historical
// reference alone would not republish any already recorded object.
$updatedInformation = new DocumentInformation(new Title(new TextString('Révision incrémentale ' . $number)));
$revision = (new IncrementalUpdate(
$revision,
new FileBody(new IndirectObject($contents, $contentIdentifier)),
trailerEntries: new DictionaryType(new Entry(new NameType('Info'), new IndirectReference($updatedInformation))),
))->toRevision();
}
// The first ID stays identical; the writer renews the second each time.
// Initial-revision bytes always form the PDF's exact prefix.
if (!str_starts_with($revision->bytes, $initial->bytes)) {
throw new LogicException('The initial revision must remain byte-for-byte intact.');
}
$path = $argv[1] ?? __DIR__ . '/incremental-update.pdf';
$revision->writeTo($path);
echo $path . PHP_EOL;
