A T.800 decoder in TypeScript
@azohra/meteo.j2k is a JPEG 2000 (ITU-T T.800) decoder in pure
TypeScript, scoped to exactly the codestream subset ECCC’s GRIB2 feeds
ship. It was written because the forecast engine’s hottest loop used to run
through a non-SIMD WASM build of OpenJPEG — and, for 20-bit fields,
through OpenJPEG.js, an asm.js-era artifact. Neither was explainable or
patchable in this repository.
This decoder is justified by explainability alone: every marker, MQ
context, and lifting step lives in ten small modules with its clause of
T.800 named. On top of that sit two structural theses no whole-image
codec can follow. EBCOT codeblocks are coded independently, so one
field’s hundreds of codeblocks can decode in parallel across workers
(src/parallel.ts
is that seam). And the same independence runs the other way:
decodeJ2kRegion
(src/region.ts)
decodes only the codeblocks a handful of requested gridpoints touch —
bit-identical to the full decode at those points, ~16× faster per core on
the largest ECCC field — which is exactly the shape a site-sampling
forecast engine asks for.
This decoder is the production path: it is
@azohra/meteo.grib/j2k-node’s default codec at every bit depth (the WASM
build stays selectable for whole-image decodes; OpenJPEG.js is retired
outright), that package’s sampled decode is this decoder’s region decode,
and its worker pool fans full decodes’ codeblock tasks across threads.
The receipts — the measured region-decode and per-core tables — are in
Performance, honestly.
Decode a real field
Section titled “Decode a real field”The workspace’s golden corpus carries real ECCC messages; the codestream is the GRIB section 7 payload (DRT 5.40). This decodes one and reads a sample:
// decode-fixture.mjs — run inside j2k/ after `pnpm build` (and a grib build)import { readFileSync } from "node:fs";import { parseFields, splitMessages } from "../grib/dist/index.js";import { decodeJ2k } from "./dist/index.js";
const bytes = readFileSync("../grib/test/fixtures/geps-orog-m00.grib2");const [field] = parseFields(splitMessages(bytes)[0]);const codestream = field.section7.subarray(5); // DRT 5.40: raw J2K after the section header
const { values, width, height, bitsPerSample, isSigned } = decodeJ2k(codestream);console.log(`${width}x${height} = ${values.length} samples, ${bitsPerSample}-bit ${isSigned ? "signed" : "unsigned"}`);console.log(`first samples: ${Array.from(values.slice(0, 4)).join(", ")}`);720x361 = 259920 samples, 12-bit unsignedfirst samples: 1166, 1166, 1166, 1166Decode four points, not three million
Section titled “Decode four points, not three million”When only a few gridpoints matter — a forecast engine sampling sites —
decodeJ2kRegion takes full-grid raster indexes and entropy-decodes only
the codeblocks those points touch, then runs window-bounded inverse
lifts. The values are bit-identical to decodeJ2k’s at those indexes
— region decode is a cheaper route to the same integers, never an
approximation — and the envelope is the package’s usual subset, guarded
by the same loud errors:
import { decodeJ2kRegion } from "./dist/index.js";
const region = decodeJ2kRegion(codestream, [879425, 1439871]);console.log(region.values); // === decodeJ2k(codestream).values at those indexesconsole.log(`${region.codeblocksDecoded}/${region.codeblocksTotal} codeblocks decoded`);On the largest ECCC field a 4-point region decode touches 49 of 911 codeblocks and runs ~16× faster than the full decode on one core; the measured table is in Performance, honestly, and the exactness gate in Two-ring correctness.
The J2kSamples seam
Section titled “The J2kSamples seam”decodeJ2k returns raw integer samples shaped exactly like
@azohra/meteo.grib’s J2kSamples, so it drops straight into
decodeFieldValues’ DecodeJ2k injection seam — no adapter:
const { values } = decodeFieldValues(field, { decodeJ2k });In @azohra/meteo.grib’s Node path this wiring already exists —
createNodeJ2kDecoder() and the worker pool default to this decoder;
see JPEG 2000 and the pool.
The documentation
Section titled “The documentation”| Page | Covers |
|---|---|
| The subset | The measured codestream shape, the loud-failure guards, the JasPer extension |
| Two-ring correctness | The cross-codec oracle ring, the end-to-end ecCodes ring, region decode’s exactness contract |
| Performance, honestly | The measured region-decode and single-thread tables, the Tier-1 profile, the codeblock-parallel thesis |
References
Section titled “References”Written against named references, per house convention, vendoring nothing:
- ITU-T T.800 — the spec; Annex B (packets, cited in
packets.ts), Annex C (MQ coder,mq.ts), Annex D (coefficient bit modelling,t1.ts), Annex F (the reversible 5/3 inverse,dwt.ts). - OpenJPEG (BSD-2, © Université catholique de Louvain) — the
behavioural reference: pass gating and midpoint arithmetic (t1.c),
lifting order and edge cases (dwt.c), tag trees (tgt.c), header
reading order (t2.c). Also the oracle, through the two codec packages
@azohra/meteo.grib/j2k-nodewraps. - pdf.js’s jpx.js and ArithmeticDecoder (Apache-2.0, Mozilla) — the pure-JavaScript cross-reference for MQ register conventions and Tier-1 neighbourhood bookkeeping, proven on MSC data via grib2class’s lineage.
Layout
Section titled “Layout”src/codestream.ts marker walk and subset guards (SIZ/COD/QCD/SOT/SOD/EOC)src/packets.ts Tier-2: geometry, tag-tree queries, packet headerssrc/tagtree.ts the B.10.2 tag treessrc/mq.ts the Annex C MQ arithmetic decodersrc/t1.ts EBCOT Tier-1: the three passes, 19 contexts, sign codingsrc/dwt.ts inverse reversible 5/3 lifting over the level laddersrc/image.ts assembly: T1 → DWT → DC shift/clamp → samplessrc/parallel.ts the per-codeblock decode plan a worker pool fans outsrc/region.ts region decode: exact samples at requested points onlysrc/errors.ts UnsupportedJ2kError and J2kFormatError, the loud-failure vocabularytest/ header parse + guards, parallel-plan equality, the JasPer shape, the region-decode exactness sweep over the whole corpus (grib-free; the two-ring golden gate is grib/test/j2k-golden.test.ts)tools/bench.ts single-thread decodeJ2k timing over the corpusZero runtime dependencies, and no Node APIs in src/ — the package is
browser-safe by construction.