Las actualizaciones incrementales de PDF permiten que una aplicación Delphi modifique un documento añadiendo únicamente los objetos modificados, dejando intacto cada byte original. losLab PDF Library implementa esto a través de AppendToStream, que escribe solo la sección incremental definida por la norma ISO 32000-1 §7.5.6, de modo que una edición de un solo marcador en un archivo de 2 GB cuesta kilobytes de salida en lugar de una reescritura completa. Este mismo mecanismo es la razón por la que los documentos firmados pueden actualizarse sin invalidar sus firmas
El problema que esto resuelve es concreto. Un guardado completo vuelve a escribir todo el archivo: cada objeto se serializa de nuevo, cada desplazamiento de referencia cruzada se recalcula y la salida no tiene ninguna relación a nivel de bytes con la entrada. Para una factura de 40 KB, esto está bien. Para un archivo escaneado de 2 GB en el que solo corrigió un error tipográfico en el título del documento, reescribir dos gigabytes para cambiar veinte bytes es absurdo — y si el archivo contenía una firma digital, la reescritura simplemente la destruyó
¿Por qué guardar un PDF rompe su firma digital?
Una firma digital de PDF no firma el contenido lógico del documento; firma rangos de bytes del archivo físico. La entrada /ByteRange en el diccionario de firmas registra exactamente qué tramos del archivo cubre el resumen criptográfico. Cualquier operación de guardado que vuelva a serializar esos bytes — incluso una que produzca un documento semánticamente idéntico — cambia el resumen criptográfico, y cada validador informará que la firma está rota. Esto es por diseño: la firma da fe de los bytes que vio el firmante, no de algún modelo de documento abstracto
Las actualizaciones incrementales son la salida de escape que proporciona la especificación PDF. Debido a que un guardado incremental añade nuevos datos después del %%EOF original y nunca toca los rangos de bytes firmados, la firma existente se sigue validando contra los bytes que cubre. Los validadores luego clasifican los cambios añadidos por separado — una segunda firma, un llenado de formulario, una anotación — y deciden si son modificaciones permitidas. Cada flujo de trabajo de firma múltiple depende de esto: cada firmante añade una sección incremental sobre la anterior. Si está construyendo flujos de firma, el artículo complementario sobre firma y validación de PAdES en Delphi cubre detalladamente cómo interactúan los rangos de bytes de firma y las secciones incrementales
Cómo funcionan las actualizaciones incrementales bajo la norma ISO 32000-1 §7.5.6
La norma ISO 32000-1 §7.5.6 define el modelo en tres reglas. Primero, el contenido original del archivo se deja completamente intacto — no se mueve ni un solo byte. Segundo, los objetos modificados y recién creados se añaden después del último %%EOF, cada uno con el mismo número de objeto que tenía antes (los objetos modificados simplemente obtienen una definición más nueva que oculta la anterior). Tercero, se añaden una nueva sección de referencia cruzada y un trailer; la entrada /Prev del trailer apunta de regreso al desplazamiento de bytes de la sección de referencia cruzada anterior, formando una cadena que el lector recorre desde la más nueva hasta la más antigua para resolver cada objeto a su definición más reciente
Dos propiedades útiles se derivan de esta estructura. Las actualizaciones son económicas en proporción a lo que cambió, no al tamaño del documento — el costo de añadir es el tamaño de los objetos modificados más una pequeña sobrecarga de xref/trailer. Y el archivo se convierte en su propio historial de versiones: cada revisión anterior sigue estando físicamente presente, por lo que un auditor puede truncar el archivo en cualquier %%EOF anterior y recuperar exactamente el documento que existía en ese momento. Para los flujos de trabajo de cumplimiento que deben demostrar cómo se veía un documento antes de cada enmienda, esta pista de auditoría integrada suele ser el argumento decisivo para los guardados incrementales
Escribir una actualización incremental con AppendToStream
losLab PDF Library expone la salida incremental a través de AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, que devuelve 1 en caso de éxito y 0 en caso de falla. El parámetro AppendMode selecciona qué se escribe en el flujo de destino. El Modo 0 escribe un archivo completo: los bytes de origen originales se copian al flujo primero, luego se añade la sección incremental. El Modo 1 escribe solo la sección incremental en sí — la delta — y omite por completo los bytes de origen. El Modo 2 escribe primero un prefijo suministrado por el llamador registrado a través de SetAppendInputFromString, luego añade la sección de actualización sobre él
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Small edit: the kind of change that should not
// trigger a rewrite of the whole file
Doc.SetInformation(3, 'Amended 2026-07-04'); // key 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: write only the incremental section.
// Original bytes + Delta = a complete, valid PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
El Modo 1 es el interesante para el diseño del sistema. Debido a que la delta es autocontenida, puede enviarla independientemente del original: almacene las revisiones como blobs separados en el almacenamiento de objetos, replique solo las deltas en un sitio remoto o reconstruya cualquier revisión concatenando el archivo base con su cadena de incrementos. La regla de reconstrucción es una simple concatenación de bytes — el archivo original primero, luego cada delta en orden — porque ese es exactamente el diseño que la norma §7.5.6 prescribe para un archivo actualizado de forma incremental
¿Cómo calcula la biblioteca los desplazamientos de xref sin copiar el archivo original?
Las entradas de referencia cruzada dentro de una sección incremental deben contener desplazamientos de bytes absolutos — posiciones medidas desde el inicio del archivo completo, no desde el inicio de la delta. Eso crea un enigma para el modo 1: el escritor nunca emite los bytes originales, pero cada desplazamiento que registra debe pretender que están allí. losLab PDF Library resuelve esto con un adaptador de flujo interno, TPDFAppendSectionStream, que presenta un espacio de coordenadas virtuales al serializador. El adaptador se crea con la longitud de bytes del archivo original como su desplazamiento base, informa su posición y tamaño como esa base más lo que se haya añadido hasta el momento, y reenvía solo los bytes recién escritos al flujo de destino del llamador
La consecuencia es que el modo 1 nunca materializa una copia del documento de origen — ni en disco, ni en memoria. La implementación ingenua (escribir el archivo completo en un búfer temporal y luego cortar la cola) tendría una copia transitoria de todo el PDF original, lo que para entradas a escala de gigabytes es precisamente el costo que las actualizaciones incrementales pretenden evitar. Esta técnica de virtualización de desplazamientos es una prima cercana del desplazamiento de referencias de bytes utilizado en otras partes de la biblioteca; el artículo sobre fusión rápida de PDF con desplazamiento de referencia de bytes muestra la misma idea aplicada a la combinación de documentos, y la guía sobre fusión y división de PDF grandes con acceso directo a archivos cubre la arquitectura de E/S circundante para archivos que no caben cómodamente en la RAM
Flujos de guardado completos con SaveToStream
La salida incremental es la mitad de la historia de la transmisión; la otra mitad es lo que sucede en un guardado completo. SaveToStream en losLab PDF Library dirige el serializador de documentos directamente contra el flujo de destino, en lugar de representar primero todo el documento en una cadena AnsiString intermedia y luego escribir ese búfer en una sola llamada. El enfoque anterior funcionaba, pero significaba que cada guardado completo contenía transitoriamente una segunda copia completa de la salida en memoria — inofensivo a 10 MB, costoso a 500 MB y un límite duro para salidas de varios gigabytes en procesos de 32 bits. La serialización directa hace que el pico de memoria siga las estructuras de objetos del documento en lugar de su longitud serializada
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... edits that justify a full rewrite ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
Una lección sobre el modo compartido: cuando AppendToFile devolvió 0
Vale la pena relatar una regresión en esta área porque el patrón de falla se generaliza. AppendToFile(FileName) añade una actualización incremental directamente a un PDF existente en el disco — la llamada natural para un flujo de trabajo de pista de auditoría en el lugar: cargar un archivo, realizar un cambio, añadir a la misma ruta. En la versión v3.71.2, esa secuencia exacta comenzó a devolver 0. La causa raíz estaba en el cargador, no en el escritor: para admitir la lectura bajo demanda de documentos grandes, LoadFromFile mantiene abierto el manejador del archivo de origen durante la vida útil del objeto de documento, y ese manejador se abrió con fmShareDenyWrite. Cuando AppendToFile intentó volver a abrir el mismo archivo para escribir, el propio modo de compartición del cargador lo denegó y la API falló antes de escribir un solo byte
La solución relajó el modo de compartición del cargador a fmShareDenyNone, lo cual es seguro precisamente por lo que es una adición incremental: añade bytes estrictamente después del final del archivo y nunca reescribe la región que atiende el manejador de larga duración del lector. La lección general para cualquiera que encapsule esta biblioteca — o construya cargadores de flujo similares — is that lazy, handle-holding readers and same-file writers are in tension, and the share mode you pick at open time is an API contract, not an implementation detail. If AppendToFile ever returns 0 in your code, check first whether something else in your process still holds the target file with a restrictive share mode
Los costos reales: cuando las actualizaciones incrementales son la herramienta incorrecta
Las actualizaciones incrementales intercambian tamaño de archivo por eficiencia de escritura, y el intercambio no siempre es favorable. Cada revisión añade sus objetos modificados mientras que las definiciones reemplazadas permanecen en el archivo, por lo que un documento editado cientos de veces acumula objetos muertos y una larga cadena /Prev que cada lector debe recorrer. Peor aún, el contenido "eliminado" no desaparece: el texto eliminado en la revisión cinco sigue estando físicamente presente en los bytes de la revisión cuatro, recuperable por cualquiera que busque truncar el archivo. Por lo tanto, la censura de información, la desinfección o cualquier eliminación de contenido confidencial exige una reescritura completa — un guardado incremental de una censura de información es una filtración de datos con pasos adicionales
Un guardado completo también es la opción correcta cuando el objetivo es la compactación (eliminar los incrementos acumulados y los objetos no utilizados), al cambiar propiedades de todo el documento como el cifrado — volver a cifrar afecta a cada cadena y flujo, por lo que no queda nada "incremental" en el cambio — o al producir un entregable limpio donde el historial de edición no deba viajar con el archivo. Una regla razonable: use AppendToStream o AppendToFile mientras un documento esté activo y cambiando, especialmente una vez que contenga firmas; use una reescritura completa con SaveToStream en los límites del ciclo de vida, cuando el documento salga de su sistema o su historial deba simplificarse
Las actualizaciones incrementales, la salida delta de desplazamiento virtual y la serialización directa al flujo forman parte de la biblioteca estándar losLab PDF Library para Delphi, C# y VB.NET; la página del producto enumera toda la superficie de la API para guardar y añadir, junto con las funciones de firma y archivos grandes analizadas anteriormente