Artículo técnico

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

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 PDF Library for Delphi. 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

PDF Library for Delphi: Diagrama del ciclo de vida del path PDF que muestra la construcción como geometría pura invisible seguida de un operador de pintura que traza, rellena o ambos, y luego consume y descarta el path
Construir el cuadrilátero no pinta nada — la llamada DrawPath lee el estado gráfico una vez, renderiza trazo, relleno o ambos, y descarta el trazado

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 PDF Library for Delphi 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

// Un cuadrilátero cerrado, primero trazado y luego relleno
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // abre el path en el primer vértice
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // segmento recto de vuelta a (150, 100)
PDF.DrawPath(2);                   // 2 = relleno y trazo; el path se consume

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

PDF Library for Delphi 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

PDF Library for Delphi: Rectángulos anidados rellenados bajo las reglas even-odd y nonzero winding: even-odd recorta el rectángulo interior como agujero mientras nonzero winding rellena sólido salvo que el bucle interno gire en sentido opuesto al externo
La misma geometría de arandela ante dos respuestas legales — DrawPathEvenOdd ignora la dirección del devanado y siempre talla el agujero, mientras que DrawPath suma devanados con signo que se refuerzan cuando los subtrazados comparten dirección
// Los mismos dos recuadros, dos reglas de relleno, dos resultados distintos
// Nonzero winding: los dos recuadros giran igual, así que el interior
// NO recorta un hueco y el recuadro exterior se rellena entero
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: el recuadro interior se cruza un número par de veces,
// así que abre un hueco rectangular limpio en el recuadro exterior
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 PDF Library for Delphi, 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

PDF Library for Delphi: Anatomía del degradado RGB axial que muestra bandas de color perpendiculares al eje del shading, la proyección sobre el eje, y Extend prolongando los colores extremos como relleno sólido más allá de los puntos finales
Un shader axial interpola a lo largo del eje entre dos colores extremos, y cada punto muestrea su proyección perpendicular sobre esa línea — Extend = 1 mantiene los colores finales corriendo más allá de los extremos para que las esquinas sigan pintadas
// Define una sola vez el degradado vertical: azul abajo y blanco arriba
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // punto inicial y RGB inicial
  0, 260,   1.00, 1.00, 1.00,    // punto final y RGB final
  1);                            // 1 = extiende los extremos con color sólido

// Selecciona el degradado como relleno y pinta con él un rectángulo
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = relleno, ahora lo pinta el 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

PDF Library for Delphi 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. Una forma Bezier rellena (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. El mismo contorno más un bucle interior, relleno even-odd para ver el hueco
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);                              // se recorta el hueco

// 3. Un rectángulo relleno con un degradado axial
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