Un objet indirect est identifié par son numéro et sa génération. Une mise à jour peut libérer son numéro puis l'affecter à une nouvelle identité. L'ancienne référence ne doit pas désigner cette nouvelle valeur.
Exécuter object-lifecycle.php :
php examples/object-lifecycle.php /tmp/object-lifecycle.pdf
Le PDF produit contient trois révisions, une page et deux références documentaires privées. La page reste visible ; la sortie console affiche les deux générations.
Écrire la première révision
Une chaîne est référencée par le catalogue sous PrivateOriginal.
FileStructure::toRevision() retourne les octets et leurs objets gelés.
binding($old)->identifier donne l'identifiant attribué à la chaîne.
$initial = $file->toRevision();
$identifier = $initial->binding($old)->identifier;
Les objets PDF restent construits par l'application. Le contexte de sérialisation attribue leurs identifiants dans ce fichier ; aucun registre global ne relie des documents différents.
Libérer le numéro
ReleaseIndirectObject demande la libération de l'identifiant exact. Le writer
ajoute une section xref ; il ne retire aucun octet de la première révision.
La génération libre devient 1 et le numéro entre dans la liste libre.
$released = (new IncrementalUpdate(
$initial,
new FileBody(),
objectOperations: new ReleaseIndirectObject($identifier),
))->toRevision();
PrivateOriginal garde son ancienne référence de génération 0. Une référence
à un objet inexistant vaut null dans PDF. Les dictionnaires peuvent donc garder
une référence facultative obsolète. Les liens obligatoires du graphe, comme les
ressources nécessaires d'une page, sont vérifiés séparément.
Les protections natives refusent la libération du catalogue racine, des pages,
de la structure xref et du dictionnaire de chiffrement. La suppression d'un
conteneur ObjStm exige d'avoir libéré tous ses membres encore comprimés.
Affecter le numéro à une nouvelle identité
ReuseObjectNumber reçoit le numéro libre et une nouvelle valeur. Celle-ci est
ici un dictionnaire ; elle ne reprend pas les alias PHP de l'ancienne chaîne.
La nouvelle référence utilise la génération libre 1.
objectOperations: new ReuseObjectNumber($identifier->objectNumber, $new)
L'exemple remplace aussi le catalogue sous son identifiant existant pour ajouter
PrivateCurrent, une référence à ce dictionnaire. reference($old) garde le
couple original ; reference($new) donne le nouveau couple. Leurs numéros sont
égaux et leurs générations diffèrent.
Une seconde suppression incrémenterait la génération libre à 2. La génération
65535 est permanente et ne peut pas être réutilisée. Les trous de numérotation
jamais affectés sont distingués des objets supprimés : leur génération est 0.
Un numéro provenant de la liste libre ne peut pas servir à un nouveau conteneur
ou membre ObjStm, même lorsqu'il provenait d'un trou de génération 0.
Conserver la preuve de ce qui a été écrit
Chaque FileRevision expose previous, les objets effectifs, les générations
retirées et une FreeListState immuable. Les snapshots conservent les références
remappées et les dictionnaires réellement émis. Pour un flux, ils conservent les
octets encodés avant chiffrement issus du même appel d'écriture. Ces snapshots
sont des preuves ; un RevisionStreamSnapshot ne peut pas être sérialisé comme
un nouveau flux. Une nouvelle écriture demande un nouvel objet de flux explicite.
LinearizedWriter::revision() fournit aussi ce snapshot. On peut lui ajouter
une révision incrémentale ; conformément à l'annexe F, le fichier résultant cesse
d'être linéarisé. Les octets initiaux, leur /L et leurs plages d'indices restent
inchangés. Ces mécanismes portent sur les fichiers générés ; lire l'historique
d'un PDF externe appartient au module lecteur.
Vérifications et références
Le corpus de libération et réutilisation vérifie 90 historiques, soit 450 PDF, sous PHP 8.3, 8.4 et 8.5. Il contrôle les listes libres, les générations, les sections physiques xref, les anciennes et nouvelles références, les objets comprimés et neuf variantes de sécurité. L'oracle utilise qpdf sans modification et vérifie ses valeurs effectives. La limite de cache de pypdf sur certaines générations obsolètes est identifiée ; ses nouvelles valeurs et flux restent vérifiés sans exception générale.
Les révisions après linéarisation ont un corpus distinct de 90 historiques, soit 270 PDF, avec contrôle exact des préfixes, des références et des indices.
Sources conservées : objets indirects et null, page 29, xref, pages 48–50, mises à jour et ObjStm, pages 52–55, annexe F, page 683.
Parcours des tutoriels · Révisions incrémentales
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-lifecycle.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\IncrementalUpdate;
use PDF\Syntax\FileStructure\ReleaseIndirectObject;
use PDF\Syntax\FileStructure\ReuseObjectNumber;
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\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';
// ISO 7.5.4: an indirect object has both a number AND a generation.
// The optional object below demonstrates deletion and recycling.
$old = new LiteralString('Valeur indirecte initiale, generation 0.');
$font = new Type1Font(StandardFont::Helvetica);
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))));
$page = new PageObject(
new Rectangle(new IntegerType(0), new IntegerType(0), new IntegerType(612), new IntegerType(792)),
$resources,
new StreamObject(new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(18)),
new MoveTextPosition(new NumericType(50), new NumericType(720)),
new ShowText(new LiteralString('Trois revisions, un numero et deux generations.')),
new EndTextObject(),
)),
);
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page), new Entry(new NameType('PrivateOriginal'), new IndirectReference($old)));
$initial = (new FileStructure(new FileHeader(), new FileBody($catalog), crossReferenceFormat: CrossReferenceFormat::Stream))->toRevision();
$identifier = $initial->binding($old)->identifier;
// Deletion removes no historical bytes. It records the number in the
// free list with the next generation, here 1. The old generation 0
// reference now represents null, following ISO 7.3.9.
$released = (new IncrementalUpdate($initial, new FileBody(), objectOperations: new ReleaseIndirectObject($identifier)))->toRevision();
// A new identity can reuse this number with its free generation.
// Its type is independent: the old string becomes a dictionary here.
$new = new DictionaryType(new Entry(new NameType('Message'), new LiteralString('Nouvelle identite, generation 1.')));
$updatedCatalog = $released->binding($catalog)->dictionary ?? throw new LogicException('Le catalogue possède un instantané.');
$updatedCatalog = $updatedCatalog->with(new Entry(new NameType('PrivateCurrent'), new IndirectReference($new)));
$reused = (new IncrementalUpdate(
$released,
new FileBody(new IndirectObject($updatedCatalog, $initial->binding($catalog)->identifier)),
objectOperations: new ReuseObjectNumber($identifier->objectNumber, $new),
))->toRevision();
// Both references stay distinct despite sharing a number. The core
// keeps obsolete aliases to write their original generation without
// aliasing them to the new value or resurrecting the old object.
$oldReference = $reused->reference($old);
$currentReference = $reused->reference($new);
$path = $argv[1] ?? __DIR__ . '/object-lifecycle.pdf';
$reused->writeTo($path);
echo $oldReference->identifier, ' R -> null ; ', $currentReference->identifier, ' R -> dictionnaire', PHP_EOL;
echo $path, PHP_EOL;
