Technical Article

HotPDF Canvas Drawing in Delphi: Vector Paths and Color

HotPDF draws vector graphics by building a path on the current page and then asking for it to be painted. There is no bitmap step in between. A line you draw with MoveTo and LineTo ends up as PDF path operators in the content stream, so it stays a true vector: crisp at 50% zoom, crisp at 1600%, and a fraction of the size a rasterized version would cost. For diagrams, table rules, chart axes, and form decorations, that is exactly what you want, and the API behind it is small enough to learn in one sitting.

The whole drawing surface lives on THotPDF.CurrentPage. Between BeginDoc and EndDoc you set color and line width on that page object, lay down geometry, and call a painting operator to commit it. The four primitives you will use most are MoveTo and LineTo for arbitrary paths, Rectangle for boxes, Circle for disks, and the two painting operators Stroke and Fill.

The coordinate system is bottom-left

This is the one thing that trips up everyone arriving from VCL. The TCanvas you paint controls with puts the origin at the top-left corner with Y growing downward. PDF does the opposite. HotPDF measures from the bottom-left corner of the page in points (1/72 inch), with Y increasing as you move up. A point at Y := 720 sits near the top of a US Letter page, which is 792 points tall, and Y := 50 sits near the bottom. If your first drawing comes out mirrored vertically, this is why: code ported from screen graphics assumes the wrong direction and runs off the bottom edge.

The same convention governs TextOut, so text and shapes share one mental model once you internalize it. Plan a layout by deciding where the bottom of each element sits, not the top, and the rest follows.

Paths: MoveTo, LineTo, Stroke

A stroked path is a pen lifted, placed, and dragged. MoveTo lifts the pen and sets the start point without marking anything. Each LineTo extends the current path to a new point. Nothing appears on the page until you call Stroke, which draws the accumulated path using the current stroke color and line width, then clears the path so the next MoveTo starts fresh.

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    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;

Two details save real debugging time. Line width is state, not an argument: SetLineWidth sets it once and every subsequent Stroke uses that value until you change it again, which is why the polyline above is thicker than the rule. And the path resets after each Stroke, so a forgotten Stroke means the geometry you so carefully laid out never renders at all. If a shape is missing from the output, the painting call is the first place to look.

The coordinates are points, and points are fractional. MoveTo and LineTo accept Single values, so a hairline at 0.5 points or a position at 72.25 is legal and meaningful, not rounded away to the nearest whole unit. That precision matters in two opposite directions. A line width below about 0.5 can render as a device-dependent thinnest-possible line that disappears on screen and reappears when printed, so a visible rule wants a width you set on purpose rather than the default. At the other end, snapping table rules and gridlines to whole-point coordinates keeps a dense grid from looking slightly uneven where adjacent lines round differently. Decide the grid spacing in points up front and the rest of the layout inherits it.

Filled shapes and color

Closed primitives can be filled instead of outlined. Rectangle takes a position and a size, Circle takes a center and a radius, and either one is committed with Fill, which paints the interior in the current fill color, or with Stroke for an outline only. Fill color and stroke color are separate pieces of state, set with SetRGBFillColor and SetRGBStrokeColor, both of which take a single TColor. That means you can reuse Delphi's color constants and the RGB helper directly.

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Watch the argument shape on Rectangle. It is position-plus-size, X, Y, Width, Height, not two opposite corners. The TCanvas.Rectangle that Delphi developers know takes (Left, Top, Right, Bottom), so muscle memory will hand HotPDF a second corner where it expects a width and a height, and the box comes out the wrong size. The (X, Y) pair is the lower-left corner, consistent with the page origin. For a circle, (X, Y) is the center and the third argument is the radius in points.

One color choice the original sample got wrong

An older version of this example seeded colors with Random($FFFFFF) on every shape. It looks lively, and it is the wrong instinct for generated documents. A PDF you build from code is usually something you also want to test, and random fill colors make the output impossible to compare run to run: a byte-for-byte diff against a known-good file fails every time, for no real reason. Pick explicit colors. When you want variety across a series of shapes, drive it from your data or a fixed palette array, so the same input always produces the same file. Determinism is worth more than novelty when the artifact moves through a release pipeline.

Where vector drawing pays off, and where it does not

Reach for these path and shape calls when the geometry is generated: chart gridlines and bars, the ruled lines of an invoice table, callout boxes on a diagram, a logo mark expressed as a handful of paths. All of it scales without blur and adds almost nothing to the file size, because a rectangle is a few numbers rather than thousands of pixels. The flip side is honest too. If what you actually have is a photograph or a screenshot, draw it as an image with AddImage and ShowImage instead; tracing a bitmap with vector calls buys you nothing. Complex curves are also out of scope here. The primitives above are straight segments, rectangles, and circles, which carry the large majority of real reporting work; anything needing freeform Bezier curves is a separate part of the API.

The remaining habit worth keeping is verification. Generated geometry can pass on your machine and fail on a customer's, usually over font substitution in any text you mix in or a page-size assumption that does not hold. Open the finished file at a few zoom levels to confirm the edges stay clean, and check that every shape lands inside the margin box you intended. With a deterministic color scheme, that check can be automated against a reference PDF rather than eyeballed.

The MoveTo, LineTo, Stroke, Fill, and color calls shown here are part of the HotPDF Component for Delphi and C++Builder.