Artículo técnico

HotPDF Delphi: object streams y revisiones incrementales

PDF 1.5 introdujo dos estructuras de almacenamiento que el formato anterior no sabía expresar: el object stream y el cross-reference stream. Un object stream es un único contenedor comprimido con Flate, marcado como /Type /ObjStm, que guarda muchos objetos indirectos pequeños empaquetados uno tras otro en vez de dispersarlos por el cuerpo del archivo. Un cross-reference stream es la tabla de búsqueda del archivo reescrita como binario comprimido con campos de ancho variable, en lugar de la tabla ASCII de ancho fijo que cerraba todos los PDF hasta la versión 1.4. Van juntos. En cuanto los objetos se pliegan dentro de un stream, la vieja tabla de texto ya no puede direccionarlos, así que el xref binario tiene que acompañarlos

Compare eso con la disposición clásica y el coste que se elimina salta a la vista. En un archivo PDF 1.4 cada objeto indirecto queda sin comprimir detrás de su propia cabecera obj, y la tabla del final gasta exactamente 20 bytes de ASCII por entrada, con la compresión prohibida. Un documento con 200.000 objetos arrastra unos 4 MB de datos de referencias cruzadas antes de dibujar un solo glifo, con todos los cuerpos de diccionario sin comprimir apilados encima. PDF 1.5 ataca ambas cifras a la vez: los diccionarios se pliegan en contenedores Flate y esos 4 MB de tabla se reducen a unos cientos de kilobytes de binario. ISO 32000-1 define las dos estructuras en §7.5.7 y §7.5.8

HotPDF: disposiciones de archivo enfrentadas que comparan objetos PDF 1.4 sin comprimir y tablas xref ASCII con object streams PDF 1.5 comprimidos y un xref stream binario
Los diccionarios plegados y un xref stream binario eliminan megabytes de sobrecarga estructural, mientras que el contenido de página y los datos de imagen conservan la compresión que ya tenían: los archivos con mucha estructura son los que más ganan

Dónde aterriza realmente el ahorro

Los object streams solo afectan a los objetos que no son streams, de modo que comprimen estructura, no píxeles. El contenido de página ya iba comprimido con Flate antes de la 1.5, y los datos de imagen llevan sus propios códecs; por eso un folleto lleno de imágenes apenas se mueve. Los archivos que se desploman son los cargados de estructura: AcroForms con miles de diccionarios de campo, árboles de marcadores profundos, elementos de estructura de PDF etiquetado. Esos objetos son diminutos, numerosos y casi idénticos entre sí, y esa repetición es justo lo que Flate explota en cuanto quedan en un mismo búfer y no repartidos por el cuerpo con cabeceras encajadas entre medias

Es fácil infravalorar cuánta parte de un archivo antiguo es sobrecarga. Un archivo de formularios que ha absorbido años de ediciones puede dedicar bastante más de la mitad de sus bytes a cabeceras de diccionario, relleno de xref y revisiones que ningún lector va a mirar jamás. Las dos funciones de esta página recuperan las dos primeras. La tercera, las revisiones acumuladas, solo cede ante la compactación, cuando el archivo ya no tiene que recordar su propia historia

En HotPDF ambas se activan mediante un par de propiedades, y cómo dependen la una de la otra importa más que el orden en que las escriba:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // xref binario, requisito previo de ObjStm
    Pdf.UseObjectStreams := True;   // empaqueta objetos en /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emite contenedores XRefStm + ObjStm
  finally
    Pdf.Free;
  end;
end;

UseObjectStreams necesita que UseXRefStream valga True. A un objeto comprimido se llega por una entrada xref de tipo 2, que registra un número de object stream más un índice, y una fila de texto clásica de 20 bytes no tiene sitio donde guardar ese par. Por eso UseObjectStreams por sí sola no hace nada visible; la configuración que funciona son las dos banderas activadas antes de BeginDoc. Si las activa después de BeginDoc, HotPDF ya se ha comprometido con la disposición antigua

Por qué ambas vienen desactivadas

HotPDF deja las dos propiedades en False de fábrica, y la razón aflora en integraciones con código antiguo aguas abajo. Un lector que solo entiende PDF 1.4 no avisa de que no sabe manejar objetos comprimidos. Se encuentra con un xref stream, no halla ninguna de las palabras clave de tráiler que espera e informa de una tabla de referencias cruzadas dañada, o sencillamente se niega a abrir el archivo. Si su salida desemboca en una pasarela de fax envejecida, en una impresora con intérprete embebido o en un analizador que alguien escribió contra la especificación 1.4 hace una década, deje ambas banderas desactivadas para ese canal y asuma el archivo más grande. Para almacenamiento de archivo y entrega web, donde todos los visores habituales llevan veinte años leyendo PDF 1.5, activarlas es compresión que consigue casi gratis

Hay un efecto de segundo orden que conviene contarle a su equipo de soporte. En cuanto los diccionarios van empaquetados en object streams, comparar dos archivos generados byte a byte deja de significar nada, porque cambiar un solo campo puede volver a comprimir un contenedor entero y descolocar todo lo que viene detrás. Compare esos archivos por contenido de objeto, no con una comparación binaria

Actualizaciones incrementales y los desplazamientos de bytes que protegen

Una firma digital cubre un /ByteRange explícito: dos tramos del archivo físico, dados como desplazamientos absolutos de bytes, sobre los que se calculó el resumen CMS. Reescriba el archivo, aunque sea en algo idéntico en pantalla, y todos esos desplazamientos se mueven. El resumen deja de coincidir y la firma se lee como rota. Ese es exactamente el problema que ISO 32000-1 §7.5.6 resuelve con las actualizaciones incrementales. Los objetos nuevos y modificados se añaden después del %%EOF existente, y luego se escribe una sección de referencias cruzadas nueva cuya entrada /Prev apunta hacia atrás, a la anterior. Los bytes originales no se tocan nunca, así que una revisión firmada sigue siendo verificable y Acrobat puede presentar cada revisión firmada por separado en el panel de firmas

HotPDF lo expone mediante su propio punto de entrada:

HotPDF: tres revisiones de solo anexado encadenadas por entradas Prev del xref con los resúmenes de ByteRange originales todavía válidos
Las revisiones anexadas se encadenan hacia atrás por las entradas Prev y jamás tocan los bytes que resumió una firma, de modo que toda revisión firmada anterior sigue validando mientras el archivo solo crece hacia la derecha
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // solo añade el delta

Hay dos cosas con las que la gente tropieza. BeginIncrementalUpdate tiene que recibir el nombre del archivo original, porque la sección xref anexada registra desplazamientos que solo tienen sentido frente a esos bytes originales exactos; apúntela a una copia renombrada o vuelta a guardar y los desplazamientos describirán un archivo que ya no existe. Y el guardado es de solo anexado por construcción, así que la salida siempre es mayor que la entrada. Ese crecimiento no es desperdicio que haya que afinar. Es la misma propiedad que deja intactas las revisiones firmadas anteriores

Modificar un archivo cargado pasa por LoadFromFile

Quienes conocieron HotPDF por su API de generación suelen chocar con un muro concreto. BeginDoc abre un documento completamente nuevo, que es la herramienta equivocada cuando lo que quiere es cambiar uno que ya existe. Editar un archivo existente pasa por las llamadas de documento cargado:

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // páginas 1-3 tras la página 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

Si mezcla ambas vías, el síntoma es un archivo de salida que contiene su contenido nuevo y nada del original, porque BeginDoc construyó tan campante un documento fresco junto al que usted creía estar editando. Lea LoadFromFile con SaveLoadedDocument como un vocabulario y BeginDoc con EndDoc como otro. Una rutina que eche mano de los dos sobre el mismo archivo está mal casi siempre

HotPDF: dos vocabularios de guardado donde BeginDoc crea un archivo nuevo y LoadFromFile con SaveLoadedDocument edita el existente
La pareja de generación construye un documento completamente nuevo mientras que la pareja de documento cargado edita lo que ya está en disco: mezclarlas es la razón de que a veces se publiquen ediciones sin ninguna de las páginas originales

Cuándo compactar un archivo anexado

El guardado de solo anexado tiene un coste lento. Un proceso nocturno que estampa una línea de estado en el mismo PDF produce 365 revisiones a lo largo de un año, y cada revisión arrastra tras de sí una nueva sección xref. Cuando esa historia ha dejado de ser útil, y ninguna firma del archivo necesita sobrevivir, puede aplanarlo entero volviéndolo a serializar por la vía de documento cargado:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

Este reguardado es una reescritura completa. Tira a propósito las revisiones anteriores y rompe cualquier firma que siga en el archivo, así que póngalo tras la misma barrera de política que aplica a cualquier otro paso destructivo. Una regla de producción que aguanta bien: compacte cuando el recuento de revisiones supere un umbral, o cuando la sobrecarga anexada crezca por encima de cierta proporción del archivo base, y no compacte nunca un documento cuyo panel de firmas tenga algo dentro

Comprobar la salida antes de publicarla

Verificar este par de funciones resulta agradablemente concreto. Abra el resultado en Adobe Acrobat y confirme tres puntos: las propiedades del documento informan de PDF 1.5 o posterior en cuanto los object streams están activos; el panel de firmas sigue validando cada revisión firmada previamente tras una actualización incremental; y el recuento de páginas y los marcadores han salido indemnes de un ciclo de carga, modificación y guardado. Para salida de archivo, pase además el fichero por veraPDF, ya que un xref comprimido es justo el tipo de estructura que un validador estricto examina con más lupa que cualquier visor indulgente. Si su trabajo implica además entradas muy grandes, los métodos de inspección de nuestro recorrido por la Direct File API para flujos de trabajo con PDF grandes encajan de forma natural con el guardado incremental, y la mecánica de firma que hay detrás de los rangos de bytes anteriores se trata a fondo en el artículo sobre firmas digitales y PAdES en HotPDF

Ambas funciones se distribuyen como parte del HotPDF Delphi Component para Delphi y C++Builder, junto a las API de generación, formularios, cifrado y firma que se tratan en otros artículos de este blog. La página del producto enlaza la referencia completa de la API por si quiere cotejar las llamadas anteriores con su propia cadena documental