Catalog · How the catalog works
smPDF
Simple, lightweight PDF writer for Delphi VCL
- ★ 26 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
smPDF — pure-Pascal PDF export library for Delphi
A self-contained library for generating PDF files from Delphi 10.1 Berlin or newer, on Win32 and Win64. No third-party dependencies; the library writes PDF 1.4 bytes directly.
At a glance
uses
Vcl.Graphics, smPDF;
var
pdf: TsmPDF;
begin
pdf := TsmPDF.Create;
try
pdf.NewPage(psA4, poPortrait, 72);
pdf.Font.Name := 'Arial';
pdf.Font.Size := 24;
pdf.Font.Bold := True;
pdf.DrawText('Hello, PDF! Łódź, Győr, Hà Nội', 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;What it does
- PDF 1.4 export: no reading, parsing or editing
- Graphics: lines, boxes, rounded rectangles, ovals, polylines, polygons and poly-polygons with holes (even-odd or non-zero fill), general Bézier paths, a clip-rectangle stack
- Text: the 14 standard PDF fonts, plus any installed Windows TrueType font,
found through GDI (
.ttf,.ttccollections, per-user fonts), subset and embedded as a CID font. Latin with every accent, Greek, Cyrillic, Chinese, Japanese, Korean, Vietnamese and symbol/icon fonts all work, and the text stays searchable and copyable - Map labels: GDI-compatible metrics (
TextOrigin = toGdiTop), halos drawn under the fill, text drawn as vector outlines (DrawTextOutlines, for icon fonts such as Ionicons), rotation at any angle, fallback fonts for characters the main font lacks - Transparency:
Opacityon Pen, Brush and Font - Layout:
DrawTextat a point or fitted to a rectangle,DrawParagraphwith word wrap (breaks between CJK characters too), underline, text background - Measurement:
TextWidth[F],TextHeight[F],TextExtent[F],MeasureParagraph,FontMetrics - Images: JPEG (passthrough as
DCTDecode), PNG (opaque, or alpha via SMask), BMP and any otherTGraphic - Output: Flate-compressed, streamed to a file or any
TStream, or returned asTBytes;/Infometadata; warnings for every fallback - Fast: a page with a million polygon vertices and 2,000 labels builds and saves in well under a second
Gallery
The images below are rendered from PDFs the bundled demos export, with no
post-processing and no third-party tools beyond mutool for the screenshot.
<table>
<tr>
<td width="50%" valign="top">
Standalone invoice — text alignment, paragraphs, table headers, ovals as a logo, custom RGB throughout, and a tiled <code>PAID</code> watermark at 45°. Standard 14 fonts only. Source: <code>Demos/Invoice/</code>.
</td>
<td width="50%" valign="top">
A3 sales-territory map — 237 territories (525,000 vertices) projected from a 74 MB GeoJSON via Mercator and drawn with <code>DrawPolyPolygon</code>. Source: <code>Demos/Map/</code>.
</td>
</tr>
<tr>
<td width="50%" valign="top">
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.
</td>
<td width="50%" valign="top">
Embedded TrueType — Arial / Calibri / Verdana subset and embedded, side by side with the Standard 14.
</td>
</tr>
<tr>
<td width="50%" valign="top">
Embedded images — TBitmap (FlateDecode RGB), PNG (opaque + alpha via SMask), JPEG (DCTDecode passthrough). Same TPicture drawn twice de-dupes to a single XObject.
</td>
<td width="50%" valign="top">
Rotated text — <code>DrawText(s, X, Y, AAngle)</code> with CCW degrees. Compass rose, vertical column header, book-spine label, and a styled diagonal "DRAFT" watermark.
</td>
</tr>
</table>
Page 8 of the showcase (build.ps1) shows the 2.0 features: text in seven
scripts, a clipped mini-map with halo labels and Ionicons rep icons, and label
stacks measured with GDI metrics. Regenerate the gallery with
images/regenerate.ps1 (needs mutool:
winget install ArtifexSoftware.mutool).
Coordinate system
- Units: pixels at the page's DPI.
NewPage(psA4, poPortrait, 300)makes a 300 DPI page;NewPage(AWidthPt, AHeightPt)takes the page size in points and defaults to 72 DPI, where one pixel is one point. To work in points, as AlignMix does, use a 72 DPI page and pass points everywhere. - Precision: coordinates are
Double(every drawing method has aDoubleoverload; theIntegerones remain). They are written with three decimals by default;CoordinatePrecision := 1writes 0.1 pt, still invisible in print, and roughly halves the size of polygon-heavy pages. - Origin: top-left, Y grows downward, as in
TCanvas.Y = 0is the top edge of the page andY = Heightthe bottom edge; the flip to PDF's bottom-left origin happens on output. - Page size: the MediaBox is the exact point size (A4 is 595.28 × 841.89).
- Font sizes and pen widths are in points.
pdf.NewPage(2383.94, 3370.39); // A0 portrait, 72 DPI: pixels are points
pdf.DrawLine(10.25, 20.5, 300.75, 20.5);
pdf.Font.Size := 7.35; // fractional sizes are written as givenPublic API
Everything is in uses smPDF. Types:
| Type | Purpose |
|---|---|
TsmPDF |
The document: pages, drawing, text, measurement, saving |
TPDFFont |
Name, Size (Double), Color, Bold, Italics, Underline, StrokeColor, StrokeStyle, StrokeWidth, StrokeMode, Opacity, FallbackFonts |
TPDFPen |
Color, Width, Style (penSolid/Dash/Dot/DashDot/None), LineCap, LineJoin, Opacity |
TPDFBrush |
Color, Style (brushSolid / brushClear), Opacity. When solid, also paints a background behind DrawText / DrawParagraph |
TPDFFillRule |
frEvenOdd, frNonZero |
TPDFTextOrigin |
toTypoTop, toGdiTop, toBaseline — what the Y of DrawText(X, Y) means |
TPDFStrokeMode |
smOverFill, smUnderFill — outline over the glyphs, or a halo under them |
TPDFTextMetrics |
Ascent, Descent, LineHeight, CapHeight, XHeight (page pixels) |
TPDFPointList / TPDFPointFList |
TList<TPoint> / TList<TPointF> |
TPDFOrientation, TPDFPaperSize, TPDFPenStyle, TPDFBrushStyle, TPDFTextPadding, TPDFStrokeStyle, TPDFLineCap, TPDFLineJoin |
as their names say |
EPDFError |
The single exception class the library raises |
SystemFallbackFonts(Family) |
A ready-made value for Font.FallbackFonts |
Pages and output:
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; // psCustom: pixels
procedure NewPage(const AWidthPt, AHeightPt: Double; const ADPI: Integer = 72;
const APaperColor: TColor = clWhite); overload;
procedure NewPage; overload; // same size as the previous page (A4 at first)
function Save(const AFileName: string): Int64; overload; // bytes written
function Save(AStream: TStream): Int64; overload; // writes at AStream.Position
function ToBytes: TBytes;Save(FileName) streams through a buffered file stream and Save(Stream)
writes straight to the stream, so neither holds a second copy of the document.
Drawing:
procedure DrawLine(x1, y1, x2, y2); // Integer and Double overloads
procedure DrawBox(x1, y1, x2, y2);
procedure DrawOval(x1, y1, x2, y2);
procedure DrawRoundRect(const R: TRectF; ARadiusX, ARadiusY: Double);
procedure DrawMultiLine(const APointList: TPDFPointList | TPDFPointFList);
procedure DrawPolyline(const APoints: array of TPointF; ACount: Integer = -1);
procedure DrawPolyPolygon(const APoints: array of TPointF | TPoint;
const ACounts: array of Integer; AFillRule: TPDFFillRule = frEvenOdd);
procedure DrawPolygon(const APointList: TPDFPointList | TPDFPointFList); overload;
procedure DrawPolygon(const APointList; AClipRect: TRect | TRectF); overload;
procedure DrawPicture(const APicture: TPicture; ARect: TRect | TRectF;
AAlignment: TAlignment = taLeftJustify; AStretch: Boolean = False);
procedure DrawPicture(const APicture: TPicture; x1, y1: Integer);
procedure PushClipRect(const R: TRectF | TRect);
procedure PopClip;
procedure BeginPath;
procedure MoveTo(X, Y: Double);
procedure LineTo(X, Y: Double);
procedure CurveTo(X1, Y1, X2, Y2, X3, Y3: Double);
procedure ClosePath;
procedure FillPath(AFillRule: TPDFFillRule = frNonZero);
procedure StrokePath;
procedure FillAndStrokePath(AFillRule: TPDFFillRule = frNonZero);Text:
procedure DrawText(const AText: string; X, Y: Integer | Double; AAngle: Double = 0);
procedure DrawText(const AText: string; ARect: TRect; AAlignment: TAlignment = taLeftJustify);
procedure DrawParagraph(const AText: string; ARect: TRect; AAlignment: TAlignment;
APadding: TPDFTextPadding);
procedure DrawTextOutlines(const AText: string; X, Y: Double; AAngle: Double = 0);
function TextWidth / TextHeight (const AText: string): Integer;
function TextWidthF / TextHeightF (const AText: string): Double;
function TextExtent(const AText: string): TSize;
function TextExtentF(const AText: string): TSizeF;
function MeasureParagraph(const AText: string; AMaxWidthPx: Integer;
APadding: TPDFTextPadding = tpSingle): TSize;
function FontMetrics: TPDFTextMetrics;
DrawText(s, Rect)fits the font size to the rectangle. It picks the largest size at which the text fits the rectangle's width and height and ignoresFont.Size. It is not VCLTCanvas.TextRect, which clips at the current size. To draw atFont.Sizeinside a box, measure withTextWidthFand callDrawText(s, X, Y), insidePushClipRectif it must be clipped.
Properties: Font, Pen, Brush, TextOrigin, CompressStreams (default
True), CoordinatePrecision (default 3), Title, Author, Subject,
Creator, Producer, CreationDate, and read-only Warnings, Width,
Height (pixels), WidthPt, HeightPt, DPI, Size, Orientation,
PaperColor. PageCount returns the number of pages.
Polygons, holes and fill rules
DrawPolyPolygon takes all the points in one array and the number of points in
each ring, so ring boundaries never depend on coordinates. Rings are closed
automatically. With frEvenOdd (the default) nested rings become holes; with
frNonZero holes need the opposite winding. The arrays are open arrays, so
existing buffers are passed without copying.
// A square with a square hole
pdf.Brush.Style := brushSolid;
pdf.DrawPolyPolygon(
[PointF(60, 430), PointF(260, 430), PointF(260, 630), PointF(60, 630), // outer
PointF(120, 490), PointF(200, 490), PointF(200, 570), PointF(120, 570)], // hole
[4, 4]);The older DrawPolygon(TPDFPointList) still works. It starts a new ring when a
point repeats the start of the current ring, so a ring that passes through its
own start point is split. Prefer DrawPolyPolygon for real geography.
Clipping and paths
pdf.PushClipRect(TRectF.Create(36, 126, 3334, 2348)); // the map frame
// ... everything drawn here is clipped ...
pdf.PopClip;Clips nest (each one intersects the previous) and belong to their page; any still open when the page is written are closed automatically.
The path API draws arbitrary shapes with the current Pen and Brush:
pdf.BeginPath;
pdf.MoveTo(100, 100);
pdf.CurveTo(150, 50, 250, 150, 300, 100);
pdf.LineTo(300, 200);
pdf.ClosePath;
pdf.FillAndStrokePath;Any other drawing, clipping, NewPage or Save while a path is open raises
EPDFError. DrawRoundRect is built from the same curves; a zero radius draws
exactly what DrawBox draws.
Text for maps
Where the text sits. TextOrigin decides what the Y of DrawText(X, Y)
means:
TextOrigin |
Y is | TextHeight is |
|---|---|---|
toTypoTop (default) |
top of the typographic ascent | 1.2 × size |
toGdiTop |
top of GDI's character cell: the baseline is at Y + usWinAscent |
usWinAscent + usWinDescent, GDI's cell height |
toBaseline |
the baseline | 1.2 × size |
Use toGdiTop to measure and stack labels exactly as TCanvas.TextOut does,
so a PDF export matches a GDI-rendered PNG. FontMetrics returns the ascent,
descent, line height, cap height and x-height for the current font, size and
origin.
Halos. Font.StrokeWidth (points) outlines the text in
Font.StrokeColor. With Font.StrokeMode := smUnderFill the outline is
stroked first, at twice the width with round joins, and the text is filled on
top, so StrokeWidth of halo shows around each glyph without thinning it:
pdf.Font.Color := clBlack;
pdf.Font.StrokeColor := clWhite;
pdf.Font.StrokeWidth := 0.6;
pdf.Font.StrokeMode := smUnderFill;
pdf.DrawText('Worcester', X, Y);smOverFill (the default) strokes on top of the fill. The older
Font.StrokeStyle (ssThin / ssMedium / ssThick) still works and gives a
width proportional to the font size; StrokeWidth > 0 overrides it.
Text as outlines. DrawTextOutlines draws the glyphs as filled vector
paths taken from the font through GDI, rather than as PDF text. Use it for icon
fonts (AlignMix draws its rep icons from Ionicons at U+F202 and U+F25D) and for
fonts that may not be embedded. It honours Font.Color, halos, rotation and
TextOrigin, and caches each glyph's outline per document.
pdf.Font.Name := 'Ionicons';
pdf.Font.Size := 14;
pdf.DrawTextOutlines(#$F202, X, Y);Rotation. DrawText(s, X, Y, AAngle) and DrawTextOutlines rotate
counter-clockwise by AAngle degrees around (X, Y), together with the
background, underline and outline.
Background and underline. When Brush.Style = brushSolid, DrawText and
DrawParagraph paint Brush.Color behind the text (the text cell for the
point overload, the rectangle for the others). Font.Underline draws a line
about 0.12 em below the baseline.
Fonts
If Font.Name is one of the 14 standard PDF fonts (Helvetica, Times,
Courier, Symbol, ZapfDingbats, and aliases such as Sans, Serif,
Mono), nothing is embedded and the viewer supplies the font.
Any other name is resolved through GDI, exactly as TCanvas would resolve
(Name, Bold, Italics), so the embedded face is the one Windows draws:
standalone .ttf files, members of .ttc collections (Cambria, Microsoft
YaHei, Yu Gothic, …) and fonts installed for the current user only. The font is
subset, keeping only the glyphs drawn, so a page of Arial adds about 20 KB
rather than 1 MB. It is written as a Type0 / CIDFontType2 font with
Identity-H encoding and a ToUnicode map.
When the family has no bold or italic face, text is emboldened or slanted the way GDI would do it.
These fall back to Helvetica and add a line to pdf.Warnings:
- a family that is not installed;
- a font with PostScript (CFF) outlines, which PDF 1.4 cannot embed as TrueType;
- a font whose licence (OS/2
fsType) forbids embedding.
DrawTextOutlines can still draw the last two, because it draws outlines
rather than embedding the font.
Fallback fonts
A label font such as Oswald has no Chinese glyphs. Font.FallbackFonts lists
families to use for characters the main font lacks:
pdf.Font.Name := 'Oswald';
pdf.Font.FallbackFonts := 'Microsoft YaHei;Yu Gothic;Malgun Gothic';
pdf.DrawText('Beijing 北京', X, Y); // "Beijing " in Oswald, "北京" in YaHeiThe text is split into runs, each drawn in the first font (the main one first)
that has its characters, one after another on the same baseline. Every
measuring function adds the runs up, so TextWidth, alignment and wrapping
stay right. SystemFallbackFonts('Oswald') returns a sensible list: the fonts
Windows itself links to that family, then Segoe UI, Microsoft YaHei, Yu Gothic,
Malgun Gothic and Nirmala UI. With FallbackFonts empty (the default), a
missing character is drawn as the main font's missing-glyph box and warned
about.
Fallback fonts are loaded only when needed. A family is loaded the first time a character is missing from the main font and from every family before it in the list, so setting a long list costs nothing on text the main font can show. A character that no font in the list has will load every family in the list, once per document, to find that out.
Encoding
Text is Unicode. What can be drawn depends on the font:
- Embedded TrueType fonts show any character the font's
cmapmaps one-to-one to a glyph: Latin with every accent (Łódź, Győr, Dvořák), Greek, Cyrillic, Chinese, Japanese, Korean, Vietnamese with precomposed characters, and symbol or private-use glyphs. Characters above U+FFFF (surrogate pairs) work when the font has them. The text can be searched and copied in PDF viewers. - The 14 standard fonts are WinAnsi (codepage 1252) only. Anything else is
drawn as
?. For names from customer data, use a TrueType font such as Arial. - A character the font does not have is drawn as the font's missing-glyph box.
Every such substitution adds one line to Warnings per font and character. For
example:
Font "Arial" has no glyph for U+5317 (北); it was drawn as the font's missing-glyph box.Not supported: complex-script shaping (Arabic, Hebrew, Indic, Thai), right-to-
left and bidirectional text, ligatures, kerning and vertical CJK text.
DrawParagraph wraps at spaces and between any two CJK characters; it is not
the full Unicode line-breaking algorithm.
In Delphi source files, save non-ASCII literals as UTF-8 with a BOM, or
write them as escapes (#$0141 for Ł); a file without a BOM is read as ANSI.
Transparency
Pen.Opacity, Brush.Opacity and Font.Opacity run from 0 (invisible) to 1
(opaque, the default). Shapes use the Pen's opacity for their outline and the
Brush's for their fill; text uses Font.Opacity for its fill and halo.
pdf.Font.Size := 120;
pdf.Font.Color := clRed;
pdf.Font.Opacity := 0.15;
pdf.DrawText('TRIAL', 150, 500, 30); // a translucent watermark
pdf.Font.Opacity := 1;Document information and warnings
pdf.Title := 'District 12 — North East';
pdf.Author := 'AlignMix';Title, Author, Subject, Creator and Producer (default
'smPDF ' + SMPDF_VERSION) are written to the /Info dictionary, with
CreationDate (default: when the TsmPDF was created). After drawing, check
pdf.Warnings for fonts and characters that had to be substituted.
Threading
A TsmPDF instance is not thread-safe, but separate instances can run on
separate threads at the same time: fonts are loaded and cached per document,
through a private GDI device context, and there is no global state.
What's deliberately not here
- PDF reading, parsing or editing; form filling; digital signatures; annotations and links; encryption. This library exports only.
- Tagged PDF / PDF/UA / PDF/A. No accessibility tagging and no archival profile.
- Complex-script shaping and right-to-left text. See Encoding above.
- Justified text, kerning, vertical text. Text is left, centre or right aligned.
- CMYK images, 16-bit PNG, palette-PNG transparency. Images are 8-bit RGB or RGBA.
Source layout
smPDF/
Source/
smPDF.pas public API (TsmPDF, TPDFFont, TPDFPen, TPDFBrush, EPDFError, enums)
smPDF.Types.pas internal records
smPDF.Geometry.pas unit conversion, Y flip, number formatting
smPDF.Buffer.pas growable byte buffer for content streams
smPDF.Writer.pas PDF object writer (dicts, arrays, streams, xref) over any TStream
smPDF.Page.pas TPDFPage: content stream, clip stack, resource registries
smPDF.Fonts.pas Standard 14 metrics, WinAnsi encoding
smPDF.TTF.pas TrueType parser and sfnt assembly
smPDF.GdiFonts.pas font loading and glyph outlines through GDI
smPDF.FontRegistry.pas per-document font resolution, glyph use, text encoding
smPDF.Subset.pas TrueType subsetting
smPDF.FontEmit.pas Type1 / Type0 / CIDFontType2 / ToUnicode objects
smPDF.Images.pas JPEG/PNG/BMP extraction, Flate compression
Tests/
smPDF_Tests.dpr console test runner (Win32 and Win64)
Source/ test framework and test units
Bench/ throughput benchmark
Demos/
Showcase/ 8-page demo of every feature (build.ps1)
Invoice/ single-page invoice, Standard 14 only
Map/ A3 sales-territory map from GeoJSON
Stress/ A0 map with a million vertices and 2,000 labels
Simple/ minimal VCL form
images/ README gallery PNGsBuilding and testing
The scripts assume RAD Studio at C:\Program Files (x86)\Embarcadero\Studio\37.0.
test.ps1builds and runs the test suite with bothdcc32anddcc64and fails if either fails. Tests that need a particular installed font (Oswald, Ionicons, Microsoft YaHei, Yu Gothic, Cambria, …) ormutoolon the PATH are skipped when it is missing.build.ps1builds the showcase and writesDemos\Showcase\_out\sample.pdf.Demos\Stress\build-stress.ps1builds and runs the stress demo and prints time, size and memory.
License
MIT — see LICENSE. Copyright (c) 2026 Steve Maughan.

