Teknisk artikel

PDF fortryd, gentag og nedbrudsgendannelse i Delphi

PDFium Component giver en PDF-editor ægte fortryd og gentag på dokumentniveau, plus en nedbrudsgendannelsespakke, via en redigeringsjournal. Du pakker ændringer ind i BeginEditTransaction og CommitEditTransaction, og hver committet transaktion bliver et bidirektionelt delta, som UndoEdit og RedoEdit kan afspille i begge retninger. SaveRecoverySnapshot gemmer den journal, så en applikation, der bliver dræbt midt i en session, kan genopbygge brugerens arbejde

Det hul, dette udfylder, er specifikt. Den native fortryd-kapacitet gælder kun det formularfelt, der aktuelt har fokus, så en tekstboks husker sine egne redigeringer, og intet andet gør. Sideindsættelse, -sletning og -omordning, ændringer af sideobjekter, annotationer, vedhæftninger og metadata har slet ingen journal. En editor bygget på det alene kan tilbyde fortryd inde i et felt og ingen andre steder, hvilket ikke er, hvad brugere mener med fortryd

Hvorfor snapshots frem for en operationslog?

Det oplagte design er at registrere semantiske operationer — "slettede side 4", "flyttede annotation 7" — og invertere dem. Det virker, indtil operationssættet vokser, hvorefter hver ny funktion har brug for en invers, og enhver operation, hvis invers ikke er eksakt, stiltiende korrumperer dokumentet over en lang fortryd-kæde

Journalen fanger i stedet dokumenttilstanden ved transaktionsgrænser og gemmer forskellen. Hvert delta bevarer det længste fælles præfiks og suffiks og bærer både de fjernede og de indsatte bytes, så det kan anvendes fremad eller baglæns. Nye redigeringsfunktioner har ikke brug for fortryd-understøttelse: hvad end de ændrer, er inde i snapshottet

Diagram af fortryd på dokumentniveau for PDF i Delphi, hvor PDFium Components redigeringsjournal stabler bidirektionelle deltaer over et rod-snapshot, hver entry sikret af SHA-256-digests under fortryd- og gentag-afspilning
Hver committet transaktion gemmer præfiks, fjernede og indsatte bytes plus SHA-256-digests af begge tilstande, så én post afspilles fremad eller baglæns uden en invers operation pr. funktion

Den tilgang afhænger af én forudsætning. En almindelig fuld gemning producerer ikke-deterministiske identifikatorer og tidsfelter, så at gemme et uændret dokument to gange kan give forskellige bytes. Snapshots normaliseres derfor først gennem den reproducerbare output-sti. Uden det trin ville en tom transaktion ligne en ændring, og fortryd ville fejle sin egen verifikation, fordi det digest, den beregnede ved commit, ikke ville matche dokumentet bagefter

Transaktioner i praksis

Hver transaktion tager en valgfri label, hvilket er, hvad en editor viser i sin fortryd-menu:

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;               // fortryd-dybde
    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;                          // siden kommer tilbage
  finally
    Pdf.Free;
  end;
end;

En commit, der fejler — fordi indfangning fejlede, et budget blev overskredet, eller deltaet ikke kunne bygges — gendanner baseline'en fanget ved BeginEditTransaction og genindlæser, frem for at efterlade en aktiv transaktion eller en halvskrevet journal. Dette er den fejltilstand, der betyder mest, fordi en journal i en inkonsistent tilstand forgifter hver efterfølgende fortryd

Verifikation ved hvert trin

Hvert delta gemmer SHA-256-digestet af dokumentet før og efter, sammen med præfikslængden og begge payloads. Fortryd, gentag, importeret afspilning og omvendt rekonstruktion tjekker alle det aktuelle digest og den region, de har til hensigt at erstatte, før de rører ved noget, og verificerer derefter det resulterende digest bagefter

Bevægelse gennem journalen er lige så forsigtig. Positionen rykker kun frem, efter at mål-PDF'en er blevet valideret af et uafhængigt dokumentobjekt og med succes har erstattet den aktuelle. Hvis det fejler, gendannes det aktuelle snapshot som fallback, så en mislykket fortryd efterlader brugeren, hvor de var, frem for i en ødelagt tilstand

Det serialiserede format tjekkes som data, ikke tiltroet som input: magisk nummer, version, entry- og positionsværdier, digest-kæden, label-længder, patch-længder og det samlede lagerbudget valideres alle. En importeret journal er en fil, der kan være blevet afkortet, redigeret eller produceret af en anden version, og at behandle den som fjendtlig er den eneste sikre holdning

Hvad sker der, når journalen bliver fuld

Tre grænser afgrænser væksten: entry-antal, kumulerede delta-bytes og størrelsen af et enkelt snapshot. Når kapaciteten er nået, udsættes den ældste entry, og rod-fingeraftrykket rykker frem

Den sidste del har en konsekvens, det er værd at designe omkring. En eksporteret journal kan kun anvendes på et dokument, der matcher det rod-fingeraftryk, den rapporterer, eller et fingeraftryk, den registrerede. Når udsættelse først er sket, rækker journalen ikke længere tilbage til den originale fil. Autosave er derfor nødt til at behandle det matchende rod-snapshot og journalen som én atomar gendannelsesenhed — at beholde journalen uden dens grundlag er at beholde noget, der ikke kan afspilles

Nedbrudsgendannelsespakker

Gendannelsespakken indlejrer bevidst ikke kilde-PDF'en. Den gemmer kildens visningsnavn, SHA-256-fingeraftryk af kilde- og genoprettede tilstande, og redigeringsjournalen. Gendannelse kræver derfor den originale fil, som den kaldende part indlæser først; pakken verificerer derefter begge fingeraftryk ved at afspille journalen baglæns og fremad:

Nedbrudsgendannelsessekvens for PDF-redigering i Delphi, der viser atomare SaveRecoverySnapshot-skrivninger efter hver commit, et nedbrud midt i sessionen, og RestoreRecoverySnapshot-fingeraftryksverifikation mod den originale PDF
Commits erstatter atomart en lille fingeraftryksforseglet pakke; gendannelse genindlæser først den originale PDF og verificerer derefter begge registrerede fingeraftryk ved at afspille journalen begge veje
// Efter hver vellykket commit skal gendannelsesfilen opdateres
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
  Pdf.SaveRecoverySnapshot('contract.recovery');

// Næste session, efter et nedbrud: indlæs originalen, og gendan så
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');

At ikke indlejre kilden holder gendannelsesfiler små, hvilket er, hvad der gør autosave pr. commit overkommeligt på et dokument af enhver størrelse. Et ydre digest dækker headeren, kildenavnet og journalen, så en afkortet eller manipuleret pakke afvises, før noget delta parses, og det samlede læsevolumen er begrænset

Filskrivninger går til en midlertidig fil i målmappen og erstattes derefter atomart, og det er eksplicit afvist at sætte gendannelsesstien til kilde-PDF'en. Autosave kører kun efter en vellykket, ændret commit, fortryd eller gentag. Hvis diskskrivningen fejler, er dokument- og journaltilstandene i hukommelsen allerede gyldige, så exceptionen rapporterer en persisteringsfejl frem for et datatab, og den kaldende part kan rette stien og prøve igen

Signerede dokumenter afvises som standard

Et transaktionssnapshot er en ny normaliseret revision, ikke de originale signerede bytes, så at committe en transaktion på et signeret dokument ugyldiggør signaturen. At begynde en transaktion på et sådant dokument afvises, medmindre den kaldende part angiver AllowSignedDocument, og journalimport og gendannelses-restore har som standard den samme afvisning

Dette er en bevidst fartbump, ikke et forbud. Nogle arbejdsforløb redigerer legitimt et signeret dokument og gensignerer det bagefter, og flaget findes til dem. Hvad det forhindrer, er det utilsigtede tilfælde, hvor en editor stiltiende fjerner gyldigheden fra et dokument, en bruger tror stadig er signeret. Verifikationssiden af den historie er dækket i inspektion af digitale PDF-signaturer og PAdES-niveauer

For en editor er der tre vaner, der får journalen til at betale sig. Pak hver brugersynlig handling ind i én labelbelagt transaktion, så fortryd-menuen læses som brugerens mentale model frem for som din call stack. Dimensionér entry-grænsen efter dokumentet, da halvtreds snapshots af en 200 MB-scanning ikke er en beskeden indstilling. Og skriv gendannelsessnapshottet ved hver commit frem for på en timer, fordi en timer garanterer, at det tabte arbejde er præcis, hvad der skete efter det seneste tick. Redigeringsoperationer på dokumentniveau, der drager fordel af dette, er dækket i inkrementelle opdateringer af krypterede PDF'er

Redigeringsjournalisering, gendannelsespakker og redigeringsprimitiverne bag dem leveres i samme komponent til Delphi, C++Builder og Lazarus; det fulde API er beskrevet på PDFium Component for Delphi-siden