PDFium Component da a un editor de PDF un deshacer y un rehacer reales a nivel de documento, además de un paquete de recuperación ante fallos, mediante un journal de edición. Envuelves los cambios en 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 cerrada abruptamente a mitad de sesión pueda reconstruir el trabajo del usuario
La brecha que esto cubre es concreta. La capacidad nativa de deshacer se aplica solo al campo de formulario que tiene el foco en ese momento, así que un cuadro de texto recuerda sus propias ediciones y nada más lo hace. La inserción, eliminación y reordenación de páginas, los cambios en objetos de página, las anotaciones, los adjuntos y los metadatos no tienen journal alguno. Un editor construido solo sobre eso puede ofrecer deshacer dentro de un campo y en ningún otro sitio, 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 — «se eliminó la página 4», «se movió la anotación 7» — e invertirlas. Funciona hasta que el conjunto de operaciones crece, momento en el cual cada función nueva necesita su inversa, y cualquier operación cuya inversa no sea exacta 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 puede aplicarse hacia delante o hacia atrás. Las funciones de edición nuevas no necesitan soporte de deshacer propio: lo que sea que cambien queda dentro de la instantánea
Ese enfoque depende de un requisito previo. Un guardado completo ordinario produce identificadores y campos de tiempo no deterministas, así que guardar dos veces un documento sin modificar puede producir bytes distintos. Por eso las instantáneas se normalizan primero mediante 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 calculado en el commit no coincidiría con el documento después
Las 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;
Un commit que falla — porque la captura falló, se superó un presupuesto o el delta no pudo construirse — restaura la línea base capturada en BeginEditTransaction y recarga, en lugar de dejar atrás una transacción activa o un journal escrito a medias. Este es el modo de fallo 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 útiles. Deshacer, rehacer, la reproducción de un journal importado y la reconstrucción inversa comprueban 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 igualmente cauteloso. La posición avanza solo después de que el PDF de destino haya sido validado por un objeto de documento independiente y haya reemplazado con éxito al actual. Si eso falla, la instantánea actual se restaura como respaldo, de modo que un deshacer fallido deja al usuario donde estaba en lugar de en un estado roto
El formato serializado se comprueba 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 etiqueta, las longitudes de parche y el presupuesto de almacenamiento total se validan todos. Un journal importado es un archivo que puede haber sido truncado, editado o producido por una versión distinta, y tratarlo como hostil es la única postura segura
Qué ocurre cuando el journal se llena
Tres límites acotan el crecimiento: el número de entradas, los bytes acumulados de delta y el tamaño de una única instantánea. Cuando se alcanza la capacidad, la entrada más antigua se descarta y la huella raíz avanza
Esa última parte tiene una consecuencia que vale la pena tener en cuenta al diseñar. Un journal exportado solo puede aplicarse a un documento que coincida con la huella raíz que reporta, o con una huella que registró. Una vez que ha ocurrido un descarte, el journal ya no alcanza el archivo original. El autoguardado, por tanto, debe tratar la instantánea raíz correspondiente y el journal como una única 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 incrusta el PDF de origen. Almacena el nombre de visualización del origen, las huellas SHA-256 de los estados de origen y de recuperación, y el journal de edición. La recuperación exige por tanto el archivo original, que el llamador carga primero; el paquete verifica entonces ambas huellas reproduciendo el journal hacia atrás y hacia delante:
// Tras cada commit exitoso, refresca el archivo de recuperación
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// Siguiente sesión, tras un fallo: carga el original, luego restaura
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
No incrustar el origen mantiene pequeños los archivos de recuperación, lo cual es lo que hace asequible un autoguardado por cada commit en un documento de cualquier tamaño. Un digest externo cubre la cabecera, 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 tiene un tope
Las escrituras de archivo van a un archivo temporal en el directorio de destino y luego se reemplazan de forma atómica, y establecer la ruta de recuperación al propio PDF de origen se rechaza explícitamente. El autoguardado se ejecuta únicamente después de un commit con cambios, un deshacer o un rehacer exitosos. Si la escritura a 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 tanto la importación de journal como la restauración de recuperación se rechazan por defecto de la misma manera
Esto es un obstáculo deliberado y no una prohibición. Algunos flujos de trabajo editan legítimamente un documento firmado y vuelven a firmarlo después, y el flag existe para ellos. Lo que impide es el caso accidental, en el que un editor le quita la validez a un documento en silencio cuando el usuario cree que sigue firmado. La cara de verificación de esa historia se trata en inspeccionar firmas digitales de PDF y niveles PAdES
Para un editor, tres hábitos hacen que el journal valga la pena. Envuelve cada acción visible para el usuario en una transacción etiquetada, de modo que el menú de deshacer se lea como el modelo mental del usuario y no como tu pila de llamadas. Dimensiona 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 escribe la instantánea de recuperación en cada commit 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 tratan en actualizaciones incrementales de PDF cifrado
El journal de edición, los paquetes de recuperación y las primitivas de edición que hay debajo se distribuyen en el mismo componente para Delphi, C++Builder y Lazarus; la API completa se describe en la página de PDFium Component para Delphi