Artículo técnico

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

Exporte un documento desde Microsoft Word o Excel con "Guardar como PDF" y el archivo resultante en disco será, con gran frecuencia, un archivo de referencia híbrida. Lleva su información de referencias cruzadas dos veces: una 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 única clave del tráiler, /XRefStm, entrelaza ambas vistas, y que una herramienta vea el documento completo depende de si sigue esa clave

Este artículo examina los archivos híbridos desde el lado del consumo: qué aspecto tienen los bytes al final del archivo, cómo se van distanciando las dos vistas al editarlo y de qué manera una canalización de Delphi puede detectar y encauzar las entradas híbridas. Cómo fusiona un cargador las 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 trata sobre cómo reconocer primero el diseño

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

PDF 1.5 introdujo dos funciones 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 sin formato, y los flujos de objetos, que empaquetan muchos objetos pequeños en un único contenedor comprimido con Flate. Un escritor que los usa produce archivos más pequeños, pero un lector de PDF 1.4 no puede abrir el resultado, porque las estructuras en las que se apoya, 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 dirige a 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 en flujos de objetos se marcan como libres en la tabla clásica, de modo que un lector 1.4 los omite sin protestar; sus ubicaciones reales solo existen en el flujo. El tráiler clásico lleva entonces una clave /XRefStm que contiene el desplazamiento en bytes de ese flujo. Un visor antiguo nunca lee la clave y representa el archivo a partir de la vista de tabla. Un visor moderno la sigue y ve el documento completo. Word y Excel llevan años emitiendo exactamente este diseño, 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 referencia híbrida donde un lector antiguo confía en la tabla xref clásica mientras un lector moderno sigue /XRefStm hacia la vista de stream comprimido
Una única clave de trailer decide la vista: la tabla clásica sirve a los lectores antiguos, mientras que /XRefStm entrega a los lectores modernos el flujo que localiza todo lo demás

Qué aspecto tiene la cola de un archivo híbrido

El diseño se entiende mejor a partir de los bytes. Esta es la cola de un archivo híbrido pequeño, con 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 que empieza por la cola descrito en nuestra descripción general de la estructura de un archivo PDF: busque %%EOF, lea startxref, salte a la tabla

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

xref                    % sección clásica: a lo que apunta startxref
0 4
0000000000 65535 f      % ranura 0: cabeza de la lista de libres, siempre presente
0000000017 00000 n      % objeto 1: el catálogo, visible para cualquier lector
0000000000 65535 f      % objeto 2: marcado como libre -- vive en 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' de arriba
%%EOF

Dos detalles de este volcado sostienen todo el mecanismo. Primero, startxref apunta a la sección clásica a propósito: esa es la dirección a la que debe llegar un lector antiguo. El flujo de referencias cruzadas solo es alcanzable a través de la clave /XRefStm dentro del diccionario del tráiler, 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; la marca de libre es lo que evita que un lector 1.4 tropiece con entradas que no puede usar. Un consumidor que confía solo en la vista clásica concluye que la mayor parte de este documento no existe

Cómo se distancian las dos vistas

Un archivo híbrido recién salido de Word es internamente consistente: ambas vistas describen el mismo documento, cada una dentro de su ámbito declarado. El problema empieza cuando el archivo lo edita una herramienta que solo entiende una de las vistas. Piense en una utilidad de estampado que añade una actualización incremental de estilo clásico: objetos nuevos, una nueva sección xref, una cadena /Prev hacia la sección anterior, y un nuevo tráiler. Si ese tráiler descarta la clave /XRefStm, la vista de flujo queda huérfana; si arrastra el valor antiguo, la vista de flujo sigue describiendo el documento tal como era antes de la edición. En cualquier caso, los dos índices ya no coinciden sobre qué contiene el archivo

El archivo resultante tiene una firma de fallo característica: los objetos visibles en una vista faltan o están obsoletos 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 directamente ninguna entrada para uno añadido. Un lector sobre 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 paso de estampado parece haber eliminado, o búsquedas que aterrizan por completo en el objeto equivocado

Lo que encarece la depuración de estos archivos es que Adobe Acrobat suele abrirlos sin protestar: cuando el índice no coincide con los bytes, reconstruye en silencio los datos de referencias cruzadas escaneando en busca de cabeceras de objeto, así que quien produjo el archivo roto no ve nada anómalo. El fallo aflora más tarde, cuando el archivo llega a un consumidor estricto, un validador de preflight, un servicio de firma, un trabajo de ingesta de archivo, que confía en la estructura declarada y notifica objetos ausentes o una discrepancia de referencias cruzadas. "Se abre bien en Acrobat" es como empieza casi todo ticket de desincronización híbrida

Detectar un archivo híbrido con Delphi puro

Clasificar las entradas no requiere una biblioteca de PDF. La clave /XRefStm solo puede aparecer dentro de un diccionario de tráiler clásico, y el tráiler 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 de la cola y buscar en ella basta para la clasificación previa:

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;

  // Todas las palabras clave implicadas son ASCII de 7 bits, así que decodificar byte a byte es seguro
  Tail := TEncoding.ANSI.GetString(Buf);

  // Busca la ÚLTIMA palabra clave 'trailer': con actualizaciones incrementales,
  // el tráiler 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 tráiler clásico: un archivo puramente de flujo xref, no híbrido

  // Un tráiler 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 alinean con los tres diseños. Un archivo puramente clásico tiene tráiler pero no /XRefStm: False. Un archivo que se decanta por completo por los flujos de referencias cruzadas no tiene ninguna palabra clave trailer, sus claves de tráiler viven en el diccionario del flujo: también False, correctamente, porque ese archivo está comprimido, no es híbrido. Solo el diseño de doble índice devuelve True

Flujo de decisión en Delphi que escanea la cola del fichero 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 sigue la ruta como híbrido verdadero

Para uso en producción, dos refuerzos merecen las líneas adicionales. Analice el entero que sigue a /XRefStm, sitúese en ese desplazamiento y confirme que allí reside en realidad un objeto de flujo con /Type /XRef; un archivo truncado puede llevar la clave mientras el flujo ha desaparecido, lo cual pertenece a una categoría distinta de 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 de tráiler inusualmente grande puede empujar la palabra clave fuera de rango, y ampliar la ventana es preferible a declarar el archivo clásico por accidente

Encauzar archivos híbridos a través de una canalización de Delphi

La detección le compra una decisión de encauzamiento. Para archivos que solo se leen, renderizan o validan, use un cargador que resuelva ambas vistas y después verifique el comportamiento en lugar de los bytes. El PDFium Component analiza la cadena /XRefStm durante la carga, así que la tabla de objetos que ve su código es la fusionada, y las comprobaciones descritas en nuestro artículo sobre la validación de flujos de objetos y referencias cruzadas se aplican sin cambios. Si un híbrido desincronizado está tan dañado que se niega a cargar, el motor lo notifica 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. No se apoye en esa señal, sin embargo: PDFium es permisivo 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 comprobación de consistencia significativa es comparar lo que encuentra un recorrido completo de objetos con lo que declara el /Size del tráiler

Para los archivos que su canalización modifica, la política más segura es evitar por completo que sean híbridos. Una carga seguida de un guardado completo mediante HotPDF reescribe el documento con una única referencia cruzada autoconsistente en una sola forma: sin /XRefStm, sin una segunda vista que pueda desincronizarse, cada objeto perteneciente exactamente a una entrada de índice. Esa normalización es lo que conviene 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 entrar, el mecanismo que el artículo de HotPDF sobre referencia híbrida recorre en detalle

La única clase de archivos que hay que dejar tranquila es la de los documentos firmados digitalmente. Una reescritura completa mueve todos los bytes, lo cual invalida cualquier firma calculada sobre los intervalos originales. Un cambio en un híbrido firmado debe entrar como una actualización incremental adecuada que mantenga ambas vistas; un archivo que solo necesita lectura debe pasar sin tocarlo. La normalización es para los archivos que usted posee; a los archivos firmados solo se les añade contenido

Los PDF de referencia híbrida no están mal formados; son el propio puente de compatibilidad del formato, y las aplicaciones de Office los seguirán produciendo 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 un indicador adicional en el tráiler