Technical Article

Carga de archivos PDF con referencias híbridas de Word y Excel en Delphi

Abra un PDF generado por Microsoft Word o Excel, recorra sus páginas y nada parecerá inusual. Cárguelo en un programa Delphi, lea el recuento de páginas y el número será correcto. Luego, vuelva a guardarlo con el cifrado activado y la tarea fallará con un EListError, o la salida se abrirá con una advertencia de referencia cruzada dañada. El archivo nunca estuvo corrupto. Es un archivo de referencia híbrida, y la misma estructura que permite a un visor de hace quince años abrirlo es la estructura que derrota a un cargador que deja de leer demasiado pronto

Esta es una de las formas más comunes en que una canalización de PDF que superó todas las pruebas internas se encuentra con un archivo que no puede procesar de ida y vuelta. Todas las entradas se generaron internamente, por lo que nunca fueron híbridas. El primer archivo híbrido llega el día en que un cliente reenvía una factura exportada desde una hoja de cálculo

Lo que realmente escriben Word y Excel

La norma ISO 32000-1 describe el diseño de referencia híbrida en la sección 7.5.8.4. Una aplicación que desea características de PDF 1.5, como flujos de objetos, y al mismo tiempo permitir que un lector de PDF 1.4 abra el archivo, escribe la información de referencia cruzada dos veces. Hay una tabla de referencia cruzada clásica, las filas ASCII de ancho fijo que finalizaban cada PDF hasta la versión 1.4, y hay un flujo de referencia cruzada que indexa el resto. El trailer de la sección clásica lleva una entrada /XRefStm cuyo valor es el desplazamiento de bytes de ese flujo

La división del trabajo es deliberada. Los objetos a los que debe llegar un lector antiguo, entre ellos el catálogo y el árbol de páginas, son accesibles desde la tabla clásica. Los objetos que se agruparon en flujos de objetos comprimidos se marcan como libres en la tabla clásica, con una entrada de tipo f, de modo que un lector 1.4 los salta directamente y nunca se tropieza con una estructura que no puede analizar. Sus ubicaciones reales residen solo en el flujo de referencias cruzadas. La firma de dicho archivo es su cola: una sección clásica corta, frecuentemente nada más que xref seguido de un encabezado de subsección 0 0, cuyo trailer apunta al /XRefStm donde se encuentran los datos de recuperación reales

Por qué un recuento de páginas correcto no demuestra nada

Debido a que el catálogo y el árbol de páginas son accesibles desde la tabla clásica a propósito, un cargador que lee solo esa tabla encuentra /Root, recorre el árbol de páginas y reporta el número correcto de páginas. Todo lo que necesita un lector antiguo está presente, por lo que el archivo parece en buen estado. Los objetos que se perdieron son los empaquetados en flujos de objetos: diccionarios de campos AcroForm, elementos de estructura PDF etiquetados y la larga cola de pequeños diccionarios que nunca tuvieron que ser visibles para un visor heredado

No se nota la brecha hasta que algo toca esos objetos, y una operación completa de guardado posterior los toca a todos. Recorrer el documento para volver a cifrarlo o reescribirlo es precisamente la operación que solicita cada número de objeto por turno, razón por la cual el síntoma surge al guardar en lugar de al cargar, lejos de su causa

La trampa es un detector que ve xref y se detiene

La forma económica de decidir cómo se indexa un archivo es seguir startxref e inspeccionar los primeros bytes a los que apunta. La palabra clave xref significa una tabla clásica; un objeto de flujo significa un flujo de referencia cruzada. Esa prueba es correcta para cualquier archivo que se comprometa con un esquema. Es incorrecta para un archivo híbrido, cuyo startxref apunta a una sección clásica con el único propósito de satisfacer a los lectores antiguos, mientras que el /XRefStm en el trailer de esa sección es donde realmente se indexa la mayor parte del documento. Un detector que devuelve "classic" en el primer xref que encuentra nunca lee /XRefStm, y cada objeto que vive solo en el flujo se vuelve invisible

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

Con el detector de salida anticipada instalado, la carga parece correcta y el guardado posterior es donde se anuncian los objetos ausentes. La solución no es leer más bytes al principio; es reconocer el trailer híbrido y seguir /XRefStm antes de decidir que el archivo está listo

El orden de fusión no es negociable

Una vez leídos ambos índices, solo se pueden combinar en una dirección. El flujo de referencia cruzada tiene que fusionarse primero, completando las entradas clásicas a su alrededor. La razón es el pequeño engaño en el corazón del formato. Un archivo híbrido marca sus objetos comprimidos como libres en la tabla clásica para que los lectores antiguos los ignoren. Un cargador que honra una política de "el primero visto gana" y lee la tabla clásica primero registrará esos números de objeto como libres, luego descartará las entradas del flujo que realmente los ubican, porque las ranuras ya están ocupadas. Invierta el orden y las entradas de tipo 2 del flujo, cada una con un número de flujo de objetos más un índice, ganarán las ranuras que les corresponden, y las entradas clásicas se acomodarán a su alrededor

La misma disciplina protege contra la posibilidad de que una revisión anterior resucite un objeto eliminado. Las actualizaciones incrementales se encadenan hacia atrás a través de /Prev, y una entrada libre de tipo 0 es un centinela de que una sección más reciente ha retirado un número de objeto. No se debe permitir que una sección posterior y más antigua de la cadena sobrescriba ese centinela con una ubicación obsoleta. Trate la primera vista como autoritativa para los marcadores libres y el objeto eliminado seguirá eliminado; trátelo con descuido y la propia historia del archivo reanimará el contenido que la última revisión eliminó

Lo que esto significa en HotPDF

El motor resuelve los archivos de referencia híbrida por usted, y lo hace en cada ruta que debe analizar los datos de referencia cruzada. Cargue un documento con LoadFromFile o LoadFromStream, realice sus cambios y llame a SaveLoadedDocument; o ejecute una operación directa como EncryptFile que lee una entrada y escribe una salida. De cualquier manera, la recuperación lee /XRefStm, fusiona la sección del flujo antes de las entradas clásicas y resuelve los objetos que residen en flujos antes de que la escritura los enumere. La ruta de cifrado AES-256 es donde se mostró el problema por primera vez, porque cifrar un documento reescribe cada objeto y, por lo tanto, exige que cada objeto ya haya sido ubicado

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

El detalle que vale la pena recordar se encuentra antes de la API. Los archivos que provienen de Word, Excel, PowerPoint y una larga lista de canales "Guardar como PDF" son habitualmente híbridos, por lo que es posible que un cargador que solo pruebe contra la salida de su propio generador nunca se encuentre con uno en las pruebas. Alimente sus pruebas con documentos exportados de aplicaciones reales de Office, no solo con archivos generados por su propio código

Comprobación de un archivo bajo sospecha

Dos inspecciones resuelven la cuestión rápidamente. Abra el archivo en una vista hexadecimal y lea los bytes después del startxref final; un archivo híbrido muestra una sección clásica corta cuyo diccionario de trailer contiene /XRefStm. Or compare the object count a full parse reports against the highest object number that /Size declares in the trailer. Una gran diferencia significa que los objetos se esconden en flujos que el cargador no ha abierto, que es la misma deficiencia que se convierte en un fallo al guardar más tarde

La parte del escritor en esta historia, sobre cómo se producen los flujos de objetos y las referencias cruzadas comprimidas en primer lugar, se detalla en nuestro artículo sobre flujos de objetos y actualizaciones incrementales. Cuando el archivo híbrido en cuestión también es muy grande, las técnicas de carga descritas en la guía de la API de archivo directo para flujos de trabajo con PDF de gran tamaño le permiten inspeccionarlo sin leer todo en la memoria. Ambos se complementan de forma natural con la recuperación descrita aquí, que se distribuye como parte de HotPDF Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras secciones de este blog