Nuestro artículo complementario sobre el orden de páginas PDF cubre la regla básica: el orden de visualización procede de un recorrido en profundidad, de izquierda a derecha, de los arrays /Kids en el árbol /Pages, nunca de los números de objeto. Este artículo examina el árbol desde otro ángulo — su forma. ¿Por qué los generadores de PDF maduros producen jerarquías de nodos intermedios cuando un único array plano sería perfectamente válido? ¿Qué cambia realmente cuando una herramienta aplana o reconstruye el árbol? ¿Y qué ocurre cuando la contabilidad de /Count, que hace que toda la estructura sea rápida, deja de decir la verdad
El fan-out es una decisión de rendimiento
Nada obliga a un generador a anidar. Un documento de 10.000 páginas con un único nodo raíz /Pages y 10.000 referencias hoja en un solo array /Kids cumple con la especificación. Aun así, el PDF Reference recomienda un árbol equilibrado para documentos grandes, y los generadores más habituales siguen ese consejo con un fan-out moderado, normalmente unas pocas docenas de hijos por nodo intermedio
La razón está en lo que un visualizador debe leer antes de poder mostrar nada. Consideremos un salto directo a la página 8214 de ese fichero de 10.000 páginas. Con un árbol plano, el visualizador primero tiene que analizar el nodo raíz, y ese nodo raíz es un array enorme: con aproximadamente ocho bytes por referencia indirecta, un objeto de 80 KB que debe tokenizarse de principio a fin antes de poder resolver la entrada 8213. Con un árbol equilibrado de fan-out 32, el mismo salto lee la raíz, compara los totales acumulados de /Count para elegir el hijo correcto, y desciende — tres o cuatro diccionarios pequeños en total, cada uno de unos pocos cientos de bytes. Ese es el acceso aleatorio O(log n) que el árbol fue diseñado para ofrecer, y es la razón de ser del /Count en los nodos intermedios: permite a un lector saltarse todo un subárbol sin abrir ni un solo objeto dentro de él
La forma del árbol también determina el coste de la edición. Una actualización incremental que inserta una página debe reescribir todos los nodos cuyo /Kids o /Count haya cambiado, es decir, la ruta desde el padre de la nueva hoja hasta la raíz. En un árbol equilibrado esa ruta son un puñado de diccionarios pequeños añadidos al fichero. En un árbol plano la «ruta» es el único array raíz gigante, duplicado por completo en cada revisión. Un contrato que pasa por treinta ciclos de revisión y anotación puede acabar arrastrando treinta copias obsoletas del mismo array de 80 KB en su flujo de bytes
Los nodos interiores llevan atributos heredados
Los nodos intermedios no son solo enrutamiento. Los cuatro atributos de página heredables — /Resources, /MediaBox, /CropBox y /Rotate — pueden colocarse en cualquier nodo /Pages, donde se aplican a cada hoja por debajo de él a menos que un descendiente los anule. Un generador que produce un informe con un apéndice apaisado puede expresar esa disposición en el propio árbol:
5 0 obj % raíz del documento
<< /Type /Pages /Count 6 /Kids [6 0 R 7 0 R] >>
endobj
6 0 obj % cuerpo del informe: A4 vertical, fuente de cuerpo
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [30 0 R 31 0 R 32 0 R]
/MediaBox [0 0 595 842]
/Resources << /Font << /F1 8 0 R >> >> >>
endobj
7 0 obj % apéndice: A4 apaisado, rotado, con fuente propia
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [40 0 R 41 0 R 42 0 R]
/MediaBox [0 0 842 595] /Rotate 90
/Resources << /Font << /F2 9 0 R >> >> >>
endobj
40 0 obj % página del apéndice: hereda tamaño, rotación, fuentes
<< /Type /Page /Parent 7 0 R /Contents 43 0 R >>
endobj
Los objetos 40 a 42 están casi vacíos. Su tamaño de página, rotación y recursos de fuente llegan todos por herencia desde el nodo 7, lo que mantiene el fichero compacto y autosuficiente: añada una cuarta página bajo el nodo del apéndice y saldrá apaisada automáticamente
El mismo mecanismo crea el clásico riesgo de mover una página. Supongamos que una herramienta mueve el objeto 40 al cuerpo del informe editando los dos arrays /Kids y redirigiendo /Parent hacia el nodo 6. El movimiento es estructuralmente válido, pero el objeto 40 ahora hereda el /MediaBox vertical, ninguna rotación, y la fuente /F1 — mientras que su flujo de contenido sigue seleccionando /F2, que ya no se resuelve. La página se encoge, pierde la rotación y pierde su texto en una sola edición. Por eso el código robusto de reordenación materializa los valores resueltos de los cuatro atributos heredables en el diccionario de la página antes de reasignarle un nuevo padre. Si alguna vez ha arrastrado una página en un editor y la ha visto cambiar de tamaño u orientación, este es el mecanismo que presenció
Aplanado: legal, habitual, a veces costoso
Muchas herramientas hacen lo contrario. Los generadores minimalistas producen un árbol de un solo nivel porque es sencillo, y muchas utilidades de fusión y división reconstruyen cualquier árbol que leen en un único array /Kids plano, porque generar una estructura equilibrada supone trabajo extra y la salida plana siempre es conforme. Una reconstrucción correcta debe resolver la herencia al mismo tiempo: cada atributo que una hoja heredaba tiene que copiarse en la hoja, o elevarse a la nueva raíz si es uniforme en todo el documento — de lo contrario la salida cambia la geometría exactamente igual que en el caso del movimiento de página
Para documentos típicos, el aplanado es inofensivo. Duele a gran escala, de las dos formas ya descritas: el array raíz se convierte en un único objeto grande que cada apertura y cada salto de página deben analizar por completo, y cada edición estructural lo reescribe entero. Lo que el aplanado no destruye es la compartición mediante referencias indirectas — un árbol plano en el que las 10.000 páginas apuntan al mismo objeto de diccionario /Resources sigue estando deduplicado. Lo único que se pierde es la opción de omitir la entrada en la página y dejar que un ancestro la proporcione
Cuando /Count miente
/Count es pura contabilidad: debe ser igual al número de páginas hoja en el subárbol del nodo, y nada en el formato del fichero lo impone. Dos patrones de corrupción explican la mayoría de los recuentos falsos que se ven en la práctica
El primero es el recuento obsoleto que deja tras de sí una actualización incremental. Un editor inserta una página, reescribe el padre inmediato con un nuevo /Kids y un /Count actualizado, añade ambos al fichero — y nunca toca los ancestros:
% Revisión original
12 0 obj
<< /Type /Pages /Count 9 /Kids [13 0 R 14 0 R 15 0 R] >>
endobj
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 3
/Kids [50 0 R 51 0 R 52 0 R] >>
endobj
% Revisión añadida: se insertó una página en la rama central.
% El objeto 14 queda sustituido; el objeto 12 nunca se reescribe
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 4
/Kids [50 0 R 51 0 R 90 0 R 52 0 R] >>
endobj
El árbol ahora contiene diez hojas, pero la raíz sigue diciendo nueve. Un visualizador que confía en la raíz informa de nueve páginas en su contador. Uno que usa los recuentos interiores para hacer una búsqueda binaria en un salto de página calcula un índice erróneo para cada página posterior al punto de inserción. Un recorrido completo encuentra diez. Tres respuestas distintas, un solo fichero
El segundo patrón es el recuento que nunca podría ser correcto: negativo, cero en un nodo con contenido, o absurdamente enorme. Estos proceden de fuzzing, de daños en la transmisión, y ocasionalmente de errores aritméticos en los editores. Son peligrosos específicamente para el código que confía en /Count para la asignación de memoria — dimensionar un array a partir de un /Count de -3 produce, en el mejor de los casos, un error de rango, y hacerlo a partir de un /Count de dos mil millones es una asignación de tipo denegación de servicio. El valor es una entrada no confiable, como cualquier otro número del fichero
Los analizadores se dividen en dos bandos respecto a todo esto. Los consumidores estrictos — herramientas de preflight, validadores PDF/A, flujos de archivado — comparan /Count con el resultado del recorrido y rechazan o marcan el fichero. Los visualizadores interactivos son casi universalmente permisivos: recorren el árbol, calculan el recuento real, e ignoran silenciosamente el almacenado, que es precisamente la razón por la que un fichero con recuento obsoleto puede circular durante años sin quejas hasta que se encuentra con un analizador más estricto dentro de algún flujo automatizado. El término medio defensivo para el código de biblioteca es tratar /Count como una pista — útil para la preasignación, y para omitir subárboles una vez verificado — dejando que el recorrido siga siendo la fuente de verdad
Para el propio algoritmo de recorrido, las reglas de búsqueda de herencia y el trayecto desde el catálogo hasta la hoja, empiece por el artículo explicativo sobre el orden de páginas. Para ver cómo son estos modos de fallo cuando un documento real de un cliente llega al código de producción, lea el caso práctico de depuración del orden de páginas, que sigue un incidente de páginas desordenadas desde el síntoma hasta la causa raíz
El HotPDF Delphi Component se ocupa de todo esto internamente: recorre árboles anidados de cualquier profundidad, resuelve los atributos heredados cuando las páginas se copian o se mueven, y verifica /Count frente al recuento real de hojas en lugar de confiar en él, de modo que los índices de página en su API siempre significan páginas lógicas