Artículo técnico

Ordenamiento de páginas PDF: Cómo el árbol de páginas controla la secuencia

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

Un fichero PDF es una colección de objetos indirectos numerados. Las páginas están entre esos objetos, pero su secuencia de visualización no tiene nada que ver con dónde se asientan en el fichero o qué números llevan. El orden de visualización está determinado enteramente por el árbol /Pages, una estructura enlazada arraigada en el catálogo del documento. Si ignora el árbol y escanea los objetos numéricamente, ensamblará las páginas en el orden incorrecto para una fracción significativa de los ficheros del mundo real

El árbol de páginas: qué fija realmente el orden

Todo PDF comienza con un catálogo de documento (ISO 32000-2 §7.7.2). El catálogo alberga una entrada /Pages que apunta al nodo raíz del árbol de páginas. Ese nodo raíz es un diccionario con /Type /Pages, una matriz /Kids de referencias indirectas, y un /Count que da el recuento total de páginas de hojas bajo este. El orden de visualización es el recorrido de ese árbol en profundidad de izquierda a derecha, punto final

Un fichero mínimo de tres páginas hace esto 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

La matriz /Kids dice [20 0 R 4 0 R 9 0 R], de modo 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 recolecte aquellos con /Type /Page producirá la secuencia equivocada en este fichero

¿Por qué los generadores producen diseños no secuenciales? Varias razones. Una biblioteca que preasigna números de objeto para todas las páginas antes de escribir su contenido los numerará en orden de creación, luego escribirá los bytes reales en cualquier orden que le 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 terminan dispersos por la tabla de objetos combinada mientras que la nueva matriz /Kids raíz mantiene 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 reside cerca del final del flujo de bytes incluso si pertenece 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 solo nodo /Pages raíz cuya matriz /Kids no contiene nada más que objetos de hoja /Page. Eso es fácil de recorrer: un nivel de profundidad, una pasada

Los documentos grandes utilizan rutinariamente un árbol balanceado en su lugar. La matriz /Kids del nodo /Pages raíz contiene nodos /Pages intermedios, cada uno de los cuales, a su vez, alberga una matriz /Kids propia. El /Count en cada nodo intermedio reporta el número total de páginas de hoja en su subárbol, de modo 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 balanceado con 10 páginas por nodo de hoja puede localizar la página 750 mediante búsqueda binaria a través de tres o cuatro búsquedas de diccionario en lugar de escanear 750 entradas /Kids

La consecuencia para el código de procesamiento: no puede asumir que el primer nivel de /Kids contiene objetos /Page. Cada hijo debe ser comprobado. Si su /Type es /Pages, se recurre a él. Si su /Type es /Page, es una hoja. Detenerse en el primer nivel descarta silenciosamente subárboles enteros en cualquier documento en el que el generador haya optado por anidar

Atributos de página heredados

El árbol de páginas también conlleva 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 recorre la cadena /Parent hacia arriba hasta que encuentra el atributo o llega a la raíz. Situar un diccionario de fuentes compartido en el nodo /Pages raíz en lugar de copiarlo en cada página de hoja puede reducir notablemente el tamaño del fichero para los documentos que utilizan las mismas tipografías en todas partes

La regla de herencia crea una sutileza para el código que lee las propiedades de página. Leer /MediaBox directamente desde un objeto /Page y tratar una clave faltante como un error es incorrecto; la clave simplemente puede ser heredada. El código que resuelve correctamente la geometría de la página debe seguir la cadena padre. También necesita un protector de ciclos: un fichero dañado puede tener una referencia /Parent que apunte de vuelta a un nodo ya visitado, lo que generaría un bucle infinito sin una comprobación de objetos visitados

La tabla xref y las secuencias de referencias cruzadas

La búsqueda de objetos indirectos pasa por la tabla de referencias cruzadas (o su sucesora, la secuencia de referencias cruzadas introducida en PDF 1.5). El xref mapea cada número de objeto a un desplazamiento en bytes dentro del fichero. Un lector compatible 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 a través del xref, lee el nodo /Pages raíz, resuelve una entrada /Kids, y así sucesivamente, tocando únicamente los objetos que necesita

Las actualizaciones incrementales añaden una nueva sección xref al final del fichero con un tráiler que encadena a la anterior. Un objeto actualizado en una revisión obtiene una nueva entrada en la sección xref anexada; los bytes originales se mantienen en su lugar pero son sustituidos. Así es como los PDFs firmados digitalmente siguen siendo verificables incluso después de que se añadan anotaciones o se rellenen formularios: el rango de bytes firmado nunca se toca, y el nuevo contenido reside en la sección anexada. El árbol de páginas también se puede actualizar, de modo que las adiciones o eliminaciones de páginas en una revisión producen una nueva raíz /Pages con una matriz /Kids revisada, mientras que el objeto raíz antiguo todavía ocupa su posición original en el fichero

Lo que sale mal sin el 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 el equivocado, y equivocado de una manera que depende del generador, el número de revisiones y de si alguna página fue fusionada de orígenes externos. Un corpus de prueba de ficheros producidos por una sola herramienta puede pasar por completo; los ficheros de una herramienta diferente o un flujo de trabajo de fusión fallarán. Esa inconsistencia es la razón por la cual los parches heurísticos nunca se mantienen

Los ficheros de actualización incremental son especialmente propensos a esto porque las páginas añadidas o reorganizadas en revisiones posteriores llevan números de objeto altos, mientras que el orden de visualización es controlado por la matriz /Kids actualizada. Un escaneo que procese objetos en orden numérico colocará esas páginas numeradas posteriormente al final independientemente de dónde el árbol dice que pertenecen

La solución no es complicada. Empiece en el catálogo, resuelva la referencia /Pages, recorra la matriz /Kids recursivamente, y emita las hojas en el orden en que se las encuentre. Ese es el orden de visualización por definición, sin importar los números de objeto, los desplazamientos en bytes, o la estructura del fichero. La mayoría de las bibliotecas PDF maduras exponen un recuento de páginas y un descriptor de acceso de página indexado que ya hace esto correctamente; el riesgo reside en el código que omite el modelo de página de la biblioteca y toca la capa de objetos directamente

Una anomalía estructural digna de ser manejada explícitamente: el valor /Count en un nodo /Pages intermedio puede estar equivocado en ficheros malformados. Confiar en /Count para la comprobación de límites y luego detenerse antes de un recorrido completo omitirá páginas silenciosamente cuando el recuento es subestimado. Utilizar /Count sólo como un indicio de rendimiento para la preasignación de capacidad o búsqueda binaria, y derivar el recuento real a partir del recorrido es el patrón más seguro para los documentos importantes

Siguiente artículo