← All projects

active

gobig2

Pure-Go JBIG2 decoder. Within 1.3–1.6× of jbig2dec on every fixture, no cgo, with hard caps on every attacker-controlled allocation.

GoJBIG2ITU-T T.88PDFCGO-free

Highlights

  • Pure Go, no cgo, no third-party runtime deps — drops into any Go toolchain unchanged.
  • Within 1.3–1.6× of jbig2dec across the full benchmark corpus; beats every PDF-toolchain decoder on every fixture.
  • Every attacker-controlled allocation gated by a Limits cap before allocation — safe to feed adversarial bytes from a PDF crawler.
  • Two constructors covering both wire forms: standalone .jb2 files (T.88 Annex E header) and PDF-embedded /JBIG2Decode streams with optional /JBIG2Globals.
  • DecodePacked returns 1-bpp bilevel data directly — saves ~12 ms wall + ~35 MB alloc on a 600 dpi A4 page vs. the image.Gray path.

JBIG2 is the bilevel-image compression scheme inside the /JBIG2Decode filter that the majority of scanned PDFs lean on. The dominant open-source decoder is jbig2dec — C, hand-tuned, the de-facto reference. Every Go program that wanted to decode JBIG2 either reached for it through cgo or did without. gobig2 was built to remove that compromise.

Why pure Go

PDF tooling that links cgo inherits a cross-compile story it doesn’t want. Go services that index, OCR, or render PDFs for crawlers run on linux/arm64 containers, on lambda runtimes, on developer laptops — the moment cgo enters the picture, every one of those build paths needs a C toolchain matched to the target. The pure-Go constraint isn’t aesthetic; it’s the difference between go build and a multi-stage cross-compile.

The cost is real: jbig2dec is C with two decades of hand-tuning. Closing to within 1.3–1.6× of it without asm or cgo took uncomfortably careful work on the arithmetic decoder, the symbol dictionary cache, and the generic region decode loop. The benchmark table in the README has the numbers per fixture; the short version is the gap shows up on small bitmaps where decoder overhead dominates and disappears on real pages where the work is in the codec, not the framing.

Safe by construction

JBIG2 is a denial-of-service vector. A 100-byte segment header can declare a 30 GiB region. A symbol dictionary can claim millions of symbols. A halftone grid can claim a billion cells. Every decoder that trusts those declarations and allocates straight from them is one malicious PDF away from OOM.

The codec exposes a Limits struct that caps every attacker-controlled count before allocation: image pixels, symbols per dictionary, halftone grid cells, IAID code length, refinement aggregates, per-symbol pixels. The default Limits are calibrated for real PDFs; an adversary who declares a value past the cap gets a fast ErrResourceBudget rejection instead of pressure on the host’s memory. Errors classify into three sentinels — ErrMalformed, ErrResourceBudget, ErrUnsupported — so callers can route gracefully (skip the image, raise the cap, fall back to another decoder).

Status

Pre-1.0. The public API is settling but not yet frozen; the module version is "0.0.0-dev" until the first tagged release. Conformance against the ITU-T T.88 Annex A corpus is documented in the repo — TT1, TT9, TT10 decode; TT2–TT8 fail for reasons shared with every other open-source JBIG2 decoder (the corpus encoder ships spec-deviating shapes no production encoder emits).

CLI tools under cmd/ cover the two everyday workflows: cmd/gobig2 decodes standalone or PDF-embedded streams to PNG/PBM/raw, and cmd/extract-jbig2 walks a PDF and dumps every JBIG2 image XObject as separate .jb2 files — the input shape for new test fixtures.