Technical Article

Crea un PDF mínimo a mano: los cinco objetos necesarios

Un PDF es en esencia un contenedor de texto plano. Abre la mayoría de los ficheros en un editor hexadecimal y la parte superior es legible: un comentario de versión, luego una serie de objetos numerados, después un pequeño índice y un puntero al final que indica al lector por dónde empezar. Si se elimina la compresión, el formato es lo bastante accesible como para teclear un documento funcional en un editor de texto y abrirlo en un visor. Hacerlo una vez enseña más sobre cómo se sostiene un PDF que cualquier cantidad de lectura de la especificación, porque hay que cablear los objetos entre sí a mano y el fichero se niega a abrirse hasta que el cableado sea correcto.

Este recorrido construye el PDF más pequeño que renderiza algo: una página, las palabras "Hello, World!" con una fuente integrada, en papel US Letter. El fichero terminado necesita exactamente cinco objetos y unas pocas líneas de contabilidad en torno a ellos. Escribiremos los objetos primero y luego ensamblaremos la cabecera, la tabla de referencias cruzadas y el trailer que los unen en un fichero que un lector aceptará.

Los cinco objetos que exige un visor

Un lector no escanea un PDF de arriba abajo buscando contenido. Empieza en el trailer, sigue una referencia al catálogo del documento y recorre una cadena de objetos desde allí. Cada objeto de esa cadena debe existir o la apertura falla. Para un documento de una sola página la cadena es corta, y cada eslabón tiene un único cometido:

  • Catalog es la raíz. Es el objeto al que apunta el trailer, y su única entrada obligatoria aquí es una referencia al árbol de páginas.
  • Pages es el nodo del árbol de páginas. Lista las páginas del documento e informa de cuántas hay.
  • Page describe una página física: su tamaño, los recursos con los que dibuja y qué flujo de contenido la pinta.
  • El flujo de contenido contiene los operadores de dibujo, los comandos en notación postfija que colocan texto y gráficos en esa página.
  • Font declara la tipografía a la que se refiere el flujo de contenido. Con una de las 14 fuentes estándar no hay que incrustar nada.

Cada objeto está numerado y es direccionable. Un objeto indirecto se escribe como N 0 obj ... endobj, donde N es el número de objeto y el 0 es su número de generación (siempre 0 en un fichero escrito desde cero). En cualquier otro lugar del fichero se apunta a ese objeto con una referencia: 5 0 R significa "objeto 5". Esas referencias son el cableado. El catálogo contiene 2 0 R en nuestra numeración para llegar al árbol de páginas, el árbol de páginas contiene una referencia hacia abajo a la página, etc. Un número incorrecto y el lector sigue un puntero colgante que no lleva a ningún sitio.

Nombres, diccionarios y flujos

Tres elementos de sintaxis transportan casi todo. Un nombre comienza con una barra: /Type, /Page, /F0. Los nombres son identificadores que distinguen mayúsculas de minúsculas, no cadenas de texto, y PDF los usa como claves de diccionario y para etiquetar qué es un objeto. Un diccionario es un conjunto de pares clave-valor envuelto en dobles corchetes angulares, donde cada clave es un nombre: << /Type /Page /MediaBox [0 0 612 792] >>. Los valores pueden ser números, nombres, arrays entre corchetes, referencias o diccionarios anidados. La mayoría de los objetos PDF son diccionarios.

Un flujo es un diccionario seguido de un bloque de bytes entre las palabras clave stream y endstream. Ahí es donde viven los operadores de dibujo de páginas, y en ficheros reales también las imágenes comprimidas y las fuentes incrustadas. El diccionario del flujo describe los bytes; en un fichero de producción debe incluir una entrada /Length con el recuento exacto de bytes, y a menudo un /Filter como /FlateDecode cuando los datos están comprimidos. Dejaremos que una herramienta rellene /Length, porque contar bytes a mano es la parte de este ejercicio sin valor didáctico y con alta probabilidad de un error por uno que rompa el fichero.

Escritura de los objetos

Aquí están los cinco objetos en orden. El detalle de coordenadas a tener en cuenta antes de leer el flujo de contenido: PDF mide desde la esquina inferior izquierda de la página en puntos, donde un punto equivale a 1/72 de pulgada, y Y crece hacia arriba. Una página US Letter mide 612 por 792 puntos, por lo que 50 700 está cerca de la esquina superior izquierda, no de la inferior.

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

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

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

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

Al leer las referencias aflora la estructura. El objeto 1, el catálogo, apunta su entrada /Pages al objeto 2. El objeto 2, el árbol de páginas, lista el objeto 3 en /Kids y declara /Count 1. El objeto 3, la página, apunta /Parent de vuelta al objeto 2 (el árbol y la página se referencian mutuamente, lo cual es obligatorio), se dimensiona con /MediaBox, expone la fuente bajo el nombre local /F0 en sus /Resources y nombra el objeto 5 como su contenido. El objeto 4 es la fuente: /BaseFont /Helvetica selecciona una de las 14 tipografías estándar que todo lector conforme ya tiene, por lo que no hay que incrustar nada. El objeto 5 es el flujo de contenido.

Qué dice realmente el flujo de contenido

El cuerpo del flujo es un pequeño programa en el lenguaje de descripción de páginas de PDF, que es postfijo: primero los operandos y después el operador que los consume. Cinco líneas hacen el trabajo. BT y ET abren y cierran un objeto de texto; todo lo que posiciona o muestra texto debe estar entre ellos. /F0 36 Tf establece la fuente actual al recurso llamado /F0 a 36 puntos (Tf es "establecer fuente y tamaño de texto"). 50 700 Td mueve la posición del texto a (50, 700) en coordenadas de página. (Hello, World!) Tj muestra la cadena, que PDF escribe como texto literal entre paréntesis, usando Tj para pintarla en la posición actual. Si se omite BT/ET, un lector estricto rechaza los operadores de texto; si se olvida establecer una fuente antes de Tj, no hay fuente actual con la que dibujar.

El valor /Length 44 en el diccionario del flujo es el recuento de bytes entre stream y endstream, y debe ser exacto. Este es el valor que merece la pena delegar a una herramienta en lugar de contar saltos de línea a mano, especialmente porque si el editor escribe los finales de línea como LF o CRLF cambia el total.

Cabecera, xref y trailer

Los objetos son el contenido. Tres piezas estructurales los convierten en un fichero. La primera es la cabecera, la primera línea, que nombra el formato y la versión:

%PDF-1.7

El % inicia un comentario en la sintaxis PDF, pero un lector trata este comentario particular como la firma del formato y lee la versión a partir de él. Un escritor real lo sigue inmediatamente con una segunda línea de comentario de bytes de bit alto, una indicación a las herramientas de transferencia de ficheros de que el fichero es binario y no debe tratarse como texto.

Al final del fichero viene la tabla de referencias cruzadas, el índice que hace posible el acceso aleatorio. Registra el desplazamiento de bytes de cada objeto desde el inicio del fichero, para que un lector pueda saltar directamente al objeto 3 sin analizar primero los objetos 1 y 2. La tabla es rígida: las entradas tienen anchura fija, 20 bytes cada una incluyendo el final de línea, con formato de desplazamiento de 10 dígitos, generación de 5 dígitos, una palabra clave (n para en uso, f para libre) y un terminador de dos bytes. Una tabla correcta para nuestras seis entradas (el objeto 0 es siempre la cabeza de la lista libre) tiene este aspecto:

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

Esos desplazamientos son la parte frágil de escribir PDF a mano. Cada uno es la posición de byte exacta donde comienza el N 0 obj correspondiente, y cada desplazamiento cambia en el momento en que se añade un carácter en cualquier lugar anterior. El trailer es el punto de entrada que un lector usa al final y al principio: /Root 1 0 R nombra el catálogo, /Size 6 indica el recuento de objetos y startxref 408 da el desplazamiento de bytes de la palabra xref en sí. Un lector abre el fichero, salta al final, lee startxref, busca la tabla de referencias cruzadas y desde allí alcanza el catálogo y todo lo que hay debajo. %%EOF marca el último byte.

Dejar que una herramienta corrija los recuentos de bytes

Los desplazamientos anteriores son ilustrativos; en la práctica estarán mal cuando se termine de teclear, porque dependen del diseño exacto de bytes del fichero. En lugar de recalcularlos, se escribe la estructura con valores de marcador de posición y se deja que una utilidad reconstruya la tabla de referencias cruzadas y las longitudes de los flujos. El pdftk, gratuito y multiplataforma, hace esto en una sola pasada:

pdftk hello-draft.pdf output hello.pdf

Analiza los objetos, recalcula cada desplazamiento de bytes, rellena los valores correctos de /Length, escribe una tabla xref y un trailer válidos, y emite hello.pdf. Al abrirlo en cualquier visor se obtiene una página con "Hello, World!" en Helvetica de 36 puntos cerca de la parte superior. Qpdf hace el mismo trabajo, y muchos visores también repararán sobre la marcha un fichero ligeramente malformado. La razón para apoyarse en una herramienta aquí no es pereza, sino que la aritmética de desplazamientos es la única parte del formato sin contenido conceptual y con la mayor tasa de errores, de modo que automatizarla permite que la estructura siga siendo lo que se está aprendiendo.

Por qué esto escala a documentos reales

Nada de un informe de cien páginas cambia la forma que se acaba de construir. El catálogo sigue en la raíz, el árbol de páginas sigue reuniendo las páginas y cada página sigue apuntando a sus recursos y a un flujo de contenido. Lo que crece es la amplitud, no el esqueleto: el árbol de páginas se ramifica para que un lector pueda saltar subárboles enteros, los flujos de contenido llevan cientos de operadores en lugar de cinco, las fuentes se incrustan como objetos de flujo propios con tablas de anchuras y codificaciones, y las imágenes llegan como flujos con filtros específicos para imágenes. Los ficheros modernos también tienden a empaquetar muchos objetos en flujos de objetos comprimidos y a reemplazar la tabla xref plana por un flujo de referencias cruzadas, lo que explica por qué abrir un PDF real en un editor de texto suele mostrar una pared de binarios. El modelo subyacente es idéntico al del fichero hecho a mano. Para el grafo de objetos más amplio y cómo se relacionan el catálogo, el árbol de páginas y los diccionarios de recursos en un documento mayor, el recorrido en profundidad por la estructura de documentos PDF continúa donde este termina, y la visión general de la estructura del fichero cubre las actualizaciones incrementales y cómo se encadenan los trailers entre revisiones.

De la escritura manual a una biblioteca

Teclear objetos a mano es un ejercicio de aprendizaje, no una técnica de producción. En cuanto se necesitan fuentes reales, texto ajustado, imágenes o algo más que una página trivial, la contabilidad de bytes que pdftk parcheó se convierte en todo el trabajo, y se desea una biblioteca que se encargue de ello. Los mismos cinco objetos siguen escribiéndose, pero una biblioteca calcula cada desplazamiento, gestiona los diccionarios de fuentes y recursos, y comprime los flujos de contenido sin que haya que rastrear ni un solo byte. En Delphi y C++Builder, el HotPDF Component reduce este fichero completo a unas pocas llamadas: configurar el documento, llamar a BeginDoc, a SetFont y a TextOut para colocar el mismo saludo, y luego a EndDoc para escribir un catálogo, árbol de páginas, xref y trailer correctos. Entender los objetos subyacentes es lo que permite razonar sobre el resultado cuando un documento no se renderiza como se esperaba.