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.

Résultat PDF object-lifecycle : page 1 sur 1
Page 1 sur 1
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;

Source de ce guide au commit documenté ↗