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
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
¿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
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