gopdfrab
July 10, 2026 · View on GitHub
PDF/A processing for go!
Verify and convert PDF documents with a small, predictable open source library.
Disclaimer
This project is at an early stage and under active development. The API is not final and will change heavily until the first proper release.
Features
- PDF structural integrity verification (Arlington model)
- PDF/A verification
- PDF/A conversion
Roadmap
PDF/A-1b verification and conversion is still at an early stage, and appropriate testing infrastructure must be created to harden it.
Next up are the implementation of capabilities for verification and conversion of:
- PDF/A-2
- PDF/A-3
- PDF/A-4
Getting Started
A full example can be found under main/main.go
Add gopdfrab
go get github.com/voidrab/gopdfrab
Import gopdfrab
import (
"github.com/voidrab/gopdfrab"
)
Initialize a Document
doc, err := gopdfrab.Open(path)
if err != nil {
log.Fatal(err)
}
PDF/A Validation
v, err := doc.Verify(gopdfrab.PDFA_1B)
if err != nil {
log.Println(err)
}
if v.Valid {
fmt.Println("Document is PDF/A-1b compliant")
} else {
fmt.Println("Document is not PDF/A-1b compliant")
fmt.Println("Issues:")
for i, v := range v.Issues {
fmt.Printf("#%v: %v\n", i+1, v)
}
}
Finally, close doc.
doc.Close()
Verify a File
Verify opens, verifies, and closes a file.
result, err := gopdfrab.Verify(path, gopdfrab.PDFA_1B)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Valid)
Verifying In-Memory Data
VerifyBytes is Verify for an in-memory PDF.
result, err := gopdfrab.VerifyBytes(data, gopdfrab.PDFA_1B)
Verifying Multiple Files
VerifyAll opens, verifies, and closes a batch of files concurrently.
results, err := gopdfrab.VerifyAll(paths, gopdfrab.PDFA_1B)
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)
}
Inspecting Issues
Each PDFError in v.Issues exposes the Check that flagged it, along with its page and underlying messages.
for _, issue := range v.Issues {
c := issue.Check()
fmt.Println(c.Clause(), c.Subclause(), c.Name(), c.Description())
fmt.Println(issue.Page(), issue.Messages())
}
Result has helpers for grouping and summarizing issues:
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)
Document Helpers
ok, err := doc.IsPDFA() // shorthand for Verify(A_1B).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
xmp, err := doc.XMPMetadata() // raw XMP packet bytes, decoded to UTF-8
Converting 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.
cr, err := gopdfrab.Convert(path, gopdfrab.PDFA_1B)
if err != nil {
log.Fatal(err)
}
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
Converting an Open Document
cr, err := doc.Convert(gopdfrab.PDFA_1B)
Converting In-Memory Data
ConvertBytes is Convert for an in-memory PDF.
cr, err := gopdfrab.ConvertBytes(data, gopdfrab.PDFA_1B)
Converting Multiple Files
ConvertAll opens, converts, and closes a batch of files concurrently.
results, err := gopdfrab.ConvertAll(paths, gopdfrab.PDFA_1B)
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 Residuals
Even though Convert always returns its best attempt, the result may still carry residual issues if no automatic remediation — including the raster last resort — fully resolved them.
residual := cr.Residual()
for _, iss := range residual {
check := iss.Check()
fmt.Println(check.Clause(), check.Name())
fmt.Println(iss.Page(), iss.Messages())
}
Selective Check Profiles
Verification can be narrowed to a specific set of rules using Verify.
Start from the full profile and remove checks
p := gopdfrab.PDFA_1B.
RemoveCheck(gopdfrab.Checks.Structure.FileHeaderSignature).
RemoveCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)
res, err := doc.Verify(p)
Start from an empty profile and add checks
p := gopdfrab.PDFA_1B.Clear().
AddCheck(
gopdfrab.Checks.Transparency.ImageWithSoftMask,
gopdfrab.Checks.Metadata.PDFAIdentifierMissing,
)
res, err := doc.Verify(p)
Available check groups
| 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 — see below |
Use gopdfrab.AllChecks() to enumerate all registered checks with their names, descriptions, and clause numbers. gopdfrab.CheckByClause("6.3.4", 1) and gopdfrab.ChecksForClause("6.3.4") look up checks by clause directly.
PDF Object-Model Conformance
Checks.ObjectModel holds six checks — MissingRequiredKey, WrongValueType, DisallowedValue, IndirectRequired, KeyIntroducedAfterPDF14, ConstraintViolated — 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.
res, err := gopdfrab.VerifyObjectModel(path)
VerifyObjectModelBytes is the in-memory equivalent, and doc.VerifyObjectModel() runs it on an already-open Document:
res, err := gopdfrab.VerifyObjectModelBytes(data)
res, err := doc.VerifyObjectModel()
These are shorthand for Verify/VerifyBytes/doc.Verify with gopdfrab.ObjectModelOnly(), a profile enabling only the six checks above:
res, err := doc.Verify(gopdfrab.ObjectModelOnly())
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.
cr, err := gopdfrab.ConvertObjectModel(path)
cr, err := gopdfrab.ConvertObjectModelBytes(data)
cr, err := doc.ConvertObjectModel()
Performance
gopdfrab's PDF/A-1b verification performance is (unfairly) measured against the Java-based veraPDF and PDFBox Preflight on the combined Isartor + veraPDF corpora (773 files); see benchmarks/README.md for methodology.
| Benchmark | gopdfrab vs veraPDF | gopdfrab vs PDFBox Preflight |
|---|---|---|
| Startup time | 149x faster | 22x faster |
| Single file (cold, median file) | 160x faster | 50x faster |
| Batch throughput | 20x faster | 15x faster |
| Batch peak memory | 11x smaller | 15x smaller |
| Deployment footprint | 9x smaller | 3x smaller |
Absolute numbers from the same run: the full 773-file batch verifies in 0.29 s (2749 files/s) at 67 MB peak RSS, and a cold single-file verification of the median corpus file takes ~5 ms including process startup.
Due to JVM startup overhead, startup time and cold single-file verification are significantly slower for veraPDF and Preflight.
Isartor Compatibility
The Isartor test suite is the old reference test suite for PDF/A-1b document compatibility before the veraPDF project was initiated.
If you require PDF/A-1b compatibility based on Isartor for your application, use the Legacy_1B profile.
Fuzzing & Stress Testing
Because gopdfrab's whole job is to read untrusted, frequently-malformed PDFs, the
internal/pdfgen package programmatically generates "crazy, broken" PDF documents
— structurally-valid skeletons deliberately corrupted with truncation, bad
cross-reference offsets, negative stream lengths, dangling and circular
references, deep nesting, and more. Everything is generated in memory from a seed
(no external document files), so any crash is reproducible from its seed alone via
pdfgen.Generate(seed).
The generator also builds fresh random object graphs from a small PDF grammar
(pdfgen.GenerateGrammar) to reach shapes that corrupting a fixed seed never
produces.
These inputs drive native Go fuzz targets at three levels:
- Whole pipeline —
FuzzOpenBytes/FuzzLexer(parser),FuzzVerifyBytes,FuzzConvertBytes,FuzzConvertRoundTrip, andFuzzGeneratedSeed(which lets the fuzzer explore the generator's own seed space under coverage guidance). - Isolated subsystems — the decoders and parsers that whole-file fuzzing only
reaches shallowly:
FuzzDecodeStream,FuzzInflateZlib,FuzzDecodeASCIIHex,FuzzDecodeASCII85,FuzzDecodeLZW,FuzzDecodeCCITT,FuzzUndoPredictor,FuzzTokenizeContent,FuzzParseFunction,FuzzResolveColor, and the writer targets (FuzzWritePDF,FuzzWriteContentStream,FuzzBuildInlineImageBytes). - Semantic oracles — beyond "does not panic":
FuzzVerifyDeterministicandFuzzConvertDeterministic(repeat runs must match byte-for-byte),FuzzConvertHonest(a conversion reported valid must independently re-verify as valid), andFuzzConvertConverges.
Every target seeds its corpus in code, so the generated broken PDFs replay on
every go test run; TestGeneratedCorpusDoesNotPanic additionally drives a
deterministic batch through the public API on every build, and named
TestCrasher_* reproducers guard each previously-fixed crash. Concurrency and
resource bounds are covered by TestGeneratedCorpusRace /
TestConcurrentDecodeIsSafe (run under -race) and TestGeneratedCorpusTimeBounded.
To actively hunt for new crashes locally:
go test -run '^$' -fuzz=FuzzOpenBytes -fuzztime=60s ./internal/pdf/
go test -run '^$' -fuzz=FuzzParseFunction -fuzztime=60s ./internal/pdf/
go test -run '^$' -fuzz=FuzzConvertRoundTrip -fuzztime=60s .
go test -race -run 'TestGeneratedCorpusRace|TestConcurrentDecodeIsSafe' ./...
Licensing
This work is dual-licensed under GNU AGPL 3.0 and our commercial license. Get in touch for more information about our commercial licensing options.
Contributing
Contributions are welcome! Whether it's a bug report, a failing test file, a new check, or a performance improvement — all of it helps.
- Bug reports & questions — open an issue.
- Code changes — fork the repo, make your change on a feature branch, and open a pull request. Please keep pull requests focused: one concern per PR.
- New checks — if you have a PDF document that contains properties which aren't covered yet, open an issue first so we can agree on the approach before you write the code.
- Test files — if you have PDFs that expose edge cases or regressions, attach them to an issue.
All contributions are made under the AGPL.