PDFium Component le da a un editor de PDF un verdadero deshacer y rehacer a nivel de documento, más un paquete de recuperación ante fallos, mediante un journal de edición. Usted envuelve los cambios entre BeginEditTransaction y CommitEditTransaction, y cada transacción confirmada se convierte en un delta bidireccional que UndoEdit y RedoEdit pueden reproducir en cualquier dirección. SaveRecoverySnapshot persiste ese journal, de modo que una aplicación terminada a mitad de sesión pueda reconstruir el trabajo del usuario
La brecha que esto cierra es específica. La capacidad nativa de deshacer se aplica solo al campo de formulario que tiene el foco en ese momento, de modo que un cuadro de texto recuerda sus propias ediciones y nada más lo hace. La inserción, eliminación y reordenamiento de páginas, los cambios de objetos de página, las anotaciones, los adjuntos y los metadatos no tienen ningún journal en absoluto. Un editor construido solo sobre eso puede ofrecer deshacer dentro de un campo y en ningún otro lugar, que no es lo que los usuarios entienden por deshacer
¿Por qué instantáneas en lugar de un registro de operaciones?
El diseño obvio es registrar operaciones semánticas — "página 4 eliminada", "anotación 7 movida" — e invertirlas. Funciona hasta que el conjunto de operaciones crece, momento en el cual cada función nueva necesita un inverso, y cualquier operación cuyo inverso no sea exacto corrompe el documento en silencio a lo largo de una cadena larga de deshacer
El journal, en cambio, captura el estado del documento en los límites de cada transacción y almacena la diferencia. Cada delta conserva el prefijo y el sufijo comunes más largos, y lleva tanto los bytes eliminados como los insertados, de modo que se puede aplicar hacia adelante o hacia atrás. Las nuevas funciones de edición no necesitan soporte de deshacer propio: lo que sea que cambien queda dentro de la instantánea
Ese enfoque depende de un prerrequisito. Un guardado completo ordinario produce identificadores y campos de tiempo no deterministas, de modo que guardar dos veces un documento sin modificar puede producir bytes distintos. Por eso las instantáneas se normalizan primero a través de la ruta de salida reproducible. Sin ese paso, una transacción vacía parecería una modificación, y deshacer fallaría su propia verificación porque el digest que calculó al confirmar no coincidiría con el documento después
Transacciones en la práctica
Cada transacción admite una etiqueta opcional, que es lo que un editor muestra en su menú de deshacer:
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; // profundidad de deshacer
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; // la página vuelve
finally
Pdf.Free;
end;
end;
Una confirmación que falla — porque la captura falló, se excedió un presupuesto o el delta no se pudo construir — restaura la línea base capturada en BeginEditTransaction y recarga, en lugar de dejar atrás una transacción activa o un journal a medio escribir. Este es el modo de falla que más importa, porque un journal en un estado inconsistente envenena cada deshacer posterior
Verificación en cada paso
Cada delta almacena el digest SHA-256 del documento antes y después, junto con la longitud del prefijo y ambas cargas. Deshacer, rehacer, la reproducción de un journal importado y la reconstrucción inversa verifican todos el digest actual y la región que pretenden reemplazar antes de tocar nada, y luego verifican el digest resultante después
El movimiento a través del journal es igual de cauteloso. La posición avanza solo después de que el PDF destino ha sido validado por un objeto de documento independiente y ha reemplazado con éxito al actual. Si eso falla, la instantánea actual se restaura como reserva, de modo que un deshacer fallido deja al usuario donde estaba en lugar de en un estado roto
El formato serializado se verifica como datos, no se confía en él como entrada: el número mágico, la versión, los valores de entrada y posición, la cadena de digests, las longitudes de las etiquetas, las longitudes de los parches y el presupuesto de almacenamiento total se validan todos. Un journal importado es un archivo que puede haber sido truncado, editado o producido por otra versión, y tratarlo como hostil es la única postura segura
Qué pasa cuando el journal se llena
Tres límites acotan el crecimiento: la cantidad de entradas, los bytes acumulados de delta y el tamaño de una sola instantánea. Cuando se alcanza la capacidad, se descarta la entrada más antigua y la huella raíz avanza
Esa última parte tiene una consecuencia sobre la que vale la pena diseñar. Un journal exportado solo se puede aplicar a un documento que coincida con la huella raíz que reporta, o con una huella que haya registrado. Una vez que ha ocurrido un descarte, el journal ya no alcanza hasta el archivo original. Por eso el autoguardado tiene que tratar la instantánea raíz correspondiente y el journal como una sola unidad de recuperación atómica — conservar el journal sin su base es conservar algo que no se puede reproducir
Paquetes de recuperación ante fallos
El paquete de recuperación deliberadamente no embebe el PDF de origen. Almacena el nombre para mostrar del origen, las huellas SHA-256 de los estados de origen y recuperado, y el journal de edición. Por lo tanto, la recuperación requiere el archivo original, que el llamador carga primero; el paquete luego verifica ambas huellas reproduciendo el journal hacia atrás y hacia adelante:
// Después de cada confirmación exitosa, actualizar el archivo de recuperación
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// En la siguiente sesión, tras un fallo: cargar el original, luego restaurar
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
No embeber el origen mantiene pequeños los archivos de recuperación, lo cual es lo que hace asequible el autoguardado por confirmación en un documento de cualquier tamaño. Un digest externo cubre el encabezado, el nombre de origen y el journal, de modo que un paquete truncado o manipulado se rechaza antes de analizar ningún delta, y el volumen total de lectura queda acotado
Las escrituras de archivo van a un archivo temporal dentro del directorio destino y luego se reemplazan de forma atómica, y establecer la ruta de recuperación como el PDF de origen se rechaza explícitamente. El autoguardado se ejecuta solo después de una confirmación exitosa con cambios, un deshacer o un rehacer. Si la escritura en disco falla, los estados en memoria del documento y del journal ya son válidos, así que la excepción reporta un fallo de persistencia y no una pérdida de datos, y el llamador puede corregir la ruta y reintentar
Los documentos firmados se rechazan por defecto
Una instantánea de transacción es una nueva revisión normalizada, no los bytes firmados originales, así que confirmar una transacción sobre un documento firmado invalida la firma. Iniciar una transacción sobre un documento así se rechaza a menos que el llamador pase AllowSignedDocument, y la importación de journal y la restauración de recuperación tienen por defecto el mismo rechazo
Esto es un obstáculo deliberado y no una prohibición. Algunos flujos de trabajo legítimamente editan un documento firmado y lo vuelven a firmar después, y la bandera existe para ellos. Lo que evita es el caso accidental, donde un editor le quita en silencio la validez a un documento que un usuario cree que sigue firmado. El lado de verificación de esa historia se cubre en inspeccionar firmas digitales de PDF y niveles PAdES
Para un editor, tres hábitos hacen que el journal valga la pena. Envuelva cada acción visible para el usuario en una sola transacción etiquetada, de modo que el menú de deshacer se lea como el modelo mental del usuario y no como su pila de llamadas. Dimensione el límite de entradas según el documento, ya que cincuenta instantáneas de un escaneo de 200 MB no es un ajuste modesto. Y escriba la instantánea de recuperación en cada confirmación en lugar de con un temporizador, porque un temporizador garantiza que el trabajo perdido sea exactamente lo que ocurrió después del último tick. Las operaciones de edición a nivel de documento que se benefician de esto se cubren en actualizaciones incrementales de PDF cifrado
El journal de edición, los paquetes de recuperación y las primitivas de edición que los sostienen se entregan en el mismo componente para Delphi, C++Builder y Lazarus; la API completa se describe en la página de PDFium Component para Delphi