Una línea de pie de página tomada de un ejemplo de flujo de contenido PDF dice 72 72 m 540 72 l S: mover a (72, 72), dibujar una línea hasta (540, 72), trazarla. En PDF puro 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 línea cae una pulgada por debajo del borde superior, justo a través del membrete. La línea en sí está bien; lo único que cambió fue el marco de referencia, y pasar de un marco al otro en una página de 792 puntos es una sola 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 y Y hacia abajo, el mismo marco que usa el TCanvas de VCL. El espacio de usuario de PDF, definido en ISO 32000-1 §8.3.2.3, arranca en la esquina inferior izquierda con Y creciendo hacia arriba, y HotPDF convierte cada Y que usted pasa al escribir el flujo de contenido. Así, Y := 72 sobre THotPDF.CurrentPage significa una pulgada por debajo del borde superior, sin importar el tamaño de la página
El tamaño de la página importa en el otro extremo. En una página US Letter, de 612 por 792 puntos, Y := 720 queda una pulgada por encima del borde inferior. Una página nueva de HotPDF es A4 salvo que usted indique otra cosa, y A4 mide 842 puntos de alto, así que el mismo Y := 720 deja 122 puntos, cerca de 1.7 pulgadas, por debajo. Los pies de página posicionados restando a 792 se desplazan hacia arriba en A4 por exactamente esa razón. Lea la altura desde CurrentPage.Height en lugar de fijarla en el código, o establezca CurrentPage.Size explícitamente justo después de BeginDoc
La unidad es el punto, 1/72 de pulgada, porque THotPDF.Resolution vale 72 por omisión. Subir Resolution hace que las coordenadas y los tamaños se midan en unidades de 1/Resolution de pulgada. Para trabajo vectorial déjelo en 72: el ancho de línea, la longitud de los guiones y las coordenadas quedan así en la misma unidad que guarda el archivo PDF, y un valor que usted ve en el flujo de contenido coincide con un valor de su código fuente
El texto sigue el mismo modelo. Por omisión, 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 cuadrícula. Unas cuantas llamadas operan sobre el archivo PDF y no sobre la superficie de dibujo, y usan coordenadas PDF de abajo hacia arriba: los métodos que fijan la caja de página como SetCropBox(Left, Bottom, Right, Top), y la familia StampLoadedPageText / StampLoadedPageImage que estampa un documento ya cargado. Mezclarlas con llamadas de rutas es de donde sale la mayoría de los diseños volteados, así que manténgalas en rutinas separadas
Construir una ruta y pintarla
Una ruta se construye primero y se pinta después. MoveTo fija un punto de partida sin marcar nada, cada LineTo agrega 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 ancho de línea corrientes. En términos de PDF, Stroke emite el operador S, y todo operador de pintado cierra la ruta actual, así que el siguiente MoveTo arranca 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 omisión es A4
// El ancho y el color son estado de página: se fijan una vez y los usa cada Stroke posterior
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// Una línea una pulgada por debajo del borde superior, márgenes de 72 pt a ambos lados
Pdf.CurrentPage.MoveTo(72, 72);
Pdf.CurrentPage.LineTo(540, 72);
Pdf.CurrentPage.Stroke;
// La línea de pie de página 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 una sola ruta, más gruesa 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 ancho de línea es estado y no un argumento. SetLineWidth escribe un operador w y cada trazo posterior en la página usa ese ancho hasta que usted lo cambie; por eso la polilínea sale a 3 puntos mientras las dos líneas se quedan en 1.5. Si nunca lo establece, rige el valor por omisión de PDF, 1.0 (ISO 32000-1 §8.4.3.2). Las formas que no aparecen tienen una causa más prosaica: la geometría depositada sin una llamada de pintado después jamás se dibuja
Las coordenadas y los anchos son Single, así que 72.25 y un trazo finísimo de 0.5 puntos llegan intactos al archivo. El cero es el caso especial. La especificación define un ancho de 0 como la línea más delgada que el dispositivo de salida puede dibujar: un píxel en pantalla a cualquier zoom y 1/600 de pulgada en una impresora de 600 dpi, lo que hace que la misma línea se vea gruesa en un visor y casi desaparezca en papel. Elija un ancho real como 0.25 o 0.5 para los trazos finos 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 regresan al punto de partida se ve cerrado, pero en PDF sigue siendo una subruta abierta. ISO 32000-1 §8.5.3.2 aplica la unión de líneas solo donde los segmentos se encuentran dentro de una subruta; los dos extremos libres reciben remates de línea. Con el remate plano por omisión y una pluma de 6 puntos, la esquina de partida muestra una mordida cuadrada donde las otras dos esquinas tienen ingletes limpios. ClosePath (el operador h) agrega el último segmento y le indica al renderizador que lo una con el primero
Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// Parece cerrado y no lo es: los remates planos 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;
// Subruta cerrada: 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
// El relleno no necesita ClosePath: rellenar cierra cada subruta 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 con ClosePath o ClosePathStroke todo polígono que vaya a trazar, aun cuando el último punto ya coincida con el primero. Con 1 punto la muesca es invisible en pantalla; con 6 puntos se nota al zoom normal, y en la leyenda de una gráfica impresa parece un error de renderizado. Para formas rellenas la pregunta no se plantea, y EoFill está ahí para cuando una ruta se cruza a sí misma y usted prefiere la regla par-impar en lugar del devanado distinto de cero
Rectángulos, círculos y el hábito del TCanvas
Las primitivas cerradas ahorran la contabilidad de 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 las dos se pinta con Fill, Stroke o FillAndStroke. El color de relleno y el color de trazo son independientes: SetRGBFillColor y SetRGBStrokeColor reciben cada una un TColor, así que las constantes de Delphi y RGB(...) funcionan directamente, y SetRGBColor fija ambos de una 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 en VCL entrenan los dedos a pasar una segunda esquina. Pásele Rectangle(72, 202, 232, 292) a HotPDF y obtiene una caja de 232 puntos de ancho por 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 recibe coordenadas de esquinas 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 de Bézier cúbica con el factor de punto de control de siempre, 0.5523, algo que puede comprobar si descomprime el flujo de contenido de la página: un m seguido de cuatro operadores c. Ellipse(X, Y, Width, Height) hace lo mismo dentro de una caja contenedora cuya esquina superior izquierda es (X, Y)
HotPDF también expone THotPDF.Canvas, un TCanvas estándar para la página actual, y el programa CanvasDraw de las demos Graphic llena una página Ledger con llamadas corrientes de Canvas.Rectangle, Ellipse y RoundRect. Ese camino tiene sentido al portar código de pintado VCL existente. El código de dibujo nuevo sale ganando con CurrentPage, porque cada llamada se corresponde con un operador PDF en puntos y la salida es predecible hasta el último número
Una caja de llamada en cuatro confirmaciones
Una caja de llamada usa todo lo anterior a la vez: un panel rellenado y delineado, una línea puntero, un punto de anclaje y dos líneas de texto colocadas dentro del panel con las mismas coordenadas superiores izquierdas. FillAndStroke (el operador B) pinta el interior y el borde de una ruta 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, una ruta, 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 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, con 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 sobre la misma cuadrícula
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 invoca entre BeginDoc y EndDoc:
// DrawCallout(Pdf.CurrentPage);
Cada bloque establece su propio color de relleno, color de trazo y ancho justo antes de la forma que los necesita, aun cuando el bloque anterior haya dejado el mismo valor en su lugar. Eso cuesta unos cuantos operadores redundantes en el archivo. A cambio, cualquier bloque se puede mover, eliminar o convertir en su propio procedimiento sin heredar un relleno rojo perdido de lo que haya corrido antes, y para código de dibujo que cambia al ritmo de los reportes, ese trueque vale la pena todas las veces
Los colores merecen la misma disciplina. Sembrar los rellenos con Random($FFFFFF) hace una página de demo animada y un reporte generado imposible de comparar entre corridas, porque dos builds con datos idénticos producen bytes distintos. Derive el color de los datos o de un arreglo 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, igual que el ancho de línea. SetDash([3, 3], 0) vuelve cada trazo posterior tres puntos encendidos y tres apagados, con el segundo argumento desplazando dónde arranca el patrón, y NoDash regresa a una pluma sólida. El arreglo de guiones es array of Byte, así que las longitudes son puntos enteros de 1 a 255, y se escriben al flujo de contenido tal cual se dan. Un patrón de guiones fijado para un grupo de líneas de cuadrícula sigue activo en cada línea posterior, así que envuelva la sección discontinua en GStateSave y GStateRestore: esos métodos emiten q y Q, y restaurar el estado guardado recupera el guion, el ancho y los colores anteriores en un solo paso
Las curvas extienden la misma ruta. CurveToC(X1, Y1, X2, Y2, X3, Y3) agrega una Bézier 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. Una sola ruta 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 gráfico con nombre, porque PDF guarda el alfa en un diccionario ExtGState y no en el color. THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha) recibe valores de 0 a 1 y devuelve un nombre; CurrentPage.SetGraphicsState(Name) lo aplica a todo lo que se pinte 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 la usen. El alfa requiere PDF 1.4, y HotPDF sube la versión de salida a 1.4 cuando hace falta; con StrictVersionLock activado deja la versión intacta y devuelve un nombre vacío. Cuando PDFACompliance selecciona un perfil PDF/A-1, un alfa menor que 1 lanza una excepción, porque 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
// Registre 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: pluma sólida, opacidad completa otra vez
end;
Las llamadas vectoriales encajan con la geometría que su programa calcula: líneas de tablas, ejes y barras de gráficas, cajas de llamada, un logotipo armado con una docena de rutas. Una foto o captura de pantalla pertenece a AddImage y ShowImage; perseguir píxeles con rutas solo engorda el archivo. Al revisar la salida, haga zoom al 1600% en las esquinas de los trazos gruesos y en cualquier línea discontinua, porque las muescas de rutas sin cerrar y los guiones que se escaparon de su sección son evidentes con ese aumento y fáciles de pasar por alto al 100%
Cada llamada de este artículo vive en el objeto de página del HotPDF Delphi Component, que funciona en proyectos VCL de Delphi y C++Builder y escribe el resultado como operadores de ruta PDF corrientes