Technický článek

Zpět, znovu a obnova po pádu pro PDF v Delphi

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

Diagram funkce zpět na úrovni dokumentu PDF v Delphi, kde deník úprav PDFium Component skládá obousměrné delty nad kořenovým snímkem, přičemž každou položku chrání SHA-256 digesty během přehrávání zpět a znovu
Každá potvrzená transakce ukládá prefix, odstraněné a vložené bajty plus SHA-256 digesty obou stavů, takže jeden záznam se přehraje vpřed nebo vzad bez inverzní operace pro každou funkci

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:

Sekvence obnovy po pádu pro úpravy PDF v Delphi zobrazující atomické zápisy SaveRecoverySnapshot po každém potvrzení, pád uprostřed relace a verifikaci otisku RestoreRecoverySnapshot proti původnímu PDF
Potvrzení atomicky nahradí malý balíček zapečetěný otiskem; obnova nejdřív znovu načte původní PDF a pak ověří oba zaznamenané otisky přehráním deníku oběma směry
// 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