PDFium Component dává editoru PDF skutečné zpět a znovu na úrovni dokumentu, plus balíček pro obnovu po pádu, přes deník úprav. Změny obalíte do BeginEditTransaction a CommitEditTransaction a každá potvrzená transakce se stane obousměrnou deltou, kterou UndoEdit a RedoEdit dokážou přehrát v obou směrech. SaveRecoverySnapshot tento deník persistuje, takže aplikace zabitá uprostřed relace dokáže znovu sestavit uživatelovu práci
Mezera, kterou toto vyplňuje, je konkrétní. Nativní schopnost zpět se vztahuje jen na pole formuláře, které má momentálně fokus, takže textové pole si pamatuje jen vlastní úpravy a nic jiného. Vkládání, mazání a přeuspořádání stránek, změny objektů stránky, anotace, přílohy a metadata nemají žádný deník vůbec. Editor postavený jen na tom umí nabídnout zpět uvnitř pole a nikde jinde, což není to, co uživatelé myslí funkcí zpět
Proč snímky místo protokolu operací?
Zjevný návrh je zaznamenávat sémantické operace — "smazána stránka 4", "přesunuta anotace 7" — a invertovat je. Funguje to, dokud množina operací neroste; pak každá nová funkce potřebuje svou inverzi a jakákoli operace, jejíž inverze není přesná, tiše poškozuje dokument v dlouhém řetězci kroků zpět
Deník místo toho zachytává stav dokumentu na hranicích transakcí a ukládá rozdíl. Každá delta si drží nejdelší společný prefix a suffix a nese odstraněné i vložené bajty, takže ji lze aplikovat vpřed i vzad. Nové editační funkce nepotřebují žádnou podporu pro zpět: cokoli mění, je uvnitř snímku
Tento přístup má jednu podmínku. Běžné plné uložení produkuje nedeterministické identifikátory a časová pole, takže dvojí uložení nezměněného dokumentu může vyprodukovat různé bajty. Snímky proto nejdřív procházejí reprodukovatelnou výstupní cestou pro normalizaci. Bez tohoto kroku by prázdná transakce vypadala jako úprava a funkce zpět by selhala u vlastní verifikace, protože digest spočítaný při potvrzení by se poté s dokumentem neshodoval
Transakce v praxi
Každá transakce přijímá volitelný popisek, což je to, co editor zobrazí ve své nabídce zpět:
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; // hloubka historie zpět
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; // stránka se vrátí zpátky
finally
Pdf.Free;
end;
end;
Potvrzení, které selže — protože se nepodařilo zachycení, byl překročen rozpočet nebo nešlo sestavit deltu — obnoví základní stav zachycený při BeginEditTransaction a znovu se načte, místo aby zanechalo aktivní transakci nebo napůl zapsaný deník. Toto je způsob selhání, na kterém záleží nejvíc, protože deník v nekonzistentním stavu otráví každé následující zpět
Verifikace při každém kroku
Každá delta ukládá SHA-256 digest dokumentu před i po, spolu s délkou prefixu a oběma payloady. Zpět, znovu, přehrání importovaného deníku i zpětná rekonstrukce vždy zkontrolují aktuální digest a oblast, kterou hodlají nahradit, dřív, než se čehokoli dotknou, a poté ověří výsledný digest
Pohyb deníkem je stejně opatrný. Pozice postoupí až poté, co byl cílový PDF ověřen nezávislým objektem dokumentu a úspěšně nahradil ten aktuální. Pokud to selže, obnoví se jako záchranná varianta aktuální snímek, takže neúspěšné zpět zanechá uživatele tam, kde byl, ne v rozbitém stavu
Serializovaný formát se kontroluje jako data, ne jako důvěryhodný vstup: magické číslo, verze, hodnoty položek a pozice, řetězec digestů, délky popisků, délky patchů a celkový rozpočet úložiště — vše se validuje. Importovaný deník je soubor, který mohl být zkrácen, upraven nebo vyprodukován jinou verzí, a zacházet s ním jako s nepřátelským je jediný bezpečný postoj
Co se stane, když se deník zaplní
Růst omezují tři limity: počet položek, kumulativní bajty delt a velikost jednoho snímku. Po dosažení kapacity se odstraní nejstarší položka a kořenový otisk postoupí
Poslední část má důsledek, se kterým je třeba počítat při návrhu. Exportovaný deník lze aplikovat jen na dokument odpovídající kořenovému otisku, který hlásí, nebo otisku, který zaznamenal. Jakmile jednou dojde k vytěsnění, deník už nedosáhne zpět k původnímu souboru. Automatické ukládání proto musí zacházet s odpovídajícím kořenovým snímkem a deníkem jako s jednou atomickou jednotkou obnovy — držet deník bez jeho základu znamená držet něco, co nelze přehrát
Balíčky pro obnovu po pádu
Balíček pro obnovu záměrně nevkládá zdrojové PDF. Ukládá zobrazovaný název zdroje, otisky SHA-256 zdrojového a obnoveného stavu a deník úprav. Obnova proto vyžaduje původní soubor, který volající načte jako první; balíček pak ověří oba otisky přehráním deníku vzad i vpřed:
// Po každém úspěšném potvrzení obnovte soubor pro obnovu
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Příští relace, po pádu: načtěte originál, pak obnovte
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
Nevkládání zdroje udržuje soubory pro obnovu malé, což je to, co dělá automatické ukládání po každém potvrzení dostupné u dokumentu jakékoli velikosti. Vnější digest pokrývá hlavičku, název zdroje a deník, takže se zkrácený nebo pozměněný balíček odmítne dřív, než se zparsuje jakákoli delta, a celkový objem čtení je omezen
Zápisy souborů míří do dočasného souboru v cílovém adresáři a pak jsou atomicky nahrazeny, a nastavení cesty pro obnovu na zdrojové PDF je explicitně odmítnuto. Automatické ukládání běží jen po úspěšném změněném potvrzení, zpět nebo znovu. Pokud selže zápis na disk, dokument a stavy deníku v paměti jsou už platné, takže výjimka hlásí selhání persistence, ne ztrátu dat, a volající může cestu opravit a zkusit to znovu
Podepsané dokumenty se ve výchozím stavu odmítají
Snímek transakce je nová normalizovaná revize, ne původní podepsané bajty, takže potvrzení transakce na podepsaném dokumentu znehodnotí podpis. Zahájení transakce na takovém dokumentu je odmítnuto, pokud volající nepředá AllowSignedDocument, a import deníku a obnova výchozí volí stejné odmítnutí
Toto je záměrná zpomalovací zábrana, ne zákaz. Některé workflow legitimně upravují podepsaný dokument a poté ho znovu podepíší, a příznak existuje pro ně. Co brání, je náhodný případ, kdy editor tiše odstraní platnost dokumentu, o kterém si uživatel myslí, že je stále podepsaný. Ověřovací stranu tohoto příběhu popisuje článek kontrola digitálních podpisů PDF a úrovní PAdES
Pro editor se tři zvyky vyplatí. Obalte každou uživatelem viditelnou akci do jedné pojmenované transakce, aby nabídka zpět odpovídala mentálnímu modelu uživatele, ne vašemu zásobníku volání. Nastavte limit počtu položek podle dokumentu, protože padesát snímků 200MB skenu není skromné nastavení. A zapisujte snímek pro obnovu při každém potvrzení místo podle časovače, protože časovač garantuje, že ztracená práce bude přesně to, co se stalo po posledním tiknutí. Operace úprav na úrovni dokumentu, které z toho těží, popisuje článek inkrementální aktualizace šifrovaného PDF
Deníkování úprav, balíčky pro obnovu a editační primitiva pod nimi jsou dodávány ve stejné komponentě pro Delphi, C++Builder a Lazarus; úplné API popisuje stránka PDFium Component pro Delphi