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