PDFium Component daje edytorowi PDF prawdziwe cofanie i ponawianie na poziomie dokumentu, plus pakiet odzyskiwania po awarii, za pomocą dziennika edycji. Zmiany opakowujesz w BeginEditTransaction i CommitEditTransaction, a każda zatwierdzona transakcja staje się dwukierunkową deltą, którą UndoEdit i RedoEdit mogą odtworzyć w dowolnym kierunku. SaveRecoverySnapshot zapisuje ten dziennik trwale, dzięki czemu aplikacja zabita w połowie sesji może odbudować pracę użytkownika
Luka, którą to wypełnia, jest konkretna. Natywna zdolność cofania dotyczy tylko pola formularza, które akurat ma fokus, więc pole tekstowe pamięta swoje własne edycje i nic poza tym. Wstawianie, usuwanie i zmiana kolejności stron, zmiany obiektów strony, adnotacje, załączniki i metadane w ogóle nie mają dziennika. Edytor zbudowany wyłącznie na tym może oferować cofanie wewnątrz pola i nigdzie indziej, co nie jest tym, co użytkownicy rozumieją przez cofanie
Dlaczego migawki, a nie dziennik operacji?
Oczywistym projektem jest rejestrowanie operacji semantycznych — „usunięto stronę 4”, „przesunięto adnotację 7” — i ich odwracanie. Działa to, dopóki zbiór operacji nie urośnie, a wtedy każda nowa funkcja potrzebuje swojej odwrotności, a każda operacja, której odwrotność nie jest dokładna, po cichu psuje dokument w długim łańcuchu cofania
Dziennik zamiast tego przechwytuje stan dokumentu na granicach transakcji i przechowuje różnicę. Każda delta zachowuje najdłuższy wspólny prefiks i sufiks oraz niesie zarówno usunięte, jak i wstawione bajty, dzięki czemu można ją zastosować do przodu albo do tyłu. Nowe funkcje edycyjne nie potrzebują żadnej obsługi cofania: cokolwiek zmieniają, znajduje się wewnątrz migawki
To podejście zależy od jednego warunku wstępnego. Zwykły pełny zapis generuje niedeterministyczne identyfikatory i pola czasu, więc dwukrotny zapis niezmodyfikowanego dokumentu może dać różne bajty. Dlatego migawki są najpierw normalizowane przez ścieżkę odtwarzalnego wyjścia. Bez tego kroku pusta transakcja wyglądałaby jak modyfikacja, a cofanie nie przeszłoby własnej weryfikacji, ponieważ skrót obliczony przy zatwierdzeniu nie pasowałby potem do dokumentu
Transakcje w praktyce
Każda transakcja przyjmuje opcjonalną etykietę, którą edytor pokazuje w swoim menu cofania:
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; // głębokość cofania
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; // strona wraca
finally
Pdf.Free;
end;
end;
Zatwierdzenie, które zawodzi — ponieważ przechwytywanie się nie powiodło, przekroczono budżet albo nie udało się zbudować delty — przywraca punkt odniesienia przechwycony przy BeginEditTransaction i ponownie wczytuje dokument, zamiast pozostawiać aktywną transakcję albo częściowo zapisany dziennik. To najważniejszy tryb błędu, ponieważ dziennik w niespójnym stanie zatruwa każde następne cofnięcie
Weryfikacja na każdym kroku
Każda delta przechowuje skrót SHA-256 dokumentu przed i po, wraz z długością prefiksu i obydwoma ładunkami. Cofanie, ponawianie, odtwarzanie zaimportowanego dziennika i odwrotna rekonstrukcja sprawdzają bieżący skrót i region, który zamierzają zastąpić, zanim czegokolwiek dotkną, a następnie weryfikują wynikowy skrót po fakcie
Poruszanie się po dzienniku jest równie ostrożne. Pozycja przesuwa się dopiero po zwalidowaniu docelowego PDF przez niezależny obiekt dokumentu i po pomyślnym zastąpieniu nim bieżącego. Jeśli to się nie powiedzie, jako zabezpieczenie przywracana jest bieżąca migawka, więc nieudane cofnięcie pozostawia użytkownika tam, gdzie był, a nie w zepsutym stanie
Zserializowany format jest sprawdzany jako dane, a nie ufany jako wejście: numer magiczny, wersja, wartości wpisów i pozycji, łańcuch skrótów, długości etykiet, długości łatek i całkowity budżet pamięci są wszystkie walidowane. Zaimportowany dziennik to plik, który mógł zostać obcięty, zedytowany albo wyprodukowany przez inną wersję, a traktowanie go jako wrogiego jest jedyną bezpieczną postawą
Co się dzieje, gdy dziennik się zapełni
Trzy limity ograniczają wzrost: liczbę wpisów, skumulowaną liczbę bajtów delty i rozmiar pojedynczej migawki. Gdy pojemność zostaje osiągnięta, najstarszy wpis jest usuwany, a odcisk główny przesuwa się dalej
Ta ostatnia część ma konsekwencję, wokół której warto zaprojektować rozwiązanie. Wyeksportowany dziennik można zastosować tylko do dokumentu pasującego do zgłaszanego przez niego odcisku głównego albo do zarejestrowanego przez niego odcisku. Gdy nastąpi usunięcie wpisu, dziennik nie sięga już z powrotem do oryginalnego pliku. Autozapis musi więc traktować pasującą migawkę główną i dziennik jako jedną atomową jednostkę odzyskiwania — zachowanie dziennika bez jego podstawy oznacza zachowanie czegoś, czego nie da się odtworzyć
Pakiety odzyskiwania po awarii
Pakiet odzyskiwania celowo nie osadza źródłowego PDF. Przechowuje wyświetlaną nazwę źródła, odciski SHA-256 stanu źródłowego i odzyskanego oraz dziennik edycji. Odzyskiwanie wymaga więc oryginalnego pliku, który wywołujący wczytuje jako pierwszy; pakiet następnie weryfikuje oba odciski, odtwarzając dziennik do tyłu i do przodu:
// Po każdym udanym zatwierdzeniu odśwież plik odzyskiwania
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Następna sesja, po awarii: wczytaj oryginał, a następnie przywróć
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
Nieosadzanie źródła utrzymuje pliki odzyskiwania w małym rozmiarze, co sprawia, że autozapis przy każdym zatwierdzeniu jest opłacalny na dokumencie dowolnej wielkości. Zewnętrzny skrót obejmuje nagłówek, nazwę źródła i dziennik, więc obcięty albo zmanipulowany pakiet jest odrzucany, zanim jakakolwiek delta zostanie sparsowana, a całkowita objętość odczytu jest ograniczona
Zapisy plików trafiają do pliku tymczasowego w katalogu docelowym, a następnie są atomowo zastępowane, a ustawienie ścieżki odzyskiwania na źródłowy PDF jest jawnie odrzucane. Autozapis uruchamia się tylko po udanym zatwierdzeniu zmiany, cofnięciu albo ponowieniu. Jeśli zapis na dysk się nie powiedzie, stany dokumentu i dziennika w pamięci są już prawidłowe, więc wyjątek zgłasza błąd trwałości, a nie utratę danych, i wywołujący może naprawić ścieżkę i spróbować ponownie
Podpisane dokumenty są domyślnie odrzucane
Migawka transakcji to nowa znormalizowana rewizja, a nie oryginalne podpisane bajty, więc zatwierdzenie transakcji na podpisanym dokumencie unieważnia podpis. Rozpoczęcie transakcji na takim dokumencie jest odrzucane, chyba że wywołujący przekaże AllowSignedDocument, a import dziennika i przywracanie odzyskiwania domyślnie stosują tę samą odmowę
To celowe spowolnienie, a nie zakaz. Niektóre przepływy pracy zasadnie edytują podpisany dokument i podpisują go ponownie później, a ta flaga istnieje właśnie dla nich. To, czemu zapobiega, to przypadek przypadkowy, w którym edytor po cichu pozbawia ważności dokument, o którym użytkownik sądzi, że wciąż jest podpisany. Stronę weryfikacji tej historii opisano w inspekcji podpisów cyfrowych PDF i poziomów PAdES
Dla edytora trzy nawyki sprawiają, że dziennik się opłaca. Opakuj każdą widoczną dla użytkownika akcję w jedną etykietowaną transakcję, aby menu cofania czytało się jak mentalny model użytkownika, a nie jak twój stos wywołań. Dostosuj limit wpisów do dokumentu, ponieważ pięćdziesiąt migawek 200-megabajtowego skanu to nieskromne ustawienie. I zapisuj migawkę odzyskiwania przy każdym zatwierdzeniu, a nie na czasomierzu, ponieważ czasomierz gwarantuje, że utracona praca to dokładnie to, co wydarzyło się po ostatnim tyknięciu. Operacje edycji na poziomie dokumentu, które na tym korzystają, opisano w przyrostowych aktualizacjach zaszyfrowanego PDF
Dziennikowanie edycji, pakiety odzyskiwania i stojące za nimi prymitywy edycyjne są dostarczane w tym samym komponencie dla Delphi, C++Builder i Lazarus; pełne API opisano na stronie PDFium Component dla Delphi