Las actualizaciones incrementales de PDF permiten que una aplicación Delphi modifique un documento añadiendo solo los objetos cambiados, dejando intacto cada byte original. losLab PDF Library implementa esto mediante AppendToStream, que escribe únicamente la sección incremental definida por ISO 32000-1 §7.5.6, de modo que editar un marcador en un archivo de 2 GB cuesta kilobytes de salida en lugar de una reescritura completa. El mismo mecanismo es la razón por la que los documentos firmados pueden actualizarse sin invalidar sus firmas
El dolor que esto resuelve es concreto. Un guardado completo reescribe todo el archivo: cada objeto se vuelve a serializar, cada desplazamiento de referencia cruzada se recalcula, y la salida no guarda ninguna relación a nivel de byte con la entrada. Para una factura de 40 KB no pasa nada. Para un archivo escaneado de 2 GB en el que solo se corrigió una errata en el título del documento, reescribir dos gigabytes para cambiar veinte bytes es absurdo — y si el archivo llevaba una firma digital, la reescritura acaba de destruirla
¿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 del diccionario de firma 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, y todos los validadores informarán de que la firma está rota. Esto es así por diseño: la firma da fe de los bytes que vio el firmante, no de algún modelo abstracto de documento
Las actualizaciones incrementales son la vía de escape que ofrece la especificación PDF. Como un guardado incremental añade datos nuevos después del %%EOF original y nunca toca los rangos de bytes firmados, la firma existente sigue validándose contra los bytes que cubre. Los validadores clasifican entonces por separado los cambios añadidos — una segunda firma, un relleno de formulario, una anotación — y deciden si son modificaciones permitidas. Todo flujo de trabajo multifirma depende de esto: cada firmante añade una sección incremental encima de la anterior. Si estás construyendo pipelines de firma, el artículo complementario sobre firma y validación PAdES en Delphi cubre en detalle cómo interactúan los rangos de bytes de firma y las secciones incrementales
Cómo funcionan las actualizaciones incrementales según ISO 32000-1 §7.5.6
ISO 32000-1 §7.5.6 define el modelo en tres reglas. Primera, el contenido original del archivo se deja completamente intacto — no se mueve ni un byte. Segunda, los objetos cambiados y los 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 cambiados simplemente reciben una definición más nueva que eclipsa la antigua). Tercera, se añaden una nueva sección de referencias cruzadas y un nuevo trailer; la entrada /Prev del trailer apunta al desplazamiento de bytes de la sección de referencias cruzadas anterior, formando una cadena que un lector recorre de la más nueva a la más antigua para resolver cada objeto a su definición más reciente
De esta estructura se derivan dos propiedades útiles. Las actualizaciones son baratas en proporción a lo que cambió, no al tamaño del documento — el coste 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 físicamente presente, así que un auditor puede truncar el archivo en cualquier %%EOF anterior y recuperar exactamente el documento que existía en ese momento. Para flujos de trabajo de cumplimiento que deben demostrar cómo era un documento antes de cada enmienda, esta pista de auditoría integrada suele ser el argumento decisivo a favor de los guardados incrementales
Escribir una actualización incremental con AppendToStream
losLab PDF Library expone la salida incremental mediante AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, que devuelve 1 en caso de éxito y 0 en caso de fallo. El parámetro AppendMode selecciona qué aterriza en el stream de destino. El modo 0 escribe un archivo completo: primero se copian al stream los bytes originales de origen y después se añade la sección incremental. El modo 1 escribe solo la propia sección incremental — el delta — y omite por completo los bytes de origen. El modo 2 escribe primero un prefijo suministrado por quien llama y registrado mediante SetAppendInputFromString, y después añade la sección de actualización encima
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// Edición pequeña: el tipo de cambio que no debería
// provocar una reescritura de todo el archivo
Doc.SetInformation(3, 'Amended 2026-07-04'); // clave 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: escribir solo la sección incremental.
// Bytes originales + Delta = un PDF completo y válido.
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 de sistemas. Como el delta es autocontenido, puedes distribuirlo independientemente del original: almacenar revisiones como blobs separados en un almacenamiento de objetos, replicar solo los deltas a un sitio remoto, o reconstruir cualquier revisión concatenando el archivo base con su cadena de incrementos. La regla de reconstrucción es la simple concatenación de bytes — primero el archivo original, luego cada delta en orden — porque esa es exactamente la disposición que §7.5.6 prescribe para un archivo actualizado incrementalmente
¿Cómo calcula la biblioteca los desplazamientos xref sin copiar el archivo original?
Las entradas de referencias cruzadas dentro de una sección incremental deben contener desplazamientos de bytes absolutos — posiciones medidas desde el inicio del archivo completo, no desde el inicio del delta. Eso crea un rompecabezas para el modo 1: el escritor nunca emite los bytes originales, y sin embargo cada desplazamiento que registra tiene que fingir que están ahí. losLab PDF Library resuelve esto con un adaptador de stream interno, TPDFAppendSectionStream, que presenta al serializador un espacio de coordenadas virtual. El adaptador se crea con la longitud en bytes del archivo original como desplazamiento base, informa de 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 stream 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 recortar la cola) cargaría con una copia transitoria de todo el PDF original, que para entradas del orden del gigabyte es precisamente el coste que las actualizaciones incrementales existen para evitar. Esta técnica de virtualización de desplazamientos es prima cercana del desplazamiento de referencias de bytes usado en otras partes de la biblioteca; el artículo sobre fusión rápida de PDF con desplazamiento de referencias de bytes muestra la misma idea aplicada a combinar documentos, y la guía sobre fusión y división de PDF grandes con acceso directo a archivo cubre la arquitectura de E/S circundante para archivos que no caben cómodamente en RAM
Guardados completos en streaming con SaveToStream
La salida incremental es la mitad de la historia del streaming; la otra mitad es lo que ocurre en un guardado completo. SaveToStream en losLab PDF Library dirige el serializador del documento directamente contra el stream de destino, en lugar de renderizar primero el documento entero en un AnsiString intermedio y escribir después ese búfer en una sola llamada. El enfoque antiguo funcionaba, pero significaba que cada guardado completo mantenía transitoriamente en memoria una segunda copia completa de la salida — inofensivo a 10 MB, doloroso a 500 MB, y un muro infranqueable para salidas de varios gigabytes en procesos de 32 bits. La serialización directa hace que el pico de memoria siga a las estructuras de objetos del documento en lugar de a su longitud serializada
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... ediciones que justifican una reescritura completa ...
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 modos de compartición: cuando AppendToFile devolvía 0
Una regresión en esta área merece contarse porque el patrón de fallo se generaliza. AppendToFile(FileName) añade una actualización incremental directamente a un PDF existente en disco — la llamada natural para un flujo de trabajo de pista de auditoría en el sitio: cargar un archivo, hacer un cambio, añadirlo a la misma ruta. En la v3.71.2 esa secuencia exacta empezó a devolver 0. La causa raíz estaba en el cargador, no en el escritor: para permitir la lectura bajo demanda de documentos grandes, LoadFromFile mantiene abierto el handle del archivo de origen durante toda la vida del objeto documento, y ese handle se abría con fmShareDenyWrite. Cuando AppendToFile intentaba después reabrir el mismo archivo para escritura, el propio modo de compartición del cargador se lo denegaba, y la API fallaba antes de escribir un solo byte
La corrección relajó el modo de compartición del cargador a fmShareDenyNone, lo que es seguro precisamente por lo que es un añadido incremental: agrega bytes estrictamente después del final del archivo y nunca reescribe la región que el handle de larga vida del lector está sirviendo. La lección general para quien envuelva esta biblioteca — o construya cargadores en streaming similares — es que los lectores perezosos que retienen handles y los escritores sobre el mismo archivo están en tensión, y el modo de compartición que se elige al abrir es un contrato de API, no un detalle de implementación. Si AppendToFile devuelve alguna vez 0 en tu código, comprueba primero si algo más en tu proceso sigue reteniendo el archivo de destino con un modo de compartición restrictivo
Los costes honestos: cuándo las actualizaciones incrementales son la herramienta equivocada
Las actualizaciones incrementales cambian tamaño de archivo por eficiencia de escritura, y el intercambio no siempre es favorable. Cada revisión añade sus objetos cambiados mientras las definiciones reemplazadas permanecen en el archivo, así que un documento editado cientos de veces acumula objetos muertos y una larga cadena /Prev que todo lector debe recorrer. Peor aún, el contenido "eliminado" no desaparece: el texto quitado en la revisión cinco sigue físicamente presente en los bytes de la revisión cuatro, recuperable por cualquiera que trunque el archivo. La redacción, el saneamiento o cualquier eliminación de contenido sensible exigen por tanto una reescritura completa — un guardado incremental de una redacción es una fuga de datos con pasos adicionales
Un guardado completo es también la decisión correcta cuando el objetivo es la compactación (exprimir los incrementos acumulados y los objetos no usados), cuando se cambian propiedades de todo el documento como el cifrado — volver a cifrar toca cada cadena y cada stream, así que no queda nada de "incremental" en el cambio — o cuando se produce un entregable limpio en el que el historial de edición no debe viajar con el archivo. Una regla razonable: usa AppendToStream o AppendToFile mientras un documento está vivo y cambiando, sobre todo una vez que lleva firmas; usa una reescritura completa con SaveToStream en los límites del ciclo de vida, cuando el documento sale de tu sistema o su historial debe aplanarse
Las actualizaciones incrementales, la salida de deltas con desplazamientos virtuales y la serialización directa a stream forman parte de la losLab PDF Library estándar para Delphi, C# y VB.NET; la página del producto enumera toda la superficie de la API de guardado y añadido junto con las funciones de firma y archivos grandes tratadas arriba