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 las que una canalización de PDF que superó todas las pruebas internas encuentra un archivo que no puede procesar de ida y vuelta. 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.
Qué escriben realmente 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 funciones de PDF 1.5, como flujos de objetos, y al mismo tiempo permite 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 contiene una entrada /XRefStm cuyo valor es el desplazamiento de bytes (offset) de ese flujo.
La división del trabajo es deliberada. Los objetos que un lector antiguo debe alcanzar, 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, por lo que un lector 1.4 los omite directamente y nunca tropieza con una estructura que no puede analizar. Sus ubicaciones reales existen únicamente en el flujo de referencia cruzada. La firma de dicho archivo es su parte final: una sección clásica corta, que a menudo no es más que xref seguido de un encabezado de subsección 0 0, cuyo trailer apunta a la entrada /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 solo lee esa tabla encuentra el objeto /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 de PDF etiquetados y la larga lista de pequeños diccionarios que nunca tuvieron que ser visibles para un visor heredado.
No notará la brecha hasta que algo interactúe con esos objetos, y una operación de guardado completo interactúa con todos ellos. Recorrer el documento para volver a cifrarlo o reescribirlo es precisamente la operación que solicita cada número de objeto a la vez, por lo que el síntoma surge al momento de guardar en lugar de cargar, lejos de su causa raíz.
La trampa es un detector que ve xref y se detiene
La forma más económica de determinar cómo está indexado 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 limite a 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 la entrada /XRefStm en el trailer de esa sección es donde realmente se indexa la mayor parte del documento. Un detector que devuelve "clásico" ante el primer xref que encuentra nunca lee /XRefStm, y cada objeto que existe 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 activo, la carga parece correcta y el proceso de volver a guardar es donde los objetos ausentes se hacen notar. La solución no es leer más bytes al inicio; consiste en reconocer el trailer híbrido y seguir /XRefStm antes de decidir que el archivo está completo.
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 debe fusionarse primero, completando las entradas clásicas a su alrededor. La razón es el pequeño engaño en el núcleo 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 respete la política de "el primero que se ve gana" y lea primero la tabla clásica registrará esos números de objeto como libres y luego descartará las entradas del flujo que realmente los ubican, porque los espacios ya están ocupados. Invierta el orden y las entradas de tipo 2 del flujo (cada una compuesta por un número de flujo de objetos más un índice) ganarán los espacios que les corresponde poseer, 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 en la cadena sobrescriba ese centinela con una ubicación obsoleta. Si trata la primera aparición como autoritativa para los marcadores libres, el objeto eliminado permanecerá eliminado; si lo hace descuidadamente, la propia historia del archivo reanimará el contenido que la última revisión eliminó.
Qué significa esto 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 los flujos antes de que la escritura los enumere. La ruta de cifrado AES-256 es donde se presentó el problema por primera vez, porque cifrar un documento reescribe cada objeto y, por lo tanto, exige que cada uno de ellos 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 canalizaciones de "Guardar como PDF" son habitualmente híbridos, por lo que es posible que un cargador que solo pruebe con la salida de su propio generador nunca encuentre uno en las pruebas. Alimente sus entornos de prueba con documentos exportados de aplicaciones reales de Office, no solo con archivos generados por su propio código.
Cómo comprobar un archivo sospechoso
Dos inspecciones resuelven la duda rápidamente. Abra el archivo en una vista de bytes 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 el recuento de objetos que reporta un análisis completo con el número de objeto más alto que declara /Size en el trailer. Una gran diferencia significa que los objetos se están ocultando en flujos que el cargador no ha abierto, que es la misma deficiencia que luego se convierte en una falla al guardar.
La perspectiva del escritor en esta historia, cómo se producen los flujos de objetos y las referencias cruzadas comprimidas en primer lugar, se cubre 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 en la guía de la API de archivo directo para flujos de trabajo de PDF grandes le permiten inspeccionarlo sin leerlo por completo en la memoria. Ambos se complementan de manera 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 partes de este blog.