Abra un PDF generado por Microsoft Word o Excel, hojéelo, y nada parece fuera de lo normal. Cárguelo en un programa Delphi, lea de nuevo el recuento de páginas, y el número es correcto. Después vuelva a guardarlo con el cifrado activado y el proceso falla con un EListError, o el resultado se abre con un aviso 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 que hace fracasar a un cargador que deja de leer demasiado pronto
Esta es una de las formas más habituales en que un flujo de procesamiento de PDF que ha superado todas las pruebas internas se topa con un archivo que no puede procesar de ida y vuelta. Todas las entradas se generaron internamente, 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
Lo que realmente escriben Word y Excel
ISO 32000-1 describe el diseño de referencia híbrida en el §7.5.8.4. Una aplicación que quiere funciones de PDF 1.5, como los flujos de objetos, sin dejar de permitir que un lector de PDF 1.4 abra el archivo, escribe la información de referencias cruzadas dos veces. Existe una tabla de referencias cruzadas clásica, las filas ASCII de ancho fijo que cerraban todo 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 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, de modo que un lector 1.4 pasa de largo junto a ellos y nunca tropieza con una estructura que no puede analizar. Sus ubicaciones reales solo viven en el flujo de referencias cruzadas. La firma de un archivo así es su cola: una sección clásica breve, a menudo nada más que xref seguido de un encabezado de subsección 0 0, cuyo tráiler apunta al /XRefStm donde reside en realidad la información de recuperación
/XRefStm, mientras que el lado de flujo guarda el índice real de objetosPor qué un recuento 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 solo lee esa tabla encuentra /Root, recorre el árbol de páginas y devuelve el número correcto de páginas. Todo lo que necesita un lector antiguo está presente, así que el archivo parece sano. Los objetos que faltan son los empaquetados en flujos de objetos: diccionarios de campos de AcroForm, elementos de estructura de PDF etiquetado, la larga cola de pequeños diccionarios que nunca tuvieron que ser visibles para un visor heredado
No se advierte el hueco hasta que algo toca esos objetos, y volver a guardar el documento por completo los toca 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 aparece al guardar y no al cargar, lejos de su causa
La trampa es un detector que ve xref y se detiene
La forma 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 decante por un único esquema. Es incorrecta para un archivo híbrido, cuyo startxref apunta a una sección clásica con el único fin de satisfacer a los lectores antiguos, mientras que el /XRefStm del tráiler de esa sección es donde en realidad se indexa la mayor parte del documento. Un detector que devuelve "classic" ante el primer xref que encuentra nunca lee /XRefStm, y todo objeto que solo vive en el flujo se vuelve invisible
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // el recuento 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 sitio, la carga parece correcta y es en el nuevo guardado donde los objetos ausentes se manifiestan. La solución no es leer más bytes al principio; es reconocer el tráiler híbrido y seguir /XRefStm antes de dar el archivo por terminado
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 referencias cruzadas debe fusionarse primero, y las entradas clásicas se rellenan a su alrededor. La razón es el pequeño engaño que hay 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 que se ve, gana" y lee primero la tabla clásica registrará esos números de objeto como libres, y después descartará las entradas del flujo que en realidad los localizan, 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, se quedan con 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 mediante /Prev, y una entrada libre de tipo 0 es un centinela que indica que una sección más reciente ha retirado un número de objeto. No se debe permitir que una sección posterior, pero más antigua en la cadena, sobrescriba ese centinela con una ubicación obsoleta. Trate el primer valor visto 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 revisión más reciente eliminó
Qué significa esto en HotPDF
El motor resuelve por usted los archivos de referencia híbrida, y lo hace en cada ruta que tiene que analizar los datos de 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. En ambos casos la recuperación lee /XRefStm, fusiona la sección del flujo antes que 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 manifestó por primera vez, porque cifrar un documento reescribe todos los objetos y, por tanto, exige que todos los objetos ya estén localizados
// Un solo paso: lee la entrada híbrida y 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 retener está aguas arriba de la API. Los archivos que llegan de Word, Excel, PowerPoint y una larga lista de flujos de "Guardar como PDF" son habitualmente híbridos, así que un cargador que solo se ejercita contra la salida de su propio generador puede que nunca se encuentre con uno en las pruebas. Nutra sus conjuntos de prueba con documentos exportados desde aplicaciones de Office reales, no solo con archivos que produjo su propio código
Cómo comprobar un archivo sospechoso
Dos comprobaciones resuelven la duda con rapidez. Abra el archivo en una vista hexadecimal y lea los bytes posteriores al último startxref; un archivo híbrido muestra una sección clásica breve cuyo diccionario de tráiler contiene /XRefStm. O compare el recuento de objetos que informa un análisis completo con el número de objeto más alto que declara /Size en el tráiler. Una diferencia grande significa que hay objetos escondidos en flujos que el cargador no ha abierto, la misma carencia que más tarde se convierte en un fallo al guardar
La cola de una exportación típica de Excel hace tangible la primera comprobación. Todo lo que sigue a la última palabra clave xref es ASCII plano, así que la firma se puede leer directamente en una vista hexadecimal (desplazamientos ilustrativos, anotaciones añadidas)
xref
0 0 % subsección clásica vacía: no hay ninguna fila
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 señal reveladora: una tabla clásica con cero entradas existe solo para llevar el tráiler, y el tráiler existe sobre todo para decir /XRefStm 87325. Un detector que se detiene en la palabra clave xref ha visto, llegado a este punto, un índice de la nada. Cuando prefiera automatizar la comprobación en lugar de hacerla a simple vista, el marcador siempre se encuentra dentro de los últimos kilobytes del archivo, así que basta con una lectura acotada hacia atrás
// Devuelve el desplazamiento de /XRefStm desde la cola del archivo, o -1 si
// el marcador está ausente (el archivo no es híbrido, o no 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 tráiler vive en la cola
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // lectura acotada hacia atrás: 2 KB máx.
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); // omite los espacios 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 indica 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 una clasificación previa, no como un analizador: le indica qué archivos de un lote merecen atención antes de que se ejecute un trabajo de reguardado, y nada más. Lo que un cargador debe hacer después con el desplazamiento que encuentra, seguir la cadena de secciones, fusionar las entradas del flujo antes que las clásicas, respetar los centinelas de entradas libres, se explica paso a paso en nuestro artículo complementario sobre el tratamiento de PDF de referencia híbrida procedentes de aplicaciones de Office
La parte de la historia relativa a quien escribe, cómo se producen en primer lugar los flujos de objetos y las referencias cruzadas comprimidas, se trata 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 el recorrido por la API de archivo directo para flujos de trabajo con PDF de gran tamaño le permiten inspeccionarlo sin cargarlo por completo en memoria. Ambas encajan de forma natural con la recuperación descrita aquí, que se incluye como parte del HotPDF Delphi Component para Delphi y C++Builder junto con las API de carga, edición, cifrado y firma tratadas en otras partes de este blog