個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證PDF/A processing for Go
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
PDF/A processing for go!
Verify and convert PDF documents with a small, predictable open source library.
PDF/A-1b verification and conversion are implemented and tested against the full
Isartor and veraPDF conformance suites (see Performance and
Fuzzing & Stress Testing). The API is stable as of
1.0 — CHANGELOG.md states the versioning and stability policy.
To report a vulnerability, see SECURITY.md.檢視完整 README ↗PDF/A processing for go!
Verify and convert PDF documents with a small, predictable open source library.
PDF/A-1b verification and conversion are implemented and tested against the full Isartor and veraPDF conformance suites (see Performance and Fuzzing & Stress Testing). The API is stable as of 1.0 — CHANGELOG.md states the versioning and stability policy. To report a vulnerability, see SECURITY.md.
PDF/A-1b is complete: every check in scope is implemented and both conformance suites pass. PDF/A-2, -3 and -4 come next.
A CLI ships under cmd/gopdfrab:
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:
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
PASS, FAIL or ERROR; the path is the rest of the
line. FAIL gives the number of issues and of issue lines.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) |
wasm/ is a syscall/js wrapper that runs verification and conversion in the
browser, with no server involved:
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.
go get github.com/voidrab/gopdfrab
import (
"github.com/voidrab/gopdfrab"
)
doc, err := gopdfrab.Open(path)
if err != nil {
log.Fatal(err)
}
OpenBytes does the same for a PDF already in memory.
Encrypted documents are decrypted transparently on open when they use the empty
user password. Supply a user or owner password
explicitly with OpenWithPassword:
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.
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.
doc.Close()
Verify opens, verifies, and closes a file.
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.
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)
}
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.
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)
Result, PDFError and Check marshal to a stable JSON shape, for CLI, service, or CI integration:
b, _ := json.Marshal(v)
// {"type":"A-1b","valid":false,"issueCount":2,"issues":[{"check":{"name":"...","clause":"6.1.3",...},"page":0,"documentLevel":true,"messages":["..."],"text":"..."}]}
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
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.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).
_, err := cr.WriteTo(w) // e.g. an http.ResponseWriter or a bytes.Buffer
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.
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.
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:
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.
cr, err := doc.Convert(gopdfrab.PDFA1B)
ConvertBytes is Convert for an in-memory PDF.
cr, err := gopdfrab.ConvertBytes(data, gopdfrab.PDFA1B)
ConvertAll opens, converts, and closes a batch of 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
}
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).
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.
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())
}
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:
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.
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.
p := gopdfrab.PDFA1B.
RemoveCheck(gopdfrab.Checks.Structure.FileHeaderSignature).
RemoveCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)
res, err := doc.Verify(p)
p := gopdfrab.PDFA1B.Clear().
AddCheck(
gopdfrab.Checks.Transparency.ImageWithSoftMask,
gopdfrab.Checks.Metadata.PDFAIdentifierMissing,
)
res, err := doc.Verify(p)
NewProfile returns an empty profile for a conformance level, for building a
rule set up from scratch:
p := gopdfrab.NewProfile(gopdfrab.A1B).
AddCheck(gopdfrab.Checks.Font.SimpleNotEmbedded)
res, err := doc.Verify(p)
| 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.
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.PDF, the profile enabling only the six checks above:
res, err := doc.Verify(gopdfrab.PDF)
ObjectModelOnly() builds a fresh profile equal to PDF, for a caller who wants
one that nothing else shares.
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()
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.
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 Legacy1B profile.
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:
FuzzOpenBytes/FuzzLexer (parser), FuzzVerifyBytes,
FuzzConvertBytes, FuzzConvertRoundTrip, and FuzzGeneratedSeed (which lets
the fuzzer explore the generator's own seed space under coverage guidance).FuzzDecodeStream, FuzzInflateZlib, FuzzDecodeASCIIHex,
FuzzDecodeASCII85, FuzzDecodeLZW, FuzzDecodeCCITT, FuzzUndoPredictor,
FuzzTokenizeContent, FuzzParseFunction, FuzzResolveColor, and the writer
targets (FuzzWritePDF, FuzzWriteContentStream, FuzzBuildInlineImageBytes).FuzzVerifyDeterministic and
FuzzConvertDeterministic (repeat runs must match byte-for-byte),
FuzzConvertHonest (a conversion reported valid must independently re-verify as
valid), and FuzzConvertConverges.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' ./...
gopdfrab parses untrusted, frequently-hostile input by design. To report a suspected vulnerability, follow SECURITY.md — please do not open a public issue for one.
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.
gopdfrab bundles third-party fonts and ICC profiles, and embeds them into converted files. NOTICE says where each one comes from and under what licence.
Code contributions are covered by a one-time Contributor Licence Agreement; see below.
CONTRIBUTING.md goes over how to run the test suites, where the test corpora come from, and walkthroughs. ARCHITECTURE.md is a map of the codebase.
The short version:
Code and documentation are dual-licensed, AGPL 3.0 and a commercial licence, and that only works if one party can release the whole codebase under both, so pull requests need a one-time signature on the Contributor Licence Agreement. You keep your copyright; you are granting permission, not handing anything over. A bot will ask you to confirm it in a comment the first time you open a pull request.