A small, self-contained library for generating PDF files from Delphi XE8 (2015) or newer. No third-party dependencies; the library writes PDF 1.4 bytes directly.
uses
Vcl.Graphics, smPDF;
var
pdf: TsmPDF;
begin
pdf := TsmPDF.Create;
try
pdf.NewPage(psA4, poPortrait, 72);
pdf.Font.Name := 'Helvetica';
pdf.Font.Size := 24;
pdf.Font.Bold := True;
pdf.DrawText('Hello, PDF!', 50, 50);
pdf.Pen.Color := clBlue;
pdf.Pen.Width := 1.5;
pdf.DrawLine(50, 100, 545, 100);
pdf.Save('hello.pdf');
finally
pdf.Free;
end;
end;That's the whole shape of the API. Text, lines, boxes, ovals, polygons, embedded TrueType fonts, and images all sit behind similarly small calls.
- PDF 1.4 export, no PDF reading, no parsing, no editing
- Graphics: lines, boxes, ovals, polylines, polygons (with holes via even-odd fill, optional clip rectangle)
- Text: the 14 standard PDF fonts plus any installed Windows TrueType font
(
Calibri,Arial,Verdana, ...) embedded as full/FontFile2streams - Layout: per-line
DrawTextwith rect alignment; multi-lineDrawParagraphwith word wrapping and configurable line spacing; underlines - Text styling: outlined glyphs (
Font.StrokeStyle/StrokeColor), text-background fill (Brushacts as VCLTextRectbehind text), and rotation around the(X, Y)anchor (DrawText(s, X, Y, AAngle)— CCW degrees) - Measurement:
TextWidth,TextHeight,TextExtent,MeasureParagraphfor laying out around text before drawing it - Images: JPEG (passthrough as
DCTDecode), PNG (opaque + alpha via SMask), BMP (any otherTGraphic→ bitmap fallback) - Pen / brush: solid, dash, dot, dash-dot, none; line caps & joins; solid
and clear brushes; arbitrary RGB colour (
TColor) - Multi-page documents with mixed paper sizes, orientations, and DPIs
All six images below are rendered from PDFs the bundled demos export — no
post-processing, no third-party tools beyond mutool for the screenshot.
Standalone invoice — text alignment, paragraphs, table headers, ovals as a logo, custom RGB throughout, and a tiled |
A3 sales-territory map at 300 DPI — 237 polygons projected from a 74 MB GeoJSON via Mercator, even-odd holes for inland water, ~4 MB output. Source: |
Graphics primitives — pen styles (solid/dash/dot), line widths, filled and outlined boxes/ovals, polygons-with-holes, polygon clipped to a rectangle, open polylines. From the showcase. |
Embedded TrueType — Arial / Calibri / Verdana embedded as full |
Embedded images — TBitmap (FlateDecode RGB), PNG (opaque + alpha via SMask), JPEG (DCTDecode passthrough). Same TPicture drawn twice de-dupes to a single XObject. |
Rotated text — |
Regenerate the gallery from the current demo PDFs with
images/regenerate.ps1
(needs mutool — winget install ArtifexSoftware.mutool).
Everything you need is in uses smPDF:
| Type / member | Purpose |
|---|---|
TsmPDF |
The document. Create, NewPage, Save, all the Draw* methods, Pen, Brush, Font properties |
TPDFFont |
Name, Size, Color, Bold, Italics, Underline, plus StrokeColor / StrokeStyle for outlined text (ssNone / ssThin / ssMedium / ssThick) |
TPDFPen |
Color, Width, Style (penSolid/Dash/Dot/DashDot/None), LineCap, LineJoin |
TPDFBrush |
Color, Style (brushSolid / brushClear). When brushSolid, also acts as a text-background fill behind DrawText / DrawParagraph (matches VCL TCanvas). |
TPDFOrientation |
poPortrait, poLandscape |
TPDFPaperSize |
psLetter, psLegal, psA2, psA3, psA4, psA5, psCustom (custom requires explicit width + height in pixels on NewPage) |
TPDFTextPadding |
tpNone, tpTight, tpSingle, tpDouble (line spacing for DrawParagraph) |
TPDFStrokeStyle |
ssNone, ssThin, ssMedium, ssThick — outline width for Font.StrokeStyle |
TPDFLineCap |
lcButt, lcRound, lcSquare — Pen.LineCap end style |
TPDFLineJoin |
ljMiter, ljRound, ljBevel — Pen.LineJoin corner style |
TPDFPointList |
TList<TPoint> — used by DrawMultiLine and DrawPolygon |
EPDFError |
Single exception class for everything the library raises |
Document lifecycle:
constructor Create;
procedure NewPage(const APaperSize: TPDFPaperSize; const AOrientation: TPDFOrientation;
const ADPI: Integer = 300; const APaperColor: TColor = clWhite;
const AWidth: Integer = 0; const AHeight: Integer = 0); overload;
procedure NewPage; overload; // A4 portrait, 300 dpi, white paper
procedure Save(const AFileName: string); overload;
procedure Save(const AFileName: string; const AEmbedFonts: Boolean); overload;
function PageCount: Integer;Drawing:
procedure DrawText(const AText: string; X, Y: Integer; AAngle: Double = 0); overload;
procedure DrawText(const AText: string; ARect: TRect); overload;
procedure DrawText(const AText: string; ARect: TRect; AAlignment: TAlignment); overload;
procedure DrawParagraph(const AText: string; ARect: TRect; AAlignment: TAlignment;
APadding: TPDFTextPadding);
procedure DrawLine(const x1, y1, x2, y2: Integer);
procedure DrawBox(const x1, y1, x2, y2: Integer);
procedure DrawOval(const x1, y1, x2, y2: Integer);
procedure DrawMultiLine(const APointList: TPDFPointList);
procedure DrawPolygon(const APointList: TPDFPointList); overload;
procedure DrawPolygon(const APointList: TPDFPointList; AClipRect: TRect); overload;
procedure DrawPicture(const APicture: TPicture; x1, y1: Integer); overload;
procedure DrawPicture(const APicture: TPicture; ARect: TRect;
AAlignment: TAlignment = taLeftJustify;
AStretch: Boolean = False); overload;Measurement (in pixels at the current page DPI; all require NewPage first):
function TextWidth (const AText: string): Integer;
function TextHeight(const AText: string): Integer; // current font's line-height
function TextExtent(const AText: string): TSize; // both at once
function MeasureParagraph(const AText: string; AMaxWidthPx: Integer;
APadding: TPDFTextPadding = tpSingle): TSize;MeasureParagraph runs the same word-wrap path as DrawParagraph, so what you
measure is what you draw — useful for sizing a rect before laying it out.
State properties:
| Property | Access | Purpose |
|---|---|---|
Font, Pen, Brush |
r/w | current drawing state (see types above) |
Width, Height |
read-only | current page size in pixels |
Size, Orientation, DPI |
read-only | current page paper size, orientation, DPI |
PaperColor |
read-only | current page's APaperColor from the most recent NewPage call (clWhite by default) |
CompressStreams |
r/w | default True. Set False to emit uncompressed content streams for byte-level inspection of the PDF |
- Units: pixels at the page's configured DPI (set on
NewPage, default 300). The library converts to PDF points internally. - Origin: top-left, Y grows downward — same as
TCanvas,TPaintBox,TPrinter.Canvas. The Y-flip to PDF's bottom-left coordinate space happens on emit. - Font sizes: in points (per Delphi convention for
TFont.Size). - Pen widths: in points (line widths are rarely DPI-relative in practice).
DrawPolygon walks the point list as a single closed polygon. When a point
equals the start of the current subpath, that subpath closes and the next
point begins a new subpath. Nested subpaths automatically become holes via
PDF's even-odd fill rule.
// Square with a square hole
pts := TPDFPointList.Create;
try
pts.Add(TPoint.Create(60, 430)); // outer subpath
pts.Add(TPoint.Create(260, 430));
pts.Add(TPoint.Create(260, 630));
pts.Add(TPoint.Create(60, 630));
pts.Add(TPoint.Create(60, 430)); // closes outer
pts.Add(TPoint.Create(120, 490)); // starts inner (becomes hole)
pts.Add(TPoint.Create(200, 490));
pts.Add(TPoint.Create(200, 570));
pts.Add(TPoint.Create(120, 570));
pts.Add(TPoint.Create(120, 490)); // closes inner
pdf.DrawPolygon(pts);
finally
pts.Free;
end;The (points, clipRect) overload installs clipRect as a PDF clip path before
the polygon, so geometry outside the rectangle is hidden:
pdf.DrawPolygon(starPoints, TRect.Create(360, 460, 520, 600));Font.StrokeStyle outlines the glyphs (PDF text-rendering mode 2 — fill + stroke).
Default is ssNone so existing call sites are unaffected:
pdf.Font.Color := clRed;
pdf.Font.StrokeColor := clBlack;
pdf.Font.StrokeStyle := ssMedium; // ssThin / ssMedium / ssThick
pdf.Font.Size := 24;
pdf.DrawText('Outlined', 100, 100); // red fill, black outlineBrush mirrors VCL TCanvas: when Brush.Style = brushSolid, DrawText and
DrawParagraph paint a Brush.Color rectangle behind the text (the text-cell
size for the X,Y overload, the supplied rect for the rect overloads). Default
is brushClear — no background. Pen is not consulted for text:
pdf.Brush.Style := brushSolid;
pdf.Brush.Color := clYellow;
pdf.DrawText('Highlighted', 100, 200); // yellow rectangle behind black textFont.Underline adds a horizontal stroke ~0.12em below the baseline using
Font.Color and a thickness scaled to the font size.
The X,Y overload of DrawText takes an optional AAngle (CCW degrees, matching
TFont.Orientation). The text rotates around the supplied (X, Y) anchor; the
Brush background, the underline, and the StrokeStyle outline all rotate with
the text:
pdf.DrawText('REVENUE', 100, 30, -90); // vertical column header (reads downward)
pdf.DrawText('Volume IV', 270, 170, 90); // book-spine label (reads upward)
pdf.DrawText('Quarterly', 420, 60, 5); // slight tilt sub-headline
pdf.DrawText('DRAFT', 100, 740, -20); // diagonal watermarkWhen AAngle is omitted (or zero), the output is byte-identical to pre-feature
PDFs — no cm operator is emitted.
A repeating diagonal watermark is the angled-text feature plus a two-deep loop over a regular grid of anchor points. Drawing it after the document content puts the watermark on top (the classic stamp look); drawing it first puts it underneath. The Invoice demo bundles a "PAID" watermark you can lift wholesale:
const
STEP = 180; // px between watermark anchors on both axes
PAGE_W = 595;
PAGE_H = 842; // A4 portrait at 72 dpi
var
x, y: Integer;
begin
pdf.Font.Name := 'Helvetica';
pdf.Font.Bold := True;
pdf.Font.Size := 60;
pdf.Font.Color := $00D0D0D0; // light grey
pdf.Font.StrokeStyle := ssNone;
pdf.Brush.Style := brushClear; // no background fill behind glyphs
// Range starts past the page edges so the rotated bounding boxes still
// cover the corners; anything outside the MediaBox is clipped by the viewer.
y := -STEP;
while y < PAGE_H + STEP do
begin
x := -STEP;
while x < PAGE_W + STEP do
begin
pdf.DrawText('PAID', x, y, 45); // 4-arg overload: rotate around (x, y)
Inc(x, STEP);
end;
Inc(y, STEP);
end;
end;PDF 1.4 has no transparency (no gs extended graphics state in this library),
so "soft" watermarks are achieved by picking a light grey colour rather than
true alpha blending. The DPI of the page determines pixel/point scaling, so
adjust STEP (and the font Size) proportionally for higher-DPI pages.
See the rendered result in the gallery above (the Invoice tile).
If Font.Name matches one of the 14 built-in PDF fonts (Helvetica, Times,
Courier, Symbol, ZapfDingbats, plus aliases like Sans/Serif/Mono)
the library emits a Type1 font dict and uses its bundled metrics.
Anything else is looked up in HKLM\…\Fonts, the matching .ttf file is
loaded, and the full font is embedded as a /FontFile2 stream:
pdf.Font.Name := 'Calibri';
pdf.Font.Bold := True;
pdf.DrawText('Calibri Bold', 50, 100); // calibrib.ttf embedded automaticallyEach (family, bold, italic) combination resolves to a separate physical font
file, so all four Arial variants used in one document embed four TTFs.
WinAnsi (codepage 1252) only. Strings passed to the library are converted to
single-byte WinAnsi via TEncoding.GetEncoding(1252); characters outside that
range become ?. CJK, Cyrillic, Greek, Vietnamese, and emoji are out of scope
(they would require CID fonts and /Identity-H encoding).
When you put non-ASCII characters into a Delphi source file, save the file with
a UTF-8 BOM or use Pascal escapes — #$2014 for an em-dash, #$00C9 for
É, etc. — to avoid encoding mishaps in the compiler.
- PDF reading / parsing / form filling / digital signatures / annotations / encryption — this library exports only.
- Tagged PDF / PDF/UA / PDF/A — no accessibility tagging, no archival profile.
- Full Unicode — see Encoding above.
- TTF subsetting — Phase 4 embeds the full font file. Subsetting (down to ~50 KB per face) is on the wish list but not implemented.
- Justified text / kerning / vertical text — text is left/centre/right aligned only.
- CMYK images, 16-bit PNG, palette-PNG transparency — 8-bit RGB / RGBA only.
PDF/
Source/
smPDF.pas public API (TsmPDF, TPDFFont, TPDFPen, TPDFBrush, EPDFError, enums)
smPDF.Types.pas internal records
smPDF.Geometry.pas pixel/DPI ↔ point conversion, Y-flip, number formatting
smPDF.Writer.pas low-level PDF byte writer (objects, dicts, arrays, xref, streams)
smPDF.Page.pas TPDFPage: content stream, graphics state, font + image registries
smPDF.Fonts.pas Standard 14 metrics, WinAnsi encoding
smPDF.TTF.pas TrueType font parser (head/hhea/hmtx/cmap/OS/2/post/name)
smPDF.WinFonts.pas Windows registry font lookup
smPDF.Images.pas JPEG/PNG/BMP extraction + Flate compression
Tests/
smPDF_Tests.dpr console test runner (272 tests, dcc32 build, no DUnitX)
Source/ test framework + 12 test units
Demos/
Showcase/ smPDF_ExportDemo.dpr — 7-page demo exercising every feature
Invoice/ smPDF_InvoiceDemo.dpr — standalone single-page invoice (Standard 14 only)
Map/ smPDF_MapDemo.dpr — A3 landscape sales-territory map at 300 DPI
Simple/ Simple_Demo.dpr — minimal VCL form demonstrating Brush/Pen/Font
images/ — README gallery PNGs + regenerate.ps1
build.ps1, test.ps1, CLAUDE.md, README.md, LICENSE
MIT — see LICENSE. Copyright (c) 2026 Steve Maughan.





