Artículo técnico

Manejo de archivos PDF de referencia híbrida de aplicaciones de Office en Delphi

Al exportar un documento desde Microsoft Word o Excel con Guardar como PDF, el archivo resultante en disco es, la mayoría de las veces, un archivo de referencia híbrida. Este transporta su información de referencias cruzadas por duplicado: una vez como la tabla clásica de ancho fijo que cerraba todo PDF hasta la versión 1.4, y otra como un flujo de referencias cruzadas comprimido del que en realidad depende la mayor parte del documento. Una sola clave del trailer, /XRefStm, une ambas vistas, y que una herramienta vea el documento completo depende de si sigue esa clave

Este artículo aborda los archivos híbridos desde el lado del consumidor: cómo son los bytes al final del archivo, cómo las dos vistas se desincronizan al editar, y cómo una canalización en Delphi puede detectar y enrutar entradas híbridas. Cómo un cargador fusiona ambas vistas, y por qué el orden no es negociable, es el tema de nuestro artículo de HotPDF sobre la carga de archivos de referencia híbrida; este se centra en reconocer primero el formato

Por qué las exportaciones de Office escriben el índice dos veces

PDF 1.5 introdujo dos características que cambiaron la forma del archivo: los flujos de referencias cruzadas, que almacenan el índice de objetos como datos binarios comprimidos en lugar de una tabla de texto plano, y los flujos de objetos, que empaquetan muchos objetos pequeños en un solo contenedor comprimido con Flate. Un generador que los usa produce archivos más pequeños, pero un lector de PDF 1.4 no puede abrir el resultado, porque las estructuras de las que depende, la palabra clave xref y el diccionario trailer, ya no están

ISO 32000-1 §7.5.8.4 define el compromiso. Un archivo de referencia híbrida escribe ambas cosas: una tabla de referencias cruzadas clásica que direcciona los objetos que un lector antiguo debe alcanzar, entre ellos el catálogo y el árbol de páginas, y un flujo de referencias cruzadas que indexa todo lo demás. Los objetos plegados dentro de flujos de objetos se marcan como libres en la tabla clásica, de modo que un lector de 1.4 los omite sin quejarse; sus ubicaciones reales existen solo en el flujo. El trailer clásico lleva entonces una clave /XRefStm que contiene el desplazamiento en bytes de ese flujo. Un visor antiguo nunca lee esa clave y renderiza el archivo a partir de la vista de tabla. Un visor moderno la sigue y ve el documento completo. Word y Excel han emitido exactamente este formato durante años, razón por la cual los archivos híbridos no son un caso extremo exótico sino una gran parte de lo que reciben las canalizaciones empresariales

PDF: cola de un PDF de referencias híbridas donde un lector heredado confía en la tabla xref clásica mientras un lector moderno sigue /XRefStm hacia la vista de stream comprimido
Una clave de trailer decide la vista: la tabla clásica atiende a los lectores viejos, mientras que /XRefStm entrega a los lectores modernos el stream que localiza todo lo demás

Cómo se ve la cola de un archivo híbrido

El formato se entiende mejor a partir de los bytes. Aquí está la cola de un pequeño archivo híbrido, con los desplazamientos acortados; en una exportación real de Office, el valor de /XRefStm suele ser un desplazamiento grande cercano al final del archivo. El orden de lectura es el recorrido desde el final descrito en nuestra descripción general de la estructura de archivos PDF: buscar %%EOF, leer startxref, saltar a la tabla

% ... objetos del cuerpo, incluyendo flujos de objetos y, en el byte 116,
% el flujo de referencias cruzadas (un objeto stream con /Type /XRef) ...

xref                    % sección clásica: a donde apunta startxref
0 4
0000000000 65535 f      % entrada 0: cabeza de la lista libre, siempre presente
0000000017 00000 n      % objeto 1: el catálogo, visible para cualquier lector
0000000000 65535 f      % objeto 2: marcado libre -- vive dentro de un flujo de objetos
0000000000 65535 f      % objeto 3: igual; solo la vista de flujo lo localiza
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % desplazamiento en bytes del flujo de referencias cruzadas
>>
startxref
7164                    % desplazamiento en bytes de la palabra clave 'xref' anterior
%%EOF

Dos detalles en este volcado explican todo el mecanismo. Primero, startxref apunta a la sección clásica a propósito: esa es la dirección donde debe aterrizar un lector antiguo. El flujo de referencias cruzadas solo es accesible a través de la clave /XRefStm dentro del diccionario trailer, así que un analizador que nunca busca esa clave nunca se entera de que el flujo existe. Segundo, los objetos 2 y 3 son mentiras de tipo benigno. La tabla clásica los declara libres, pero son objetos reales que residen dentro de un contenedor comprimido; esa marca de libre es lo que evita que un lector de 1.4 tropiece con entradas que no puede usar. Un consumidor que confía únicamente en la vista clásica concluye que la mayor parte de este documento no existe

Cómo las dos vistas se desincronizan

Un archivo híbrido recién salido de Word es internamente consistente: ambas vistas describen el mismo documento, cada una dentro de su alcance declarado. El problema comienza cuando el archivo es editado por una herramienta que solo entiende una de las dos vistas. Considere una utilidad de estampado que agrega una actualización incremental de estilo clásico: nuevos objetos, una nueva sección xref, una cadena /Prev hacia la sección anterior, y un nuevo trailer. Si ese trailer omite la clave /XRefStm, la vista de flujo queda huérfana; si arrastra el valor anterior, la vista de flujo sigue describiendo el documento tal como estaba antes de la edición. De cualquier forma, los dos índices ahora discrepan sobre lo que contiene el archivo

El archivo resultante tiene una firma de fallo característica: los objetos visibles en una vista faltan o están desactualizados en la otra. Un lector que resuelve a través de la vista de flujo encuentra la versión previa a la edición de un objeto actualizado, o ninguna entrada para uno agregado. Un lector que usa la vista de tabla ve la edición, pero pierde el rastro de los objetos comprimidos que solo el flujo localiza. En la práctica esto se manifiesta como campos de formulario que sobreviven en un visor y desaparecen en otro, anotaciones que un proceso de estampado parece haber eliminado, o búsquedas que terminan apuntando al objeto equivocado

Lo que hace costosos de depurar a estos archivos es que Adobe Acrobat normalmente los abre sin quejarse: cuando el índice no coincide con los bytes, reconstruye silenciosamente los datos de referencias cruzadas escaneando los encabezados de objeto, de modo que quien generó el archivo dañado no ve ningún problema. El fallo aparece más tarde, cuando el archivo llega a un consumidor estricto, un validador de preflight, un servicio de firma, un proceso de ingesta de archivo, que confía en la estructura declarada y reporta objetos faltantes o una discrepancia de referencias cruzadas. "Se abre bien en Acrobat" es como comienza casi todo reporte de desincronización híbrida

Detección de un archivo híbrido en Delphi puro

Clasificar las entradas no requiere una biblioteca de PDF. La clave /XRefStm solo puede aparecer dentro de un diccionario trailer clásico, y el trailer activo se encuentra dentro de los últimos kilobytes del archivo, porque la especificación exige que %%EOF aparezca cerca del final físico. Leer una ventana acotada del final y buscar en ella basta para la clasificación:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Toda palabra clave involucrada es ASCII de 7 bits, así que decodificar byte a byte es seguro
  Tail := TEncoding.ANSI.GetString(Buf);

  // Buscar la ÚLTIMA palabra clave 'trailer': con actualizaciones incrementales,
  // el trailer más reciente es el que gobierna el archivo
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // sin trailer clásico: un archivo puro de flujo xref, no híbrido

  // Un trailer híbrido lleva /XRefStm entre 'trailer' y 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Los tres resultados se corresponden con los tres formatos. Un archivo puramente clásico tiene un trailer pero no /XRefStm: False. Un archivo que se compromete por completo con los flujos de referencias cruzadas no tiene ninguna palabra clave trailer, sus claves de trailer viven en el diccionario del flujo: también False, correctamente, porque ese archivo está comprimido, no es híbrido. Solo el formato de doble índice devuelve True

Flujo de decisión de Delphi que escanea la cola del archivo buscando el último trailer y /XRefStm, y luego enruta los PDF híbridos hacia validación, normalización o manejo de solo adición
Una búsqueda acotada de la cola produce tres veredictos, y solo el caso de doble índice continúa la ruta como híbrido verdadero

Para uso en producción, dos refuerzos justifican las líneas adicionales. Analice el entero que sigue a /XRefStm, posiciónese en ese desplazamiento y confirme que allí realmente hay un objeto stream con /Type /XRef; un archivo truncado puede llevar la clave mientras el flujo ya no existe, lo cual pertenece a una categoría distinta a la de un híbrido sano. Y trate el tamaño de la ventana como un parámetro: 2 KB cubre la salida habitual de Office, pero un diccionario trailer inusualmente grande puede empujar la palabra clave fuera de rango, y ampliar la ventana es preferible a declarar el archivo clásico por accidente

Enrutamiento de archivos híbridos en una canalización Delphi

La detección le da una decisión de enrutamiento. Para archivos que solo se leen, renderizan o validan, use un cargador que resuelva ambas vistas y luego verifique el comportamiento en lugar de los bytes. El PDFium Component analiza la cadena /XRefStm durante la carga, de modo que la tabla de objetos que ve su código es la fusionada, y las verificaciones descritas en nuestro artículo sobre la validación de flujos de objetos y de referencias cruzadas se aplican sin cambios. Si un híbrido desincronizado está tan dañado que se niega a cargar, el motor lo reporta a través de su conjunto de errores, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY y FPDF_ERR_PAGE, siendo FPDF_ERR_FORMAT el que produce el daño estructural. Sin embargo, no confíe únicamente en esa señal: PDFium es tolerante por diseño y reconstruye en silencio la mayoría de los archivos inconsistentes, así que una carga exitosa demuestra que el archivo era recuperable, no que sus dos vistas coincidan. La verificación de consistencia realmente significativa es comparar lo que encuentra un recorrido completo de objetos contra lo que declara el /Size del trailer

Para los archivos que su canalización modifica, la política más segura es evitar que sean híbridos en primer lugar. Una carga seguida de un guardado completo con HotPDF reescribe el documento con una única referencia cruzada autoconsistente: sin /XRefStm, sin una segunda vista que pueda desincronizarse, cada objeto perteneciendo exactamente a una entrada de índice. Esa normalización es lo que conviene aplicar antes de la ingesta de archivo, antes de un RIP o servicio de firma estricto aguas abajo, y después de cualquier edición aplicada a una entrada híbrida. Funciona porque el cargador fusionó correctamente las vistas al ingresar, el mecanismo que el artículo de HotPDF sobre referencia híbrida explica en detalle

La única clase de archivos que debe dejarse intacta es la de los documentos firmados digitalmente. Una reescritura completa mueve cada byte, lo que invalida cualquier firma calculada sobre los rangos originales. Un cambio a un híbrido firmado debe ingresar como una actualización incremental adecuada que mantenga ambas vistas; un archivo que solo necesita leerse debe pasar sin modificaciones. La normalización es para los archivos que usted controla; a los archivos firmados solo se les debe agregar contenido, nunca reescribirlos

Los PDF de referencia híbrida no están mal formados; son el propio puente de compatibilidad del formato, y las aplicaciones de Office seguirán produciéndolos mientras sobrevivan lectores de PDF 1.4 en la base instalada. Una canalización capaz de detectar la clave /XRefStm, validar el documento fusionado con el PDFium Component, y regenerar una salida limpia de índice único con el HotPDF Delphi Component los trata como lo que son: entradas ordinarias con una señal adicional en el trailer