Documentation

Getting started

gopdfrab has a small, predictable API: open a document, run a verification profile, read the issues.
gopdfrab is pre-1.0
PDF/A-1b verification and conversion are implemented and tested against the full Isartor and veraPDF conformance suites, but the API is still being refined and may change until the first tagged release. Pin a version if you depend on current behaviour — the changelog states the versioning and stability policy.

Install

Add gopdfrab to your module.

terminal
go get github.com/voidrab/gopdfrab

Open a document

Open parses the file and builds the structures needed for verification. Always close the document when you're done with it.

main.go
doc, err := gopdfrab.Open(path)
if err != nil {
	log.Fatal(err)
}
defer doc.Close()

Verify PDF/A

doc.Verify runs the full PDF/A-1b profile and returns a Result with a single Valid flag and a list of issues.

verify.go
v, _ := doc.Verify(gopdfrab.PDFA1B)

if v.Valid {
	fmt.Println("Document is PDF/A-1b compliant")
} else {
	fmt.Println("Issues:")
	for i, issue := range v.Issues {
		fmt.Printf("#%v: %v\n", i+1, issue)
	}
}
verify.go
// Verify opens, verifies, and closes a file in one call
result, err := gopdfrab.Verify(path, gopdfrab.PDFA1B)
if err != nil {
	log.Fatal(err)
}
fmt.Println(result.Valid)

VerifyBytes is Verify for an in-memory PDF.

verify.go
// VerifyBytes is Verify for an in-memory PDF
result, err := gopdfrab.VerifyBytes(data, gopdfrab.PDFA1B)

VerifyAll opens, verifies, and closes a batch of files concurrently.

verify_all.go
results, err := gopdfrab.VerifyAll(paths, gopdfrab.PDFA1B)
if err != nil {
	log.Fatal(err)
}
for _, r := range results {
	if r.Err != nil {
		log.Println(r.Path, r.Err)
		continue
	}
	fmt.Println(r.Path, r.Result.Valid)
}

Convert to PDF/A

Convert produces a PDF/A conformant rewrite. It runs pre-emptive fixups, then a verify/fix loop, and rasterizes pages as a last resort when no in-place fixer can repair them. The output spills to a temp file when it is large, so always Close() the result.

convert.go
cr, err := gopdfrab.Convert(path, gopdfrab.PDFA1B)
if err != nil {
	log.Fatal(err)
}
defer cr.Close() // releases the output (a large one spills to a temp file)

if err := cr.Save("out.pdf"); err != nil {
	log.Fatal(err)
}

fmt.Println(cr.Iterations)      // how many verify/fixup passes it took
fmt.Println(cr.Result.Valid)    // true if the output is fully PDF/A conformant

Conversion is also available on an already-open document, on in-memory data, across a batch of files, and the result always exposes any residual issues that survived every remediation pass.

convert_variants.go
// Converting an open document
cr, err := doc.Convert(gopdfrab.PDFA1B)
defer cr.Close()

// Converting in-memory data
cr, err = gopdfrab.ConvertBytes(data, gopdfrab.PDFA1B)

// Converting multiple files concurrently
results, err := gopdfrab.ConvertAll(paths, gopdfrab.PDFA1B)
if err != nil {
	log.Fatal(err)
}
for _, r := range results {
	if r.Err != nil {
		log.Println(r.Path, r.Err)
		continue
	}
	fmt.Println(r.Path, r.Result.Result.Valid) // r.Result is a ConvertResult
}

// Inspecting residual issues after conversion
residual := cr.Residual()
for _, iss := range residual {
	c := iss.Check()
	fmt.Println(c.Clause(), c.Name())
	fmt.Println(iss.Page(), iss.Messages())
}

Encrypted PDFs

Encrypted documents are decrypted transparently on open when they use the empty user password. Supply a user or owner password explicitly with OpenWithPassword, or through Options.Password on the context-taking entry points. A file that needs a password is reported with ErrPasswordRequired rather than producing a broken result.

The standard security handler is supported end to end: RC4 40-bit and 128-bit, AES-128, and AES-256.

encrypted.go
// Encrypted documents are decrypted transparently on open when they use
// the empty user password. Supply a user or owner password explicitly:
doc, err := gopdfrab.OpenWithPassword(path, []byte("secret"))
if errors.Is(err, gopdfrab.ErrPasswordRequired) {
	log.Fatal("a correct password is required to open this file")
}
defer doc.Close()

// Verify and Convert decrypt the same way, via Options.Password
res, err := gopdfrab.VerifyContext(ctx, path, gopdfrab.PDFA1B, gopdfrab.Options{
	Password: []byte("secret"),
})

Typed errors

Open, verify and convert failures can be matched with errors.Is instead of inspecting message text.

Error Meaning
ErrNotPDFthe input is not a PDF (no %PDF- header)
ErrDamageda PDF whose cross-reference or trailer structure could not be parsed
ErrEncryptedan encryption scheme gopdfrab does not implement
ErrPasswordRequireda correct password is required to open the file
ErrUnresolvableGraphConvert could not resolve the object graph, so no output was produced
errors.go
res, err := gopdfrab.Verify(path, gopdfrab.PDFA1B)

switch {
case errors.Is(err, gopdfrab.ErrNotPDF):
	log.Println("not a PDF -- no %PDF- header")
case errors.Is(err, gopdfrab.ErrPasswordRequired):
	log.Println("a correct password is required")
case errors.Is(err, gopdfrab.ErrEncrypted):
	log.Println("unsupported encryption scheme")
case errors.Is(err, gopdfrab.ErrDamaged):
	log.Println("xref or trailer structure could not be parsed")
case err != nil:
	log.Println(err)
default:
	fmt.Println(res.Valid)
}

Damage is reported, not fatal

An individual object that fails to parse — a wrong cross-reference offset, a corrupt body — does not fail the whole document. The object is re-located by scanning for its real N G obj header, or resolved to null when no intact copy exists; either way the damage is reported as a 6.1.6 issue and every other check still runs.

The same applies to whole-table damage: a missing or unusable startxref triggers a full-file object scan that rebuilds the cross-reference table and recovers the trailer from the document catalog, reported as a 6.1.4 issue rather than a hard error. A badly damaged file still verifies and converts — and a conversion that had to null an unrecoverable object keeps that loss in Residual() and never reports the result as valid.

Object-model conformance

Checks.ObjectModel holds six checks derived from the Arlington PDF Model, the machine-readable ISO 32000 object model. They answer “is this even valid PDF”, independent of any PDF/A conformance level.

MissingRequiredKeyWrongValueTypeDisallowedValueIndirectRequiredKeyIntroducedAfterPDF14ConstraintViolated
objectmodel.go
// "Is this even valid PDF?" -- independent of any PDF/A conformance level
res, err := gopdfrab.VerifyObjectModel(path)

res, err = gopdfrab.VerifyObjectModelBytes(data)
res, err = doc.VerifyObjectModel()

// Equivalent to verifying with the object-model-only profile
res, err = doc.Verify(gopdfrab.ObjectModelOnly())

// Shorthand for VerifyObjectModel().Valid
ok, err := doc.IsPDF()

ConvertObjectModel is the conversion counterpart: it produces a rewrite repaired against the object-model checks only, applying every fix that is safe and semantics-preserving and reporting anything else as a residual.

convert_objectmodel.go
// Repairs against the object-model checks only, applying every fix that is
// safe and semantics-preserving and reporting anything else as a residual.
cr, err := gopdfrab.ConvertObjectModel(path)
defer cr.Close()

cr, err = gopdfrab.ConvertObjectModelBytes(data)
cr, err = doc.ConvertObjectModel()

Options and cancellation

The two-argument forms — Convert(path, profile), Verify(path, profile), and their Bytes/All variants — cover the common case. Each has a …Context counterpart that adds a context.Context for cancellation and an Options struct for tuning. The zero Options value is the default behaviour.

options.go
cr, err := gopdfrab.ConvertContext(ctx, path, gopdfrab.PDFA1B, gopdfrab.Options{
	Password:      []byte("secret"), // decrypt an encrypted input
	RasterDPI:     300,              // raster last-resort resolution (default 150)
	MaxIterations: 8,                // verify/fix loop bound (default 4)
	Workers:       4,                // batch concurrency (default runtime.NumCPU)
})
defer cr.Close()

// To set options without a deadline, pass context.Background()
res, err := gopdfrab.VerifyContext(context.Background(), path, gopdfrab.PDFA1B,
	gopdfrab.Options{Password: []byte("secret")})
Field Type Description Applies to
Password[]byteUser or owner password for an encrypted input. nil is the empty password.Verify + Convert
RasterDPIintResolution for the raster last resort and transparency flattening. 0 = 150.Convert
MaxIterationsintBounds the verify/fix loop. 0 = 4.Convert
CheckFidelityboolRender input and output and populate Fidelity with a per-page comparison. Roughly doubles the work.Convert
WorkersintBounds batch concurrency. 0 = runtime.NumCPU.ConvertAll + ConvertEach

Options.Password applies at the open step, so it works on ConvertContext/VerifyContext but not on the *Document methods, whose file is already open — use OpenWithPassword there. ConvertContext checks the context before each verify/fix iteration and each raster pass; the batch forms stop dispatching new files once it is cancelled and record ctx.Err() for the rest.

Resource limits

By default a single stream may decode to at most 256 MB, a guard against decompression bombs, and one open document keeps at most 64 MB of rebuildable caches. Raise them for legitimately large PDFs, or lower them to harden against hostile input.

limits.go
// A single stream decodes to at most 256 MB by default, guarding against
// decompression bombs. Raise it for legitimately large PDFs, or lower it to
// harden against hostile input.
gopdfrab.SetLimits(gopdfrab.Limits{
	MaxDecodedStreamBytes: 512 << 20, // 512 MB
	MaxResidentBytes:      32 << 20,  // caches one open document keeps (default 64 MB)
})

gopdfrab.CurrentLimits()  // the effective caps
gopdfrab.DefaultLimits()  // the built-in caps
The caps are process-wide
They are enforced deep in the decode path, reached from many callers that hold no document handle, so one value applies uniformly rather than per call. Set them once at startup, before any concurrent verify or convert. A zero or negative field resets that cap to its default.

Fidelity

Conforming to PDF/A is not the same as looking like the input — a page blanked during conversion still verifies clean. Options.CheckFidelity renders the input and the output and reports a per-page comparison so you can catch that.

fidelity.go
// Conforming to PDF/A is not the same as *looking* like the input -- a page
// blanked during conversion still verifies clean.
cr, err := gopdfrab.ConvertContext(ctx, path, gopdfrab.PDFA1B,
	gopdfrab.Options{CheckFidelity: true})
defer cr.Close()

for _, pf := range cr.Fidelity {
	if pf.Blanked() {
		log.Printf("page %d lost its content during conversion", pf.Page)
	}
}

// Content the rasterizer could not draw is reported, never silently omitted
for _, d := range cr.RasterDrops {
	log.Printf("page %d: dropped %v", d.Page, d.Features)
}

cr.RasterizedPages // pages rebuilt as a flat image

Both sides are drawn by the same rasterizer, so its limitations cancel and the comparison isolates what the conversion changed. Blanked() flags unambiguous content loss without tripping on benign changes like font substitution.

When conversion has to rasterize a page as a last resort, anything the rasterizer cannot draw — shadings, inline images, Type 3 fonts — is reported per page in RasterDrops rather than silently omitted, so that loss is loud even though the pixel comparison (which drops it symmetrically) cannot see it.

Streaming and batching

Save writes the output to a file and WriteTo streams it to any io.Writer — both without holding a second copy. Output() returns the bytes when you need them in memory. All three error when there is no output. A large output spills to a temp file rather than staying resident, so call Close() when done.

streaming.go
// Save writes to a file; WriteTo streams to any io.Writer -- both without
// holding a second copy. Output returns the bytes when you need them in memory.
err := cr.Save("out.pdf")
_, err = cr.WriteTo(w)  // e.g. an http.ResponseWriter or a bytes.Buffer
b, err := cr.Output()

// For a batch too large to hold every output at once, ConvertEach streams:
// it calls the callback on each result as it completes and closes it for you.
err = gopdfrab.ConvertEach(paths, gopdfrab.PDFA1B, gopdfrab.Options{Workers: 4},
	func(r gopdfrab.FileResult[gopdfrab.ConvertResult]) error {
		if r.Err != nil {
			return nil // skip this file, keep going
		}
		return r.Result.Save(filepath.Join(outDir, filepath.Base(r.Path)))
	})

For a batch too large to hold every output in memory at once, ConvertEach streams instead: it calls a callback on each result as it completes, serialized and in completion order, and closes each result for you. Returning a non-nil error from the callback stops the batch. Options.Workers bounds the concurrency of both batch forms.

Selective check profiles

Narrow verification to the rules you care about. Start from the full PDFA1B profile and remove checks, or start from an empty profile and add only what you need. Profiles are immutable — AddCheck, RemoveCheck and Clear each return a clone — so one profile can be shared across concurrent calls.

profiles.go
// Start from the full profile and remove checks
p := gopdfrab.PDFA1B.
	RemoveCheck(gopdfrab.Checks.Structure.FileHeaderSignature).
	RemoveCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)

res, err := doc.Verify(p)

// Or start from an empty profile and add only what you need
p2 := gopdfrab.PDFA1B.Clear().
	AddCheck(
		gopdfrab.Checks.Transparency.ImageWithSoftMask,
		gopdfrab.Checks.Metadata.PDFAIdentifierMissing,
	)

res2, err := doc.Verify(p2)

Checks are grouped by spec area in the Checks registry:

Registry field Spec area
Checks.Structure6.1.x — file header, trailer, xref, object framing, limits
Checks.Colour6.2.2 OutputIntent, 6.2.3.x device colours, 6.2.9–10
Checks.Image6.2.4–6.2.7 image/form/PostScript XObjects
Checks.Transparency6.2.8 transfer functions, 6.4 soft masks/blend modes/alpha
Checks.Font6.3.x embedding, subsets, metrics, encoding
Checks.Annotation6.5.x annotation types and dictionaries
Checks.Action6.6.x action types and additional actions
Checks.Metadata6.7.x XMP metadata, extension schemas, PDF/A identifier
Checks.Form6.9 interactive forms
Checks.ObjectModelGeneric ISO 32000 object-model conformance, independent of PDF/A

AllChecks() enumerates every registered check, and CheckByClause / ChecksForClause look checks up directly by clause.

lookup.go
gopdfrab.AllChecks()                       // every registered check, with names, descriptions, clauses
gopdfrab.CheckByClause("6.3.4", 1)          // a single check by clause + index
gopdfrab.ChecksForClause("6.3.4")           // all checks registered under a clause

Inspecting issues

Each PDFError exposes the Check that flagged it, along with its page and underlying messages. Result has helpers for grouping and summarizing issues.

diagnostics.go
for _, issue := range v.Issues {
	c := issue.Check()
	fmt.Println(c.Clause(), c.Subclause(), c.Name(), c.Description())
	fmt.Println(issue.Page(), issue.Messages())
}

fmt.Println(v.Summary())   // human-readable report, one line per Check
v.Checks()                 // distinct Checks violated, sorted by clause
v.IssuesByCheck()          // map[Check][]PDFError
v.IssuesOnPage(1)          // issues found on page 1 (0 = document-level)

JSON output

Result, PDFError and Check marshal to a stable JSON shape, for CLI, service, or CI integration. The command-line tool emits the same shape under --json.

json.go
// Result, PDFError and Check marshal to a stable JSON shape,
// for CLI, service, or CI integration.
b, err := json.Marshal(v)

// {"type":"A-1b","valid":false,"issueCount":2,"issues":[
//   {"check":{"name":"...","clause":"6.1.3",...},
//    "page":0,"documentLevel":true,"messages":["..."],"text":"..."}]}

Document helpers

Shortcuts on an open document for the most common questions: whether it's PDF/A compliant or even valid PDF, what conformance it claims, its page count and version, and its Info-dictionary and raw XMP metadata.

helpers.go
ok, err := doc.IsPDFA()   // shorthand for Verify(PDFA1B).Valid

ok, err = doc.IsPDF()     // shorthand for VerifyObjectModel().Valid

part, level, err := doc.ClaimedConformance() // e.g. "1", "B" -- what the file claims, not whether it's valid

n, err := doc.PageCount()   // number of pages

version, err := doc.Version()  // PDF version from the header, e.g. "1.7"

info, err := doc.Metadata()    // Info dictionary entries (Title, Author, ...)

xmp, err := doc.XMPMetadata()  // raw XMP packet bytes, decoded to UTF-8

Legacy Isartor profile

The Isartor test suite is the old reference test suite for PDF/A-1b compatibility, predating the veraPDF project. If your application needs PDF/A-1b compatibility judged against Isartor specifically, use the Legacy1B profile instead of the default PDFA1B. Legacy1B is the strict, fully spec-literal reading; PDFA1B is tuned to match veraPDF's interpretation where the two diverge.

legacy.go
// Verify against the legacy Isartor-derived profile instead of the
// veraPDF-aligned default
v, err := doc.Verify(gopdfrab.Legacy1B)