Skip to content

Latest commit

 

History

History
385 lines (320 loc) · 17.1 KB

File metadata and controls

385 lines (320 loc) · 17.1 KB

smPDF — pure-Pascal PDF export library for Delphi

Version 1.0.0 License: MIT Delphi XE8+ PDF 1.4 Platform: Windows Tests: 272 passing Dependencies: zero

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.

At a glance

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.

What it does

  • 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 /FontFile2 streams
  • Layout: per-line DrawText with rect alignment; multi-line DrawParagraph with word wrapping and configurable line spacing; underlines
  • Text styling: outlined glyphs (Font.StrokeStyle / StrokeColor), text-background fill (Brush acts as VCL TextRect behind text), and rotation around the (X, Y) anchor (DrawText(s, X, Y, AAngle) — CCW degrees)
  • Measurement: TextWidth, TextHeight, TextExtent, MeasureParagraph for laying out around text before drawing it
  • Images: JPEG (passthrough as DCTDecode), PNG (opaque + alpha via SMask), BMP (any other TGraphic → 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

Gallery

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 generated by smPDF

Standalone invoice — text alignment, paragraphs, table headers, ovals as a logo, custom RGB throughout, and a tiled PAID watermark at 45°. ~11 KB output, Standard 14 fonts only. Source: Demos/Invoice/.

A3 sales-territory choropleth map

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: Demos/Map/.

Graphics primitives

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 fonts

Embedded TrueType — Arial / Calibri / Verdana embedded as full /FontFile2 streams, side-by-side with the Standard 14. Latin-1 supplement (café / résumé / £€) round-trips through WinAnsi.

Embedded images

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

Rotated textDrawText(s, X, Y, AAngle) with CCW degrees. Compass rose at 30° intervals, vertical column header, book-spine label, slight-tilt sub-headline, plus a styled diagonal "DRAFT" watermark where Brush, StrokeStyle, and rotation all combine.

Regenerate the gallery from the current demo PDFs with images/regenerate.ps1 (needs mutoolwinget install ArtifexSoftware.mutool).

Public API

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, lcSquarePen.LineCap end style
TPDFLineJoin ljMiter, ljRound, ljBevelPen.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

Coordinate system

  • 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).

Polygons with holes and clipping

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

Text styling

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 outline

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

Font.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 watermark

When AAngle is omitted (or zero), the output is byte-identical to pre-feature PDFs — no cm operator is emitted.

Tiled watermarks (PAID, DRAFT, CONFIDENTIAL …)

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

Fonts: Standard 14 + any installed TrueType

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 automatically

Each (family, bold, italic) combination resolves to a separate physical font file, so all four Arial variants used in one document embed four TTFs.

Encoding

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.

What's deliberately not here

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

Source layout

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

License

MIT — see LICENSE. Copyright (c) 2026 Steve Maughan.