Una página PDF no almacena píxeles, ni tampoco un árbol de objetos de forma como hace SVG. Almacena un programa. Cada línea, curva, relleno e imagen colocada en la página es el resultado de ejecutar una secuencia de operadores en un flujo de contenido, de arriba abajo, sobre un estado de gráficos en ejecución. Comprender ese único hecho hace que la mayor parte del comportamiento del formato deje de sorprender: por qué un relleno necesita un operador de pintura separado después de construir el trazado, por qué los colores y los anchos de línea se filtran de una forma a la siguiente si no se encierran entre corchetes, por qué el mismo código de dibujo puede acabar en lugares completamente distintos tras una única transformación de coordenadas. Este es un recorrido por ese modelo de ejecución tal como lo define ISO 32000: los operadores que se encuentran al abrir un flujo de contenido y las reglas que determinan qué aparece en la página.
El flujo de contenido es bytecode en notación postfija
Un flujo de contenido es una secuencia plana de bytes compuesta por operandos seguidos de operadores. Los operandos van primero y el operador que los consume va al final, lo contrario a una llamada de función e idéntico a una máquina de pila: se apilan los números y luego se emite el verbo. No hay anidamiento, ni sintaxis de expresiones, ni variables. El contorno de un triángulo ocupa cinco líneas:
100 100 m % moveto: start a new subpath at (100, 100)
200 200 l % lineto: add a segment to (200, 200)
300 100 l % lineto: add a segment to (300, 100)
h % closepath: connect back to the start
S % stroke: paint the path outline
Los operadores son intencionadamente concisos. Una página real contiene miles de ellos, generalmente comprimidos con FlateDecode. El coste de esa compacidad es que el flujo no lleva ninguna estructura que se pueda consultar: un visualizador no puede preguntar "¿dónde está el encabezado en esta página?", solo puede ejecutar el programa y ver dónde cae la tinta. Esa es la razón fundamental por la que la extracción de texto de PDF arbitrarios es difícil.
El origen está en la esquina inferior izquierda y Y crece hacia arriba
Antes de que cualquier coordenada tenga sentido hay que saber dónde está (0, 0). PDF sitúa el origen en la esquina inferior izquierda de la página, con X creciendo hacia la derecha y Y creciendo hacia arriba, medido en puntos a 72 puntos por pulgada (ISO 32000-2 §8.3.2). En una página US Letter, el borde superior se sitúa en y = 792, no en y = 0. Cualquiera que venga de gráficos de pantalla, donde el origen está en la parte superior izquierda y Y crece hacia abajo, se equivoca en el primer intento y dibuja la primera línea fuera del borde inferior de la página. La unidad también es independiente del medio: 72 unidades equivalen a una pulgada tanto si la página se renderiza en la pantalla de un teléfono como en un fotocomponedor.
La mayoría de las bibliotecas de dibujo de páginas heredan esta convención directamente. En HotPDF, por ejemplo, TextOut y las llamadas de trazado miden desde la esquina inferior izquierda en puntos, por lo que un valor cercano a la altura de la página sitúa el contenido en la parte superior:
// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emits the moveto/lineto/stroke operators
Esa secuencia de llamadas se compila exactamente en los operadores m, l y S anteriores. La biblioteca es un mecanógrafo para el flujo de contenido, nada más, y saber lo que emite es lo que permite razonar sobre el resultado cuando una forma acaba en un lugar inesperado.
Construye el trazado y luego píntalo
PDF separa la construcción del trazado de su pintura, y esa separación no es pedantería. Primero se describe una forma con operadores de construcción que no añaden nada visible y luego se emite un único operador de pintura que decide qué hacer con el trazado acumulado. El mismo triángulo puede ser un contorno, un relleno sólido o ambas cosas, dependiendo únicamente del verbo con el que se termine.
Los operadores de construcción son pocos. m inicia un nuevo subtrazado en un punto. l añade un segmento recto. c añade una curva Bezier cúbica a partir de seis operandos: dos puntos de control y un punto final. re es un atajo que añade un rectángulo completo a partir de un cuádruplo x, y, ancho, alto. h cierra el subtrazado actual volviendo a su punto de inicio. Ninguno de ellos pone tinta en la página; solo acumulan geometría.
200 250 m % start the subpath
300 350 400 450 500 250 c % cubic Bezier: two control points, then endpoint
150 200 re % a 150 x 200 rectangle, added as its own subpath
h % close
El ejemplo original usaba la variante y del operador de curva, ahora obsoleta; c con sus tres puntos explícitos es la forma que se verá en la práctica y la que hay que utilizar. Una vez que existe el trazado, un operador de pintura lo finaliza. El vocabulario es pequeño y vale la pena memorizarlo, porque cada forma en cada página termina con uno de estos:
Straza el contorno del trazado usando el ancho de línea y el color de trazo actuales.frellena el interior usando el color de relleno actual y la regla de devanado no nulo.f*rellena usando la regla par-impar, que importa para las formas que se intersectan y las que tienen agujeros.Brellena y luego traza en una sola operación;bcierra primero el trazado.nno pinta nada, que es cómo un trazado se convierte en región de recorte sin dejar una marca visible.
La regla de devanado es la parte que la gente suele aplicar mal. No nulo (f, B) cuenta los cruces con signo de un rayo desde el punto de prueba y rellena donde el recuento no sea cero, de modo que un agujero solo permanece vacío si su subtrazado se devana en sentido contrario al exterior. Par-impar (f*, B*) alterna en cada cruce independientemente de la dirección. Si una forma de "rosquilla" sale sólida, el círculo interior está devanado en el mismo sentido que el exterior, y hay que invertirlo o cambiar a par-impar.
El color es un modo, no un parámetro
El color en un flujo de contenido es persistente. Se fija un color y permanece establecido hasta que se fija otro o se restaura un estado anterior, que es la razón por la que un cambio de color sin delimitadores tinta silenciosamente todo lo que se dibuja después. PDF también mantiene el color de relleno y el color de trazo como dos ajustes independientes, con operadores en minúscula para el relleno y en mayúscula para el trazo. Los espacios de color de dispositivo tienen cada uno su propia abreviatura:
0.5 g % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg % DeviceRGB fill
0.8 0.2 0.1 RG % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k % DeviceCMYK fill
DeviceRGB es adecuado para la salida en pantalla, DeviceCMYK es lo que espera la producción de impresión y DeviceGray es la opción más pequeña para el contenido monocromo. Los espacios de dispositivo son cómodos pero no están calibrados: la misma terna RGB puede renderizarse de forma diferente en dos monitores, que es el problema que los espacios de color basados en ICC y los output intents de PDF/A están diseñados para resolver. Para trabajos con color crítico se selecciona un espacio calibrado con cs y CS y se establecen los componentes con sc y scn, pero para los documentos ordinarios las abreviaturas de dispositivo cubren la carga. Una biblioteca envuelve estos en llamadas tipadas. HotPDF, por ejemplo, toma un único TColor y emite los operadores correspondientes:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, width, height
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, radius
Pdf.CurrentPage.Fill;
El estado de gráficos y la pila q/Q
Todo lo que no es el propio trazado vive en el estado de gráficos: matriz de transformación actual, colores de relleno y trazo, ancho de línea, patrón de guiones, región de recorte y alfa. El estado es global y mutable, por lo que la única forma segura de hacer un cambio local es guardar todo el estado, modificarlo, dibujar y revertirlo. Eso es lo que hacen q y Q. q empuja una copia del estado actual a una pila; Q la saca, descartando todos los cambios realizados desde el q correspondiente.
q % save the entire graphics state
2 0 0 2 100 100 cm % concatenate a transform: scale 2x, translate to (100,100)
0.8 g % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q % restore: transform and color revert
Los q y Q desequilibrados son una forma habitual en que un flujo de contenido construido a mano o ensamblado acaba fallando. Un q suelto sin un Q correspondiente deja la pila profunda cuando termina la página; un Q de más provoca un desbordamiento por debajo. En cualquier caso, un visualizador puede mantener activo un recorte o una transformación antigua, y el contenido desaparece o aparece en el lugar equivocado. Cuando los gráficos desaparecen sin que el trazado pueda explicarlo, audita primero la pila de estados.
La CTM transforma todas las coordenadas
La matriz de transformación actual se interpone entre los números de los operadores y la página real. Cada coordenada se multiplica por la CTM antes de que se dibuje nada, por lo que cambiar la matriz cambia dónde y cómo aparece todo el dibujo posterior sin tocar ni una sola coordenada de trazado. El operador cm concatena una nueva matriz sobre la actual, tomando seis operandos que se corresponden con la matriz afín [a b c d e f]:
1 0 0 1 100 50 cm % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm % rotate 45 degrees (cos/sin in a, b, c, d)
Hay dos cosas que confunden a la gente. Primera: cm compone en lugar de reemplazar, por lo que las transformaciones se acumulan y el orden importa: escalar y luego trasladar no es lo mismo que trasladar y luego escalar. Segunda: la rotación y el escalado pivotan alrededor del origen actual, no del centro de la forma, por lo que para rotar algo en su sitio hay que trasladarlo al origen, rotar y luego trasladarlo de vuelta, todo envuelto en q/Q. Esta misma matriz es la que coloca las imágenes, la última pieza que merece atención.
Las imágenes y el contenido reutilizable son XObjects
Las imágenes de trama no residen de forma inline en el flujo de contenido. Se almacenan como XObjects de imagen, objetos externos con su propio diccionario que describe el ancho, el alto, la profundidad de bits, el espacio de color y el filtro de compresión, y el flujo de contenido solo los referencia. Una foto respaldada por JPEG se declara así:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % the image data is a JPEG stream
>>
Un XObject de imagen dibuja dentro del cuadrado unitario: siempre ocupa la región de (0, 0) a (1, 1) en el espacio de usuario. No se le pasa una posición ni un tamaño. En cambio, se establece la CTM para que ese cuadrado unitario se corresponda con el rectángulo deseado y luego se invoca con Do. Por eso colocar una imagen es siempre una transformación seguida de una invocación, envuelta en un guardar/restaurar para que la escala no se filtre a la siguiente operación:
q
640 0 0 480 50 300 cm % map the unit square to a 640x480 box at (50, 300)
/Photo Do % paint the image XObject
Q
El mismo mecanismo Do impulsa los form XObjects, que contienen un fragmento reutilizable de gráficos (un logotipo o un sello repetido) como su propio flujo de contenido con un cuadro delimitador. Se define una vez, se invoca muchas veces con una CTM diferente y los bytes aparecen en el fichero una sola vez. La mayoría de las bibliotecas ocultan esto detrás de una única llamada de colocación: HotPDF registra un mapa de bits con AddImage y lo coloca con ShowImage, tomando una x, y, ancho y alto explícitos en lugar de pedirte que construyas la matriz a mano:
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (bottom-left), width, height, rotation angle
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Detrás de esa única línea, la biblioteca escribe el diccionario del XObject de imagen, establece la CTM para dimensionar y posicionar el cuadrado unitario y emite Do. El modelo subyacente es el que vale la pena conocer, porque explica todos los resultados extraños: una imagen estirada es una CTM con factores de escala desiguales, un logotipo idéntico en cuarenta páginas es un form XObject invocado cuarenta veces, y una imagen que se renderiza boca abajo es un cambio de signo en la matriz, no un fichero corrupto.
A dónde lleva todo esto
El modelo de gráficos es pequeño una vez que se comprende su forma. Un flujo de contenido es bytecode en notación postfija ejecutado sobre un estado mutable; las coordenadas parten de la esquina inferior izquierda y pasan por la CTM; los trazados se construyen en silencio y se pintan con un operador deliberado; los ajustes de color y línea persisten hasta que se delimitan con q/Q; las imágenes y los gráficos reutilizables son XObjects colocados transformando un cuadrado unitario. Casi todos los resultados de renderizado confusos se reducen a una de esas cinco reglas. Si se quiere ver cómo estos operadores de gráficos encajan en el modelo de objetos más amplio, los diccionarios de página y la tabla de referencias cruzadas que apuntan a ellos, la descripción técnica de la estructura de ficheros PDF cubre esa capa, y construir un PDF simple desde cero recorre los bytes de principio a fin. El dibujo de texto vive en su propia familia de operadores y tiene sus propios problemas, tratados en el artículo complementario sobre texto y gestión de fuentes en PDF.
Las llamadas de dibujo de Delphi mostradas aquí, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage y ShowImage, forman parte del HotPDF Component para Delphi y C++Builder, que emite estos operadores del flujo de contenido por ti.