Una página PDF no almacena píxeles y no almacena un árbol de objetos de forma de la manera en que lo 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 a abajo, frente a un estado de gráficos en ejecución. Entienda ese único hecho y la mayor parte del comportamiento del formato dejará de ser sorprendente: por qué un relleno necesita un operador de pintura separado después de que se construye la ruta, por qué los colores y los anchos de línea se filtran de una forma a la siguiente a menos que los agrupe, por qué el mismo código de dibujo puede aterrizar en lugares completamente diferentes después de una sola transformación de coordenadas. Este es un recorrido por ese modelo de ejecución tal como lo define la norma ISO 32000: los operadores con los que se encuentra cuando abre un flujo de contenido y las reglas que deciden qué se muestra en la página
El flujo de contenido es bytecode posfijo
Un flujo de contenido es una secuencia de bytes plana de operandos seguidos de operadores. Los operandos van primero, el operador que los consume va al final, lo cual es el reverso de una llamada a función e idéntico a una máquina de pila: apilar los números, luego emitir el verbo. No hay anidamiento, ni sintaxis de expresión, ni variables. El contorno de un triángulo consta de cinco líneas de esto:
100 100 m % moveto: iniciar una nueva subruta en (100, 100)
200 200 l % lineto: agregar un segmento hasta (200, 200)
300 100 l % lineto: agregar un segmento hasta (300, 100)
h % closepath: conectar de vuelta al inicio
S % stroke: pintar el contorno de la ruta
Los operadores son concisos a propósito. Una página real tiene miles de estos, generalmente comprimidos con FlateDecode. El costo de esa compacidad es que el flujo no lleva ninguna estructura que usted pueda consultar: un visor no puede preguntar "¿dónde está el encabezado en esta página?", solo puede ejecutar el programa y ver qué tinta cae y dónde. Esa es la razón principal por la que la extracción de texto de archivos PDF arbitrarios es difícil
El origen está abajo a la izquierda, y la Y crece hacia arriba
Antes de que cualquier coordenada tenga sentido, debe saber dónde está (0, 0). PDF coloca el origen en la esquina inferior izquierda de la página, con X aumentando hacia la derecha e Y aumentando hacia arriba, medido en puntos a 72 puntos por pulgada (ISO 32000-2 §8.3.2). En una página tamaño Carta de EE. UU., el borde superior se encuentra en y = 792, no en y = 0. Cualquiera que llegue desde los gráficos de pantalla, donde el origen está en la parte superior izquierda y la Y crece hacia abajo, entiende esto al revés en el primer intento y dibuja la primera línea fuera de la parte inferior de la página. La unidad también es independiente del medio: 72 unidades equivalen a una pulgada, ya sea que la página se renderice en la pantalla de un teléfono o en una filmadora
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 ruta se miden desde la parte inferior izquierda en puntos, por lo que un valor cercano a la altura de la página coloca el contenido en la parte superior:
// HotPDF, Delphi: y medida desde el borde inferior hacia arriba, en puntos
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // cerca de la parte superior de la página
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emite los operadores moveto/lineto/stroke
Esa secuencia de llamadas se compila exactamente en los operadores m, l y S anteriores. La biblioteca es una mecanógrafa para el flujo de contenido, nada más, y saber lo que emite es lo que le permite razonar sobre la salida cuando una forma aterriza en un lugar que no esperaba
Construya la ruta, luego píntela
PDF separa la construcción de la ruta de la pintura de la ruta, y la separación no es pedantería. Primero describe una forma con operadores de construcción que no agregan nada visible, luego emite un solo operador de pintura que decide qué hacer con la ruta acumulada. El mismo triángulo puede ser un contorno, un relleno sólido o ambos, dependiendo solo del verbo con el que termine
Los operadores de construcción son pocos. m inicia una nueva subruta en un punto. l agrega un segmento recto. c agrega una curva Bezier cúbica a partir de seis operandos, dos puntos de control y un punto final. re es un atajo que agrega un rectángulo completo a partir de un cuádruple x, y, ancho, alto. h cierra la subruta actual volviendo a su inicio. Ninguno de ellos pone tinta en la página; solo acumulan geometría
200 250 m % iniciar la subruta
300 350 400 450 500 250 c % Bezier cúbica: dos puntos de control, luego el punto final
150 200 re % un rectángulo de 150 x 200, agregado como su propia subruta
h % cerrar
El ejemplo original usó la variante y, ahora obsoleta, del operador de curva; c con sus tres puntos explícitos es la forma que verá en la práctica y la que debe usar. Una vez que la ruta existe, un operador de pintura la termina. 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 de la ruta utilizando el ancho de línea y el color de trazo actualesfrellena el interior utilizando el color de relleno actual y la regla de devanado distinto de cerof*rellena usando la regla par-impar, que importa para formas que se autointersecan y formas con agujerosBrellena y luego traza en una sola operación;bcierra la ruta primeronno pinta nada, que es como una ruta se convierte en una región de recorte sin dejar una marca visible
La regla de devanado es la parte en la que la gente se equivoca. Distinto de cero (f, B) cuenta los cruces con signo de un rayo desde el punto de prueba y rellena donde el recuento no sea cero, por lo que un agujero solo permanece vacío si su subruta se devana en dirección opuesta a la 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 de la misma manera que el exterior, y usted lo invierte o cambia a par-impar
El color es un modo, no un parámetro
El color en un flujo de contenido es persistente. Usted establece un color y permanece establecido hasta que establezca otro o restaure un estado anterior, por lo que un cambio de color sin agrupar tiñe silenciosamente todo lo dibujado después de él. PDF también mantiene el color de relleno y el color de trazo como dos configuraciones independientes, con operadores en minúsculas para el relleno y mayúsculas para el trazo. Los espacios de color del dispositivo tienen cada uno su propia abreviatura:
0.5 g % relleno DeviceGray, gris medio (0 = negro, 1 = blanco)
0.2 0.6 0.8 rg % relleno DeviceRGB
0.8 0.2 0.1 RG % trazo DeviceRGB (mayúsculas = trazo)
0.2 0.8 0.0 0.1 k % relleno DeviceCMYK
DeviceRGB se adapta a la salida de 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 monocromático. Los espacios de dispositivos son convenientes pero no están calibrados: el mismo triple RGB puede renderizarse de manera diferente en dos monitores, que es el problema que existen para resolver los espacios de color basados en ICC y los intentos de salida PDF/A. Para trabajos críticos para el color, seleccione un espacio calibrado con cs y CS y establezca los componentes con sc y scn, pero para documentos ordinarios las abreviaturas de los dispositivos llevan la carga. Una biblioteca envuelve esto en llamadas tipificadas. HotPDF, por ejemplo, toma un solo TColor y emite los operadores coincidentes:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, ancho, alto
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, radio
Pdf.CurrentPage.Fill;
El estado de los gráficos y la pila q/Q
Todo lo que no es la ruta en sí vive en el estado de los gráficos: matriz de transformación actual, colores de relleno y trazo, ancho de línea, patrón de guiones, región de recorte, alfa. El estado es global y mutable, por lo que la única forma segura de realizar un cambio local es guardar todo, modificarlo, dibujar y revertirlo. Eso es lo que hacen q y Q. q inserta una copia del estado actual en una pila; Q lo extrae, descartando todos los cambios realizados desde la q coincidente
q % guardar todo el estado de los gráficos
2 0 0 2 100 100 cm % concatenar una transformación: escalar 2x, trasladar a (100,100)
0.8 g % relleno gris, en el alcance de este bloque
% ... dibujar contenido escalado en gris ...
Q % restaurar: transformación y color vuelven al estado anterior
Una q y Q desequilibradas son una forma común en que un flujo de contenido construido a mano o ensamblado sale mal. Una q extraviada sin una Q coincidente deja la pila profunda cuando termina la página; un Q adicional produce un desbordamiento inferior. De cualquier manera, un visor puede mantener en vigor un recorte o una transformación antiguos, y el contenido desaparece o aterriza en el lugar equivocado. Cuando los gráficos se desvanecen sin razón que la ruta pueda explicar, audite primero la pila de estado
El CTM transforma cada coordenada
La matriz de transformación actual (CTM) se encuentra entre los números de sus operadores y la página real. Cada coordenada se multiplica por el CTM antes de que se dibuje algo, por lo que cambiar la matriz cambia dónde y cómo aparece todo el dibujo posterior sin tocar una sola coordenada de la ruta. El operador cm concatena una nueva matriz en la actual, tomando seis operandos que se mapean a la matriz afín [a b c d e f]:
1 0 0 1 100 50 cm % trasladar por (100, 50): e y f llevan el desplazamiento
2 0 0 1.5 0 0 cm % escalar x por 2, y por 1.5: a y d son los factores de escala
0.707 0.707 -0.707 0.707 0 0 cm % rotar 45 grados (cos/sin en a, b, c, d)
Dos cosas hacen tropezar a la gente. Primero, cm se 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. Segundo, la rotación y la escala pivotan alrededor del origen actual, no en el centro de su forma, por lo que para rotar algo en su lugar se traslada al origen, se rota y luego se vuelve a trasladar, todo envuelto en q/Q. Esta misma matriz es la que coloca las imágenes, la última pieza que vale la pena ver
Las imágenes y el contenido reutilizable son XObjects
Las imágenes rasterizadas no viven en línea 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 hace referencia. Una foto respaldada por JPEG se declara así:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % los datos de la imagen son un flujo JPEG
>>
Un XObject de imagen se dibuja en el cuadrado unitario: siempre ocupa la región de (0, 0) a (1, 1) en el espacio de usuario. No le pasa una posición o un tamaño. En su lugar, establece el CTM para que el cuadrado unitario se asigne al rectángulo que desee, luego lo invoca con Do. Es por eso que colocar una imagen es siempre una transformación seguida de una invocación, envuelta en un guardado/restaurado para que la escala no se filtre en la siguiente operación:
q
640 0 0 480 50 300 cm % asignar el cuadrado unitario a un cuadro de 640x480 en (50, 300)
/Photo Do % pintar el XObject de imagen
Q
El mismo mecanismo Do impulsa los XObjects de formulario, que contienen un trozo reutilizable de gráficos, un logotipo o un sello repetido, como su propio flujo de contenido con un cuadro delimitador. Defínalo una vez, invóquelo muchas veces con un CTM diferente, y los bytes aparecen en el archivo solo una vez. La mayoría de las bibliotecas ocultan esto detrás de una sola llamada de ubicació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 pedirle que construya 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 (abajo a la izquierda), ancho, alto, ángulo de rotación
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Debajo de esa única línea, la biblioteca escribe el diccionario del XObject de imagen, establece el CTM en el tamaño y la posición del cuadrado unitario y emite Do. El modelo subyacente es el que vale la pena conocer, porque explica cada resultado extraño: una imagen estirada es un CTM con factores de escala incompatibles, un logotipo idéntico en cuarenta páginas es un XObject de formulario invocado cuarenta veces, y una imagen que se renderiza al revés es un cambio de signo en la matriz, no un archivo corrupto
A dónde lleva esto
El modelo de gráficos es pequeño una vez que ve su forma. Un flujo de contenido es bytecode posfijo que se ejecuta contra un estado mutable; las coordenadas comienzan en la parte inferior izquierda y pasan por el CTM; las rutas se construyen silenciosamente y se pintan con un operador deliberado; la configuración de color y línea persiste hasta que la agrupa con q/Q; las imágenes y los gráficos reutilizables son XObjects ubicados mediante la transformación de un cuadrado unitario. Casi todos los resultados de renderización confusos se reducen a una de esas cinco reglas. Si desea ver cómo estos operadores de gráficos se asientan dentro del modelo de objetos más grande, los diccionarios de página y la tabla de referencias cruzadas que los apuntan, la descripción técnica de la estructura de archivos PDF cubre esa capa, y la creación de un PDF simple desde cero recorre los bytes de un extremo a otro. El dibujo de texto vive en su propia familia de operadores y tiene sus propios inconvenientes, que se cubren en el artículo complementario sobre el texto PDF y el manejo de fuentes
Las llamadas de dibujo de Delphi que se muestran aquí, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage y ShowImage, son parte del Componente HotPDF para Delphi y C++Builder, que emite estos operadores de flujo de contenido por usted