Getting started
Install
Add gopdfrab to your module.
go get github.com/voidrab/gopdfrabOpen a document
Open parses the file and builds the structures needed for verification. Always close the document when you're done with it.
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.
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 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.
// 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.
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.
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 conformantConversion 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.
// 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 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 |
|---|---|
| ErrNotPDF | the input is not a PDF (no %PDF- header) |
| ErrDamaged | a PDF whose cross-reference or trailer structure could not be parsed |
| ErrEncrypted | an encryption scheme gopdfrab does not implement |
| ErrPasswordRequired | a correct password is required to open the file |
| ErrUnresolvableGraph | Convert could not resolve the object graph, so no output was produced |
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.
// "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.
// 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.
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 | []byte | User or owner password for an encrypted input. nil is the empty password. | Verify + Convert |
| RasterDPI | int | Resolution for the raster last resort and transparency flattening. 0 = 150. | Convert |
| MaxIterations | int | Bounds the verify/fix loop. 0 = 4. | Convert |
| CheckFidelity | bool | Render input and output and populate Fidelity with a per-page comparison. Roughly doubles the work. | Convert |
| Workers | int | Bounds 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.
// 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 capsFidelity
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.
// 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.
// 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.
// 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.Structure | 6.1.x — file header, trailer, xref, object framing, limits |
| Checks.Colour | 6.2.2 OutputIntent, 6.2.3.x device colours, 6.2.9–10 |
| Checks.Image | 6.2.4–6.2.7 image/form/PostScript XObjects |
| Checks.Transparency | 6.2.8 transfer functions, 6.4 soft masks/blend modes/alpha |
| Checks.Font | 6.3.x embedding, subsets, metrics, encoding |
| Checks.Annotation | 6.5.x annotation types and dictionaries |
| Checks.Action | 6.6.x action types and additional actions |
| Checks.Metadata | 6.7.x XMP metadata, extension schemas, PDF/A identifier |
| Checks.Form | 6.9 interactive forms |
| Checks.ObjectModel | Generic ISO 32000 object-model conformance, independent of PDF/A |
AllChecks() enumerates every registered check, and CheckByClause / ChecksForClause look checks up directly by clause.
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 clauseInspecting issues
Each PDFError exposes the Check that flagged it, along with its page and underlying messages. Result has helpers for grouping and summarizing issues.
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.
// 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.
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-8Legacy 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.
// Verify against the legacy Isartor-derived profile instead of the
// veraPDF-aligned default
v, err := doc.Verify(gopdfrab.Legacy1B)