A pure-Go, zero-C PDF 1.7 writer with a Go-idiomatic API. It embeds TrueType and OpenType/CFF fonts as subsetted composite fonts, draws vector graphics and text, and places JPEG and raster images. Fonts are parsed and shaped with go-opentype; nothing outside the Go standard library and our own pure-Go libraries is required.
pdfkit is the Go-native counterpart to our Ruby Prawn port
(go-ruby-prawn); here the API is
Go-idiomatic rather than a gem port.
- Documents — catalog, page tree, cross-reference table and trailer; write
to any
io.Writer; optional Flate stream compression. - Graphics — paths (move/line/cubic/rect/close), fill & stroke (nonzero and
even-odd), DeviceGray/RGB/CMYK colour, line width/cap/join/miter/dash, CTM
transforms (translate/scale/rotate/skew), clipping,
q/Qstate, and constant-alpha transparency via ExtGState. - Text — embeds fonts as Type0 (Identity-H) composite fonts with glyph
subsetting, a per-glyph
/Wwidth array and a/ToUnicodeCMap for copy/paste. TrueTypeglyf→ FontFile2 / CIDFontType2 (compact subset with a/CIDToGIDMapstream); CFF/OpenType → FontFile3 / CIDFontType0 with CFF charstring subsetting (only the used glyphs' charstrings are embedded). Char/word spacing, leading, render modes, a simple wrapping helper, and an optional shaped-text API (GSUB/GPOS via go-opentype) for Arabic/Indic/CJK. - Images — JPEG embedded directly (DCTDecode); PNG and any
image.Imagerasterised as XObjects (FlateDecode) with an/SMaskfor alpha. - Pages — standard sizes (A3/A4/A5/Letter/Legal/Tabloid), portrait/landscape,
custom sizes;
Pt/Mm/Inunit helpers. - Widget bridge —
Page.AddWidgetandPage.AddWidgetVector"print" a go-widgets/toolkit widget tree onto a page.AddWidgetrasterises the tree and places it as an image XObject — pixel-identical to the screen.AddWidgetVectorinstead emits PDF vector operators, so fills/strokes stay crisp and text stays selectable, including a TrueType-font widget label's own face embedded as real Type0 text. - Deterministic — with the zero
Options, output has no timestamps and a content-derived/ID, so identical inputs produce byte-identical PDFs.
go get github.com/go-pdfkit/pdfkitpackage main
import (
"os"
"github.com/go-pdfkit/pdfkit"
)
func main() {
ttf, _ := os.ReadFile("font.ttf")
font, _ := pdfkit.LoadFont(ttf)
doc := pdfkit.New(pdfkit.Options{Title: "Hello"})
p := doc.AddPage(pdfkit.A4)
p.SetFont(font, 24)
p.Text(pdfkit.Mm(20), p.Height()-pdfkit.Mm(20), "Hello, pdfkit")
p.SetStrokeColor(pdfkit.RGB8(0x0d, 0x94, 0x88))
p.SetLineWidth(2)
p.MoveTo(pdfkit.Mm(20), pdfkit.Mm(20))
p.LineTo(pdfkit.Mm(190), pdfkit.Mm(20))
p.Stroke()
f, _ := os.Create("out.pdf")
defer f.Close()
doc.Write(f)
}Shaped (complex-script) text uses p.TextShaped(x, y, s, features...), which
runs the go-opentype shaper so Arabic, Indic and CJK position correctly. The
default Text path stays a simple left-to-right cmap mapping.
import "github.com/go-widgets/toolkit"
root := toolkit.NewContainer(toolkit.NewBoxLayout())
btn := toolkit.NewButton("Submit", nil)
btn.Style = toolkit.ButtonProminent
root.AddWidget(btn)
root.AddWidget(toolkit.NewLabel("Status: ready"))
rect := pdfkit.Rect{X: pdfkit.Mm(20), Y: pdfkit.Mm(200), Width: pdfkit.Mm(80), Height: pdfkit.Mm(30)}
// Raster: pixel-identical to the screen, not selectable.
_ = p.AddWidget(root, rect, nil)
// Vector: crisp fills/strokes, selectable text (needs an embedded Font).
rect.Y -= pdfkit.Mm(40)
_ = p.AddWidgetVector(root, rect, &pdfkit.WidgetOptions{Font: font})WidgetOptions.Scale sets the layout-pixels-per-point ratio (default
DefaultWidgetScale, 2); WidgetOptions.Theme selects the toolkit theme
(default toolkit.DefaultLight()).
GOWORK=off CGO_ENABLED=0 go test ./... runs the suite at exact 100%
statement coverage. Correctness is checked against an independent parser:
generated documents are re-opened with rsc.io/pdf
and their structure verified. The embedded TrueType subset is re-parsed with
go-opentype and each drawn glyph, resolved through the /CIDToGIDMap, is
confirmed contour-identical to the original; the embedded CFF subset is asserted
smaller than the whole CFF table and re-parsed so each kept glyph still renders
intact. Tests are deterministic and network-free: they use a synthesised
TrueType font, a synthesised CFF2 font and a bundled OFL OpenType/CFF font.
- Both outline flavours are subsetted: TrueType
glyffonts viago-opentype'sSubsetTrueType(compact renumbering + a/CIDToGIDMapstream) and CFF/OpenType fonts viaSubsetCFF(charstring subsetting, glyph numbering preserved). All subsetting and the font-descriptor metrics come straight fromgo-opentype;pdfkitkeeps no private sfnt re-parse. - A CID-keyed CFF or a CFF2 (variable) font cannot be charstring-subsetted
by the preserve-numbering path, so it gracefully falls back to embedding the
whole
CFF/CFF2table. - Encryption, tagged/PDF-A, forms and annotations are not yet implemented.
BSD-3-Clause — see LICENSE. Copyright (c) 2026 the go-pdfkit/pdfkit authors.