Pure-Go decoder for ITU-T T.88 / ISO/IEC 14492 JBIG2 streams.
gobig2 was built first for PDF readers - the /JBIG2Decode
filter is the dominant use of JBIG2 in the wild - and then for
general-purpose standalone .jb2 / .jbig2 decoding. No cgo,
no third-party runtime dependencies; every length, count, and
dimension that derives from input bytes is gated against a
configurable Limits cap before allocation, so the
codec is safe to feed adversarial bytes from a PDF crawler or
similar untrusted source.
Warning
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 status against the ITU-T T.88 Annex A corpus is
documented in docs/design/ITU-SPEC-PROBLEMS.md -
the short version is that TT1, TT9, TT10 decode and TT2-TT8 fail
for reasons shared with every other open-source JBIG2 decoder
(the corpus encoder ships spec-deviating shapes no production
encoder emits).
Cross-decoder wall-clock benchmark on ubuntu-24.04 (GitHub-hosted
runner), best-of-7 with one warm-up, decoded straight to PBM so
the cell tracks decode work rather than encoder overhead. Numbers
in milliseconds; the per-push run lives at
.github/workflows/perf-linux.yml.
| fixture | gobig2 | jbig2dec | mutool | pdfimages |
|---|---|---|---|---|
bitmap |
2.71 | 1.78 | 4.19 | 10.08 |
bitmap-mmr |
2.26 | 1.28 | 3.58 | 8.53 |
bitmap-halftone |
2.53 | 1.65 | 3.73 | 8.81 |
bitmap-symbol |
1.94 | 1.29 | 3.67 | 8.65 |
bitmap-symbol-symhuff-texthuff |
2.05 | 2.12 | 4.43 | 8.86 |
perf-text-generic (33 Mpx) |
231.81 | 175.12 | 252.60 | 313.08 |
perf-text-symbol (33 Mpx) |
25.94 | 18.31 | 95.38 | 57.33 |
- Pure Go, no asm, no cgo - within 1.3-1.6x of jbig2dec (C, hand-tuned reference) across every fixture.
- Beats jbig2dec on
bitmap-symbol-symhuff-texthuffand beats every PDF-toolchain decoder on every fixture.
Reproduce locally (Linux / macOS, needs jbig2enc + imagemagick
for fixture synthesis):
task bench:corpus # generate the perf-text-* fixtures under ./tmp/perf-corpus/
task bench:cross CORPUS_DIR=./tmp/perf-corpusgo get github.com/dkrisman/gobig2Go 1.25 toolchain or newer (see go.mod).
The canonical flow a PDF reader uses: pull the
JBIG2Decode-filtered image XObject stream and the optional
/JBIG2Globals parameter object out of the PDF, hand both to
NewDecoderEmbedded, and decode the page bitmap.
package main
import (
"bytes"
"fmt"
"image"
"os"
"github.com/dkrisman/gobig2"
)
func main() {
imageStream, err := os.ReadFile("page.jb2")
if err != nil {
panic(err)
}
var globalsBytes []byte // pulled from /JBIG2Globals if present, else nil
dec, err := gobig2.NewDecoderEmbedded(bytes.NewReader(imageStream), globalsBytes)
if err != nil {
// Adversarial / non-JBIG2 input is rejected up front,
// before any allocation derived from declared dimensions.
fmt.Println("decode error:", err)
return
}
img, err := dec.Decode()
if err != nil {
fmt.Println("decode error:", err)
return
}
// *image.Gray with ink as 0 (black), paper as 255 (white).
g := img.(*image.Gray)
fmt.Printf("decoded %dx%d\n", g.Bounds().Dx(), g.Bounds().Dy())
}See example_pdf_test.go for the same flow
as a runnable Example.
JBIG2 has two on-the-wire forms - pick the constructor that matches your input:
| Input shape | Constructor |
|---|---|
Standalone .jb2 / .jbig2 with T.88 Annex E file header |
NewDecoder |
PDF-embedded segment stream (header stripped by /JBIG2Decode) |
NewDecoderEmbedded |
| Either shape, with optional external globals | NewDecoderWithGlobals |
The standalone constructor auto-registers with image.Decode
under the format name "jbig2".
| Method | Returns | Use when |
|---|---|---|
Decoder.Decode |
image.Image (*image.Gray) |
You want a stdlib image you can png.Encode |
Decoder.DecodePacked |
PackedPage |
You consume bilevel data directly - PBM writers, 1-bpp PNG, bit-blit. Saves ~12 ms wall + ~35 MB alloc on a 600 dpi A4 page over the image.Gray conversion. |
Decoder.DecodeContext / Decoder.DecodePackedContext |
same, plus context.Context |
You need cancellation / a wall-clock budget |
PackedPage.Data aliases the decoder's internal
buffer until the next call on the same Decoder; copy it if
you need it to outlive that boundary.
JBIG2 is a denial-of-service vector - a 100-byte segment header
can declare a 30 GiB region. Every attacker-controlled allocation
is gated by a cap on the Limits struct (image
pixels, symbols per dict, halftone grid cells, IAID code length,
refinement aggregates, per-symbol pixels, etc.). Always start
from DefaultLimits and override the fields you
want; a bare struct literal silently disables every other cap
because zero means "no cap".
limits := gobig2.DefaultLimits()
limits.MaxImagePixels = 100 * 1024 * 1024
limits.Apply()Apply is process-wide and not safe to call concurrently with
active decodes; configure once at startup, then spawn workers.
Pair with a wall-clock budget via
Decoder.DecodeContext - the segment-parser loop
checks ctx.Err() between segments.
Every decode failure wraps one of three sentinels:
| Sentinel | Meaning | Caller action |
|---|---|---|
ErrMalformed |
Input bytes are not legal JBIG2 | Skip the image |
ErrResourceBudget |
A configured Limits cap fired |
Raise the cap or accept the rejection |
ErrUnsupported |
Legal but uses an unimplemented feature | Fall back to another decoder |
Decoder.Decode returns io.EOF after the final page on
multi-page input; cancellation paths wrap context.Canceled /
context.DeadlineExceeded. See errors.go for the
recommended switch idiom.
Binaries under cmd/:
cmd/gobig2- decode a standalone or PDF-embedded JBIG2 stream into a PNG, PBM, or raw bitmap. Flags and exit codes documented in the binary's package doc.cmd/extract-jbig2- walk a PDF and dump every/JBIG2Decodeimage XObject (and any/JBIG2Globalsstream) as separate.jb2files, suitable as gobig2 test fixtures.cmd/perf-cross- dev / CI tool that drives the cross-decoder benchmark table at the top of this README. Wrapped bytask bench:cross.
Build with:
task build # ./... compile-check
task build:release # stripped, PGO-optimized binaries in ./bin/The canonical command runner is Taskfile.yml. Common targets:
task test # all tests
task test:race # race detector (requires CGO)
task test:conformance # SerenityOS corpus + ITU-T T.88 Annex A if JBIG2_CONFORMANCE_DIR set
task lint # golangci-lint v2
task fuzz # 3s smoke fuzz across every Fuzz* target
task fuzz:long # 10m sustained fuzz
task bench # in-process micro-benchmarks
task bench:cross # cross-decoder wall-clock bench
task ci # full gate: fmt:check + check + test:raceThe internal/gobig2test package owns the public-API contract
tests, conformance corpus harness, fuzz targets, and pathological
input regressions.
- jbig2.go, errors.go, limits.go - public API surface.
- internal/ - decoder packages, one per JBIG2 spec area (see QUICKSTART.md for the code map).
- cmd/ - CLI binaries.
- docs/ - project docs and design notes.
- testdata/ - SerenityOS conformance fixtures, PDF-embedded samples, perf corpora, fuzz seeds.
Apache 2.0. See NOTICE for attribution.