Go
The rustpdf-go module wraps the rust-pdf C core with idiomatic Go types over cgo. It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. The native library is statically linked from a prebuilt archive bundled per platform, so go get + go build just works — no separate native install.
Document authors a new PDF; EditableDoc loads and manipulates an existing one. Each holds a native handle, so defer d.Close() to free it promptly. Methods return an error (not the receiver), so they don't chain.Installation
Add the module with go get. A prebuilt static libpdf_ffi.a for your platform ships inside the module (under lib/<os>_<arch>/), so there is nothing else to install or compile.
go get github.com/rustpdf/rustpdf-go@latestRequires Go 1.21+ and a C compiler on PATH (cgo, enabled by default). Supported platforms: darwin/arm64, darwin/amd64, linux/amd64, linux/arm64, windows/amd64. Import it (the package is named rustpdf):
import rustpdf "github.com/rustpdf/rustpdf-go"
fmt.Println(rustpdf.Version()) // native library versionQuick start
A one-page document with a filled rectangle, saved to disk. Errors are elided with _ for brevity — see Error handling for the real pattern:
package main
import rustpdf "github.com/rustpdf/rustpdf-go"
func main() {
doc, _ := rustpdf.New() // A4 by default
defer doc.Close()
_ = doc.AddPage()
_ = doc.SetFillRGB(0.86, 0.20, 0.18)
_ = doc.Rect(72, 640, 200, 120) // x, y, width, height (points)
_ = doc.Fill()
_ = doc.Save("out.pdf")
}Get the bytes instead of writing a file with ToBytes:
doc, _ := rustpdf.New()
defer doc.Close()
font, _ := doc.AddFontFile("Roboto-Regular.ttf")
_ = doc.AddPage()
_ = doc.SetFillRGB(0.1, 0.1, 0.12)
_ = doc.Rect(0, 800, 595, 42)
_ = doc.Fill()
_ = doc.ShowText(font, 24, 72, 740, "Olá, açúcar — café", 0)
data, _ := doc.ToBytes() // []byte instead of a filecgo & deployment
The binding links the native library statically, so a built binary carries it — there is no shared library to ship alongside.
- Build host needs a C toolchain. cgo is on by default (
CGO_ENABLED=1); install a C compiler (Xcode CLT on macOS,build-essentialon Linux, MinGW-w64 on Windows). - Cross-compilation needs a cross C toolchain for the target (set
CC), because cgo invokes the platform C compiler. PureGOOS/GOARCHcross-builds without a cross-CCwon't link. - The bundled archive is per-platform. Building for a target outside the supported set above has no archive to link against.
- Containers: build inside an image that has the C toolchain; the resulting binary needs only the usual system libs at runtime (no rust-pdf shared object).
Licensing & activation
Basic generation (everything above) is always free. The corporate features (PDF/A, digital signatures/PAdES, encryption, accessibility) require an active license token. Without one, those calls return an *Error and produce no output.
Activation needs no rebuild. Easiest is an environment variable, auto-activated the first time a corporate feature is used:
export RUSTPDF_LICENSE="010f0000…" # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txtOr activate explicitly in code:
if err := rustpdf.ActivateLicense(token); err != nil {
log.Fatal(err) // forged / expired / malformed
}Coordinate system
- Units are points (1 pt = 1/72 inch). A4 is
595 × 842, US Letter612 × 792. - The origin
(0, 0)is the bottom-left corner;ygrows upward. - For text,
(x, y)is the baseline of the first glyph. - Drawing/text always targets the most recently added page.
Goroutines & concurrency
Every native call is synchronous and the core is Send but not Sync: a single handle must never be touched by two goroutines at once.
- Documents are independent. Each
Document/EditableDocshares no state, so building several in parallel goroutines is fine — one handle per goroutine. - Don't share a live handle across goroutines; give each its own (or guard it with a mutex).
- Errors are per-thread. The native last-error is thread-local, so a failure in one goroutine never clobbers another's, and the message surfaces on the calling side.
- License is process-global.
ActivateLicense(or the env var) applies everywhere; activate once at startup.
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
doc, _ := rustpdf.New() // one document per goroutine
defer doc.Close()
_ = doc.AddPage()
_ = doc.SetFillRGB(0.1, 0.1, 0.12)
_ = doc.Rect(72, 700, 200, 80)
_ = doc.Fill()
_ = doc.Save(fmt.Sprintf("out-%d.pdf", n))
}(i)
}
wg.Wait()Authoring: create & save
rustpdf.New() (*Document, error) freeCreates an empty document (A4 default page size). Call Close() to free the native handle.
| Method | Description |
|---|---|
AddPage() error | Append a page at the default size. |
AddPageSized(w, h float64) error | Append a page with an explicit size in points. |
SetDefaultSize(w, h float64) error | Default size for subsequently added pages. |
SetVersion(v int) error | Set the PDF header version (e.g. 14 → PDF 1.4, 17 → 1.7). |
PageCount() int | Number of pages so far. |
ToBytes() ([]byte, error) | Render the document to bytes. |
Save(path string) error | Render and write to a file. |
Close() | Free the native handle. |
Pages & vector graphics
Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.
| Method | Description |
|---|---|
SetFillRGB(r, g, b float64) error | Fill color. |
SetStrokeRGB(r, g, b float64) error | Stroke color. |
SetLineWidth(w float64) error | Stroke width in points. |
Rect(x, y, w, h float64) error | Add a rectangle subpath. |
Fill() error | Fill the current path with the fill color. |
Stroke() error | Stroke the current path with the stroke color. |
doc, _ := rustpdf.New()
defer doc.Close()
_ = doc.AddPage()
_ = doc.SetStrokeRGB(0.10, 0.45, 0.90)
_ = doc.SetLineWidth(3)
_ = doc.Rect(72, 600, 300, 160)
_ = doc.Stroke()
_ = doc.SetFillRGB(0.95, 0.77, 0.06)
_ = doc.Rect(120, 640, 120, 80)
_ = doc.Fill()
_ = doc.Save("shapes.pdf")Fonts & text
Fonts are embedded and subsetted, with HarfBuzz-quality shaping, kerning and full Unicode (Type0/CIDFontType2 with ToUnicode, so text extracts and copies correctly). Register a font once, then reference it by its integer id.
AddFontFile(path string) (int, error) AddFont(data []byte) (int, error)ShowText(font int, size, x, y float64, text string, headingLevel int) errorheadingLevel (1–6) tags the run as H1–H6 in an accessible document (see Accessibility); pass 0 for ordinary text.
doc, _ := rustpdf.New()
defer doc.Close()
regular, _ := doc.AddFontFile("Roboto-Regular.ttf")
// …or from bytes you already have in memory:
// data, _ := os.ReadFile("Roboto-Regular.ttf"); regular, _ := doc.AddFont(data)
_ = doc.AddPage()
_ = doc.ShowText(regular, 28, 72, 760, "Invoice #1024", 0)
_ = doc.ShowText(regular, 12, 72, 720, "日本語 · Ελληνικά · العربية", 0)
_ = doc.Save("text.pdf")Paragraphs
The paragraph layer wraps, aligns and justifies text inside a fixed width (greedy line breaking using shaped glyph widths).
Paragraph(font int, size, x, y, width float64, text string, align Align) errorintro := "A long paragraph that wraps to the given width and is justified " +
"automatically; extra space is distributed between words."
doc, _ := rustpdf.New()
defer doc.Close()
f, _ := doc.AddFontFile("Roboto-Regular.ttf")
_ = doc.AddPage()
_ = doc.Paragraph(f, 12, 72, 700, 451, intro, rustpdf.Justify)
_ = doc.Save("paragraph.pdf")See the Align enum for the alignment options.
Images
JPEGs are embedded verbatim (DCTDecode, no re-encode). PNGs are decoded and re-encoded (FlateDecode); alpha becomes an /SMask, palette becomes an Indexed color space. Register an image once, draw it many times.
| Method | Description |
|---|---|
AddImageFile(path string) (int, error) | Load JPEG/PNG from a file; returns the image id. |
AddImagePNG(data []byte) (int, error) | Register a PNG from memory. |
AddImageJPEG(data []byte) (int, error) | Register a JPEG from memory. |
DrawImage(image int, x, y, w, h float64) error | Draw at (x, y) scaled to w × h points. |
Figure(image int, x, y, w, h float64, alt string) error | Draw as a tagged /Figure with alt text (accessibility). |
doc, _ := rustpdf.New()
defer doc.Close()
logo, _ := doc.AddImageFile("logo.png")
_ = doc.AddPage()
_ = doc.DrawImage(logo, 72, 680, 160, 90)
_ = doc.Save("with_image.pdf")PDF/A licensed
Produce archival-grade output. Pdfa() defaults to A-2b; PdfaLevel(level) selects a specific PdfaLevel. An embedded sRGB ICC profile, output intent, XMP metadata and document /ID are added automatically; A-1b also forces PDF 1.4 and emits a /CIDSet.
Pdfa() error PdfaLevel(level PdfaLevel) errordoc, _ := rustpdf.New()
defer doc.Close()
_ = doc.PdfaLevel(rustpdf.A2b)
_ = doc.SetInfo(rustpdf.Info{Title: "Q3 Report", Author: "Acme Inc."})
f, _ := doc.AddFontFile("Roboto-Regular.ttf")
_ = doc.AddPage()
_ = doc.ShowText(f, 20, 72, 760, "Archival report", 0)
if err := doc.Save("report_pdfa.pdf"); err != nil {
log.Fatal(err) // *Error without a license granting PDF/A
}SetInfo(rustpdf.Info{Title: …}).Accessibility (Tagged PDF / PDF/UA) licensed
Tagged() builds a logical structure tree (PDF/UA-1). Combine with PdfaLevel(rustpdf.A2a) for archival and accessible output. Use headingLevel on ShowText for H1–H6, and Figure(..., alt) for described images.
Tagged() errordoc, _ := rustpdf.New()
defer doc.Close()
_ = doc.PdfaLevel(rustpdf.A2a)
_ = doc.Tagged()
_ = doc.SetInfo(rustpdf.Info{Title: "Accessible report"})
f, _ := doc.AddFontFile("Roboto-Regular.ttf")
_ = doc.AddPage()
_ = doc.ShowText(f, 26, 72, 760, "Annual report", 1) // H1
_ = doc.ShowText(f, 14, 72, 720, "Overview", 2) // H2
_ = doc.ShowText(f, 11, 72, 690, "Body paragraph of the section…", 0)
chart, _ := doc.AddImageFile("chart.png")
_ = doc.Figure(chart, 72, 520, 300, 150, "Revenue grew 18% year over year")
_ = doc.Save("accessible.pdf")Attachments (PDF/A-3) licensed
PDF/A-3 allows embedding arbitrary source files (e.g. the XML behind an e-invoice). Each attachment carries a MIME type and an AFRelationship.
AttachFile(name, mime string, data []byte, rel AFRelationship, desc string) errorxml, _ := os.ReadFile("invoice.xml")
doc, _ := rustpdf.New()
defer doc.Close()
_ = doc.PdfaLevel(rustpdf.A3b)
_ = doc.SetInfo(rustpdf.Info{Title: "E-invoice 1024"})
f, _ := doc.AddFontFile("Roboto-Regular.ttf")
_ = doc.AddPage()
_ = doc.ShowText(f, 18, 72, 760, "Invoice 1024", 0)
_ = doc.AttachFile("invoice.xml", "text/xml", xml,
rustpdf.Source, "Structured invoice data")
_ = doc.Save("einvoice.pdf")AcroForm fields
Build interactive forms with generated appearance streams (no NeedAppearances). Rectangles are [4]float64{x0, y0, x1, y1}; page is a 0-based page index. Dotted names ("a.b.c") create hierarchical fields.
| Method | Description |
|---|---|
TextField(name string, page int, rect [4]float64, value string, size float64) error | Text input (size=0 → auto font size). |
Checkbox(name string, page int, rect [4]float64, checked bool) error | Checkbox. |
Dropdown(name string, page int, rect [4]float64, options []string, selected int, size float64) error | Combo box. |
RadioGroup(name string, page int, buttons []RadioButton, selected int) error | RadioButton{Rect, Export} per option. |
doc, _ := rustpdf.New()
defer doc.Close()
_ = doc.AddPage()
_ = doc.TextField("applicant.name", 0, [4]float64{72, 700, 320, 720}, "", 0)
_ = doc.Checkbox("agree", 0, [4]float64{72, 660, 88, 676}, false)
_ = doc.Dropdown("plan", 0, [4]float64{72, 620, 240, 640},
[]string{"Starter", "Pro", "Enterprise"}, 1, 0)
_ = doc.RadioGroup("billing", 0, []rustpdf.RadioButton{
{Rect: [4]float64{72, 580, 88, 596}, Export: "monthly"},
{Rect: [4]float64{140, 580, 156, 596}, Export: "annual"},
}, 1)
_ = doc.Save("form.pdf")Fill fields later with EditableDoc.FillTextField.
Metadata
SetInfo(info Info) error — Info{Title, Author, Subject, Keywords, Creator string}Sets the document information dictionary (and, for PDF/A, the matching XMP). Set only the fields you need.
_ = doc.SetInfo(rustpdf.Info{
Title: "Q3 Report",
Author: "Acme Inc.",
Subject: "Quarterly results",
Keywords: "finance, q3",
})Manipulation: load an existing PDF
EditableDoc parses an existing document (classic & xref streams, object streams, all standard filters, RC4/AES decryption) into an editable model. Pages are a flat list; the page tree is rebuilt on output.
rustpdf.Load(data []byte) (*EditableDoc, error)data, _ := os.ReadFile("in.pdf")
ed, _ := rustpdf.Load(data)
defer ed.Close()
fmt.Println(ed.PageCount())Pages: merge, split, reorder, rotate
| Method | Description |
|---|---|
Merge(other *EditableDoc) error | Append all pages of another EditableDoc (objects renumbered & remapped). |
RotatePage(index, degrees int) error | Rotate one page (90 / 180 / 270). |
DeletePage(index int) error | Remove a page. |
ReorderPages(order []int) error | Reorder with a full permutation of indices. |
ExtractPages(indices []int) (*EditableDoc, error) | New document containing just those pages. |
PageCount() int | Current page count. |
ad, _ := os.ReadFile("a.pdf")
bd, _ := os.ReadFile("b.pdf")
a, _ := rustpdf.Load(ad)
defer a.Close()
b, _ := rustpdf.Load(bd)
_ = a.Merge(b) // a now has a's pages followed by b's
b.Close()
_ = a.RotatePage(0, 90)
_ = a.Save("merged.pdf")
sub, _ := a.ExtractPages([]int{0, 2}) // pages 1 and 3
defer sub.Close()
_ = sub.Save("subset.pdf")Metadata, overlay & form fill
| Method | Description |
|---|---|
SetInfo(key, value string) error | Set one info entry (e.g. "Title"). |
GetInfo(key string) (string, error) | Read an info entry. |
SetXMP(xml []byte) error | Replace the XMP metadata stream. |
OverlayPage(index int, content []byte) error | Overlay a content-stream fragment onto a page (stamps/watermarks). |
FillTextField(name, value string) (bool, error) | Fill an AcroForm text field; reports whether it was found. |
data, _ := os.ReadFile("form.pdf")
ed, _ := rustpdf.Load(data)
defer ed.Close()
_ = ed.SetInfo("Title", "Filled form")
found, _ := ed.FillTextField("applicant.name", "Jane Doe")
title, _ := ed.GetInfo("Title")
fmt.Println("filled:", found, "| title:", title)
_ = ed.Save("filled.pdf")Optimize & compact
| Method | Description |
|---|---|
Optimize() error | Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects. |
Compact(on bool) error | Pack objects into object streams + emit a cross-reference stream. |
data, _ := os.ReadFile("big.pdf")
ed, _ := rustpdf.Load(data)
defer ed.Close()
_ = ed.Optimize()
_ = ed.Compact(true)
_ = ed.Save("small.pdf")Encryption licensed
Apply standard-handler encryption at output. AES-256 (V5/R6) uses OS-CSPRNG keys/IVs.
Encrypt(method Encryption, user, owner string, readOnly bool) errordata, _ := os.ReadFile("in.pdf")
ed, _ := rustpdf.Load(data)
defer ed.Close()
if err := ed.Encrypt(rustpdf.Aes256, "", "owner-secret", true); err != nil {
log.Fatal(err) // *Error without an Encryption license
}
_ = ed.Save("secured.pdf")See the Encryption enum for RC4 / AES-128 / AES-256.
Output & incremental update
| Method | Description |
|---|---|
ToBytes() ([]byte, error) | Serialize the manipulated document. |
Save(path string) error | Serialize to a file. |
ToBytesIncremental(original []byte) ([]byte, error) | Append only changes to the original bytes (signature-safe, non-destructive). |
original, _ := os.ReadFile("in.pdf")
ed, _ := rustpdf.Load(original)
defer ed.Close()
_ = ed.SetInfo("Subject", "reviewed")
incremental, _ := ed.ToBytesIncremental(original) // original bytes preserved verbatim
_ = os.WriteFile("reviewed.pdf", incremental, 0o644)Digital signatures licensed
Sign a PDF with a PKCS#7 detached signature via an incremental update (the original bytes are preserved). Keys and certificates are passed as DER bytes. PAdES: true switches to PAdES-B-B.
rustpdf.Sign(pdf, keyDER, certDER []byte, opts SignOptions) ([]byte, error)pdf, _ := os.ReadFile("contract.pdf")
keyDER, _ := os.ReadFile("signing-key.pkcs8.der") // PKCS#8 private key (DER)
certDER, _ := os.ReadFile("signing-cert.der") // X.509 certificate (DER)
signed, err := rustpdf.Sign(pdf, keyDER, certDER, rustpdf.SignOptions{
Reason: "Approved", Location: "New York",
Name: "Jane Doe", PAdES: true,
})
if err != nil {
log.Fatal(err)
}
_ = os.WriteFile("contract.signed.pdf", signed, 0o644)
// Verify in a shell: pdfsig contract.signed.pdf → "Signature is Valid."Timestamp & DSS (PAdES LTV) licensed
Build long-term-validation signatures offline. AddDss appends a Document Security Store (/DSS with certs/CRLs, PAdES-B-LT); Timestamp appends an RFC 3161 document timestamp (/DocTimeStamp, PAdES-B-LTA).
rustpdf.AddDss(pdf []byte, certs, crls [][]byte) ([]byte, error)rustpdf.Timestamp(pdf, tsaKeyDER, tsaCertDER []byte, date string) ([]byte, error)signed, _ := os.ReadFile("contract.signed.pdf")
// B-LT: embed validation material (caller supplies DER certs/CRLs)
lt, _ := rustpdf.AddDss(signed, [][]byte{certDER}, [][]byte{crlDER})
// B-LTA: add a document timestamp signed by a TSA key/cert (date "" = now)
lta, _ := rustpdf.Timestamp(lt, tsaKeyDER, tsaCertDER, "")
_ = os.WriteFile("contract.lta.pdf", lta, 0o644)Text extraction
Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference.
rustpdf.ExtractText(data []byte) (string, error) freedata, _ := os.ReadFile("report.pdf")
text, _ := rustpdf.ExtractText(data)
fmt.Println(text)Enums
PdfaLevel
| Value | Level |
|---|---|
rustpdf.A1b | PDF/A-1b (basic, PDF 1.4) |
rustpdf.A2b | PDF/A-2b (basic): same as Pdfa() |
rustpdf.A2a | PDF/A-2a (accessible: pair with Tagged()) |
rustpdf.A3b | PDF/A-3b (basic, allows attachments) |
rustpdf.A3a | PDF/A-3a (accessible + attachments) |
Align
| Value | Meaning |
|---|---|
rustpdf.Left | Left-aligned (default) |
rustpdf.Right | Right-aligned |
rustpdf.Center | Centered |
rustpdf.Justify | Justified (space distributed between words) |
AFRelationship
| Value | Meaning |
|---|---|
rustpdf.Source | Source data for the document (e.g. the invoice XML) |
rustpdf.Data | Data used to derive the visual content |
rustpdf.Alternative | Alternative representation |
rustpdf.Supplement | Supplementary material |
rustpdf.Unspecified | Unspecified relationship |
Encryption
| Value | Cipher |
|---|---|
rustpdf.Rc4 | RC4 (legacy) |
rustpdf.Aes128 | AES-128 |
rustpdf.Aes256 | AES-256 (V5/R6): recommended |
Error handling
Every failing native call returns an *Error carrying the Status (PdfStatus) code and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too. Use errors.As to inspect the code.
import "errors"
doc, _ := rustpdf.New()
defer doc.Close()
_ = doc.Pdfa()
_ = doc.SetInfo(rustpdf.Info{Title: "x"})
_ = doc.AddPage()
if err := doc.Save("out.pdf"); err != nil {
var pe *rustpdf.Error
if errors.As(err, &pe) {
fmt.Printf("failed: status=%d: %s\n", pe.Status, pe.Message)
// e.g. status=7: feature 'pdfa' requires a valid license
}
}Utilities
| Function | Description |
|---|---|
rustpdf.Version() string | Native library version string. |
rustpdf.ActivateLicense(token string) error | Activate a license token (returns an error on invalid/expired). |