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 al formato como un contenedor de dos cosas: secuencias de texto y algunos mapas de bits ubicados. Esa visión es correcta hasta cierto punto, y deja sin usar 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, todos como vectores que se mantienen nítidos en 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 correcta, 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 que la especificación sea concreta, porque la API se ajusta estrechamente a ella, y entender uno le enseña el otro

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, usted construye una ruta, que es geometría pura sin resultados visibles. Luego pinta esa ruta en una sola operación que traza su contorno, rellena su interior o hace ambas cosas. Nada aparece en la página durante la construcción. La ruta es una secuencia abstracta de puntos y segmentos mantenida en el estado de gráficos hasta que un operador de pintura la consume, momento en el que se renderiza y se desecha

Una ruta se compone de una o más subrutas. Una subruta comienza en un punto y crece anexando segmentos: líneas rectas, curvas de Bézier cúbicas y, en algunas plataformas, rectángulos enteros añadidos como su propia subruta cerrada. En PDFlibPas, se abre una ruta con StartPath, lo que establece el punto de inicio, y luego se extiende con AddLineToPath y AddCurveToPath. Cada llamada avanza un punto actual implícito, de modo que el siguiente segmento continúa desde donde terminó el anterior. ClosePath dibuja un segmento recto final de regreso al inicio de la subruta, lo que es importante 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, trazado y luego rellenado
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // abra la ruta en el primer vértice
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // segmento recto de regreso a (150, 100)
PDF.DrawPath(2);                   // 2 = rellenar y trazar; la ruta se consume

Las curvas usan AddCurveToPath, que toma dos puntos de control de Bézier y un punto final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La curva va desde el punto actual hasta (EndX, EndY), atraída hacia los dos puntos de control en el camino. Los arcos circulares están disponibles mediante 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 Bézier. Los rectángulos tienen un atajo, AddBoxToPath(Left, Top, Width, Height), que anexa un rectángulo cerrado completo como su propia subruta sin un StartPath precedente

Dos reglas de relleno y por qué no concuerdan

Cuando rellena una ruta que se cruza a sí misma o que contiene un bucle interno, el renderizador necesita una regla para decidir qué regiones están dentro de la figura y cuáles son agujeros. ISO 32000-1 §8.5.3.3 define dos, y pueden pintar la misma geometría de manera diferente. La regla del número de devanado distinto de cero cuenta los cruces con signo de un rayo proyectado desde un punto de prueba hacia el 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á adentro cuando el total no es cero. La regla par-impar ignora la dirección y simplemente cuenta los cruces, llamando al punto interior cuando el recuento es impar

El caso clásico en el que divergen es una figura con un agujero, una dona o una arandela. Dibuje un límite exterior y un límite interior dentro de él. Según la regla par-impar, el bucle interior siempre esculpe un agujero, porque cualquier punto entre los dos límites se cruza una vez y cualquier punto dentro del bucle interior se cruza dos veces. Según la regla de devanado distinto de cero, el agujero solo aparece si el bucle interior gira en dirección opuesta al exterior; si los gira de la misma manera, 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 que se interseca a sí mismo muestra la misma división: el método par-impar deja el pentágono central vacío, mientras que el devanado distinto de cero lo rellena

PDFlibPas selecciona la regla mediante la llamada que usted realiza para pintar, no mediante una bandera. DrawPath rellena con la regla de devanado distinto de cero; DrawPathEvenOdd rellena con la regla par-impar. Ambas toman el mismo modo entero: 0 traza solo el contorno, 1 solo rellena y 2 rellena y traza. La regla par-impar es la herramienta más fácil para los agujeros troquelados precisamente porque no requiere que usted gestione la dirección de las subrutas

// Las mismas dos cajas, dos reglas de relleno, dos resultados diferentes.
// Devanado distinto de cero: ambas cajas se devanan en el mismo sentido, por lo que la interior
// NO corta un agujero y toda la caja exterior se rellena sólida.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // exterior
PDF.AddBoxToPath(140, 130, 120,  60);   // interior
PDF.DrawPath(1);                         // 1 = rellenar, devanado distinto de cero

// Par-impar: la caja interior se cruza un número par de veces,
// por lo que perfora un agujero rectangular limpio a través de la caja exterior.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // exterior
PDF.AddBoxToPath(140, 330, 120,  60);   // corte interior
PDF.DrawPathEvenOdd(1);                  // 1 = rellenar, par-impar

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. ISO 32000-1 §8.7.4.5 lo especifica como un sombreado axial de Tipo 2: usted da dos puntos que definen un eje, un color de inicio en el primer punto y un color de fin en el segundo, y el renderizador interpola el color a lo largo de ese eje. Cada punto de la región rellenada 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 nombrado que usted crea una vez y luego selecciona como la pintura activa. NewRGBAxialShader lo registra. La firma es NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): los dos extremos 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 de los extremos continúan como un relleno sólido más allá de los extremos del eje, que es lo que usted generalmente quiere para que las esquinas de una región fuera del eje no se queden sin pintar; 0 los deja intactos. Una vez que existe el sombreador, lo vincula con SetFillShader para regiones rellenadas, SetLineShader para contornos trazados, o SetTextShader para texto. El vínculo permanece activo para las llamadas de dibujo que siguen, de manera que la ruta que usted pinta a continuación toma el degradado en lugar de un color plano

// Defina un degradado vertical una vez: azul en la parte inferior y blanco en la superior.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // punto de inicio y RGB de inicio
  0, 260,   1.00, 1.00, 1.00,    // punto de finalización y RGB final
  1);                            // 1 = extender extremos como color sólido

// Seleccione el degradado como relleno y luego pinte un rectángulo con él.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = rellenar, ahora rellenado por el sombreador

El eje aquí es vertical, de y=100 a y=260 en un x fijo, por lo que las bandas de color se ejecutan horizontalmente y el rectángulo se desvanece del azul en su base al blanco en su parte superior. Debido a que el sombreador está codificado por nombre, una definición puede rellenar cualquier cantidad de figuras en la página, y volver a un color plano es solo otra llamada a SetFillColor antes de la siguiente ruta

Los patrones de mosaico repiten una celda

Mientras que un degradado varía un solo color suavemente, un patrón de mosaico 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 pieza independiente de contenido, que el renderizador replica en una cuadrícula fija para revestir el área que se está pintando. Así es como se crea un sombreado para un relleno de ingeniería, un motivo de marca repetitivo detrás de un encabezado, o un fondo texturizado que se mantiene vectorial y nítido, y que casi no pesa sin importar cuán grande 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 capturado de la página. Usted captura una página o una región con CapturePage, convierte la captura en un patrón con nombre usando NewTilingPatternFromCapturedPage(PatternName, CaptureID), y luego selecciona ese patrón como el relleno actual con SetFillTilingPattern(PatternName). A partir de ese momento, cualquier ruta que usted rellene se pinta con la celda repetitiva en lugar de un color plano, exactamente de la misma manera que funciona un relleno de sombreador 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 resulta familiar, trate el patrón como una operación de dos etapas: produzca primero la celda capturada, luego vincúlela como relleno por nombre antes de dibujar la región que desea revestir

Juntando las primitivas

Las piezas se componen directamente. Una gota de Bézier rellenada es una ruta de curvas pintada con DrawPath. El mismo contorno pintado con DrawPathEvenOdd después de añadir un bucle interno muestra un agujero que el relleno de devanado habría cerrado. Un rectángulo rellenado con degradado es una caja vinculada a un sombreador. El siguiente ejemplo dibuja los tres en secuencia para que la diferencia entre las dos reglas de relleno sea visible en una página, y luego coloca un panel de degradado debajo de ellos

// 1. Una figura de Bézier rellenada (devanado distinto de cero).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // lóbulo superior
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // lóbulo inferior
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = rellenar

// 2. El mismo contorno, más un bucle interior, rellenado con par-impar para mostrar un agujero.
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);                              // nueva subruta: el agujero
PDF.AddArcToPath(200, 300, 360);                     // un círculo completo
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // el agujero es perforado

// 3. Un rectángulo rellenado 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 retener 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 el devanado distinto de cero y el par-impar, y para figuras con agujeros la regla par-impar le evita tener que razonar sobre la dirección de la subruta. Y el estado de gráficos se muestrea en el momento en que se pinta: configure sus colores, ancho de línea y vínculo de sombreador antes de la llamada de pintura, porque ese es el estado que lee el motor. Construya primero, configure el estado y pinte al final, y el modelo vectorial se comportará de manera predecible en todo momento

A partir de aquí, los siguientes pasos naturales son volver a leer vectores y texto de un documento existente, tratados en nuestro artículo sobre extracción de texto, imágenes y fuentes, y renderizar el mismo modelo de dibujo en un contexto de dispositivo de Windows para su vista previa en pantalla e impresión, que se aborda en la guía de impresión y vista previa. Las llamadas de rutas, sombreadores y patrones descritas aquí se incluyen como parte de Delphi PDF Library junto con las API de texto, imágenes, formularios y firmas tratadas en otras secciones de este blog