Awesome Go

gopdfrab

CategoryFile Handling
SubcategoryFile Handling
Stars2

PDF/A processing for Go

About gopdfrab

This large README is shown as a plain-text preview. Read the full README at the source

# gopdfrab

[![codecov](https://codecov.io/gh/voidrab/gopdfrab/branch/main/graph/badge.svg)](https://codecov.io/gh/voidrab/gopdfrab)
[![Go Reference](https://pkg.go.dev/badge/github.com/voidrab/gopdfrab.svg)](https://pkg.go.dev/github.com/voidrab/gopdfrab)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)

PDF/A processing for go!

Verify and convert PDF documents with a small, predictable open source library.

## Status

PDF/A-1b verification and conversion are implemented and tested against the full
Isartor and veraPDF conformance suites (see [Performance](#performance) and
[Fuzzing & Stress Testing](#fuzzing--stress-testing)). The API is **stable as of
1.0** — [CHANGELOG.md](CHANGELOG.md) states the versioning and stability policy.
To report a vulnerability, see [SECURITY.md](SECURITY.md).

## Features

- PDF structural integrity verification (Arlington model)
- PDF/A-1b verification
- PDF/A-1b conversion
- Decryption of encrypted PDFs (RC4 40/128, AES-128, AES-256), with the empty or
  a supplied user/owner password

## Roadmap

PDF/A-1b is complete: every check in scope is implemented and both conformance
suites pass. PDF/A-2, -3 and -4 come next.

## Command-line tool

A CLI ships under `cmd/gopdfrab`:

```bash
go install github.com/voidrab/gopdfrab/cmd/gopdfrab@latest

gopdfrab verify docs/                     # verify every PDF under a directory
gopdfrab verify --json report.pdf         # machine-readable output
gopdfrab convert in.pdf out.pdf           # rewrite towards PDF/A-1b
gopdfrab convert --dpi 300 in.pdf         # tune the raster fallback
gopdfrab verify --max-decoded-mb 64 x.pdf # cap decoded stream output at 64 MB
```

Subcommands are `verify`, `convert`, `version` and `help`. Exit codes are `0`
conformant, `1` non-conformant, `2` error, so it drops into scripts and CI
directly. `verify` walks directories recursively.

`verify` prints a fixed layout that scripts can parse:

```text
profile: pdfa1b

FAIL  issues=974 distinct=36  docs/L34a.pdf
  6.1.2/5     count=1    pages=doc        objs=1    "ViewerPreferences entry /PrintScaling requires PDF 1.5 or later"
  6.2.3.3/2   count=149  pages=1-2        objs=127  "device colour (gray) used in content stream without matching OutputIntent"
  6.3.4/1     count=3    pages=doc,1      objs=3    "font Courier-Bold is not embedded"
  6.6.1/1     count=258  pages=doc,1-2,4  objs=258  "forbidden action type /JavaScript"
  ...
  objmodel/1  count=3    pages=1-2        objs=0    "Appearance is missing required key \"N\""
PASS  docs/ok.pdf
ERROR docs/broken.pdf
  error "failed to parse structure: pdf: not a PDF document: no %PDF- header"

summary: pass=1 fail=1 error=1 files=3
```

- A file line starts with `PASS`, `FAIL` or `ERROR`; the path is the rest of the
  line. `FAIL` gives the number of issues and of issue lines.
- Indented lines belong to the file above. An issue line covers every report of
  one check with one message: clause[/subclause], `count=<n>` reports,
  `pages=` the pages they were on (`doc` for document-level, runs as ranges),
  `objs=<n>` distinct objects, and the messages as one Go-quoted string. Lines
  are sorted by clause, then message.
- `--detail` lists every location instead, one line per page and object:
  clause, `page=<n|doc>`, `obj=<num>.<gen>` or `obj=-`, `count=<n>` repeats there,
  and the messages.
- `convert` prints `profile:`, `input:`, `output:`, `iterations:`, then
  optionally `rasterized: pages=<ranges>` for pages rebuilt as an image,
  `dropped: pages=<ranges> features=<n>` with one `pages=<ranges>` line per
  feature the rasterizer could not draw (the feature Go-quoted), and
  `lost: objects=<n> distinct=<n>`, and last `result: conformant` or
  `result: residual issues=<n> distinct=<n>`, with the same issue lines beneath.

`--json` reports every occurrence, ungrouped.

Both subcommands take:

| Flag | Meaning |
|---|---|
| `--profile` | `pdfa1b` (default), `legacy1b`, or `pdf` |
| `--password` | password for an encrypted input. It is visible to other users in `ps`; set `GOPDFRAB_PASSWORD` instead to keep it out of the process list |
| `--max-decoded-mb` | cap a single stream's decoded output in MB (0 = default 256) |
| `--max-resident-mb` | cap a document's rebuildable caches in MB (0 = default 64) |
| `--json` | emit machine-readable JSON |
| `--detail` | list every page and object an issue was found on, instead of grouping by check and message |

`convert` also takes:

| Flag | Meaning |
|---|---|
| `--dpi` | raster fallback resolution (0 = default 150) |
| `--max-iterations` | verify/fix loop bound (0 = default 4) |
| `-o` | output path (default: the input with a `.pdfa.pdf`/`.fixed.pdf` suffix) |

## WebAssembly

`wasm/` is a `syscall/js` wrapper that runs verification and conversion in the
browser, with no server involved:

```bash
GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o gopdfrab.wasm ./wasm
```

It registers two globals, both taking a `Uint8Array` and returning a Promise:

- `gopdfrabVerify(bytes)` resolves to `{valid, summary, profile, issueCount, doc, issues}`.
- `gopdfrabConvert(bytes)` resolves to `{valid, iterations, output, doc, before, resolved, residual, rasterizedPages, rasterDrops, lostObjects}`, where `output` is the converted PDF as a `Uint8Array`.

Each issue carries `{clause, subclause, name, description, group, page, documentLevel, messages}`; `doc` reports what the file says about itself (`pageCount`, `version`, `claimedPart`, `claimedLevel`, `title`, `author`), each field best-effort. The doc comments in `wasm/main.go` are the full reference.

On js/wasm there is no filesystem, so only the `*Bytes` entry points apply.

## Getting Started

### Add gopdfrab

```bash
go get github.com/voidrab/gopdfrab
```

### Import gopdfrab

```go
import (
  "github.com/voidrab/gopdfrab"
)
```

### Initialize a Document

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

`OpenBytes` does the same for a PDF already in memory.

### 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`:

```go
doc, err := gopdfrab.OpenWithPassword(path, []byte("secret"))
if errors.Is(err, gopdfrab.ErrPasswordRequired) {
  log.Fatal("a correct password is required to open this file")
}
```

`OpenBytesWithPassword` is the same for in-memory data.

`Verify` and `Convert` decrypt the same way. A file that needs a
password is reported with `ErrPasswordRequired` rather than
producing a broken result.

### PDF/A Validation

```go
v, err := doc.Verify(gopdfrab.PDFA1B)
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.

```go
doc.Close()
```

### Verify a File

`Verify` opens, verifies, and closes a file.

```go
result, err := gopdfrab.Verify(path, gopdfrab.PDFA1B)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result.Valid)
```

### Verifying In-Memory Data

`VerifyBytes` is `Verify` for an in-memory PDF.

```go
result, err := gopdfrab.VerifyBytes(data, gopdfrab.PDFA1B)
```

### Verifying Multiple Files

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

```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)
}
```

### Typed Errors

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

| Error | Meaning |
|---|---|
| `gopdfrab.ErrNotPDF` | the input is not a PDF (no `%PDF-` header) |
| `gopdfrab.ErrDamaged` | a PDF whose cross-reference or trailer structure could not be parsed |
| `gopdfrab.ErrEncrypted` | an encryption scheme gopdfrab does not implement |
| `gopdfrab.ErrPasswordRequired` | a correct password is required to open the file |
| `gopdfrab.ErrUnresolvableGraph` | `Convert` could not resolve the object graph, so no output was produced |
| `gopdfrab.ErrInternal` | a defect in gopdfrab itself, recovered from a panic; please report it with the input file |

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 an issue and every other check
still runs. A conversion that had to null an unrecoverable object keeps that
loss in `Residual()` and never reports the result as valid.

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, so a badly damaged file still verifies and converts.

### Inspecting Issues

Each `PDFError` in `v.Issues` exposes the `Check` that flagged it, along with its page and underlying messages.

```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())
}
```

`Result` has helpers for grouping and summarizing issues:

```go
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)
```

`Result`, `PDFError` and `Check` marshal to a stable JSON shape, for CLI, service, or CI integration:

```go
b, _ := 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

```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
```

### 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.

```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
```

`cr.Save(path)` writes the output to a file and `cr.WriteTo(w)` streams it to any `io.Writer` (it implements `io.WriterTo`) — both without holding a second copy; `cr.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 `cr.Close()` when done (`ConvertEach` closes each result for you).

```go
_, err := cr.WriteTo(w) // e.g. an http.ResponseWriter or a bytes.Buffer
```

### 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 — `VerifyContext`, `VerifyBytesContext`, `VerifyAllContext`, `ConvertContext`, `ConvertBytesContext`, `ConvertAllContext`. The zero `Options` value is the default behavior.

```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)
})
```

To set options without a deadline, pass `context.Background()`. `Options.Password` applies at the open step (so it works on `ConvertContext`/`VerifyContext` but not the `*Document` methods, whose file is already open — use `OpenWithPassword`). `RasterDPI` and `MaxIterations` are convert-only; `Verify` reads only `Password`.

`ConvertContext` checks the context before each verify/fix iteration and each raster pass; `ConvertAllContext`/`VerifyAllContext` 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. Raise it for legitimately large PDFs or lower it to harden against hostile input with `SetLimits`:

```go
gopdfrab.SetLimits(gopdfrab.Limits{MaxDecodedStreamBytes: 512 << 20}) // 512 MB
```

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

### Converting an Open Document

```go
cr, err := doc.Convert(gopdfrab.PDFA1B)
```

### Converting In-Memory Data

`ConvertBytes` is `Convert` for an in-memory PDF.

```go
cr, err := gopdfrab.ConvertBytes(data, gopdfrab.PDFA1B)
```

### Converting Multiple Files

`ConvertAll` opens, converts, and closes a batch of files concurrently.

```go
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
}
```

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, in completion order), so you can write each output and let it be collected. `Options.Workers` bounds the concurrency of both forms (0 = `runtime.NumCPU`).

```go
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)))
    })
```

Returning a non-nil error from the callback stops the batch and is returned from `ConvertEach`. `ConvertEachContext` is the same with a `context.Context`.

### 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.

```go
residual := cr.Residual()
for _, iss := range residual {
    check := iss.Check()
    fmt.Println(check.Clause(), check.Name())
    fmt.Println(iss.Page(), iss.Messages())
}
```

### 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:

```go
cr, _ := gopdfrab.ConvertContext(ctx, path, gopdfrab.PDFA1B,
    gopdfrab.Options{CheckFidelity: true})
for _, pf := range cr.Fidelity {
    if pf.Blanked() {
        log.Printf("page %d lost its content during conversion", pf.Page)
    }
}
```

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
can't draw — shadings, inline images, Type 3 fonts — is reported per page in
`cr.RasterDrops` rather than silently omitted, so that loss is loud even though
the pixel comparison (which drops it symmetrically) cannot see it.

Content the conversion could not carry over at all — an object the reader could
not resolve, a stream nothing can decode — is listed in `cr.LostObjects`. That is
a separate fact from conformance: the file that was written can meet PDF/A-1b and
still be missing something the input had, and `cr.Result.Valid` answers only the
first question.

## Selective Check Profiles

Verification can be narrowed to a specific set of rules using `Verify`.

`PDFA1B`, `Legacy1B` and `PDF` are package variables holding the ready-made
profiles. A profile is immutable — `AddCheck`, `RemoveCheck` and `Clear` each
return a clone — but the variables are not: reassigning one changes the default
for the whole process.

### Start from the full profile and remove checks

```go
p := gopdfrab.PDFA1B.
    RemoveCheck(gopdfrab.Checks.Structure.FileHeaderSignature).
    RemoveCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)

res, err := doc.Verify(p)
```

### Start from an empty profile and add checks

```go
p := gopdfrab.PDFA1B.Clear().
    AddCheck(
        gopdfrab.Checks.Transparency.ImageWithSoftMask,
        gopdfrab.Checks.Metadata.PDFAIdentifierMissing,
    )

res, err := doc.Verify(p)
```

### Start from nothing

`NewProfile` returns an empty profile for a conformance level, for building a
rule set up from scratch:

```go
p := gopdfrab.NewProfile(gopdfrab.A1B).
    AddCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)

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 |

An eleventh group, `Checks.LogicalStructure` (6.8.x tagging, structure tree,
role map, natural language), is registered but left out of the table: all five
of its checks are PDF/A-1a only. `AllChecks()` returns 159 checks over all
eleven groups, 154 over the ten above.

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](https://github.com/pdf-association/arlington-pdf-model), the machine-readable ISO 32000 object model. They answer "is this even valid PDF," independent of any PDF/A conformance level.

```go
res, err := gopdfrab.VerifyObjectModel(path)
```

Frequently Asked Questions

What is gopdfrab?

gopdfrab is a File Handling library for the Go programming language. PDF/A processing for Go

How do I install gopdfrab?

Install gopdfrab with the Go module system using `go get voidrab/gopdfrab`. Check the repository for the current installation instructions.

What category does gopdfrab belong to?

gopdfrab is listed under File Handling, specifically File Handling.

← Back to File Handling