A pure-Go (no cgo), MRI-faithful reimplementation of Ruby's
prawn PDF-generation gem. It mirrors
prawn's document DSL — text, fonts (Core-14 and embedded/subset TrueType),
colors (RGB and CMYK), vector graphics, images, transformations, a grid,
bounding boxes, repeaters/page numbering, a table, plus the Prawn::Errors
tree — and writes the PDF with its own native pure-Go PDF object model and
content-stream writer. The emitted content-stream operators match Ruby prawn /
PDF::Core operator-for-operator, so the output is validated against the real
gem with a PDF::Inspector-style differential oracle. No C PDF library, no Ruby
runtime, and no third-party PDF generator: the module has zero non-stdlib runtime
dependencies and builds with CGO disabled on every supported 64-bit target
(and to WebAssembly).
It is a PDF backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-rqrcode, go-ruby-regexp and go-ruby-yaml.
What it is — and isn't. Emitting a PDF (page layout, text placement, vector graphics, image embedding) is deterministic and needs no interpreter, so it lives here as pure Go. Binding
Prawn::Documentto live Ruby objects is the host's job; this library hands back a plain, well-formed PDF byte stream.
Prawn's block-form generator
Prawn::Document.generate("hello.pdf") do |pdf|
pdf.text "Hello World", size: 24, style: :bold
pdf.move_down 20
pdf.stroke_rectangle [0, pdf.cursor], 200, 100
endbecomes
prawn.Generate("hello.pdf", func(pdf *prawn.Document) error {
pdf.Text("Hello World", &prawn.TextOptions{Size: 24, Style: prawn.StyleBold, StyleSet: true})
pdf.MoveDown(20)
pdf.StrokeRectangle(0, pdf.Cursor(), 200, 100)
return nil
})Ruby keyword-argument hashes become option structs (a nil selects prawn's
defaults); Ruby symbols become typed Go constants (Style*, Align*, page-size
and layout strings). Coordinates are PDF points (1/72 inch) with prawn's origin:
the lower-left corner of the margin box (bounds).
Faithful port of the frequently used prawn DSL, verified by generating documents and parsing them back:
- Document & pages —
New/Generate/GenerateTo/GenerateWith,Render,RenderFile,StartNewPage,Cursor,MoveCursorTo,Bounds,MoveDown/MoveUp,Pad/PadTop/PadBottom,PageCount. Page sizes (LETTER … A0–A10, B0–B6, C0–C6, custom) and portrait / landscape layout, with prawn's 36pt default margins (uniform or per-side). - Text —
Text(flowing, wrapping, auto-paginating),DrawText(absolute),TextBox(positioned box with:truncate/:expand/:erroroverflow),Font/FontSize/Leading, alignment (left / center / right / justify). - Fonts — the 14 standard AFM fonts (
Font/FontSize/Leading, kerning), plus TrueType embedding with subsetting (RegisterFontTTF): glyf/loca/hmtx subset following composite references, written as a Type0/CIDFontType2 font with Identity-H, aFontFile2stream, a/Wwidth array and a/ToUnicodeCMap. - Formatted text —
FormattedText(styled runs) andTextInline(<b>/<i>/<color>markup). - Color —
FillColor/StrokeColor(hex"RRGGBB") and CMYK (FillColorCMYK/StrokeColorCMYK), emitted via prawn'scs/scn,CS/SCNoperators. - Graphics —
MoveTo/LineTo/CurveTo,Rectangle,Line,Polygon,Circle/Ellipse,Stroke/Fill/FillAndStroke/CloseAndStroke,StrokeBounds,LineWidth,CapStyle/JoinStyle,Dash/Undash. - Layout —
BoundingBox, the column/rowGrid,Repeat/NumberPages, and affine transformsRotate/Scale/Translate(withorigin),TransformationMatrix,SaveGraphicsState/RestoreGraphicsState,Transparency. - Images —
Image(file) /ImageReader(io) for PNG (alpha → SMask) and JPEG (DCTDecode passthrough), withat,width,heightandfit. - Table — a basic grid (
Table): explicit or automatic column widths, optional bold header row, cell padding and borders, positioning. - Errors — the
Prawn::Errorstree (ErrCannotFit,ErrUnknownFont,ErrInvalidPageLayout,ErrUnsupportedImageType,ErrEmptyGraphicStateStack,ErrIncompatibleStringEncoding, …).
CGO-free, 100% test coverage, gofmt + go vet clean, race-clean, and green
across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le,
s390x), two WebAssembly targets and three OSes.
The output is validated against the real Ruby prawn gem the way prawn validates
itself — with a PDF::Inspector-style parse of the content stream, not raw
byte-equality (PDFs carry timestamps and ids). For each scenario, the PDF
operators this library emits (BT/Td/Tf/kerned TJ, re/l/m/c,
S/f, cs/scn, CS/SCN, cm, Do) are compared operator-for-operator
against the gem's for the same script. The gem's operator streams are captured
into testdata/oracle/*.content and committed, so the oracle runs in CI without
Ruby; a skip-gated test re-verifies them against a live gem when one is
installed. See prawn_diff_test.go / prawn_gem_test.go.
go get github.com/go-ruby-prawn/prawnpackage main
import (
"log"
"github.com/go-ruby-prawn/prawn"
)
func main() {
err := prawn.Generate("report.pdf", func(pdf *prawn.Document) error {
pdf.Font("Times", prawn.StyleBold)
pdf.Text("Quarterly Report", &prawn.TextOptions{Size: 24, Align: prawn.AlignCenter})
pdf.MoveDown(12)
pdf.FillColor("333333")
pdf.Text("Generated with pure-Go prawn.", nil)
pdf.Table([][]string{
{"Item", "Qty", "Price"},
{"Widget", "3", "9.99"},
{"Gadget", "1", "19.99"},
}, prawn.TableOptions{Header: true})
pdf.StrokeColor("CC0000")
pdf.StrokeRectangle(0, pdf.Cursor(), pdf.Bounds().Width, 40)
return nil
})
if err != nil {
log.Fatal(err)
}
}The PDF /CreationDate comes from a package clock seam, not the wall clock, so
output is reproducible and timezone independent — pin it with SetClock:
defer prawn.SetClock(prawn.SetClock(func() time.Time {
return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
}))PDF is a byte-oriented format and every dependency is pure Go, so the bytes are identical on little- and big-endian architectures alike.
The suite is self-contained: it generates PDFs in memory and parses them back
with the pure-Go rsc.io/pdf reader, asserting
that each document is a well-formed PDF (header, xref, trailer, %%EOF), that the
expected text appears on the right page with the right font and size, that images
are embedded as image XObjects, and that table cells and vector graphics are
drawn. It also runs the committed-fixture differential operator oracle (see
above) comparing the emitted operators to the real prawn gem's. No Ruby runtime
and no network are needed in CI; the live-gem re-verification skips when Ruby is
absent. 100% statement coverage is enforced.
COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1 # 100.0%Covered faithfully: document + page management, text (flowing / absolute / box), formatted/inline text, Core-14 AFM fonts and subset-embedded TrueType, color (RGB + CMYK), vector graphics, bounding boxes, grid, transformations, repeaters/page numbering, PNG/JPEG images and a basic table.
Font embedding also has a go-opentype-backed backend (RegisterFontOTF)
built on the pure-Go github.com/go-opentype/opentype
.../shapestack. On top of the glyf/TrueType path it adds:
- CFF/OpenType (
.otf, "OTTO") embedding as a CIDFontType0 descendant with a FontFile3 (/Subtype /OpenType) program; - an opt-in shaped-text path (
OTFOptions{Shape: true}) so Arabic joining, Indic reordering, CJK and Latin ligatures/kerning (GSUB + GPOS) render with the correct glyphs and spacing, emitted as a positionedTJarray; - variable-font instance selection (
OTFOptions{Variation: {"wght": 700}}), so measured widths and the per-CID/Wadvances follow the chosen axis.
The default Prawn text API (text, formatted_text, font, width_of) is
unchanged; shaping and variation are opt-in, so default output stays
Prawn-compatible.
Deferred (named in doc.go, not implemented): advanced prawn-table features
(row/column spanning, per-cell style callbacks, automatic table pagination),
GPOS mark-attachment vertical placement in the PDF (substitution + horizontal
advances are applied), glyf/CFF subsetting for the OTF backend (the whole font
program is embedded), CJK/soft-hyphen line breaking, and SVG.
BSD-3-Clause — see LICENSE. Copyright the go-ruby-prawn/prawn authors.
Being pure Go (CGO=0), this library also compiles to WebAssembly — both
GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI).
CI builds both targets on every push, alongside the six 64-bit native/qemu arches.
GOOS=js GOARCH=wasm go build ./... # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./... # WASI (wasmtime, wasmer, wasmedge, …)