Technical Article

Árboles de páginas PDF: el orden de páginas no es el orden de objetos

La página 1 de un PDF no es el objeto 1. Esa distinción es la fuente más común de errores de extracción de páginas incorrectas en los analizadores de PDF, y la solución está en leer la especificación, no los bytes del fichero.

Objetos, referencias y el catálogo

Un fichero PDF es una colección de objetos numerados. Cada uno lleva un número de objeto único y un número de generación, escritos como N G obj donde G es casi siempre 0 en ficheros que no han sido actualizados de forma incremental. Los objetos se referencian entre sí con la notación N G R, por lo que 3 0 R significa "la versión actual del objeto 3". El tráiler apunta a un objeto catálogo raíz cuya entrada /Pages conduce al árbol de páginas. Todo lo navegable en un PDF parte de esa raíz, no del primer byte del cuerpo del fichero.

La tabla de referencias cruzadas (o flujo de referencias cruzadas en PDF 1.5+) asigna números de objeto a desplazamientos en el fichero. Su función es el acceso aleatorio, no el ordenamiento. Un escritor que construye un documento de forma incremental puede añadir nuevos objetos al final con números más altos mientras esos objetos preceden lógicamente a los existentes en la secuencia de páginas. Eso no es un defecto; es por diseño.

El árbol de páginas (ISO 32000-1 §7.7.3)

La secuencia de páginas vive en el árbol de páginas. El catálogo raíz contiene una referencia /Pages que apunta a un nodo de tipo /Pages. El array /Kids de ese nodo lista sus hijos en orden de lectura. Cada hijo es un nodo hoja de tipo /Page u otro nodo intermedio /Pages con su propio /Kids. La página 1 es la primera hoja alcanzada mediante un recorrido en profundidad de izquierda a derecha de los arrays Kids. La entrada /Count de cada nodo intermedio almacena en caché el número total de páginas hoja descendientes, de modo que un visualizador puede saltar a la página 500 sin recorrer todo el árbol.

Así es como se ve un árbol mínimo de tres páginas en sintaxis PDF sin procesar:

16 0 obj
<<
  /Type /Pages
  /Count 3
  /Kids [20 0 R  1 0 R  4 0 R]
  /MediaBox [0 0 612 792]
>>
endobj

20 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 21 0 R  /Resources 22 0 R >>
endobj

1 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 2 0 R   /Resources 3 0 R >>
endobj

4 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 5 0 R   /Resources 6 0 R >>
endobj

El array Kids se lee como [20 0 R, 1 0 R, 4 0 R]. La página lógica 1 es el objeto 20, la página lógica 2 es el objeto 1 y la página lógica 3 es el objeto 4. Cualquier código que itere los números de objeto de 1 hacia arriba los encontrará en el orden 1, 4, 20 y producirá la secuencia página-2, página-3, página-1. El documento resultante se renderiza en un orden desordenado que puede parecer perfectamente normal en un visualizador que sigue el árbol y catastróficamente incorrecto en uno que no lo hace.

Herencia

Los nodos intermedios pueden llevar propiedades que heredan sus descendientes. Las entradas heredadas más comunes son /MediaBox (dimensiones de página), /CropBox, /Resources (fuentes e imágenes) y /Rotate. Una página hoja que omite /MediaBox no está rota; toma el valor del nodo ancestro más cercano que lo define. Una página que sí define /MediaBox anula lo que diga el padre, solo para esa página.

Esto importa para el análisis sintáctico. Leer un objeto /Page de forma aislada y asumir que sus propiedades son completas reportará incorrectamente las dimensiones de cualquier página que dependa de la herencia. Un lector correcto recorre la cadena /Parent, recopilando las propiedades que aún no ha visto, deteniéndose en la raíz.

Árboles anidados

Nada en la especificación limita el árbol a un único nivel. Un documento grande podría agrupar páginas bajo nodos intermedios que se corresponden aproximadamente con capítulos:

2 0 obj   % root Pages node, Count = 8
<< /Type /Pages  /Count 8  /Kids [3 0 R  4 0 R] >>
endobj

3 0 obj   % first chapter, 5 pages
<< /Type /Pages  /Parent 2 0 R  /Count 5
   /Kids [10 0 R  11 0 R  12 0 R  13 0 R  14 0 R]
   /MediaBox [0 0 612 792] >>
endobj

4 0 obj   % second chapter, 3 pages
<< /Type /Pages  /Parent 2 0 R  /Count 3
   /Kids [20 0 R  21 0 R  22 0 R]
   /MediaBox [0 0 612 792] >>
endobj

El algoritmo de recorrido es el mismo: visitar Kids en orden, recurrir a cualquier nodo /Pages, recopilar los nodos hoja /Page. Los valores /Count permiten a un visualizador saltar un subárbol completo al ir a una página que está más allá de él, razón por la que esos recuentos deben ser precisos. Algunos editores de PDF de finales de los años 90 y principios de los 2000 no los recalculaban tras ediciones in situ, por lo que un analizador defensivo verifica /Count frente al recuento real de hojas en lugar de fiarse de él para la asignación de arrays.

Dónde aparece esto en la práctica

El error de orden de páginas surge con más frecuencia en dos escenarios. El primero es un analizador personalizado que busca objetos de tipo /Page en lugar de seguir el árbol. Encuentra todas las páginas, pero en orden de número de objeto, no en orden de lectura. La solución es siempre la misma: empezar desde el tráiler, resolver el catálogo raíz, seguir /Pages y recorrer los arrays Kids.

El segundo escenario es un fichero con actualizaciones incrementales. Cuando un editor de PDF añade cambios sin reescribir el fichero completo, los nuevos objetos de página obtienen números de objeto altos mientras que el array Kids del árbol original sigue controlando su posición lógica. Una página que originalmente era el objeto 5 se reemplaza por un nuevo objeto 143, pero el array Kids ahora referencia 143 donde antes referenciaba 5, por lo que el orden lógico se preserva. Recorrer por número de objeto pondría la página de reemplazo en la posición incorrecta de la secuencia.

Los PDF linealizados (optimizados para la web) añaden una tercera variación: el fichero se reorganiza físicamente para que el contenido de la primera página aparezca cerca del inicio del fichero para una visualización rápida en una conexión lenta. La estructura del árbol de páginas sigue siendo la autoridad en cuanto al orden, pero la tabla de referencias cruzadas apunta a los desplazamientos reorganizados. Un analizador que depende de la posición en el fichero en lugar de la tabla xref malinterpretará incluso la primera página de un fichero linealizado.

El HotPDF Component gestiona internamente el recorrido del árbol de páginas, la resolución de herencia y la fusión de xref de actualizaciones incrementales. Trabajar directamente con sus objetos de página significa que el ordenamiento del array Kids ya está aplicado; los índices de página se corresponden con páginas lógicas, no con números de objeto.