Artículo técnico

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

Abra un PDF generado por Microsoft Word o Excel, hojee sus páginas y nada parecerá inusual. Cárguelo en un programa Delphi, recupere el número de páginas y la cifra será correcta. Luego vuelva a guardarlo con la encriptación activada y la tarea fallará con un EListError, o bien la salida se abrirá mostrando una advertencia de referencias cruzadas dañadas. El archivo nunca estuvo corrupto. Es un archivo de referencias híbridas, y la misma estructura que permite a un visor de hace quince años abrirlo es la que bloquea a un cargador que deja de leer demasiado pronto

Esta es una de las formas más comunes en que un flujo de trabajo PDF que ha superado cada prueba interna se topa con un archivo que no puede procesar de ida y vuelta. Todas las entradas se generaron internamente, de modo que nunca fueron híbridas. El primer archivo híbrido llega el día que un cliente reenvía una factura exportada desde una hoja de cálculo

Lo que realmente escriben Word y Excel

ISO 32000-1 describe el diseño de referencias híbridas en §7.5.8.4. Una aplicación que desea características de PDF 1.5 como los flujos de objetos, al mismo tiempo que permite a un lector de PDF 1.4 abrir el archivo, escribe la información de las referencias cruzadas dos veces. Existe una tabla de referencias cruzadas clásica, las filas ASCII de ancho fijo que terminaban todos los PDF hasta la versión 1.4, y existe un flujo de referencias cruzadas que indexa el resto. El tráiler de la sección clásica lleva una entrada /XRefStm cuyo valor es el desplazamiento en bytes de ese flujo

La división del trabajo es deliberena. Los objetos a los que un lector antiguo necesita acceder, el catálogo y el árbol de páginas entre ellos, son direccionables desde la tabla clásica. Los objetos que se han incorporado en flujos de objetos comprimidos se marcan como libres en la tabla clásica, con una entrada de tipo f, para que un lector 1.4 los salte directamente y nunca tropiece con una estructura que no pueda analizar. Sus ubicaciones reales viven únicamente en el flujo de referencias cruzadas. La firma de tal archivo es su cola: una sección clásica corta, frecuentemente nada más que un xref seguido por una cabecera de subsección 0 0, cuyo tráiler apunta al /XRefStm donde se encuentran los datos de recuperación reales

Por qué un conteo correcto de páginas no prueba nada

Debido a que el catálogo y el árbol de páginas son alcanzables a propósito desde la tabla clásica, un cargador que solo lee 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, de forma que el archivo parece estar sano. Los objetos que desaparecieron son los empaquetados en los flujos de objetos: diccionarios de campos AcroForm, elementos de estructura de PDF etiquetado, y la larga lista de pequeños diccionarios que nunca tuvieron que ser visibles para un visor heredado

No se advierte este vacío hasta que algo toca esos objetos, y un guardado completo los toca todos. Recorrer el documento para reencriptarlo o reescribirlo es precisamente la operación que solicita cada número de objeto por turnos, razón por la cual el síntoma surge en el momento de guardar y no en el de carga, lejos de su causa original

La trampa es un detector que ve xref y se detiene

La forma rápida 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 referencias cruzadas. Esa prueba es correcta para cualquier archivo que se adhiera a un solo esquema. Es errónea 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 tráiler de esa sección es donde la mayor parte del documento está realmente indexada. Un detector que devuelve "clásica" en el primer xref que encuentra nunca lee /XRefStm, y todos los objetos que viven exclusivamente en el flujo se vuelven invisibles

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 un detector de salida temprana implementado, la carga parece correcta y al guardar es cuando se anuncian los objetos ausentes. La solución no es leer más bytes al principio; consiste en reconocer el tráiler híbrido y seguir /XRefStm antes de decidir que el archivo está completo

El orden de fusión no es negociable

Una vez que se han leído ambos índices, sólo pueden combinarse en una dirección. El flujo de referencias cruzadas debe fusionarse primero, rellenando las entradas clásicas a su alrededor. La razón es un 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 honra una política de "el primero que se ve gana" y lee primero la tabla clásica registrará esos números de objeto como libres, luego descartará las entradas del flujo que realmente los localizan, porque las posiciones ya están ocupadas. 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) ocupan los lugares que les corresponden, y las entradas clásicas se acomodan a su alrededor

La misma disciplina evita que una revisión más antigua resucite un objeto eliminado. Las actualizaciones incrementales se encadenan hacia atrás mediante /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. A una sección posterior y más antigua en la cadena no se le debe permitir sobrescribir ese centinela con una ubicación obsoleta. Trate a lo que se ve primero como la autoridad para los marcadores libres y el objeto eliminado permanecerá eliminado; trátelo descuidadamente y la propia historia del archivo reanimará contenido que la última revisión eliminó

Lo que esto significa en HotPDF

El motor resuelve los archivos de referencias híbridas por usted, y lo hace en cualquier ruta que deba analizar los datos de las referencias cruzadas. Cargue un documento con LoadFromFile o LoadFromStream, realice sus cambios y llame a SaveLoadedDocument; o ejecute una operación de un solo paso como EncryptFile que lee una entrada y escribe una salida. De cualquier forma, la recuperación lee /XRefStm, fusiona la sección del flujo por delante de las entradas clásicas y resuelve los objetos que viven en flujos antes de que la escritura los enumere. La ruta de encriptación AES-256 es donde el problema se manifestó por primera vez, debido a que encriptar un documento reescribe todos los objetos y, por lo tanto, exige que cada objeto ya haya sido localizado

// 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 llevarse reside aguas arriba de la API. Los archivos que provienen de Word, Excel, PowerPoint y una larga lista de canales de "Guardar como PDF" son rutinariamente híbridos, por lo que un cargador que usted evalúa solo con la salida de su propio generador puede que nunca se encuentre con uno durante las pruebas. Alimente sus entornos de pruebas con documentos exportados desde aplicaciones de Office reales, no solo con archivos generados por su propio código

Comprobación de un archivo del que se sospecha

Dos inspecciones resuelven la duda 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 tráiler contiene /XRefStm. O compare el conteo de objetos que reporta un análisis completo con el número de objeto más alto que declara /Size en el tráiler. Una brecha grande significa que los objetos se ocultan en flujos que el cargador no ha abierto, lo que representa la misma carencia que posteriormente se convierte en un fallo en el momento del guardado

La perspectiva del escritor de esta historia, cómo se producen en primer lugar los flujos de objetos y las referencias cruzadas comprimidas, 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 el recorrido de la API de archivo directo para flujos de trabajo PDF voluminosos le permiten inspeccionarlo sin leer todo el contenido en memoria. Ambos combinan naturalmente con la recuperación descrita aquí, la cual se incluye como parte del Componente HotPDF para Delphi y C++Builder, junto con las APIs de carga, edición, encriptación y firmas cubiertas en otros lugares de este blog