Technical Article

Orden de páginas en PDF: cómo el árbol de páginas controla la secuencia

El objeto número 1 no es la página 1. Ese único hecho hace tropezar a más código de procesamiento de PDF que cualquier otro aspecto del formato, y entender por qué requiere ir más allá de lo que muestra un visor y mirar dentro del grafo de objetos que el visor realmente lee.

Un fichero PDF es una colección de objetos indirectos numerados. Las páginas se encuentran entre esos objetos, pero su secuencia de visualización no tiene nada que ver con su posición en el fichero ni con los números que llevan. El orden de visualización está determinado enteramente por el árbol /Pages, una estructura enlazada con raíz en el catálogo del documento. Si ignoras el árbol y escaneas los objetos numéricamente, ensamblarás las páginas en el orden incorrecto para una fracción significativa de los ficheros del mundo real.

El árbol de páginas: lo que realmente establece el orden

Todo PDF comienza con un catálogo de documento (ISO 32000-2 §7.7.2). El catálogo contiene una entrada /Pages que apunta al nodo raíz del árbol de páginas. Ese nodo raíz es un diccionario con /Type /Pages, un array /Kids de referencias indirectas y un /Count que indica el número total de páginas hoja por debajo de él. El orden de visualización es el recorrido en profundidad de izquierda a derecha de ese árbol, punto.

Un fichero mínimo de tres páginas lo hace concreto:

%PDF-1.7

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

2 0 obj
<< /Type /Pages /Kids [20 0 R  4 0 R  9 0 R] /Count 3 >>
endobj

% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

El array /Kids lee [20 0 R 4 0 R 9 0 R], por lo que el objeto 20 es la página 1, el objeto 4 es la página 2 y el objeto 9 es la página 3. La numeración de objetos es irrelevante. Cualquier código que itere objetos en orden numérico y recoja los que tienen /Type /Page producirá la secuencia incorrecta en este fichero.

¿Por qué los generadores producen disposiciones no secuenciales? Por varias razones. Una biblioteca que preasigna números de objeto para todas las páginas antes de escribir su contenido las numerará en orden de creación y luego escribirá los bytes reales en el orden que convenga al serializador. Una herramienta de fusión que une documentos renumera los objetos de cada documento fuente para evitar colisiones; los objetos de página renumerados acaban dispersos por la tabla de objetos combinada mientras el nuevo array raíz /Kids contiene la secuencia de visualización correcta. Las actualizaciones incrementales añaden nuevos objetos al final del fichero con números nuevos, por lo que una página añadida como revisión vive cerca del final del flujo de bytes aunque pertenezca a la posición 1 del orden de visualización.

Árboles planos y subárboles anidados

La especificación permite dos formas para el árbol de páginas. Los generadores simples producen una estructura plana: un nodo raíz /Pages cuyo array /Kids no contiene más que objetos hoja /Page. Eso es fácil de recorrer: un nivel de profundidad, una pasada.

Los documentos grandes suelen usar un árbol equilibrado en su lugar. El array /Kids del nodo raíz /Pages contiene nodos /Pages intermedios, cada uno de los cuales a su vez tiene su propio array /Kids. El /Count de cada nodo intermedio informa del número total de páginas hoja en su subárbol, por lo que un visor puede saltarse subárboles enteros al saltar a una página por índice sin analizar cada objeto. Un documento de 1.000 páginas estructurado como un árbol equilibrado con 10 páginas por nodo hoja puede localizar la página 750 mediante búsqueda binaria en tres o cuatro búsquedas en diccionario en lugar de escanear 750 entradas /Kids.

La consecuencia para el código de procesamiento: no puedes asumir que el primer nivel de /Kids contiene objetos /Page. Cada hijo debe comprobarse. Si su /Type es /Pages, recurre en él. Si su /Type es /Page, es una hoja. Detenerse en el primer nivel omite silenciosamente subárboles enteros en cualquier documento donde el generador eligió anidar.

Atributos de página heredados

El árbol de páginas también lleva un mecanismo de compartición de recursos. Ciertos atributos de página (/MediaBox, /CropBox, /Resources y /Rotate) son heredables (ISO 32000-2 §7.7.3.4). Si un diccionario /Page omite uno de ellos, un lector sube por la cadena /Parent hasta encontrar el atributo o alcanzar la raíz. Colocar un diccionario de fuentes compartido en el nodo raíz /Pages en lugar de copiarlo en cada página hoja puede reducir notablemente el tamaño del fichero en documentos que usan los mismos tipos de letra en todas partes.

La regla de herencia crea una sutileza para el código que lee propiedades de página. Leer /MediaBox directamente de un objeto /Page y tratar una clave ausente como un error es incorrecto; la clave puede simplemente estar heredada. El código que resuelve correctamente la geometría de la página debe seguir la cadena de padres. También necesita una protección contra ciclos: un fichero corrupto puede tener una referencia /Parent que apunta de vuelta a un nodo ya visitado, lo que sin una comprobación de objeto visitado daría lugar a un bucle infinito.

La tabla xref y los flujos de referencias cruzadas

La búsqueda de objetos indirectos pasa por la tabla de referencias cruzadas (o su sucesora, el flujo de referencias cruzadas introducido en PDF 1.5). El xref asigna cada número de objeto a un desplazamiento de bytes dentro del fichero. Un lector conforme usa el xref para saltar directamente a cualquier objeto; no escanea el fichero secuencialmente. Ese diseño de acceso aleatorio es lo que hace posible el salto rápido de páginas: el visor lee el catálogo, resuelve la referencia /Pages vía el xref, lee el nodo raíz /Pages, resuelve una entrada /Kids, y así sucesivamente, tocando solo los objetos que necesita.

Las actualizaciones incrementales añaden una nueva sección xref al final del fichero con un tráiler que se encadena al anterior. Un objeto actualizado en una revisión obtiene una nueva entrada en la sección xref añadida; los bytes originales se mantienen en su lugar pero quedan supersedidos. Así es como los PDF firmados digitalmente siguen siendo verificables incluso después de añadir anotaciones o revisiones de relleno de formularios: el rango de bytes firmado nunca se toca, y el nuevo contenido vive en la sección añadida. El árbol de páginas también puede actualizarse, por lo que las adiciones o eliminaciones de páginas en una revisión producen una nueva raíz /Pages con un array /Kids revisado, mientras que el objeto raíz antiguo sigue ocupando su posición original en el fichero.

Qué sale mal sin recorrido del árbol

El modo de fallo de los enfoques de escaneo de objetos es silencioso. El documento de salida parece plausible: tiene el número correcto de páginas y cada página contiene contenido reconocible. El orden simplemente es incorrecto, e incorrecto de una manera que depende del generador, del número de revisiones y de si se fusionaron páginas de fuentes externas. Un corpus de prueba de ficheros producidos por una sola herramienta puede pasar completamente; los ficheros de una herramienta diferente o un flujo de trabajo de fusión fallarán. Esa inconsistencia es por qué las correcciones heurísticas nunca se sostienen.

Los ficheros de actualización incremental son especialmente propensos a esto porque las páginas añadidas o reordenadas en revisiones posteriores llevan números de objeto altos mientras el orden de visualización está controlado por el array /Kids actualizado. Un escaneo que procesa objetos en orden numérico colocará esas páginas de número tardío al final independientemente de dónde diga el árbol que pertenecen.

La solución no es complicada. Empieza por el catálogo, resuelve la referencia /Pages, recorre el array /Kids de forma recursiva y emite las hojas en el orden en que las encuentres. Ese es el orden de visualización por definición, independientemente de los números de objeto, los desplazamientos de bytes o la estructura del fichero. La mayoría de las bibliotecas PDF maduras exponen un contador de páginas y un acceso a páginas por índice que ya hacen esto correctamente; el riesgo está en el código que omite el modelo de páginas de la biblioteca y toca la capa de objetos directamente.

Una anomalía estructural que merece tratamiento explícito: el valor /Count de un nodo /Pages intermedio puede ser incorrecto en ficheros malformados. Confiar en /Count para la comprobación de límites y detenerse antes de un recorrido completo omitirá silenciosamente páginas cuando el recuento sea inferior al real. Usar /Count solo como indicación de rendimiento para la preasignación de capacidad o la búsqueda binaria, y derivar el recuento real del recorrido es el patrón más seguro.

 Next Article