O PDFium Component dá a um editor de PDF um desfazer e refazer reais em nível de documento, além de um pacote de recuperação de falhas, por meio de um journal de edição. Você envolve mudanças com BeginEditTransaction e CommitEditTransaction, e cada transação confirmada se torna um delta bidirecional que UndoEdit e RedoEdit podem reproduzir em qualquer direção. SaveRecoverySnapshot persiste esse journal, de modo que um aplicativo encerrado no meio de uma sessão consiga reconstruir o trabalho do usuário
A lacuna que isso preenche é específica. A capacidade nativa de desfazer se aplica apenas ao campo de formulário que atualmente tem o foco, de modo que uma caixa de texto lembra suas próprias edições e nada mais lembra. Inserção, exclusão e reordenação de páginas, mudanças em objetos de página, anotações, anexos e metadados não têm journal nenhum. Um editor construído apenas com isso pode oferecer desfazer dentro de um campo e em lugar nenhum mais, o que não é o que os usuários entendem por desfazer
Por que snapshots em vez de um log de operações?
O design óbvio é registrar operações semânticas — "página 4 excluída", "anotação 7 movida" — e inverte-las. Isso funciona até o conjunto de operações crescer, ponto em que todo novo recurso precisa de uma inversa, e qualquer operação cuja inversa não seja exata corrompe silenciosamente o documento ao longo de uma longa cadeia de desfazer
O journal, em vez disso, captura o estado do documento nos limites das transações e armazena a diferença. Cada delta mantém o maior prefixo e sufixo comuns e carrega tanto os bytes removidos quanto os inseridos, de modo que pode ser aplicado para frente ou para trás. Novos recursos de edição não precisam de suporte a desfazer: o que quer que mudem está dentro do snapshot
Essa abordagem depende de um pré-requisito. Um salvamento completo comum produz identificadores e campos de tempo não determinísticos, de modo que salvar um documento não modificado duas vezes pode gerar bytes diferentes. Os snapshots são, portanto, normalizados primeiro por meio do caminho de saída reprodutível. Sem essa etapa, uma transação vazia pareceria uma modificação, e o desfazer falharia em sua própria verificação porque o digest calculado no commit não corresponderia ao documento depois
Transações na prática
Cada transação recebe um rótulo opcional, que é o que um editor mostra em seu menu de desfazer:
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; // profundidade de desfazer
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; // a página volta
finally
Pdf.Free;
end;
end;
Um commit que falha — porque a captura falhou, um orçamento foi excedido ou o delta não pôde ser construído — restaura a base capturada em BeginEditTransaction e recarrega, em vez de deixar para trás uma transação ativa ou um journal meio gravado. Esse é o modo de falha mais importante, porque um journal em estado inconsistente envenena todo desfazer subsequente
Verificação em cada etapa
Cada delta armazena o digest SHA-256 do documento antes e depois, junto com o comprimento do prefixo e ambas as cargas úteis. Desfazer, refazer, reprodução importada e reconstrução reversa, todos verificam o digest atual e a região que pretendem substituir antes de tocar em qualquer coisa, e então verificam o digest resultante depois
A movimentação pelo journal é igualmente cautelosa. A posição só avança depois que o PDF alvo foi validado por um objeto de documento independente e substituiu com sucesso o atual. Se isso falhar, o snapshot atual é restaurado como recurso de contingência, de modo que um desfazer malsucedido deixa o usuário onde estava, em vez de em um estado quebrado
O formato serializado é verificado como dado, não confiado como entrada: número mágico, versão, valores de entrada e posição, a cadeia de digests, comprimentos de rótulo, comprimentos de patch e o orçamento total de armazenamento, tudo é validado. Um journal importado é um arquivo que pode ter sido truncado, editado ou produzido por uma versão diferente, e tratá-lo como hostil é a única postura segura
O que acontece quando o journal enche
Três limites delimitam o crescimento: contagem de entradas, bytes acumulados de delta e o tamanho de um único snapshot. Quando a capacidade é atingida, a entrada mais antiga é removida e a impressão digital raiz avança
Essa última parte tem uma consequência que vale a pena planejar. Um journal exportado só pode ser aplicado a um documento que corresponda à impressão digital raiz que ele relata, ou a uma impressão digital que ele registrou. Uma vez ocorrida a remoção, o journal não alcança mais o arquivo original. O salvamento automático, portanto, precisa tratar o snapshot raiz correspondente e o journal como uma única unidade atômica de recuperação — manter o journal sem sua base é manter algo que não pode ser reproduzido
Pacotes de recuperação de falhas
O pacote de recuperação deliberadamente não incorpora o PDF de origem. Ele armazena o nome de exibição da origem, impressões digitais SHA-256 dos estados de origem e recuperado, e o journal de edição. A recuperação, portanto, exige o arquivo original, que o chamador carrega primeiro; o pacote então verifica ambas as impressões digitais reproduzindo o journal para trás e para frente:
// Após cada commit bem-sucedido, atualiza o arquivo de recuperação
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Próxima sessão, após uma falha: carrega o original, depois restaura
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
Não incorporar a origem mantém os arquivos de recuperação pequenos, o que é o que torna o salvamento automático por commit acessível em um documento de qualquer tamanho. Um digest externo cobre o cabeçalho, o nome da origem e o journal, de modo que um pacote truncado ou adulterado é rejeitado antes de qualquer delta ser analisado, e o volume total de leitura é limitado
As gravações de arquivo vão para um arquivo temporário no diretório de destino e são então substituídas atomicamente, e definir o caminho de recuperação como o PDF de origem é explicitamente recusado. O salvamento automático roda apenas após um commit, desfazer ou refazer bem-sucedido que tenha mudado algo. Se a gravação em disco falhar, os estados de documento e journal em memória já são válidos, de modo que a exceção relata uma falha de persistência, não uma perda de dados, e o chamador pode corrigir o caminho e tentar novamente
Documentos assinados são recusados por padrão
Um snapshot de transação é uma nova revisão normalizada, não os bytes originais assinados, de modo que confirmar uma transação em um documento assinado invalida a assinatura. Iniciar uma transação em tal documento é recusado, a menos que o chamador passe AllowSignedDocument, e a importação de journal e a restauração de recuperação recorrem por padrão à mesma recusa
Isso é um obstáculo deliberado, não uma proibição. Alguns fluxos de trabalho legitimamente editam um documento assinado e o reassinam depois, e a flag existe para eles. O que ela evita é o caso acidental, em que um editor remove silenciosamente a validade de um documento que um usuário acredita que ainda está assinado. O lado de verificação dessa história é abordado em inspecionando assinaturas digitais de PDF e níveis PAdES
Para um editor, três hábitos fazem o journal valer a pena. Envolva cada ação visível ao usuário em uma transação rotulada, de modo que o menu de desfazer se leia como o modelo mental do usuário, e não como sua pilha de chamadas. Dimensione o limite de entradas em relação ao documento, já que cinquenta snapshots de uma digitalização de 200 MB não é uma configuração modesta. E grave o snapshot de recuperação a cada commit, em vez de em um temporizador, porque um temporizador garante que o trabalho perdido seja exatamente o que aconteceu depois do último tique. As operações de edição em nível de documento que se beneficiam disso são abordadas em atualizações incrementais em PDF criptografado
O journaling de edição, os pacotes de recuperação e as primitivas de edição por trás deles vêm no mesmo componente para Delphi, C++Builder e Lazarus; a API completa está descrita na página do PDFium Component para Delphi