PDFium Component gibt einem PDF-Editor über ein Bearbeitungsjournal echtes dokumentweites Rückgängigmachen und Wiederholen sowie ein Absturzwiederherstellungspaket. Sie umschließen Änderungen mit BeginEditTransaction und CommitEditTransaction, und jede committete Transaktion wird zu einem bidirektionalen Delta, das UndoEdit und RedoEdit in beide Richtungen abspielen können. SaveRecoverySnapshot persistiert dieses Journal, sodass eine mitten in der Sitzung beendete Anwendung die Arbeit des Benutzers wiederherstellen kann
Die Lücke, die dies schließt, ist spezifisch. Die native Rückgängig-Fähigkeit gilt nur für das Formularfeld, das gerade den Fokus hat, sodass ein Textfeld seine eigenen Bearbeitungen erinnert und sonst nichts. Seiteneinfügung, -löschung und -umordnung, Änderungen an Seitenobjekten, Annotationen, Anhänge und Metadaten haben überhaupt kein Journal. Ein Editor, der allein darauf aufbaut, kann Rückgängig innerhalb eines Feldes anbieten und sonst nirgends, was nicht das ist, was Benutzer unter Rückgängig verstehen
Warum Snapshots statt eines Operationsprotokolls?
Der naheliegende Entwurf besteht darin, semantische Operationen aufzuzeichnen — „Seite 4 gelöscht“, „Annotation 7 verschoben“ — und sie umzukehren. Das funktioniert, bis der Operationssatz wächst; ab diesem Punkt braucht jedes neue Feature eine Umkehrung, und jede Operation, deren Umkehrung nicht exakt ist, beschädigt das Dokument still über eine lange Rückgängig-Kette
Das Journal erfasst stattdessen den Dokumentzustand an Transaktionsgrenzen und speichert die Differenz. Jedes Delta hält das längste gemeinsame Präfix und Suffix fest und trägt sowohl die entfernten als auch die eingefügten Bytes, sodass es vorwärts oder rückwärts angewendet werden kann. Neue Bearbeitungsfunktionen benötigen keine eigene Rückgängig-Unterstützung: Was immer sie ändern, steckt im Snapshot
Dieser Ansatz hängt von einer Voraussetzung ab. Ein gewöhnliches vollständiges Speichern erzeugt nicht-deterministische Kennungen und Zeitfelder, sodass das zweimalige Speichern eines unveränderten Dokuments unterschiedliche Bytes liefern kann. Snapshots werden daher zunächst über den reproduzierbaren Ausgabepfad normalisiert. Ohne diesen Schritt würde eine leere Transaktion wie eine Änderung aussehen, und Rückgängig würde seine eigene Verifikation nicht bestehen, weil der beim Commit berechnete Digest anschließend nicht mehr zum Dokument passen würde
Transaktionen in der Praxis
Jede Transaktion nimmt ein optionales Label entgegen, das ein Editor in seinem Rückgängig-Menü anzeigt:
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; // Rückgängig-Tiefe
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; // die Seite kommt zurück
finally
Pdf.Free;
end;
end;
Ein fehlgeschlagenes Commit — weil die Erfassung fehlschlug, ein Budget überschritten wurde oder das Delta nicht gebaut werden konnte — stellt die bei BeginEditTransaction erfasste Baseline wieder her und lädt neu, statt eine aktive Transaktion oder ein halb geschriebenes Journal zu hinterlassen. Das ist der Fehlermodus, der am meisten zählt, denn ein Journal in inkonsistentem Zustand vergiftet jedes nachfolgende Rückgängig
Verifikation bei jedem Schritt
Jedes Delta speichert den SHA-256-Digest des Dokuments vorher und nachher, zusammen mit der Präfixlänge und beiden Nutzdaten. Rückgängig, Wiederholen, importiertes Abspielen und die umgekehrte Rekonstruktion prüfen alle den aktuellen Digest und den Bereich, den sie ersetzen wollen, bevor sie irgendetwas berühren, und verifizieren anschließend den resultierenden Digest
Die Bewegung durch das Journal ist ebenso vorsichtig. Die Position rückt nur vor, nachdem das Ziel-PDF durch ein unabhängiges Dokumentobjekt validiert wurde und das aktuelle erfolgreich ersetzt hat. Schlägt das fehl, wird der aktuelle Snapshot als Fallback wiederhergestellt, sodass ein fehlgeschlagenes Rückgängig den Benutzer dort belässt, wo er war, statt in einem kaputten Zustand
Das serialisierte Format wird als Daten geprüft, nicht als Eingabe vertraut: Magic Number, Version, Eintrags- und Positionswerte, die Digest-Kette, Label-Längen, Patch-Längen und das Gesamt-Speicherbudget werden alle validiert. Ein importiertes Journal ist eine Datei, die abgeschnitten, bearbeitet oder von einer anderen Version erzeugt worden sein könnte, und sie als feindlich zu behandeln ist die einzig sichere Haltung
Was passiert, wenn das Journal voll ist
Drei Grenzen begrenzen das Wachstum: Eintragsanzahl, kumulative Delta-Bytes und die Größe eines einzelnen Snapshots. Ist die Kapazität erreicht, wird der älteste Eintrag verdrängt und der Wurzel-Fingerprint rückt vor
Dieser letzte Teil hat eine Konsequenz, um die man das Design herum gestalten sollte. Ein exportiertes Journal lässt sich nur auf ein Dokument anwenden, das zu dem von ihm gemeldeten Wurzel-Fingerprint passt, oder zu einem Fingerprint, den es erfasst hat. Sobald eine Verdrängung stattgefunden hat, reicht das Journal nicht mehr bis zur Originaldatei zurück. Autosave muss daher den passenden Wurzel-Snapshot und das Journal als eine atomare Wiederherstellungseinheit behandeln — das Journal ohne seine Basis zu behalten heißt, etwas zu behalten, das nicht abgespielt werden kann
Absturzwiederherstellungspakete
Das Wiederherstellungspaket bettet bewusst nicht das Quell-PDF ein. Es speichert den Anzeigenamen der Quelle, SHA-256-Fingerprints des Quell- und des wiederhergestellten Zustands sowie das Bearbeitungsjournal. Die Wiederherstellung erfordert daher die Originaldatei, die der Aufrufer zuerst lädt; das Paket verifiziert dann beide Fingerprints, indem es das Journal vorwärts und rückwärts abspielt:
// Nach jedem erfolgreichen Commit die Wiederherstellungsdatei aktualisieren
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Nächste Sitzung, nach einem Absturz: das Original laden, dann wiederherstellen
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
Die Quelle nicht einzubetten hält Wiederherstellungsdateien klein, was Autosave bei jedem Commit auch bei Dokumenten beliebiger Größe erschwinglich macht. Ein äußerer Digest deckt Header, Quellname und Journal ab, sodass ein abgeschnittenes oder manipuliertes Paket abgelehnt wird, bevor überhaupt ein Delta geparst wird, und das gesamte Lesevolumen ist gedeckelt
Dateischreibvorgänge gehen in eine temporäre Datei im Zielverzeichnis und werden dann atomar ersetzt, und das Setzen des Wiederherstellungspfads auf das Quell-PDF wird explizit verweigert. Autosave läuft nur nach einem erfolgreichen, ändernden Commit, Rückgängig oder Wiederholen. Schlägt das Schreiben auf die Festplatte fehl, sind die Dokument- und Journalzustände im Speicher bereits gültig, sodass die Exception einen Persistenzfehler statt eines Datenverlusts meldet, und der Aufrufer kann den Pfad korrigieren und es erneut versuchen
Signierte Dokumente werden standardmäßig verweigert
Ein Transaktions-Snapshot ist eine neue normalisierte Revision, nicht die ursprünglichen signierten Bytes, sodass das Committen einer Transaktion an einem signierten Dokument die Signatur ungültig macht. Das Beginnen einer Transaktion an einem solchen Dokument wird verweigert, sofern der Aufrufer nicht AllowSignedDocument übergibt, und Journal-Import sowie Wiederherstellung folgen standardmäßig derselben Verweigerung
Das ist eine bewusste Bremsschwelle und kein Verbot. Manche Workflows bearbeiten legitim ein signiertes Dokument und signieren es anschließend neu, und das Flag existiert für sie. Was es verhindert, ist der versehentliche Fall, in dem ein Editor stillschweigend die Gültigkeit eines Dokuments entfernt, das ein Benutzer für weiterhin signiert hält. Die Verifikationsseite dieser Geschichte ist unter Prüfen digitaler PDF-Signaturen und PAdES-Level behandelt
Für einen Editor sorgen drei Gewohnheiten dafür, dass sich das Journal lohnt. Umschließen Sie jede für den Benutzer sichtbare Aktion in einer beschrifteten Transaktion, damit sich das Rückgängig-Menü wie das mentale Modell des Benutzers liest statt wie Ihr Call-Stack. Bemessen Sie das Eintragslimit am Dokument, denn fünfzig Snapshots eines 200-MB-Scans sind keine bescheidene Einstellung. Und schreiben Sie den Wiederherstellungs-Snapshot bei jedem Commit statt nach einem Timer, denn ein Timer garantiert, dass die verlorene Arbeit genau das ist, was nach dem letzten Tick passiert ist. Dokumentweite Bearbeitungsoperationen, die davon profitieren, sind unter inkrementelle Updates verschlüsselter PDFs behandelt
Bearbeitungsjournal, Wiederherstellungspakete und die zugrundeliegenden Bearbeitungsprimitiven werden in derselben Komponente für Delphi, C++Builder und Lazarus ausgeliefert; die vollständige API ist auf der Seite der PDFium Component für Delphi beschrieben