Technical Article

Gráficos vectoriales en PDF con Delphi: rutas y degradados

La mayor parte del código de Delphi que manipula archivos PDF trata el formato como un contenedor para dos cosas: bloques de texto y algunos mapas de bits colocados. Esa visión es correcta hasta cierto punto, pero 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 imágenes que PostScript. Puede dibujar líneas, curvas, regiones rellenas, degradados y patrones repetitivos, todo como vectores que se mantienen nítidos con cualquier 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 recurren muchos programas en su lugar.

Este artículo recorre el modelo vectorial tal como lo define ISO 32000-1 y muestra las llamadas correspondientes en PDFlibPas. El objetivo es hacer concreta la especificación, ya que la API se asigna a ella de forma directa, y entender una ayuda a aprender la otra.

La página es una máquina de rutas

ISO 32000-1 §8.5 describe los gráficos en dos fases que nunca se superponen. Primero se construye una ruta, que es pura geometría sin resultado visible. Luego se pinta esa ruta en una única operación que delinea su contorno, rellena su interior o realiza ambas acciones. Nada aparece 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 renderiza y se descarta.

Una ruta se compone de uno o más subrutas. Una subruta comienza en un punto y crece agregando segmentos: líneas rectas, curvas de Bezier cúbicas y, en algunas plataformas, rectángulos enteros añadidos como su propio subruta cerrada. En PDFlibPas se abre una ruta con StartPath, que establece el punto inicial, 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 anterior. ClosePath dibuja un segmento recto final de regreso al inicio del subruta, lo cual es importante para el delineado 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 recibe dos puntos de control de Bezier y un punto final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La curva se extiende desde el punto actual hasta (EndX, EndY), siendo atraída por los dos puntos de control en el camino. Los arcos circulares están disponibles mediante AddArcToPath(CenterX, CenterY, TotalAngle), donde el radio se toma a partir de la distancia entre el punto actual y el centro, y el motor emite el arco como una cadena de segmentos Bezier. Los rectángulos cuentan con un atajo, AddBoxToPath(Left, Top, Width, Height), que añade un rectángulo cerrado completo como su propio subruta sin necesidad de un StartPath previo.

Dos reglas de relleno, y por qué difieren

Al rellenar 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. ISO 32000-1 §8.5.3.3 define dos, las cuales pueden pintar la misma geometría de manera diferente. La regla del devanado diferente de cero (nonzero winding) cuenta los cruces con signo 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 de par-impar (even-odd) ignora la dirección y simplemente cuenta los cruces, considerando al punto dentro de la forma cuando el conteo 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 de par-impar, el bucle interno siempre genera 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 de devanado diferente de cero, el agujero aparece solo si el bucle interno gira en la dirección opuesta al exterior; si giran en el mismo sentido, 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 autointresecante muestra la misma separación: par-impar deja vacío el pentágono central, mientras que el devanado diferente de cero lo rellena.

PDFlibPas selecciona la regla mediante la llamada que se realiza para pintar, no por medio de una bandera. DrawPath rellena con la regla de devanado diferente de cero; DrawPathEvenOdd rellena con la regla de par-impar. Ambas reciben el mismo modo entero: 0 delinea solo el contorno, 1 rellena solo y 2 rellena y delinea. La regla de par-impar es la herramienta más sencilla para troquelar agujeros precisamente porque no requiere que usted gestione la dirección de los subrutas.

// 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 mantiene un único 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. ISO 32000-1 §8.7.4.5 lo especifica como un sombreado axial de Tipo 2: se indican dos puntos que definen 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 de la región rellena toma el color de su proyección perpendicular sobre el eje, por lo que el degradado fluye en bandas en ángulo recto con la línea que une los dos puntos.

En PDFlibPas, un degradado es un recurso de documento nombrado que se crea una vez y luego se selecciona como el elemento de pintura activo. 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 tripletes 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 se extienden como relleno sólido más allá de los puntos extremos del eje, que es lo que usualmente se desea para que las esquinas de una región fuera del eje no queden sin pintar; 0 las deja intactas. Una vez que el sombreador existe, se vincula con SetFillShader para regiones rellenas, SetLineShader para contornos delineados o SetTextShader para texto. La vinculación permanece activa para las llamadas de dibujo siguientes, por lo que la ruta que se 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

Los patrones de mosaico repiten una celda

Mientras que un degradado varía un único color de manera suave, un patrón de mosaico (tiling pattern) repite una pequeña obra de arte a lo largo de una región. ISO 32000-1 §8.7.3.1 define un patrón de mosaico como una celda de patrón, una sección de contenido independiente que el renderizador duplica en una cuadrícula fija para cubrir el área que se está pintando. Así es como se construyen tramas para rellenos de ingeniería, un motivo de marca repetitivo detrás de un encabezado o un fondo texturizado que conserva su nitidez vectorial y no pesa casi nada sin importar cuán grande sea el área, ya que la celda se almacena una sola vez y se referencia en todas partes.

PDFlibPas construye la celda de patrón a partir del contenido de página capturado. Se captura una página o región con CapturePage, se convierte la captura en un patrón nombrado mediante NewTilingPatternFromCapturedPage(PatternName, CaptureID) y luego se selecciona ese patrón como el relleno actual con SetFillTilingPattern(PatternName). A partir de ese momento, cualquier ruta que se rellene se pintará con la celda repetitiva en lugar de un color plano, tal como funciona el relleno de sombreador pero usando 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 resulta familiar, trate el patrón como una operación de dos etapas: genere primero la celda capturada y luego vincúlela como relleno por su nombre antes de dibujar la región que desea cubrir con el mosaico.

Reunir las primitivas

Las piezas se componen de manera directa. Una figura de Bezier rellena es una ruta de curvas pintada con DrawPath. El mismo contorno pintado con DrawPathEvenOdd tras agregar un bucle interno muestra un agujero que el relleno de devanado habría cerrado. Un rectángulo relleno con degradado es una caja vinculada a un sombreador. El ejemplo a continuación 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 recordar dos detalles. La llamada de pintura decide la regla de relleno, por lo que la elección entre DrawPath y DrawPathEvenOdd is la elección entre devanado diferente de cero y par-impar, y para formas con agujeros, la regla de par-impar le evita tener que razonar sobre la dirección del subruta. Y el estado gráfico se muestrea en el momento en que se pinta: establezca sus colores, ancho de línea y vinculación del sombreador 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 forma predecible siempre.

Desde aquí, los siguientes pasos naturales son leer vectores y texto de vuelta desde un documento existente, lo cual se cubre en nuestro artículo sobre extracción de texto, imágenes y fuentes, y renderizar el mismo modelo de dibujo a un contexto de dispositivo de Windows para vista previa en pantalla e impresión, cubierto en la guía práctica de impresión y vista previa. Las llamadas de rutas, sombreadores y patrones descritas aquí se distribuyen como parte de la biblioteca PDF para Delphi junto con las API de texto, imágenes, formularios y firmas tratadas en otras secciones de este blog.