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 la 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 se pueden actualizar sin invalidar sus firmas
El problema que esto resuelve es concreto. Un guardado completo vuelve a escribir todo el archivo: cada objeto se vuelve a serializar, se recalcula cada desplazamiento de referencia cruzada y la salida no tiene 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 se corrigió una errata 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 acaba de destruirla
¿Por qué el guardado de 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, y todos los validadores informarán 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 vía 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 con respecto a los bytes que cubre. Luego, los validadores 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 canalizaciones de firma, el artículo complementario sobre la firma y validación de PAdES en Delphi explica en detalle cómo interactúan los rangos de bytes de firma y las secciones incrementales
Cómo funcionan las actualizaciones incrementales según 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ñade 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 antigua). Tercero, se añaden una nueva sección de referencia cruzada y un trailer; la entrada /Prev del trailer apunta al desplazamiento de bytes de la sección de referencia cruzada anterior, formando una cadena que el lector recorre de la más nueva a 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 coste de adición 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 punto. 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 incorporada 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 fallo. 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 primero en el flujo, luego se añade la sección incremental. El modo 1 escribe solo la propia sección incremental (la delta) y omite los bytes de origen por completo. El modo 2 escribe primero un prefijo proporcionado por el llamador registrado a través de SetAppendInputFromString, luego añade la sección de actualización sobre él
El modo 1 es el interesante para el diseño del sistema. Debido a que el delta es independiente, puede distribuirlo de forma independiente del original: almacene revisiones como bloques (blobs) separados en el almacenamiento de objetos, replique solo 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 (primero el archivo original, luego cada delta en orden) porque ese es exactamente el diseño que la sección §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 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 del delta. Eso plantea un dilema para el modo 1: el escritor nunca emite los bytes originales y, sin embargo, 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 virtual 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 el disco ni en la memoria. La implementación ingenua (escribir el archivo completo en un búfer temporal y luego cortar la cola) implicaría una copia transitoria de todo el PDF original, lo que para entradas a escala de gigabytes es precisamente el coste que las actualizaciones incrementales intentan evitar. Esta técnica de virtualización de desplazamientos es muy similar al desplazamiento de referencias de bytes utilizado en otras partes de la biblioteca; el artículo sobre la fusión rápida de PDF con desplazamiento de referencias de bytes muestra la misma idea aplicada a la combinación de documentos, y la guía sobre la 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
Transmisión de guardados 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 del documento 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 la 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 de uso compartido: cuando AppendToFile devolvía 0
Vale la pena volver a contar una regresión en esta área porque el patrón de fallo 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 3.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 controlador del archivo de origen durante la vida útil del objeto de documento, y ese controlador se abrió con fmShareDenyWrite. Cuando AppendToFile intentó volver a abrir el mismo archivo para escribir, el propio modo de uso compartido del cargador lo denegó y la API falló antes de escribir un byte
La solución flexibilizó el modo de uso compartido 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 vuelve a escribir en la región a la que sirve el controlador de larga duración del lector. La lección general para cualquiera que envuelva esta biblioteca (o construya cargadores de transmisión similares) es que los lectores perezosos que retienen controladores y los escritores de un mismo archivo están en tensión, y el modo de uso compartido que elija al abrir es un contrato de API, no un detalle de implementación. Si AppendToFile alguna vez devuelve 0 en su código, verifique primero si algún otro elemento de su proceso sigue reteniendo el archivo de destino con un modo de uso compartido restrictivo
Los costes reales: cuándo las actualizaciones incrementales son la herramienta incorrecta
Las actualizaciones incrementales sacrifican el tamaño del archivo a cambio de la eficiencia de escritura, y el intercambio no siempre es favorable. Cada revisión añade sus objetos modificados mientras las definiciones reemplazadas permanecen en el archivo, por lo que un documento editado cientos de veces acumula objetos inactivos 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 trunque el archivo. Por lo tanto, la redacción, el saneamiento o cualquier eliminación de contenido sensible exige una reescritura completa: un guardado incremental de una redacción es una fuga 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 (el cifrado de nuevo 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 se deba aplanar su historial
Las actualizaciones incrementales, la salida delta de desplazamiento virtual y la serialización directa a flujo forman parte de la versión estándar de losLab PDF Library para Delphi, C# y VB.NET; la página del producto detalla la superficie de la API completa de guardado y adición junto con las funciones de firma y archivos grandes analizadas anteriormente