Technical Article

Estructura de un fichero PDF: cabecera, cuerpo, Xref y tráiler

Un lector de PDF no empieza por el principio del fichero. Empieza por el final. Los últimos bytes contienen la dirección de todo lo demás, y un analizador que no entienda ese orden malinterpretará el formato desde la primera línea. Por eso la forma más útil de aprender el PDF en disco es aprenderlo como lo hace un lector: primero la cola, luego saltar hacia atrás hasta el mapa y después resolver los objetos a los que apunta el mapa.

Los propios bytes son suficientemente legibles en un editor de texto cuando nada está comprimido. Un documento mínimo de una página que dibuja "Hello, World!" cabe en menos de quinientos bytes, y todos los elementos estructurales del formato son visibles en él. Aquí está el fichero completo, con las cuatro partes marcadas:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Cuatro partes, siempre en este orden en el fichero: una cabecera, un cuerpo de objetos, una tabla de referencias cruzadas y un tráiler. La trampa es que las lees en casi el orden inverso. ISO 32000-2 §7.5.1 establece la misma anatomía de cuatro partes, y la razón del acceso de atrás hacia adelante es puramente práctica: un lector que salta directamente al objeto que necesita es mucho más rápido que uno que escanea todos los bytes desde el principio, y ese acceso aleatorio es exactamente para lo que existen el tráiler y la tabla de referencias cruzadas.

La cabecera son dos líneas, y la segunda importa

La primera línea es %PDF-1.0. El signo de porcentaje la convierte en comentario a efectos sintácticos, pero los lectores la tratan como la firma del fichero y extraen de ella el número de versión. El tratamiento de versiones es flexible en la práctica. Un lector construido para PDF 2.0 abrirá sin problemas un fichero que declare 1.0, y la mayoría de los lectores intentarán abrir un fichero cuya versión declarada sea incorrecta o cuya línea de versión esté un poco dentro del fichero en lugar de en el byte cero. El número es una indicación de qué funciones esperar, no una barrera.

La segunda línea es la que la gente borra por accidente y luego pasa una tarde depurando. También es un comentario, pero su contenido son cuatro bytes por encima de ASCII 127. Existen para que cualquier cosa que mueva el fichero en «modo texto» lo reconozca como binario y deje de reescribir los finales de línea. Un PDF lleva flujos comprimidos cuyos bytes pueden coincidir por casualidad con un retorno de carro o un avance de línea; si una herramienta de transferencia los reescribe, la longitud del flujo registrada en el diccionario ya no coincide con los bytes en disco y el fichero queda corrupto. El comentario de bytes altos es una defensa de cuarenta años contra el FTP en modo ASCII, y sigue estando en todos los ficheros que escribe una herramienta seria porque el fallo que previene es silencioso y total.

El cuerpo contiene los objetos, cada uno numerado

Todo lo que compone el documento vive en el cuerpo como una secuencia plana de objetos indirectos. Cada uno se abre con dos enteros y la palabra clave obj, contiene su contenido y se cierra con endobj. El objeto 1 en el ejemplo anterior es el nodo del árbol de páginas: 1 0 obj, luego un diccionario, luego endobj. El primer entero es el número de objeto, el segundo es el número de generación. La generación es casi siempre cero en un fichero recién escrito; solo aumenta cuando un número de objeto se reutiliza entre ediciones, lo que es suficientemente raro para tratar una generación distinta de cero como señal de que el fichero ha pasado por actualizaciones incrementales. El contenido entre las palabras clave es aquí un diccionario, escrito entre << y >>, pero podría ser igualmente un número, una cadena, un array o un flujo.

Lo que convierte esto en un grafo en lugar de una lista es el token de referencia 2 0 R. Eso significa «objeto 2, generación 0, donde quiera que resida en el fichero». El nodo del árbol de páginas anterior no contiene su página; apunta al objeto 2, que apunta a sus recursos y flujo de contenido por el mismo mecanismo. El cuerpo está dispuesto en el orden que al escritor le resultó conveniente, y las referencias lo cosen en un árbol con raíz en el catálogo. La posición en el fichero no tiene significado. La identidad proviene del número de objeto, y la ubicación de la tabla de referencias cruzadas.

La tabla de referencias cruzadas es un índice de desplazamientos de bytes

La tabla xref es lo que convierte los números de objeto en posiciones del fichero. Es la razón por la que un lector puede abrir un documento de mil páginas y renderizar la página 850 sin analizar las 849 páginas anteriores. Cada entrada registra exactamente dónde comienza su objeto, contado en bytes desde el inicio del fichero:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

La anchura fija es deliberada. Cada entrada tiene exactamente veinte bytes: un desplazamiento de diez dígitos, un espacio, una generación de cinco dígitos, un espacio, un tipo de un carácter y un final de línea de dos bytes. Como las filas son uniformes, un lector puede indexar directamente la entrada del objeto n mediante aritmética en lugar de escanear, por lo que la tabla que proporciona acceso aleatorio al cuerpo es en sí misma accesible aleatoriamente. La línea 0 6 es una cabecera de subsección: indica que las siguientes entradas describen seis objetos a partir del número 0.

El objeto 0 es especial y siempre está presente. Su tipo es f de libre, su generación es 65535, y encabeza la lista enlazada de números de objeto libres. En un fichero que nunca ha sido editado, la lista libre es solo esta entrada, una formalidad. Se gana su sitio durante las actualizaciones incrementales, cuando al eliminar un objeto se añade su número a esa lista para que una edición posterior pueda recuperarlo. Las demás entradas son del tipo n de en uso, y su número de diez dígitos es el desplazamiento al que habría que desplazarse para leer la definición de ese objeto.

El tráiler es el punto de entrada, y está al final

El tráiler es lo primero que consume realmente un lector, aunque se escribe el último. Un analizador abre el fichero, se desplaza al final y retrocede buscando %%EOF. Justo encima está startxref seguido de un único número, y ese número es el desplazamiento de bytes de la palabra clave xref. Con él, el lector salta directamente a la tabla de referencias cruzadas sin haber escaneado ningún objeto:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6              % one more than the highest object number
>>
startxref
459                  % byte offset of the xref table
%%EOF

El diccionario del tráiler contiene los dos valores que un lector necesita antes de poder hacer cualquier otra cosa. /Root apunta al catálogo del documento, el objeto 5 aquí, que es la cima del grafo de objetos y la ruta al árbol de páginas. /Size es el número de entradas que debe contener la tabla de referencias cruzadas, que es uno más que el número de objeto más alto debido a la entrada libre en la ranura cero. A partir de %%EOF se despliega toda la secuencia de lectura: encontrar el marcador, leer startxref para localizar la tabla, cargar la tabla para saber dónde vive cada objeto, leer /Root para encontrar el catálogo y resolver objetos bajo demanda a partir de ahí. La cabecera, en la parte superior, apenas se consulta hasta el final. El mapa en la parte inferior es lo que el lector necesita primero.

La actualización incremental añade un segundo mapa en lugar de reescribir

Ese diseño de cola primero rinde frutos cuando un fichero cambia. Un PDF puede editarse sin reescribir ninguno de los bytes ya en disco. Los objetos nuevos y modificados se añaden al final, seguidos de una sección de referencias cruzadas nueva y un tráiler nuevo, y el fichero original queda intacto. El único registro nuevo es una entrada /Prev en el nuevo tráiler, que contiene el desplazamiento de bytes de la tabla de referencias cruzadas anterior:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

Un lector sigue empezando por el último %%EOF, sigue startxref hasta la tabla más reciente, pero ahora sigue la cadena /Prev hacia atrás hasta las tablas más antiguas, fusionándolas de modo que la entrada más reciente para cualquier número de objeto gana. Las secciones de referencias cruzadas forman una lista enlazada a lo largo del fichero, cada una anulando a la anterior para los objetos que toca. Un objeto que una edición reemplazó sigue existiendo físicamente en su antiguo desplazamiento; simplemente ya no es accesible, porque una entrada xref posterior apunta a algo más nuevo.

Este es el mecanismo que hace verificables los PDF firmados. Una firma digital cubre un rango de bytes del fichero, y como una actualización incremental solo añade al final, los bytes firmados nunca se mueven. La firma sigue validándose contra el rango original mientras las revisiones posteriores se sitúan más allá de él, cada una con su propia xref y tráiler. También es la razón por la que un PDF puede llevar historial recuperable: cada objeto supersedido sigue en disco bajo una sección de referencias cruzadas anterior, lo que es una ventaja para el seguimiento de versiones y un riesgo para quien pensó que «eliminar» significaba que los bytes habían desaparecido.

El coste es el crecimiento. Cada edición añade al final; nada se recupera en su lugar, por lo que un fichero revisado muchas veces acumula objetos muertos y una larga cadena de secciones xref. El remedio es una reescritura completa: cargar el documento y guardarlo de nuevo, que renumera los objetos supervivientes, descarta los inaccesibles y emite una sola tabla de referencias cruzadas limpia. Las dos estrategias se contraponen directamente. Añadir al final es rápido y preserva firmas e historial; reescribir es más lento y descarta ambos, a cambio de un fichero compacto.

Leer las cuatro partes en la práctica

Conocer la disposición es suficiente para depurar manualmente la mayoría de los problemas de «este fichero no abre». Si un lector rechaza un PDF, los culpables habituales están en los dos extremos, no en el medio. Una descarga truncada pierde el tráiler, por lo que falta startxref o %%EOF y el lector no tiene punto de entrada; los lectores tolerantes recurren a escanear todo el fichero para reconstruir el xref, que es exactamente el camino lento que la tabla tenía por objeto evitar. Una transferencia en modo texto fallida corrompe los bytes del flujo o los desplazamientos dejan de coincidir con la realidad, y los objetos se cargan desde la posición incorrecta. Cuando los desplazamientos de la tabla ya no apuntan a palabras clave obj reales, el fichero está estructuralmente roto aunque cada objeto individualmente esté bien.

Para código nuevo, la lección de la disposición es dejar que una biblioteca se encargue de la contabilidad de bytes. Los desplazamientos de la tabla de referencias cruzadas deben coincidir con las posiciones reales de cada objeto al byte, el tráiler debe apuntar a la tabla correcta y las actualizaciones incrementales deben encadenarse correctamente a través de /Prev. Un componente nativo como el componente HotPDF para Delphi y C++Builder gestiona todo eso al escribir un fichero, incluida la elección entre añadir una revisión incremental y reescribir una versión compacta. Si quieres ver la misma estructura construida desde cero en lugar de diseccionada, el artículo complementario sobre construir un documento PDF desde cero recorre la emisión de la cabecera, los objetos, el xref y el tráiler en orden.