Technical Article

HotPDF Canvas Drawing in Delphi: Vector Paths and Color

A footer rule copied from a PDF content-stream example reads 72 72 m 540 72 l S: move to (72, 72), draw a line to (540, 72), stroke it. In raw PDF that line sits one inch above the bottom edge of a US Letter page. Feed the same four numbers to a drawing API that measures from the top-left corner and the rule lands one inch below the top, straight through the letterhead. The line itself is fine. Only the frame of reference changed, and moving between the two frames on a 792-point page is a single subtraction: 792 minus Y

Where is Y = 0 on a HotPDF page?

HotPDF page drawing calls put the origin at the top-left corner of the page, with X growing to the right and Y growing downward, the same frame the VCL TCanvas uses. PDF user space, defined in ISO 32000-1 §8.3.2.3, starts at the bottom-left with Y growing upward, and HotPDF converts every Y you pass when it writes the content stream. So Y := 72 on THotPDF.CurrentPage means one inch below the top edge, whatever the page size

The page size matters for the other end. On a US Letter page, 612 by 792 points, Y := 720 is one inch above the bottom. A new HotPDF page is A4 unless you say otherwise, and A4 is 842 points tall, so the same Y := 720 leaves 122 points, roughly 1.7 inches, below it. Footers positioned by subtracting from 792 drift upward on A4 for exactly that reason. Read the height from CurrentPage.Height instead of hard-coding it, or set CurrentPage.Size explicitly right after BeginDoc

Comparison of the VCL TCanvas and the HotPDF page drawing API, both measuring from the top-left corner with Y growing downward, while HotPDF writes the same point near the top of a US Letter page as y = 720 in bottom-up PDF user space
HotPDF draws in the same top-left frame as TCanvas, so a spot one inch below the top of the 612-by-792 page is Y = 72 in both, and HotPDF writes it to the file as PDF user-space y = 720

The unit is the point, 1/72 inch, because THotPDF.Resolution defaults to 72. Raising Resolution makes coordinates and sizes count in 1/Resolution inch instead. For vector work, leave it at 72: line widths, dash lengths and coordinates then all read in the same unit the PDF file stores, and a value you see in the content stream matches a value in your source

Text follows the same model. By default the Y you give TextOut is the top edge of the text, so a label and the box around it are planned against one grid. A few calls work on the PDF file rather than on the drawing surface and use bottom-up PDF coordinates: the page-box setters such as SetCropBox(Left, Bottom, Right, Top), and the StampLoadedPageText / StampLoadedPageImage family that stamps an already loaded document. Mixing those with path calls is where most flipped layouts come from, so keep them in separate routines

Building a path and painting it

A path is built first and painted second. MoveTo sets a start point without marking anything, each LineTo adds a straight segment, and nothing reaches the page until a painting call such as Stroke uses the current stroke color and line width. In PDF terms, Stroke emits the S operator, and every painting operator ends the current path, so the next MoveTo starts clean

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

    // Width and color are page state: set once, used by every later Stroke
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A rule one inch below the top, 72 pt margins on both sides
    Pdf.CurrentPage.MoveTo(72, 72);
    Pdf.CurrentPage.LineTo(540, 72);
    Pdf.CurrentPage.Stroke;

    // The footer rule from the opening: one inch above the bottom
    Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.Stroke;

    // Three connected segments in one path, thicker and blue
    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;

Line width is state rather than an argument. SetLineWidth writes a w operator and every later stroke on the page uses that width until you change it, which is why the polyline comes out at 3 points while the two rules stay at 1.5. If you never set it, the PDF default of 1.0 applies (ISO 32000-1 §8.4.3.2). Missing shapes have a duller cause: geometry laid down without a painting call afterward never renders

Coordinates and widths are Single, so 72.25 and a 0.5-point hairline both survive into the file. Zero is the special case. The specification defines a width of 0 as the thinnest line the output device can draw, one pixel on a screen at any zoom and 1/600 inch on a 600 dpi printer, which makes the same rule look bold in a viewer and almost vanish on paper. Pick a real width such as 0.25 or 0.5 for hairlines and the line keeps its weight on every device

HotPDF path model: MoveTo, LineTo, Rectangle, and Circle build an invisible current path in memory, and only the Stroke, Fill, or FillAndStroke commit paints it using the persistent graphics state of stroke color, fill color, and line width before clearing the buffer
Geometry accumulates silently in the current path until a painting operator commits it with the stored stroke color, fill color, and line width — a forgotten paint call leaves the shape undrawn

Why does a closed triangle have a notch in one corner?

A triangle drawn as three LineTo calls that return to the start point looks closed, but in PDF it is still an open subpath. ISO 32000-1 §8.5.3.2 applies the line join only where segments meet inside a subpath; the two free ends get line caps instead. With the default butt cap and a 6-point pen, the start corner shows a square bite where the other two corners have clean miters. ClosePath (the h operator) appends the last segment and tells the renderer to join it to the first one

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

// Looks closed, is not: butt caps meet at (72, 500) and leave a notch
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;

// Closed subpath: all three corners get the same miter join
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke;              // or ClosePathStroke in one call

// Fill needs no ClosePath: filling closes every subpath implicitly
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;

Close every polygon you intend to stroke with ClosePath or ClosePathStroke, even when the last point already matches the first. At 1 point the notch is invisible on screen; at 6 points it shows at normal zoom, and in a printed chart legend it reads as a rendering glitch. For filled shapes the question does not arise, and EoFill is there when a path crosses itself and you want the even-odd rule instead of nonzero winding

Rectangles, circles and the TCanvas habit

Closed primitives save the MoveTo bookkeeping. Rectangle(X, Y, Width, Height) takes the top-left corner and a size, Circle(X, Y, Radius) takes a center and a radius, and either one is painted with Fill, Stroke or FillAndStroke. Fill color and stroke color are independent: SetRGBFillColor and SetRGBStrokeColor each take a TColor, so Delphi constants and RGB(...) work directly, and SetRGBColor sets both at once

The argument shape on Rectangle is the trap. TCanvas.Rectangle takes (Left, Top, Right, Bottom), and years of VCL paint code train your fingers to pass a second corner. Hand Rectangle(72, 202, 232, 292) to HotPDF and you get a box 232 points wide and 292 tall instead of 160 by 90. When the numbers you have really are two corners, call MFRectangle(X, Y, X1, Y1), which takes corner coordinates and computes the size for you; reserve Rectangle for code that thinks in sizes

A PDF file has no circle operator. Circle writes four cubic Bezier segments with the usual 0.5523 control-point factor, which you can see if you decompress the page content stream: one m followed by four c operators. Ellipse(X, Y, Width, Height) does the same inside a bounding box whose top-left corner is (X, Y)

HotPDF also exposes THotPDF.Canvas, a standard TCanvas for the current page, and the CanvasDraw program in the Graphic demos fills a Ledger page with ordinary Canvas.Rectangle, Ellipse and RoundRect calls. That route makes sense when porting existing VCL paint code. New drawing code is better off on CurrentPage, because each call maps to one PDF operator in points and the output is predictable down to the number

A callout box from four commits

A callout uses everything above at once: a filled and outlined panel, a pointer line, an anchor dot and two lines of text placed inside the panel with the same top-left coordinates. FillAndStroke (the B operator) paints the interior and the rim of one path in a single call, so the rectangle is built once instead of twice

Anatomy of a callout box assembled from HotPDF primitives: a rectangle committed with FillAndStroke, a stroked pointer line from MoveTo and LineTo, a filled anchor dot, and TextOut label bars that share the same top-left grid as the shapes
Four commits build the callout — FillAndStroke paints panel and rim together, the pointer and dot reuse stroke and fill state, and every label offset is plain arithmetic against the box corner (90, 122)
procedure DrawCallout(Page: THPDFPage);
begin
  // 1. Panel: pale fill and a darker rim, one path, one commit
  Page.SetRGBFillColor(RGB(255, 244, 214));
  Page.SetRGBStrokeColor(RGB(180, 130, 40));
  Page.SetLineWidth(1);
  Page.Rectangle(90, 122, 240, 70);      // top-left (90, 122), 240 x 70
  Page.FillAndStroke;

  // 2. Pointer from the left edge of the panel down to the target
  Page.SetLineWidth(1.5);
  Page.MoveTo(90, 177);
  Page.LineTo(66, 246);
  Page.Stroke;

  // 3. Anchor dot at the end of the pointer, in the rim color
  Page.SetRGBFillColor(RGB(180, 130, 40));
  Page.Circle(64, 250, 3);
  Page.Fill;

  // 4. Labels: each Y is the top of the text, measured on the same grid
  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;

// Called between BeginDoc and EndDoc:
//   DrawCallout(Pdf.CurrentPage);

Each block sets its own fill color, stroke color and width right before the shape that needs them, even when the previous block left the same value in place. That costs a few redundant operators in the file. In exchange, any block can move, be deleted or become its own procedure without inheriting a stray red fill from whatever ran before it, and for drawing code that changes as reports change, that trade is worth taking every time

Colors deserve the same discipline. Seeding fills with Random($FFFFFF) makes a demo page lively and a generated report impossible to compare between runs, since two builds from identical data produce different bytes. Drive color from the data or from a fixed palette array, as the CanvasDraw demo does with its eight-entry CanvasColors table, and the same input yields the same file

Dashes, curves and transparency

SetDash is page state like line width. SetDash([3, 3], 0) makes every later stroke three points on and three off, with the second argument shifting where the pattern starts, and NoDash returns to a solid pen. The dash array is array of Byte, so lengths are whole points from 1 to 255, and they are written into the content stream exactly as given. A dash set for one group of gridlines stays on for every rule after it, so wrap the dashed section in GStateSave and GStateRestore: those emit q and Q, and restoring the saved state brings back the previous dash, width and colors in one step

Curves extend the same path. CurveToC(X1, Y1, X2, Y2, X3, Y3) adds a cubic Bezier from the current point to (X3, Y3) with two control points, while CurveToV and CurveToY cover the cases where the first or second control point coincides with an endpoint. A single path can mix LineTo and CurveToC segments before one Stroke or Fill, which is how rounded corners and smoothed chart lines are drawn

Transparency lives in a named graphics state, because PDF stores alpha in an ExtGState dictionary rather than in the color. THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha) takes values from 0 to 1 and returns a name; CurrentPage.SetGraphicsState(Name) applies it to everything painted afterward. Register each opacity once per document and reuse the name, so the file carries one dictionary per opacity no matter how many shapes use it. Alpha requires PDF 1.4, and HotPDF raises the output version to 1.4 when needed; with StrictVersionLock set it leaves the version alone and returns an empty name. When PDFACompliance selects a PDF/A-1 profile, alpha below 1 raises an exception, since ISO 19005-1 §6.4 forbids transparency there. The PDF/A-2 and PDF/A-3 profiles accept it

var
  Half: AnsiString;
begin
  // Register once per document, reuse the returned name on any page
  Half := Pdf.RegisterExtGState(0.5, 1.0);   // 50% fill, opaque stroke

  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);  // highlight over the text row
  Pdf.CurrentPage.Fill;
  Pdf.CurrentPage.GStateRestore;             // Q: solid pen, full opacity again
end;

Vector calls fit geometry your program computes: table rules, chart axes and bars, callouts, a logo built from a dozen paths. A photo or screenshot belongs in AddImage and ShowImage; tracing pixels with paths only makes the file bigger. When checking output, zoom to 1600% on the corners of thick strokes and on any dashed line, since notches from unclosed paths and dashes that leaked past their section are both obvious at that magnification and easy to miss at 100%

Every call in this article lives on the page object of the HotPDF Delphi Component, which runs in Delphi and C++Builder VCL projects and writes the result as plain PDF path operators