PDFium Component dote un éditeur PDF d'une véritable annulation et d'un véritable rétablissement au niveau du document, plus un dispositif de récupération après plantage, via un journal d'édition. Vous encapsulez les changements dans BeginEditTransaction et CommitEditTransaction, et chaque transaction validée devient un delta bidirectionnel que UndoEdit et RedoEdit peuvent rejouer dans les deux sens. SaveRecoverySnapshot persiste ensuite ce journal afin qu'une application tuée en cours de session puisse reconstruire le travail de l'utilisateur
Le vide que cela comble est précis. La capacité d'annulation native ne s'applique qu'au champ de formulaire qui a actuellement le focus, si bien qu'une zone de texte se souvient de ses propres éditions et rien d'autre ne s'en souvient. L'insertion, la suppression et le réordonnancement de pages, les changements d'objets de page, les annotations, les pièces jointes et les métadonnées n'ont aucun journal du tout. Un éditeur bâti sur cette seule base peut offrir une annulation à l'intérieur d'un champ et nulle part ailleurs, ce qui n'est pas ce que les utilisateurs entendent par annulation
Pourquoi des instantanés plutôt qu'un journal d'opérations ?
La conception évidente consiste à enregistrer des opérations sémantiques — « page 4 supprimée », « annotation 7 déplacée » — et à les inverser. Cela fonctionne jusqu'à ce que l'ensemble d'opérations grandisse, moment auquel chaque nouvelle fonctionnalité a besoin de son inverse, et toute opération dont l'inverse n'est pas exact corrompt silencieusement le document sur une longue chaîne d'annulations
Le journal capture plutôt l'état du document aux frontières des transactions et stocke la différence. Chaque delta conserve le plus long préfixe et suffixe commun et porte à la fois les octets supprimés et insérés, si bien qu'il peut s'appliquer en avant ou en arrière. Les nouvelles fonctionnalités d'édition n'ont besoin d'aucun support d'annulation dédié : quoi qu'elles changent se trouve dans l'instantané
Cette approche dépend d'un prérequis. Un enregistrement complet ordinaire produit des identifiants et des champs temporels non déterministes, si bien qu'enregistrer deux fois un document non modifié peut produire des octets différents. Les instantanés sont donc d'abord normalisés via la voie de sortie reproductible. Sans cette étape, une transaction vide ressemblerait à une modification, et l'annulation échouerait sa propre vérification puisque le condensé calculé au commit ne correspondrait plus au document ensuite
Les transactions en pratique
Chaque transaction prend une étiquette optionnelle, celle qu'un éditeur affiche dans son menu d'annulation :
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfEditJournalOptions;
Info: TPdfEditJournalInfo;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Options := TPdfEditJournalOptions.Default;
Options.MaxEntries := 50; // profondeur d'annulation
Options.MaxStoredBytes := 256 * 1024 * 1024;
Pdf.ConfigureEditJournal(Options);
Pdf.BeginEditTransaction('Delete cover page');
try
Pdf.DeletePage(1);
if not Pdf.CommitEditTransaction then
raise Exception.Create('commit failed - document was reloaded');
except
Pdf.RollbackEditTransaction;
raise;
end;
Info := Pdf.GetEditJournalInfo;
Writeln(Format('entries=%d position=%d stored=%d bytes',
[Info.EntryCount, Info.Position, Info.StoredBytes]));
if Pdf.EditCanUndo then
Pdf.UndoEdit; // la page revient
finally
Pdf.Free;
end;
end;
Un commit qui échoue — parce que la capture a échoué, qu'un budget a été dépassé ou que le delta n'a pas pu être construit — restaure la base capturée à BeginEditTransaction et recharge, plutôt que de laisser une transaction active ou un journal à moitié écrit derrière lui. C'est le mode d'échec qui compte le plus, car un journal dans un état incohérent empoisonne chaque annulation suivante
Vérification à chaque étape
Chaque delta stocke le condensé SHA-256 du document avant et après, ainsi que la longueur de préfixe et les deux charges utiles. L'annulation, le rétablissement, la relecture importée et la reconstruction inverse vérifient tous le condensé actuel et la région qu'ils s'apprêtent à remplacer avant de toucher quoi que ce soit, puis vérifient le condensé résultant ensuite
Le déplacement dans le journal est tout aussi prudent. La position n'avance qu'après que le PDF cible a été validé par un objet document indépendant et a remplacé avec succès l'actuel. Si cela échoue, l'instantané courant est restauré comme repli, si bien qu'une annulation échouée laisse l'utilisateur là où il était plutôt que dans un état cassé
Le format sérialisé est vérifié comme des données, jamais pris en confiance comme une entrée : nombre magique, version, valeurs d'entrée et de position, chaîne de condensés, longueurs d'étiquette, longueurs de correctif et budget de stockage total sont tous validés. Un journal importé est un fichier qui a pu être tronqué, édité ou produit par une version différente, et le traiter comme hostile est la seule posture sûre
Que se passe-t-il quand le journal se remplit ?
Trois limites bornent la croissance : nombre d'entrées, octets cumulés des deltas et taille d'un instantané unique. Quand la capacité est atteinte, l'entrée la plus ancienne est évincée et l'empreinte racine avance
Ce dernier point a une conséquence à laquelle il faut prêter attention lors de la conception. Un journal exporté ne peut être appliqué qu'à un document correspondant à l'empreinte racine qu'il rapporte, ou à une empreinte qu'il a enregistrée. Une fois l'éviction survenue, le journal ne remonte plus jusqu'au fichier d'origine. L'enregistrement automatique doit donc traiter l'instantané racine correspondant et le journal comme une seule unité de récupération atomique — conserver le journal sans sa base revient à conserver quelque chose qui ne peut pas être rejoué
Les dispositifs de récupération après plantage
Le dispositif de récupération n'intègre délibérément pas le PDF source. Il stocke le nom d'affichage de la source, les empreintes SHA-256 des états source et récupéré, et le journal d'édition. La récupération nécessite donc le fichier d'origine, que l'appelant charge en premier ; le dispositif vérifie ensuite les deux empreintes en rejouant le journal en arrière puis en avant :
// Après chaque commit réussi, rafraîchit le fichier de récupération
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Session suivante, après un plantage : charge l'original, puis restaure
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
Ne pas intégrer la source garde les fichiers de récupération petits, ce qui rend abordable un enregistrement automatique par commit sur un document de n'importe quelle taille. Un condensé externe couvre l'en-tête, le nom de la source et le journal, si bien qu'un dispositif tronqué ou altéré est rejeté avant même qu'un delta ne soit analysé, et le volume total de lecture est plafonné
Les écritures de fichier vont vers un fichier temporaire du répertoire cible puis sont remplacées atomiquement, et définir le chemin de récupération sur le PDF source est explicitement refusé. L'enregistrement automatique ne s'exécute qu'après un commit, une annulation ou un rétablissement réussi ayant modifié le document. Si l'écriture disque échoue, les états du document et du journal en mémoire sont déjà valides, si bien que l'exception rapporte un échec de persistance plutôt qu'une perte de données, et l'appelant peut corriger le chemin et réessayer
Les documents signés sont refusés par défaut
Un instantané de transaction est une nouvelle révision normalisée, pas les octets signés d'origine, si bien que valider une transaction sur un document signé invalide la signature. Démarrer une transaction sur un tel document est refusé sauf si l'appelant passe AllowSignedDocument, et l'import de journal ainsi que la restauration de récupération reviennent par défaut au même refus
C'est un ralentisseur délibéré plutôt qu'une interdiction. Certains flux de travail éditent légitimement un document signé puis le resignent ensuite, et l'option existe pour eux. Ce qu'elle empêche, c'est le cas accidentel, où un éditeur retire silencieusement toute validité à un document qu'un utilisateur croit encore signé. Le volet vérification de cette histoire est traité dans l'inspection des signatures numériques PDF et des niveaux PAdES
Pour un éditeur, trois habitudes rentabilisent le journal. Encapsulez chaque action visible par l'utilisateur dans une transaction étiquetée, afin que le menu d'annulation reflète le modèle mental de l'utilisateur plutôt que votre pile d'appels. Dimensionnez la limite d'entrées en fonction du document, puisque cinquante instantanés d'un scan de 200 Mo n'est pas un réglage modeste. Et écrivez l'instantané de récupération à chaque commit plutôt que sur une minuterie, car une minuterie garantit que le travail perdu est exactement tout ce qui s'est passé après le dernier tic. Les opérations d'édition au niveau du document qui en tirent parti sont traitées dans les mises à jour incrémentales de PDF chiffrés
La journalisation d'édition, les dispositifs de récupération et les primitives d'édition sous-jacentes sont livrés dans le même composant pour Delphi, C++Builder et Lazarus ; l'API complète est décrite sur la page du composant PDFium pour Delphi