HotPDF dibuja gráficos vectoriales construyendo un trazado en la página actual y luego pidiéndole que se pinte. No hay ningún paso de mapa de bits en medio. Una línea que dibujáis con MoveTo y LineTo acaba como operadores de trazado PDF en el flujo de contenido, por lo que sigue siendo un vector verdadero: nítido al 50% de zoom, nítido al 1600%, y una fracción del tamaño que costaría una versión rasterizada. Para diagramas, líneas de tabla, ejes de gráficos y decoraciones de formularios, eso es exactamente lo que queréis, y la API que hay detrás es lo suficientemente pequeña como para aprenderla en una sesión
Toda la superficie de dibujo vive en THotPDF.CurrentPage. Entre BeginDoc y EndDoc establecéis el color y el grosor de línea en ese objeto de página, colocáis geometría y llamáis a un operador de pintado para confirmarla. Las cuatro primitivas que más usaréis son MoveTo y LineTo para trazados arbitrarios, Rectangle para cajas, Circle para discos, y los dos operadores de pintado Stroke y Fill
El sistema de coordenadas tiene el origen en la esquina inferior izquierda
Esto es lo único que confunde a todos los que llegan desde VCL. El TCanvas con el que pintáis controles coloca el origen en la esquina superior izquierda con Y creciendo hacia abajo. PDF hace lo contrario. HotPDF mide desde la esquina inferior izquierda de la página en puntos (1/72 de pulgada), con Y aumentando al moverse hacia arriba. Un punto en Y := 720 se sitúa cerca de la parte superior de una página de tamaño US Letter, que tiene 792 puntos de altura, y Y := 50 se sitúa cerca de la parte inferior. Si vuestro primer dibujo sale reflejado verticalmente, es por esto: el código portado desde gráficos de pantalla asume la dirección equivocada y sale por el borde inferior
La misma convención rige TextOut, de modo que el texto y las formas comparten un mismo modelo mental una vez que lo internalizáis. Planificad un diseño decidiendo dónde se encuentra la parte inferior de cada elemento, no la superior, y el resto se deduce
Trazados: MoveTo, LineTo, Stroke
Un trazado trazado es un bolígrafo levantado, colocado y arrastrado. MoveTo levanta el bolígrafo y establece el punto de inicio sin marcar nada. Cada LineTo extiende el trazado actual hasta un nuevo punto. Nada aparece en la página hasta que llamáis a Stroke, que dibuja el trazado acumulado usando el color de trazo y el grosor de línea actuales, y luego borra el trazado para que el siguiente MoveTo empiece de nuevo
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'DrawPaths.pdf';
Pdf.BeginDoc;
// El ancho de línea está en puntos y se aplica hasta que lo cambies.
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// Una línea horizontal cerca de la parte superior de la página (Y medido desde abajo).
Pdf.CurrentPage.MoveTo(72, 720);
Pdf.CurrentPage.LineTo(523, 720);
Pdf.CurrentPage.Stroke; // confirma el trazo; nada se dibujó antes de esto
// Una polilínea conectada más gruesa: tres segmentos en un solo trazo.
Pdf.CurrentPage.SetLineWidth(3);
Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
Pdf.CurrentPage.MoveTo(72, 640);
Pdf.CurrentPage.LineTo(172, 690);
Pdf.CurrentPage.LineTo(272, 620);
Pdf.CurrentPage.LineTo(372, 680);
Pdf.CurrentPage.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dos detalles ahorran mucho tiempo de depuración. El grosor de línea es estado, no un argumento: SetLineWidth lo establece una vez y cada Stroke posterior usa ese valor hasta que se cambia de nuevo, que es la razón por la que la polilínea anterior es más gruesa que la regla. Y el trazado se reinicia tras cada Stroke, de modo que un Stroke olvidado significa que la geometría que con tanto cuidado habéis trazado nunca se renderiza. Si falta una forma en la salida, la llamada de pintado es el primer lugar donde buscar
Las coordenadas son puntos, y los puntos son fraccionarios. MoveTo y LineTo aceptan valores Single, por lo que una línea de cabello en 0.5 puntos o una posición en 72.25 es legal y significativa, no se redondea a la unidad entera más cercana. Esa precisión importa en dos direcciones opuestas. Un grosor de línea por debajo de unos 0.5 puede renderizarse como la línea más fina posible que depende del dispositivo, que desaparece en pantalla y reaparece al imprimir, así que una regla visible requiere un grosor establecido intencionadamente en lugar del valor por defecto. En el otro extremo, ajustar las reglas de tabla y las líneas de cuadrícula a coordenadas de punto entero evita que una cuadrícula densa parezca ligeramente irregular donde las líneas adyacentes redondean de forma distinta. Decidid el espaciado de la cuadrícula en puntos desde el principio y el resto del diseño lo hereda
Formas rellenas y color
Las primitivas cerradas pueden rellenarse en lugar de solo delinearse. Rectangle toma una posición y un tamaño, Circle toma un centro y un radio, y cualquiera de los dos se confirma con Fill, que pinta el interior con el color de relleno actual, o con Stroke para solo el contorno. El color de relleno y el color de trazo son piezas de estado separadas, establecidas con SetRGBFillColor y SetRGBStrokeColor, ambas con un único TColor. Eso significa que podéis reutilizar directamente las constantes de color de Delphi y el helper RGB
// Rectangle(X, Y, Width, Height): X e Y son la esquina inferior izquierda.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;
// Circle(X, Y, Radius): X e Y son el centro.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;
// Solo contorno: define un color de trazo y un ancho, luego Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;
Prestad atención a la forma de los argumentos de Rectangle. Es posición más tamaño, X, Y, Width, Height, no dos esquinas opuestas. El TCanvas.Rectangle que conocen los desarrolladores de Delphi toma (Left, Top, Right, Bottom), por lo que la memoria muscular pasará a HotPDF una segunda esquina donde espera un ancho y una altura, y el cuadro saldrá con el tamaño equivocado. El par (X, Y) es la esquina inferior izquierda, coherente con el origen de la página. Para un círculo, (X, Y) es el centro y el tercer argumento es el radio en puntos
Una elección de color que el ejemplo original hacía mal
Una versión anterior de este ejemplo generaba colores con Random($FFFFFF) en cada forma. Tiene un aspecto animado, pero es el instinto equivocado para documentos generados. Un PDF que construís desde código normalmente también queréis probarlo, y los colores de relleno aleatorios hacen imposible comparar la salida de una ejecución a otra: una comparación byte a byte contra un fichero de referencia conocido falla cada vez, sin ninguna razón real. Elegid colores explícitos. Cuando queráis variedad en una serie de formas, guiaos por vuestros datos o por un array de paleta fija, de modo que la misma entrada produzca siempre el mismo fichero. El determinismo vale más que la novedad cuando el artefacto pasa por una cadena de publicación
Uniendo las primitivas: un cuadro de llamada (callout)
Cada primitiva es sencilla por sí sola; la recompensa se aprecia cuando un puñado de ellas se combina en algo que un informe realmente necesita. Un cuadro de llamada, la caja anotada que señala una figura y la explica, usa todo lo visto hasta ahora: un rectángulo relleno con un borde, una línea de puntero trazada, un punto que ancla el puntero, y texto colocado dentro de la caja usando las mismas coordenadas de origen inferior izquierdo que usan las formas. FillAndStroke se gana aquí su sitio, pintando el interior y el contorno de un trazado en una sola confirmación en lugar de construir el rectángulo dos veces
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'Callout.pdf';
Pdf.BeginDoc;
// 1. La caja: relleno pálido más un borde visible, un trazado, una confirmación.
// Rectangle es esquina inferior izquierda más tamaño, Y medido desde abajo
Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214)); // panel ámbar pálido
Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40)); // borde más oscuro
Pdf.CurrentPage.SetLineWidth(1);
Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
Pdf.CurrentPage.FillAndStroke;
// 2. El puntero: un segmento trazado desde el borde de la caja hacia abajo
// hacia el elemento que se anota
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.MoveTo(90, 615); // borde izquierdo de la caja
Pdf.CurrentPage.LineTo(66, 546);
Pdf.CurrentPage.Stroke;
// 3. Un punto relleno ancla el puntero en su destino
Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
Pdf.CurrentPage.Circle(64, 542, 3);
Pdf.CurrentPage.Fill;
// 4. La etiqueta, posicionada respecto a la esquina inferior izquierda de la caja.
// El texto y las formas comparten un sistema de coordenadas, así que los desplazamientos
// son aritmética simple respecto a (90, 600)
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Fijaos en lo poco que necesita esta composición en cuanto a gestión de estado. El color de relleno, el color de trazo y el grosor de línea se establecen justo antes de la forma que los usa, de modo que cada bloque del dibujo se lee como una unidad autocontenida y puede reordenarse o extraerse a una función auxiliar sin arrastrar estado oculto. Envolved esto en un procedimiento que reciba el punto de anclaje y las cadenas de texto, y tendréis una anotación de diagrama reutilizable por el precio de cuarenta líneas
Cuándo el dibujo vectorial compensa, y cuándo no
Recurrid a estas llamadas de trazado y forma cuando la geometría se genera por código: líneas de cuadrícula y barras de gráficos, las líneas rayadas de una tabla de factura, cuadros de llamada en un diagrama, un logotipo expresado como un puñado de trazados. Todo ello escala sin desenfoque y añade casi nada al tamaño del fichero, porque un rectángulo son unos pocos números en lugar de miles de píxeles. La otra cara también es honesta. Si lo que realmente tenéis es una fotografía o una captura de pantalla, dibujadla como imagen con AddImage y ShowImage en su lugar; trazar un mapa de bits con llamadas vectoriales no aporta nada. Los segmentos rectos, rectángulos y círculos anteriores cubren la gran mayoría del trabajo real de informes, y los tres refinamientos que los desarrolladores preguntan a continuación, curvas, patrones de guiones y transparencia, viven en el mismo objeto de página
Curvas, guiones y transparencia, en resumen
Las curvas de forma libre extienden la misma maquinaria de trazados que ya conocéis. CurveToC(X1, Y1, X2, Y2, X3, Y3) añade un segmento Bezier cúbico desde el punto actual hasta (X3, Y3), curvándose hacia los dos puntos de control, y las variantes abreviadas CurveToV y CurveToY cubren los casos en que un punto de control coincide con un extremo. Un trazado puede mezclar libremente segmentos LineTo y CurveToC antes de que un único Stroke o Fill lo confirme, que es como se construyen las esquinas redondeadas y las líneas de gráfico suaves
Los trazos discontinuos son estado, exactamente igual que el grosor de línea. SetDash([3, 3], 0) cambia cada trazo posterior a un patrón de tres puntos encendido, tres puntos apagado, con el array detallando las longitudes de los tramos encendido/apagado en puntos y el segundo argumento indicando la fase en la que empieza el ciclo; NoDash devuelve el bolígrafo a una línea sólida. Establecedlo, trazad las líneas de cuadrícula que lo necesiten, y restablecedlo antes de la siguiente regla sólida, o el guion se colará silenciosamente en todo lo que venga después
La transparencia pasa por un estado gráfico con nombre en lugar de por un argumento de color, porque el alfa en PDF es una propiedad del diccionario de estado gráfico. Registrad uno en el documento con RegisterExtGState, pasando un alfa de relleno y un alfa de trazo entre 0 y 1, y luego aplicad el nombre que devuelve con CurrentPage.SetGraphicsState; los rellenos y trazos a partir de ese punto se pintan con la opacidad registrada. Es una ceremonia más pesada que los establecedores de color, y merece la pena la primera vez que una barra de resaltado tiene que superponerse a un texto sin ocultarlo
El hábito restante que vale la pena conservar es la verificación. La geometría generada puede pasar en vuestra máquina y fallar en la de un cliente, normalmente por la sustitución de fuentes en cualquier texto que mezcléis o por una suposición sobre el tamaño de página que no se cumple. Abrid el fichero terminado a varios niveles de zoom para confirmar que los bordes se mantienen limpios, y comprobad que cada forma cae dentro del cuadro de márgenes previsto. Con un esquema de color determinista, esa comprobación puede automatizarse contra un PDF de referencia en lugar de hacerse a ojo
Las llamadas MoveTo, LineTo, Stroke, Fill y de color mostradas aquí forman parte del HotPDF Delphi Component para Delphi y C++Builder