Artículo técnico

Dibujo vectorial en HotPDF con Delphi: trazados y color

Una regla de pie copiada de un ejemplo de content stream PDF lee 72 72 m 540 72 l S: muévete a (72, 72), traza una línea hasta (540, 72), trázala. En PDF en crudo esa línea queda una pulgada por encima del borde inferior de una página US Letter. Dele los mismos cuatro números a una API de dibujo que mide desde la esquina superior izquierda y la regla aterriza una pulgada por debajo del borde superior, atravesando el membrete. La línea en sí está bien. Solo cambió el marco de referencia, y moverse entre ambos marcos en una página de 792 puntos es una resta: 792 menos Y

¿Dónde está Y = 0 en una página de HotPDF?

Las llamadas de dibujo de página de HotPDF ponen el origen en la esquina superior izquierda de la página, con X creciendo hacia la derecha e Y hacia abajo, el mismo marco que usa el TCanvas de la VCL. El user space de PDF, definido en la ISO 32000-1 §8.3.2.3, empieza en la inferior izquierda con Y creciendo hacia arriba, y HotPDF convierte cada Y que usted pasa cuando escribe el content stream. Así, Y := 72 sobre THotPDF.CurrentPage significa una pulgada por debajo del borde superior, sea cual sea el tamaño de página

El tamaño de página importa para el otro extremo. En una página US Letter, 612 por 792 puntos, Y := 720 queda una pulgada por encima del borde inferior. Una página nueva de HotPDF es A4 salvo que diga otra cosa, y A4 mide 842 puntos de alto, así que el mismo Y := 720 deja debajo 122 puntos, unas 1,7 pulgadas. Los pies de página posicionados restando a 792 se van hacia arriba en A4 por exactamente esa razón. Lea el alto desde CurrentPage.Height en lugar de dejarlo clavado en el código, o fije CurrentPage.Size explícitamente justo después de BeginDoc

Comparación del TCanvas de VCL y la API de dibujo de página de HotPDF, ambos midiendo desde la esquina superior izquierda con Y creciendo hacia abajo, mientras HotPDF escribe el mismo punto cerca de la parte superior de una página US Letter como y = 720 en el espacio de usuario PDF de abajo hacia arriba
HotPDF dibuja en el mismo marco superior izquierdo que TCanvas, de modo que un punto una pulgada por debajo de la parte superior de la página de 612 por 792 es Y = 72 en ambos, y HotPDF lo escribe al archivo como y = 720 del espacio de usuario PDF

La unidad es el punto, 1/72 de pulgada, porque THotPDF.Resolution vale 72 por defecto. Subir Resolution hace que coordenadas y tamaños cuenten en 1/Resolution de pulgada. Para trabajo vectorial, déjelo en 72: los grosores de línea, las longitudes de guion y las coordenadas se leen entonces todos en la misma unidad que guarda el archivo PDF, y un valor que usted ve en el content stream coincide con un valor de su código

El texto sigue el mismo modelo. Por defecto, la Y que usted da a TextOut es el borde superior del texto, así que una etiqueta y la caja que la rodea se planifican sobre una sola rejilla. Unas pocas llamadas trabajan sobre el archivo PDF en lugar de sobre la superficie de dibujo y usan coordenadas PDF de abajo hacia arriba: los setters de page box como SetCropBox(Left, Bottom, Right, Top), y la familia StampLoadedPageText / StampLoadedPageImage que estampa un documento ya cargado. Mezclar esas con llamadas de trazados es de donde sale la mayoría de los layout volteados, así que guárdelas en rutinas separadas

Construir un trazado y pintarlo

Un trazado se construye primero y se pinta después. MoveTo fija un punto de partida sin marcar nada, cada LineTo añade un segmento recto, y nada llega a la página hasta que una llamada de pintado como Stroke usa el color de trazo y el grosor actuales. En términos PDF, Stroke emite el operador S, y todo operador de pintado termina el trazado actual, así que el siguiente MoveTo empieza limpio

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := Letter;    // 612 x 792 pt; por defecto es A4

    // Grosor y color son estado de página: se ponen una vez y los usa todo Stroke posterior
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Una regla una pulgada bajo el borde superior, márgenes de 72 pt a ambos lados
    Pdf.CurrentPage.MoveTo(72, 72);
    Pdf.CurrentPage.LineTo(540, 72);
    Pdf.CurrentPage.Stroke;

    // La regla del pie de la apertura: una pulgada por encima del borde inferior
    Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.Stroke;

    // Tres segmentos conectados en un trazado, más grueso y azul
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 152);
    Pdf.CurrentPage.LineTo(172, 102);
    Pdf.CurrentPage.LineTo(272, 172);
    Pdf.CurrentPage.LineTo(372, 112);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

El grosor de línea es estado, no argumento. SetLineWidth escribe un operador w y todo trazo posterior de la página usa ese grosor hasta que usted lo cambie, razón por la que la polilínea sale a 3 puntos mientras las dos reglas se quedan en 1,5. Si nunca lo fija, rige el valor por defecto de PDF, 1.0 (ISO 32000-1 §8.4.3.2). Las formas que no aparecen tienen una causa más sosa: geometría depositada sin llamada de pintado después nunca se renderiza

Coordenadas y grosores son Single, así que un 72.25 y una línea fina de 0.5 puntos sobreviven hasta el archivo. El cero es el caso especial. La especificación define un grosor 0 como la línea más delgada que el dispositivo de salida pueda dibujar, un píxel en pantalla a cualquier zoom y 1/600 de pulgada en una impresora de 600 dpi, con lo que la misma regla se ve gruesa en un visor y casi desaparece en papel. Elija un grosor real como 0.25 o 0.5 para las líneas finas y la línea conserva su peso en todos los dispositivos

Modelo de path de HotPDF: MoveTo, LineTo, Rectangle y Circle construyen un path actual invisible en memoria, y solo el commit de Stroke, Fill o FillAndStroke lo pinta usando el estado gráfico persistente de color de trazo, color de relleno y ancho de línea antes de limpiar el búfer
La geometría se acumula silenciosamente en el trazado actual hasta que un operador de pintado la confirma con el color de trazo, el color de relleno y la anchura de línea almacenados — una llamada de pintado olvidada deja la forma sin dibujar

¿Por qué un triángulo cerrado tiene una muesca en una esquina?

Un triángulo dibujado con tres llamadas LineTo que vuelven al punto de partida parece cerrado, pero en PDF sigue siendo un subtrayecto abierto. La ISO 32000-1 §8.5.3.2 aplica la unión de líneas solo donde los segmentos se encuentran dentro de un subtrayecto; los dos extremos libres reciben terminaciones de línea. Con la terminación butt por defecto y un lápiz de 6 puntos, la esquina de inicio muestra un mordisco cuadrado donde las otras dos esquinas tienen ingletes limpios. ClosePath (el operador h) añade el último segmento y dice al renderizador que lo una con el primero

Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

// Parece cerrado, no lo es: las terminaciones butt se encuentran en (72, 500) y dejan una muesca
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;

// Subtrayecto cerrado: las tres esquinas reciben la misma unión en inglete
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke;              // o ClosePathStroke en una sola llamada

// Fill no necesita ClosePath: el relleno cierra cada subtrayecto implícitamente
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.MoveTo(392, 500);
Pdf.CurrentPage.LineTo(452, 420);
Pdf.CurrentPage.LineTo(512, 500);
Pdf.CurrentPage.Fill;

Cierre todo polígono que piense trazar con ClosePath o ClosePathStroke, aun cuando el último punto ya coincida con el primero. A 1 punto la muesca es invisible en pantalla; a 6 puntos se ve a zoom normal, y en la leyenda de una gráfica impresa se lee como un fallo de renderizado. Para las formas rellenas la pregunta no surge, y EoFill está ahí para cuando un trazado se cruza a sí mismo y usted quiere la regla even-odd en vez de la de winding distinto de cero

Rectángulos, círculos y el hábito del TCanvas

Las primitivas cerradas le ahorran la contabilidad del MoveTo. Rectangle(X, Y, Width, Height) toma la esquina superior izquierda y un tamaño, Circle(X, Y, Radius) toma un centro y un radio, y cualquiera de los dos se pinta con Fill, Stroke o FillAndStroke. El color de relleno y el de trazo son independientes: SetRGBFillColor y SetRGBStrokeColor toman cada uno un TColor, así que las constantes de Delphi y RGB(...) funcionan directamente, y SetRGBColor pone ambos a la vez

La forma de los argumentos de Rectangle es la trampa. TCanvas.Rectangle toma (Left, Top, Right, Bottom), y años de código de pintado VCL le entrenan los dedos a pasar una segunda esquina. Dele a HotPDF Rectangle(72, 202, 232, 292) y obtiene una caja de 232 puntos de ancho y 292 de alto en lugar de 160 por 90. Cuando los números que tiene son de verdad dos esquinas, llame a MFRectangle(X, Y, X1, Y1), que toma coordenadas de esquina y le calcula el tamaño; reserve Rectangle para código que piensa en tamaños

Un archivo PDF no tiene operador de círculo. Circle escribe cuatro segmentos Bezier cúbicos con el factor de punto de control de 0.5523 de siempre, cosa que usted puede ver si descomprime el content stream de la página: un m seguido de cuatro operadores c. Ellipse(X, Y, Width, Height) hace lo mismo dentro de una caja envolvente cuya esquina superior izquierda es (X, Y)

HotPDF expone además THotPDF.Canvas, un TCanvas estándar para la página actual, y el programa CanvasDraw de las demos gráficas llena una página Ledger con llamadas ordinarias a Canvas.Rectangle, Ellipse y RoundRect. Ese camino tiene sentido al portar código de pintado VCL existente. El código de dibujo nuevo va mejor sobre CurrentPage, porque cada llamada se mapea a un operador PDF en puntos y la salida es predecible hasta el número

Un cuadro de llamada en cuatro commits

Un callout usa todo lo de arriba a la vez: un panel relleno y con borde, una línea de puntero, un punto de anclaje y dos líneas de texto colocadas dentro del panel con las mismas coordenadas de origen superior izquierdo. FillAndStroke (el operador B) pinta el interior y el borde de un trazado en una sola llamada, así que el rectángulo se construye una vez en lugar de dos

Anatomía de un cuadro de llamada ensamblado con primitivas de HotPDF: un rectángulo confirmado con FillAndStroke, una línea de puntero trazada con MoveTo y LineTo, un punto de ancla relleno, y barras de etiqueta de TextOut que comparten la misma rejilla de origen arriba a la izquierda que las formas
Cuatro confirmaciones construyen el globo — FillAndStroke pinta panel y borde juntos, el puntero y el punto reutilizan el estado de trazo y relleno, y cada desplazamiento de etiqueta es aritmética simple contra la esquina de la caja (90, 122)
procedure DrawCallout(Page: THPDFPage);
begin
  // 1. Panel: relleno pálido y un borde más oscuro, un trazado, una confirmación
  Page.SetRGBFillColor(RGB(255, 244, 214));
  Page.SetRGBStrokeColor(RGB(180, 130, 40));
  Page.SetLineWidth(1);
  Page.Rectangle(90, 122, 240, 70);      // superior izquierda (90, 122), 240 x 70
  Page.FillAndStroke;

  // 2. Puntero desde el borde izquierdo del panel hacia abajo, hasta el objetivo
  Page.SetLineWidth(1.5);
  Page.MoveTo(90, 177);
  Page.LineTo(66, 246);
  Page.Stroke;

  // 3. Punto de anclaje al final del puntero, en el color del borde
  Page.SetRGBFillColor(RGB(180, 130, 40));
  Page.Circle(64, 250, 3);
  Page.Fill;

  // 4. Etiquetas: cada Y es la parte superior del texto, medida en la misma rejilla
  Page.SetFont('Arial', [fsBold], 10);
  Page.TextOut(102, 134, 0, 'Check this total');
  Page.SetFont('Arial', [], 9);
  Page.TextOut(102, 152, 0, 'Rounded to two decimals per line,');
  Page.TextOut(102, 164, 0, 'then summed');
end;

// Se llama entre BeginDoc y EndDoc:
//   DrawCallout(Pdf.CurrentPage);

Cada bloque pone su propio color de relleno, color de trazo y grosor justo antes de la forma que los necesita, incluso cuando el bloque anterior dejó el mismo valor en su sitio. Eso cuesta unos operadores redundantes en el archivo. A cambio, cualquier bloque se puede mover, borrar o convertir en su propio procedure sin heredar un relleno rojo perdido de lo que corriera antes, y para código de dibujo que cambia al ritmo de los informes, ese trato conviene tomarlo siempre

Los colores merecen la misma disciplina. Sembrar rellenos con Random($FFFFFF) vuelve animada una página de demo e imposible de comparar entre ejecuciones un informe generado, ya que dos builds con datos idénticos producen bytes distintos. Maneje el color desde los datos o desde un array de paleta fija, como hace la demo CanvasDraw con su tabla CanvasColors de ocho entradas, y la misma entrada produce el mismo archivo

Guiones, curvas y transparencia

SetDash es estado de página como el grosor de línea. SetDash([3, 3], 0) pone todo trazo posterior en tres puntos con tinta y tres sin ella, el segundo argumento desplaza dónde empieza el patrón, y NoDash vuelve al lápiz sólido. El array de guion es array of Byte, así que las longitudes son puntos enteros de 1 a 255, y se escriben en el content stream tal cual. Un guion puesto para un grupo de líneas de rejilla sigue activo para toda regla posterior, así que envuelva la sección punteada en GStateSave y GStateRestore: emiten q y Q, y restaurar el estado guardado recupera de un paso el guion, el grosor y los colores anteriores

Las curvas extienden el mismo trazado. CurveToC(X1, Y1, X2, Y2, X3, Y3) añade una Bezier cúbica desde el punto actual hasta (X3, Y3) con dos puntos de control, mientras que CurveToV y CurveToY cubren los casos en que el primer o el segundo punto de control coincide con un extremo. Un solo trazado puede mezclar segmentos LineTo y CurveToC antes de un Stroke o Fill, que es como se dibujan las esquinas redondeadas y las líneas de gráficas suavizadas

La transparencia vive en un estado de gráficos con nombre, porque PDF guarda el alfa en un diccionario ExtGState y no en el color. THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha) toma valores de 0 a 1 y devuelve un nombre; CurrentPage.SetGraphicsState(Name) lo aplica a todo lo pintado después. Registre cada opacidad una vez por documento y reutilice el nombre, así el archivo lleva un diccionario por opacidad sin importar cuántas formas lo usen. El alfa exige PDF 1.4, y HotPDF eleva la versión de salida a 1.4 cuando hace falta; con StrictVersionLock activo deja la versión en paz y devuelve un nombre vacío. Cuando PDFACompliance elige un perfil PDF/A-1, un alfa menor que 1 lanza una excepción, porque la ISO 19005-1 §6.4 prohíbe la transparencia ahí. Los perfiles PDF/A-2 y PDF/A-3 la aceptan

var
  Half: AnsiString;
begin
  // Regístrelo una vez por documento y reutilice el nombre devuelto en cualquier página
  Half := Pdf.RegisterExtGState(0.5, 1.0);   // relleno al 50%, trazo opaco

  Pdf.CurrentPage.GStateSave;                // q
  Pdf.CurrentPage.SetDash([3, 3], 0);
  Pdf.CurrentPage.SetLineWidth(0.5);
  Pdf.CurrentPage.SetRGBStrokeColor(clGray);
  Pdf.CurrentPage.MoveTo(72, 400);
  Pdf.CurrentPage.LineTo(540, 400);
  Pdf.CurrentPage.Stroke;

  Pdf.CurrentPage.SetGraphicsState(Half);
  Pdf.CurrentPage.SetRGBFillColor(clYellow);
  Pdf.CurrentPage.Rectangle(72, 380, 200, 16);  // resaltado sobre la fila de texto
  Pdf.CurrentPage.Fill;
  Pdf.CurrentPage.GStateRestore;             // Q: lápiz sólido y opacidad completa de nuevo
end;

Las llamadas vectoriales encajan con la geometría que su programa calcula: reglas de tabla, ejes y barras de gráficas, callouts, un logo construido con una docena de trazados. Una foto o captura de pantalla pertenece a AddImage y ShowImage; trazar píxeles con trazados solo engorda el archivo. Al revisar la salida, póngase al 1600% en las esquinas de los trazos gruesos y en cualquier línea punteada, porque las muescas de trazados sin cerrar y los guiones que se colaron más allá de su sección se ven clarísimos a ese aumento y pasan fácil desapercibidos al 100%

Todas las llamadas de este artículo viven en el objeto de página del HotPDF Delphi Component, que corre en proyectos VCL de Delphi y C++Builder y escribe el resultado como operadores de trazado PDF planos