Une mise à jour incrémentale ajoute des objets et une nouvelle section de références croisées à la fin du fichier. Les octets déjà écrits restent intacts. Cet exemple crée un document, remplace deux fois son flux de contenu et conserve la police, la page et le catalogue initiaux.
Exécuter l'exemple
php examples/incremental-update.php incremental-update.pdf
Le script complet produit une page affichant « Revision 2 : contenu mis a jour. ». Le fichier contient aussi les octets des révisions 0 et 1, mais le lecteur utilise les derniers enregistrements xref pour résoudre le flux de la page.

1. Figer une révision initiale
FileStructure::toRevision() confie le graphe au FileWriter et reçoit un FileRevision. Cet objet contient les octets sérialisés, la position du dernier xref, le /Size, le catalogue, les entrées additionnelles du trailer et les identités documentaires. Il ne lit pas un fichier PDF existant.
$initial = $file->toRevision();
$identifier = $initial->binding($initialContents)->identifier;
Un numéro d'objet appartient à cette révision. binding() retrouve son identifiant à partir d'une instance ou d'un ObjectIdentifier explicite. reference() retourne une IndirectReference ordinaire vers la valeur effective ; les contraintes de type des dictionnaires restent donc disponibles.
2. Sélectionner le remplacement
Une nouvelle instance de StreamObject représente le nouveau contenu. L'IndirectObject conserve le numéro et la génération de l'ancien flux :
$update = new IncrementalUpdate(
$initial,
new FileBody(new IndirectObject($newContents, $identifier)),
);
$next = $update->toRevision();
Le FileBody sélectionne les objets à écrire. Une référence vers un objet d'une révision précédente produit seulement n g R : elle ne reparcourt pas son graphe et ne le réécrit pas. Une instance historique explicitement placée dans le FileBody est en revanche réécrite sous son identifiant existant. Modifier un builder après l'instantané ne change pas les octets déjà figés.
Les alias historiques sont conservés lors des remplacements. Une référence PHP ancienne au flux initial reste une référence vers le même numéro PDF, même après plusieurs mises à jour ; reference() retrouve désormais la nouvelle valeur effective. Les objets nouvellement référencés reçoivent des numéros à partir du /Size précédent, sans réutiliser les trous anciens, et commencent à génération zéro. Un remplacement conserve la famille PDF de la valeur et son /Type lorsqu'il était défini : les références historiques ne peuvent pas transformer silencieusement un flux en nombre ou un arbre de pages en autre dictionnaire.
3. Relier les références croisées
Le IncrementalWriter commence par les octets de $initial. Il mesure les nouveaux offsets depuis le début du fichier complet. La nouvelle entrée /Prev vise exactement la section xref de la révision précédente ; startxref vise celle qui vient d'être ajoutée.
Le format xref est hérité de la révision initiale. L'exemple utilise des flux xref ; les tables traditionnelles sont également prises en charge. Un nouveau ObjectStream peut contenir des objets nouveaux ou remplacés avec génération zéro. Un ancien conteneur n'est pas remplacé : on en construit un nouveau. Un membre auparavant compressé peut aussi devenir un objet autonome sous le même numéro.
CrossReferenceSection distingue l'index initial complet et les sections incrémentales partielles. Les numéros nouveaux inutilisés sont représentés par des entrées libres et chaînés à la liste libre existante. CrossReferenceLimits borne la capacité cumulée à 100 000 entrées par défaut ; un writer peut recevoir une limite explicitement différente.
4. Hériter du trailer
Toutes les entrées additionnelles du trailer précédent sont reprises, y compris les extensions privées. Les entrées fournies par la nouvelle révision les remplacent individuellement. Dans l'exemple, /Info vise un nouveau titre, tandis que le catalogue reste inchangé.
/Size, /Root, /Prev et les structures xref sont gérés par les writers. L'original /ID[0] est préservé. Si /ID existe et n'est pas remplacé explicitement, le second identifiant est renouvelé avec une empreinte déterministe du contenu de la révision. Cette empreinte est un identifiant de fichier ; elle ne constitue pas une signature. Un /ID explicite doit conserver le premier identifiant.
À essayer
- Utiliser
CrossReferenceFormat::Tablepour obtenir des sectionsxreftextuelles. - Ajouter une troisième mise à jour et compter les
startxrefet les/Prev. - Remplacer uniquement
/Infopour modifier les métadonnées sans réécrire le contenu de page. - Conserver une ancienne référence au flux et l'utiliser dans une nouvelle valeur pour observer la conservation de son numéro.
Vérification et limites
qpdf --check incremental-update.pdf
qpdf --show-xref incremental-update.pdf
Les révisions prises en charge proviennent des objets FileRevision produits par la bibliothèque. Le module optionnel pdftoolkit/pdf-reader permet déjà d'inspecter un PDF externe, comme le montre la leçon 12. L'import de ses objets dans le writer, leur remappage et leur conversion en révisions modifiables restent à construire. La suppression et le réemploi d'objets, les changements de format xref, les références hybrides, les fichiers chiffrés, les signatures et la linéarisation demandent leurs mécanismes propres. Une mise à jour incrémentale ne retire pas les contenus anciens du fichier : elle ne doit pas servir à effacer une information confidentielle.
Références : ISO 32000-1:2008, 7.5.4–7.5.8 pour xref, trailers, révisions et conteneurs d'objets, et 14.4 pour les identifiants de fichier.
← 10. Images JPEG et PNG · Tous les tutoriels
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é : 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;
