La mayor parte del código Delphi que interactúa con PDF trata el formato como un contenedor para dos cosas: fragmentos de texto y algunos mapas de bits colocados. Esa visión es correcta hasta cierto punto, y deja sin utilizar la parte más capaz del formato. Una página PDF es un lienzo 2D independiente de la resolución construido sobre el mismo modelo de imagen que PostScript. Puede dibujar líneas, curvas, regiones rellenas, degradados y patrones repetitivos, todo como vectores que se mantienen nítidos en cualquier nivel de zoom y se imprimen a la resolución completa del dispositivo. Si está dibujando un logotipo, un gráfico, una marca de agua o el borde de un certificado, la ruta vectorial es casi siempre la primitiva adecuada, y es más pequeña y nítida que la imagen rasterizada a la que muchos programas recurren en su lugar.
Este artículo recorre el modelo vectorial tal como lo define la norma ISO 32000-1 y muestra las llamadas correspondientes en PDFlibPas. El objetivo es hacer concreta la especificación, porque la API se asigna estrechamente a ella, y comprender una le enseña la otra.
La página es una máquina de rutas
La norma ISO 32000-1 §8.5 describe los gráficos en dos fases que nunca se superponen. Primero se construye una ruta, que es geometría pura sin resultado visible. Luego se pinta esa ruta en una sola operación que traza su contorno, rellena su interior o hace ambas cosas. No aparece nada en la página durante la construcción. La ruta es una secuencia abstracta de puntos y segmentos que se mantiene en el estado gráfico hasta que un operador de pintura la consume, momento en el cual se representa y se descarta.
Una ruta se compone de una o más subrutas. Una subruta comienza en un punto y crece añadiendo segmentos: líneas rectas, curvas cúbicas de Bezier y, en algunas plataformas, rectángulos enteros añadidos como su propia subruta cerrada. En PDFlibPas se abre una ruta con StartPath, que establece el punto de inicio, y luego se extiende con AddLineToPath y AddCurveToPath. Cada llamada avanza un punto actual implícito, por lo que el siguiente segmento continúa desde donde terminó el último. ClosePath dibuja un segmento recto final de regreso al inicio de la subruta, lo cual importa para el trazado porque produce una unión de línea real en el vértice de cierre en lugar de dos extremos sueltos.
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
Las curvas utilizan AddCurveToPath, que toma dos puntos de control de Bezier y un punto final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La curva va desde el punto actual hasta (EndX, EndY), siendo atraída hacia los dos puntos de control en el camino. Los arcos circulares están disponibles a través de AddArcToPath(CenterX, CenterY, TotalAngle), donde el radio se toma de la distancia entre el punto actual y el centro, y el motor emite el arco como una cadena de segmentos de Bezier. Los rectángulos tienen un atajo, AddBoxToPath(Left, Top, Width, Height), que añade un rectángulo cerrado completo como su propia subruta sin un StartPath previo.
Dos reglas de relleno y por qué no coinciden
Cuando se rellena una ruta que se cruza a sí misma o contiene un bucle interno, el renderizador necesita una regla para decidir qué regiones están dentro de la forma y cuáles son agujeros. La norma ISO 32000-1 §8.5.3.3 define dos, y pueden pintar la misma geometría de manera diferente. La regla del devanado diferente de cero cuenta los cruces firmados de un rayo proyectado desde un punto de prueba al infinito, sumando uno por cada segmento que cruza de izquierda a derecha y restando uno por cada uno que cruza en sentido contrario; el punto está dentro cuando el total no es cero. La regla par-impar ignora la dirección y simplemente cuenta los cruces, considerando el punto como interno cuando el recuento es impar.
El caso clásico donde divergen es una forma con un agujero, como una rosquilla o una arandela. Dibuje un límite exterior y un límite interior dentro de él. Bajo la regla par-impar, el bucle interno siempre crea un agujero, porque cualquier punto entre los dos límites se cruza una vez y cualquier punto dentro del bucle interno se cruza dos veces. Bajo la regla del devanado diferente de cero, el agujero aparece solo si el bucle interno gira en la dirección opuesta al exterior; si giran en la misma dirección, los devanados se refuerzan en lugar de cancelarse, y la región interna se rellena por completo. Una estrella de cinco puntas dibujada como un único contorno auto-intersecado muestra la misma división: par-impar deja el pentágono central vacío mientras que el devanado diferente de cero lo rellena.
PDFlibPas selecciona la regla según la llamada que realice para pintar, no mediante una bandera. DrawPath rellena con la regla del devanado diferente de cero; DrawPathEvenOdd rellena con la regla par-impar. Ambas toman el mismo modo entero: 0 traza solo el contorno, 1 rellena solo y 2 rellena y traza. La regla par-impar es la herramienta más sencilla para agujeros troquelados precisamente porque no requiere que gestione la dirección de la subruta.
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
Los degradados axiales varían el color a lo largo de una línea
Un color de relleno plano es un solo valor en toda la región. Un degradado varía el color de forma continua, y el tipo más simple es el degradado axial, o lineal. La norma ISO 32000-1 §8.7.4.5 lo especifica como un sombreado axial Tipo 2: se definen dos puntos que determinan un eje, un color inicial en el primer punto y un color final en el segundo, y el renderizador interpola el color a lo largo de ese eje. Cada punto en la región rellena toma el color de su proyección perpendicular sobre el eje, por lo que el degradado se ejecuta en bandas en ángulo recto con respecto a la línea entre los dos puntos.
En PDFlibPas, un degradado es un recurso de documento con nombre que se crea una vez y luego se selecciona como pintura activa. NewRGBAxialShader lo registra. La firma es NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): los dos puntos finales del eje, los triples RGB en cada extremo como valores en el rango de 0 a 1, y una bandera Extend. Con Extend establecido en 1, los colores finales continúan como relleno sólido más allá de los puntos finales del eje, que es lo que usualmente se desea para que las esquinas de una región fuera del eje no se queden sin pintar; 0 las deja intactas. Una vez que existe el shader, lo vincula con SetFillShader para regiones rellenas, SetLineShader para contornos trazados o SetTextShader para texto. La vinculación permanece activa para las llamadas de dibujo que siguen, por lo que la ruta que pinte a continuación tomará el degradado en lugar de un color plano.
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
El eje aquí es vertical, de y=100 a y=260 a una x fija, por lo que las bandas de color se ejecutan horizontalmente y el rectángulo se desvanece de azul en su base a blanco en su parte superior. Debido a que el shader está identificado por su nombre, una sola definición puede rellenar cualquier cantidad de formas en la página, y volver a cambiar a un color plano es solo otra llamada a SetFillColor antes de la siguiente ruta.
Los patrones de mosaico repiten una celda
Donde un degradado varía un solo color suavemente, un patrón de mosaico repite una pequeña pieza de diseño artístico en toda una región. La norma ISO 32000-1 §8.7.3.1 define un patrón de mosaico como una celda de patrón, una pieza independiente de contenido, que el renderizador replica en una cuadrícula fija para cubrir el área que se está pintando. Así es como se construye el sombreado para un relleno de ingeniería, un motivo de marca repetitivo detrás de un encabezado o un fondo texturizado que se mantiene nítido como vector y no pesa casi nada sin importar lo grande que sea el área, porque la celda se almacena una vez y se hace referencia a ella en todas partes.
PDFlibPas construye la celda del patrón a partir de contenido de página capturado. Captura una página o una región con CapturePage, convierte la captura en un patrón con nombre con NewTilingPatternFromCapturedPage(PatternName, CaptureID), y luego selecciona ese patrón como el relleno actual con SetFillTilingPattern(PatternName). A partir de ese momento, cualquier ruta que rellene se pintará con la celda repetitiva en lugar de con un color plano, exactamente como funciona un relleno de shader pero con una celda en mosaico como fuente de pintura. La secuencia es más compleja que una sola llamada, por lo que si el paso de captura no le es familiar, trate el patrón como una operación de dos etapas: produzca primero la celda capturada y luego vincúlela como un relleno por nombre antes de dibujar la región que desea cubrir con mosaico.
Unir las primitivas
Las piezas se componen directamente. Una mancha de Bezier rellena es una ruta de curvas pintada con DrawPath. El mismo contorno pintado con DrawPathEvenOdd tras añadir un bucle interno muestra un agujero que el relleno de devanado habría cerrado. Un rectángulo relleno de degradado es una caja vinculada a un shader. El siguiente ejemplo dibuja los tres en secuencia para que la diferencia entre las dos reglas de relleno sea visible en una sola página, y luego coloca un panel de degradado debajo de ellos.
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
Vale la pena tener en cuenta dos detalles. La llamada de pintura decide la regla de relleno, por lo que la elección entre DrawPath y DrawPathEvenOdd es la elección entre devanado diferente de cero y par-impar, y para formas con agujeros, la regla par-impar le evita tener que razonar sobre la dirección de la subruta. Y el estado gráfico se muestrea en el momento en que pinta: establezca sus colores, ancho de línea y vinculación de shader antes de la llamada de pintura, porque ese es el estado que lee el motor. Construya primero, configure el estado, pinte al final, y el modelo vectorial se comportará de manera predecible en todo momento.
A partir de aquí, los siguientes pasos naturales son leer vectores y texto de un documento existente, cubierto en nuestro artículo sobre extracción de texto, imágenes y fuentes, y representar el mismo modelo de dibujo en un contexto de dispositivo de Windows para previsualización en pantalla e impresión, cubierto en el tutorial de impresión y previsualización. Las llamadas de ruta, shader y patrón descritas aquí se distribuyen como parte de la Librería PDF para Delphi junto con las API de texto, imagen, formulario y firma cubiertas en otras partes de este blog.