Artículo técnico

PDF sin un diccionario Pages: implicaciones de análisis

El diccionario Catalog del PDF tiene exactamente una clave de navegación obligatoria: /Pages. Esa clave debe apuntar a un objeto indirecto de tipo /Pages, que a su vez contiene el arreglo /Kids y el total /Count (recuento) de páginas. Si se quita ese puntero, ningún lector conforme podrá localizar una sola página en el archivo. La norma ISO 32000-1 §7.7.2 es inequívoca en este punto: el Catalog (Catálogo) deberá tener una entrada /Pages, y el objeto referenciado deberá tener el tipo /Pages. Los archivos que violan este requisito no solo no son conformes; están estructuralmente rotos de una manera que la mayoría de los analizadores manejan deficientemente

Lo que realmente dice la especificación

Un PDF conforme mínimo tiene al menos tres objetos. El objeto 1 es el Catalog, el objeto 2 es la raíz Pages, y desde el objeto 3 en adelante son diccionarios Page individuales. El Catalog apunta a la raíz Pages; la raíz Pages enumera a sus hijos en /Kids; cada Page lleva una referencia retrospectiva /Parent. Toda la cadena es bidireccional por diseño, por lo que un analizador puede comenzar desde cualquier extremo y recorrer cualquier página en tiempo O(log n) para árboles equilibrados

% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

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

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

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

El árbol Pages puede estar anidado. Un documento con miles de páginas generalmente agrupa las páginas en objetos de nodo intermedios que también llevan el tipo /Pages, cada uno con su propio /Kids y un /Count que refleja el subárbol debajo de él. El /Count del nodo raíz siempre es igual al recuento total de páginas. Ese recuento es lo que los visores muestran en el campo de número de página antes de haber analizado una sola página, porque leer un número entero del objeto 2 es mucho más económico que recorrer todo el árbol

Cómo se ve un archivo sin Pages

Los archivos que carecen del diccionario Pages generalmente se originan a partir de generadores de PDF que escriben objetos de página directamente sin ensamblarlos en un árbol, o por una corrupción que elimina el nodo raíz mientras deja intactos los objetos Page hoja. El Catalog en un archivo de este tipo carece por completo de la clave /Pages, o contiene una referencia a un objeto que ya no existe en la tabla de referencias cruzadas

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

Un analizador que siga la especificación leerá el Catalog, intentará resolver /Pages, no encontrará nada (o una referencia muerta), y generará un error o informará cero páginas. Lo que no debe hacer es continuar como si el archivo tuviera cero páginas y tener éxito en silencio; eso produce una salida en blanco que parece correcta para las herramientas automatizadas pero incorrecta para cualquier humano que la abra

Por qué fallan los analizadores

La mayoría de los analizadores de PDF asignan su tabla de páginas interna en el momento de la carga basándose en el valor /Count de la raíz Pages. Cuando esa raíz está ausente, el analizador lee cero, no asigna nada y luego desreferencia un puntero nulo la primera vez que cualquier código solicita la página 1, o lee basura y asigna un búfer tremendamente incorrecto. Ninguno de los resultados es elegante. La infracción de acceso en 0x008E5D78 que aparece en los registros de fallos al procesar un archivo de este tipo es exactamente esto: una desreferencia de puntero nulo dentro de la ruta de acceso a la página, provocada por la ausencia de la estructura que el analizador asumió que siempre estaría allí

La suposición de diseño subyacente es razonable. La inmensa mayoría de los archivos PDF existentes tienen un diccionario Pages. Los analizadores que omiten la verificación de existencia para ahorrar algunas instrucciones no están siendo imprudentes; están optimizando para el caso común. Los archivos que castigan esa optimización son lo suficientemente raros como para que el código de producción nunca se encuentre con uno hasta que lo hace, momento en el que el fallo es reproducible y desconcertante si el ingeniero no ha leído el apartado 7.7.2

Recuperación sin un árbol Pages

Si un analizador debe manejar estos archivos en lugar de rechazarlos, la recuperación sigue un camino predecible: escanear cada objeto indirecto en la tabla de referencias cruzadas, recopilar aquellos con /Type /Page y ordenarlos por número de objeto. No se garantiza que el orden del número de objeto coincida con el orden de lectura en la especificación, pero en la práctica, los generadores que omiten el árbol Pages tienden a emitir páginas secuencialmente, por lo que el orden del número de objeto es correcto la mayoría de las veces

La comprobación en sí es económica. Antes de recorrer el puntero /Pages del Catalog, confirme que el puntero existe, que se resuelve en un objeto real y que el /Type del objeto resuelto es igual a /Pages. Si falla cualquiera de esas tres condiciones, pase al escaneo lineal. El escaneo es más lento que el recorrido del árbol para documentos grandes, porque lee cada encabezado de objeto en lugar de seguir un camino equilibrado, pero funciona, y para un archivo que ya está malformado, la corrección tiene prioridad sobre la velocidad

Un caso extremo que el escaneo lineal no resuelve automáticamente: el orden de las páginas. Sin un arreglo /Kids para definir la secuencia, el orden "correcto" está indefinido por la especificación. El orden por número de objeto es el valor predeterminado pragmático; si el archivo es lo suficientemente importante como para procesarlo con cuidado, vale la pena el trabajo adicional de comprobar si los objetos Page llevan un /StructParents explícito o referencias de anotaciones que impliquen una secuencia de lectura

Implicaciones para los generadores de PDF

Para cualquiera que escriba un generador de PDF en lugar de un analizador, la lección es específica: siempre emita la raíz Pages antes de cerrar el archivo. El Catalog sin una entrada /Pages no es un PDF válido en ninguna revisión de la especificación. Los generadores que construyen objetos de página sobre la marcha y ensamblan el árbol en la finalización (el enfoque que usa la mayoría de los escritores de secuencias o streams) están bien siempre que la finalización se ejecute realmente. El modo de fallo común es una excepción o un retorno anticipado que aborta la escritura antes de que se complete el tráiler, dejando un archivo que se abre en algunos visores (que tienen heurísticas de recuperación) y falla en otros (que no las tienen)

PDF/A y PDF/UA imponen restricciones adicionales en el árbol de páginas más allá de lo que exige la especificación base, pero ninguno relaja el requisito de /Pages. Un validador que compruebe la conformidad con las normas ISO 19005 o ISO 14289 detectará un diccionario Pages faltante como una infracción de la especificación base incluso antes de que alcance las reglas específicas del perfil