Artículo técnico

Cargar PDF de referencia híbrida de Word y Excel en Delphi

Abra un PDF producido por Microsoft Word o Excel, hojéelo, y nada se ve fuera de lo normal. Cárguelo en un programa Delphi, lea el conteo de páginas, y el número es correcto. Luego vuelva a guardarlo con el cifrado activado y el trabajo falla con un EListError, o la salida se abre 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 que un visor de hace quince años lo abra 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 un pipeline de PDF que pasó todas las pruebas internas se topa con un archivo que no puede procesar de ida y vuelta. Las entradas se generaron todas en casa, así 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

ISO 32000-1 describe el diseño de referencia híbrida en §7.5.8.4. Una aplicación que quiere características de PDF 1.5, como los flujos de objetos, y aun así permitir que un lector de PDF 1.4 abra el archivo, escribe la información de referencias cruzadas dos veces. Hay una tabla clásica de referencias cruzadas, las filas ASCII de ancho fijo que cerraban todo PDF hasta la versión 1.4, y hay un flujo de referencias cruzadas que indexa el resto. El trailer 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 deliberada. Los objetos que un lector antiguo tiene que alcanzar, entre ellos el catálogo y el árbol de páginas, son direccionables desde la tabla clásica. Los objetos que se plegaron en flujos de objetos comprimidos se marcan como libres en la tabla clásica, con una entrada de tipo f, así que un lector 1.4 los pasa de largo y nunca tropieza con una estructura que no puede analizar. Sus ubicaciones reales viven solo en el flujo de referencias cruzadas. La firma de un archivo así es su cola: una sección clásica corta, con frecuencia nada más que xref seguido de un encabezado de subsección 0 0, cuyo trailer apunta al /XRefStm donde están los datos reales de recuperación

Diagrama de HotPDF de la cola de un PDF de referencia híbrida de Word o Excel donde el trailer del xref clásico lleva /XRefStm 87325, apuntando de vuelta al flujo de referencias cruzadas que indexa campos de formulario y estructura etiquetada invisibles para un cargador que se detiene en la tabla clásica
La cola híbrida conserva una tabla clásica simbólica cuya única carga útil es el desplazamiento /XRefStm, mientras que el lado del flujo guarda el índice real de objetos

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

Como el catálogo y el árbol de páginas son alcanzables 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 un lector antiguo necesita está presente, así que el archivo parece sano. Los objetos que se perdieron son los empaquetados en flujos de objetos: diccionarios de campos AcroForm, elementos de estructura de PDF etiquetado, la larga cola de diccionarios pequeños que nunca tuvieron que ser visibles para un visor heredado

Usted no nota el hueco hasta que algo toca esos objetos, y un reguardado completo los toca todos. Recorrer el documento para volver a cifrarlo o reescribirlo es precisamente la operación que pide cada número de objeto por turno, y por eso el síntoma aflora al guardar y no al cargar, lejos de su causa

La trampa es un detector que ve xref y se detiene

La manera barata de decidir 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 referencias cruzadas. Esa prueba es correcta para cualquier archivo que se compromete con 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 del trailer de esa sección es donde la mayor parte del documento está realmente indexada. Un detector que devuelve "clásico" en el primer xref que encuentra nunca lee /XRefStm, y todo 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');  // el conteo es correcto
    // inspeccione o edite aquí el documento cargado
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // recorre cada objeto
  finally
    Pdf.Free;
  end;
end;

Con el detector de salida temprana en su lugar, la carga se ve bien y el reguardado es donde los objetos ausentes se anuncian. 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á terminado

El orden de fusión no es negociable

Una vez leídos ambos índices, solo pueden combinarse en una dirección. El flujo de referencias cruzadas tiene que fusionarse primero, con las entradas clásicas rellenadas 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 respeta una política de el-primero-gana y lee 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 las ranuras ya están ocupadas. Invierta el orden y las entradas de tipo 2 del flujo, cada una un número de flujo de objetos más un índice, ganan las ranuras que les corresponden, y las entradas clásicas se acomodan a su alrededor

La misma disciplina protege contra que una revisión más antigua 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 retiró un número de objeto. A una sección posterior en la cadena, más antigua, no se le debe permitir sobrescribir ese centinela con una ubicación obsoleta. Trate lo visto primero como autoritativo para los marcadores libres y el objeto eliminado permanece eliminado; trátelo con descuido y la propia historia del archivo reanima contenido que la última revisión quitó

Diagrama de HotPDF que contrasta dos órdenes de fusión para datos xref híbridos: leer primero la tabla clásica marca el objeto 12 como libre y descarta su entrada tardía de tipo 2, así que el reguardado falla con EListError, mientras que fusionar primero el flujo de referencias cruzadas permite que cada entrada reclame su ranura y la tabla clásica se acomode a su alrededor
Leer primero el flujo permite que las entradas de tipo 2 reclamen sus ranuras, de modo que la tabla clásica se acomoda a su alrededor en lugar de borrarlas

Qué significa esto en HotPDF

El motor resuelve los archivos de referencia híbrida por usted, y lo hace en cada ruta que tiene que analizar los datos de referencias cruzadas. Cargue un documento con LoadFromFile o LoadFromStream, haga 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. En ambos casos la recuperación lee /XRefStm, fusiona la sección del flujo antes de las entradas clásicas y resuelve los objetos que viven en flujos antes de que la escritura los enumere. La ruta de cifrado AES-256 es donde el problema se mostró por primera vez, porque cifrar un documento reescribe cada objeto y por lo tanto exige que cada objeto ya haya sido ubicado

// Un solo paso: lee la entrada híbrida, escribe una copia cifrada con AES-256
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

El detalle que vale la pena llevarse está aguas arriba de la API. Los archivos que llegan de Word, Excel, PowerPoint y una larga lista de pipelines de "Guardar como PDF" son híbridos de manera rutinaria, así que un cargador que usted ejercita solo contra la salida de su propio generador puede no encontrarse nunca con uno en las pruebas. Siembre sus fixtures con documentos exportados desde aplicaciones Office reales, no solo con archivos que su propio código produjo

Comprobar un archivo sospechoso

Dos inspecciones zanjan la cuestión rápidamente. Abra el archivo en una vista hexadecimal y lea los bytes después del último startxref; un archivo híbrido muestra una sección clásica corta cuyo diccionario de trailer contiene /XRefStm. O compare el conteo de objetos que reporta un análisis completo contra el número de objeto más alto que /Size declara en el trailer. Una brecha grande significa que hay objetos escondidos en flujos que el cargador no ha abierto, que es el mismo faltante que luego se convierte en una falla al guardar

La cola de una exportación típica de Excel hace concreta la primera comprobación. Todo lo que sigue a la última palabra clave xref es ASCII plano, así que la firma puede leerse directamente desde una vista hexadecimal (desplazamientos ilustrativos, anotaciones agregadas)

xref
0 0                          % subsección clásica vacía: ninguna fila en absoluto
trailer
<< /Size 216                 % uno más que el número de objeto más alto en uso
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % desplazamiento en bytes del flujo de referencias cruzadas
>>
startxref
88710                        % apunta a la sección clásica de arriba
%%EOF

La subsección 0 0 es la pista delatora: una tabla clásica con cero entradas existe solo para llevar el trailer, y el trailer existe principalmente para decir /XRefStm 87325. Un detector que se detiene en la palabra clave xref ha visto, a estas alturas, un índice de nada. Cuando prefiera automatizar la comprobación en lugar de mirarla a ojo, el marcador siempre está dentro de los últimos dos kilobytes del archivo, así que una lectura hacia atrás acotada es suficiente

// Devuelve el desplazamiento /XRefStm desde la cola del archivo, o -1 si el
// marcador está ausente (el archivo no es híbrido, o ni siquiera es un PDF)
function FindXRefStm(const FileName: string): Int64;
var
  FS: TFileStream;
  Tail: AnsiString;
  Len, P: Integer;
begin
  Result := -1;
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Len := 2048;                        // el trailer vive en la cola
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // lectura hacia atrás acotada: 2 KB máximo
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // no hay marcador híbrido en la cola
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // salta el espacio en blanco tras la clave
  Result := 0;
  while (P <= Len) and (Tail[P] in ['0'..'9']) do
  begin
    Result := Result * 10 + Ord(Tail[P]) - Ord('0');
    Inc(P);
  end;
end;

// Uso: un resultado no negativo nombra el byte donde comienza el flujo
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hybrid-reference file: resave will need the /XRefStm section');

Trate la sonda como triaje, no como un analizador: le dice qué archivos de un lote merecen atención antes de que corra un trabajo de reguardado, y nada más. Lo que un cargador debe hacer luego con el desplazamiento que encuentra, seguir la cadena de secciones, fusionar las entradas del flujo antes de las clásicas, respetar los centinelas de entradas libres, se recorre paso a paso en nuestro artículo complementario sobre el manejo de PDF de referencia híbrida de aplicaciones Office

El lado 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 de el recorrido por la Direct File API para flujos de trabajo con PDF grandes le permiten inspeccionarlo sin leer todo el archivo en memoria. Ambos se combinan naturalmente con la recuperación descrita aquí, que se incluye como parte del componente HotPDF para Delphi para Delphi y C++Builder junto con las API de carga, edición, cifrado y firma cubiertas en otras partes de este blog