jackskip22/rapier-jxl

JPEG XL in JavaScript, 12kB gzipped core. Rapier's full encoding system is ~50kB gzipped with lossless, lossy, HDR, exact alpha, JPEG transcoding, WASM and parallel workers. Zero dependencies. MIT.

JavaScript

15

0 commits

updated Oct 12, 2026

See the code

README

Rapier JXL

JPEG XL encoder for integer and floating-point RGBA samples and JPEG coefficients. JavaScript, optional inlined WebAssembly, no runtime dependencies. Native 8-, 10-, 12- and 16-bit integers, IEEE binary16 and binary32, PQ and HLG transfer functions, and Rec. 2020 primaries. MIT.

npm install rapier-jxl
import {encode} from 'rapier-jxl';

const {data, width, height} = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
const bytes = encode(data, width, height, {quality: 100});
const jxl = new Blob([bytes], {type: 'image/jxl'}); // save as image.jxl

quality: 100 (default) is lossless; 1–99 is lossy. Alpha stays exact. For smaller lossless files, import encode from rapier-jxl/effort and set effort: 1–9 (default 1). rapier-jxl/wasm runs that search with optional integer kernels and identical output bytes.

Native precision and HDR

SourceInputDeclaration
8-bit RGBAUint8Array or Uint8ClampedArrayInferred
10- or 12-bit RGBAUint16Array, unscaled code valuesbitDepth: 10 or 12
16-bit RGBAUint16ArrayInferred
IEEE binary16Uint16Array, raw half-float wordssampleFormat: 'float16'
IEEE binary32Float32ArrayInferred; linear transfer by default
PNG16 or scanline OpenEXRFile bytes through rapier-jxl/sourceSamples and supported colour declarations read together
import {readSource} from 'rapier-jxl/source';
import {encode} from 'rapier-jxl';

const {data, width, height, ...source} = await readSource(fileBytes);
const bytes = encode(data, width, height, {...source, quality: 100});

Colour and alpha declarations describe the supplied samples. Encoding does not convert colour, tone-map, clamp HDR values or unpremultiply RGB. The optional source reader supports PNG16, including Adam7, and uniform HALF/FLOAT OpenEXR scanlines with NONE, RLE, ZIPS or ZIP compression. Source formats, error bounds and decoder qualifications.

Encoding under a CPU budget

The fixed-work benchmark uses Node 22.23.3 on an AMD EPYC 9V74 x86-64 host, with affinity to four virtual CPUs and an aggregate limit of 0.5 process CPU seconds per wall second. Four original-size inputs total 1,157,120 pixels: 640 × 448 text, 640 × 432 screen, 640 × 448 drawing and a 512 × 600 photograph. The text, screen and drawing inputs are authored corpus rasters. Every call uses quality 100.

Each table entry is the sum of four per-image medians, with one warmup and three measured repetitions per image. The worker count is additional threads; the caller also codes groups. Worker startup belongs to each job. These results describe this constrained x86 host; phone latency, browser scheduling, thermal performance and energy require target-device measurements.

Caller-thread CPU, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1174.4159.7154.1140.3
2418.2498.0422.4416.0
3574.0615.4487.6542.7
41045.21024.31031.21071.5
51959.11972.81940.12040.7
62501.92566.12553.32591.2
72640.92629.12621.12639.0
82577.12489.52540.62525.6
92507.52574.52583.52556.7

Aggregate process CPU, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1205.2192.6171.6147.2
2439.4523.5454.7434.9
3615.3696.6563.2621.0
41294.51190.41265.71298.2
52471.22437.12490.82631.1
63202.83201.13141.13262.8
73347.23287.23261.13340.6
83279.43151.43224.33211.4
93130.43252.43197.03239.7

Elapsed time under the CPU budget, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1390.0390.4328.9303.7
2900.81031.8915.5886.3
31244.01421.61141.11231.8
42603.32364.82526.82604.0
54962.44864.64976.55240.9
66511.46392.16330.96517.5
76631.96573.86509.26669.3
86569.06313.86456.16431.7
96246.56495.56365.16443.9

The benchmark records complete stream hashes, source module hashes, input hashes, individual CPU and wall samples, caller profiles and actual governor accounting. node bench/phone-budget.mjs --inputs=inputs.json --out=results --max-pixels=1157120 --efforts=1,2,3,4,5,6,7,8,9 --workers=0,1,2,4 --rounds=3 --cpu-share=0.5 --cpus=0-3 --profile runs the same declared work. The manifest is a JSON array; each entry names an id, a kind, width, height, the rgbaHash (SHA-256 of the raw RGBA bytes) and pixels, the path of those bytes relative to the manifest. Split the efforts into smaller invocations when limiting individual command duration.

Encoder payload

Encoder payloadMinified JS + WASM bytesgzip bytes
Rapier JXL core26,99711,408
libjxl via jxl-wasm 0.7.02,533,758961,538
@jsquash/jxl 1.3.01,388,572525,782
@squoosh-kit/jxl 0.2.101,371,789511,927

Executable payloads, gzip 9; external packages measured 2026-09-30. These are encoder download sizes. All comparisons, capabilities and measurement method.

API, options and errors · Imports, sizes and inlining · Machine-readable facts · Browser example · Worker example · Contributing

browser
encoder
hdr-image
image-compression
image-encoding
javascript
jpeg-xl
jpegxl
jpeg-xl-encoder
jxl
jxl-encoder
lossless-image-compression
lossless-images
lossy-compression
lossy-image-compression
offline-first
photo-compression
single-file
webassembly
zero-dependency

jackskip22/rapier-jxl

JPEG XL in JavaScript, 12kB gzipped core. Rapier's full encoding system is ~50kB gzipped with lossless, lossy, HDR, exact alpha, JPEG transcoding, WASM and parallel workers. Zero dependencies. MIT.

JavaScript

15

0 commits

updated Oct 12, 2026

See the code

README

Rapier JXL

JPEG XL encoder for integer and floating-point RGBA samples and JPEG coefficients. JavaScript, optional inlined WebAssembly, no runtime dependencies. Native 8-, 10-, 12- and 16-bit integers, IEEE binary16 and binary32, PQ and HLG transfer functions, and Rec. 2020 primaries. MIT.

npm install rapier-jxl
import {encode} from 'rapier-jxl';

const {data, width, height} = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
const bytes = encode(data, width, height, {quality: 100});
const jxl = new Blob([bytes], {type: 'image/jxl'}); // save as image.jxl

quality: 100 (default) is lossless; 1–99 is lossy. Alpha stays exact. For smaller lossless files, import encode from rapier-jxl/effort and set effort: 1–9 (default 1). rapier-jxl/wasm runs that search with optional integer kernels and identical output bytes.

Native precision and HDR

SourceInputDeclaration
8-bit RGBAUint8Array or Uint8ClampedArrayInferred
10- or 12-bit RGBAUint16Array, unscaled code valuesbitDepth: 10 or 12
16-bit RGBAUint16ArrayInferred
IEEE binary16Uint16Array, raw half-float wordssampleFormat: 'float16'
IEEE binary32Float32ArrayInferred; linear transfer by default
PNG16 or scanline OpenEXRFile bytes through rapier-jxl/sourceSamples and supported colour declarations read together
import {readSource} from 'rapier-jxl/source';
import {encode} from 'rapier-jxl';

const {data, width, height, ...source} = await readSource(fileBytes);
const bytes = encode(data, width, height, {...source, quality: 100});

Colour and alpha declarations describe the supplied samples. Encoding does not convert colour, tone-map, clamp HDR values or unpremultiply RGB. The optional source reader supports PNG16, including Adam7, and uniform HALF/FLOAT OpenEXR scanlines with NONE, RLE, ZIPS or ZIP compression. Source formats, error bounds and decoder qualifications.

Encoding under a CPU budget

The fixed-work benchmark uses Node 22.23.3 on an AMD EPYC 9V74 x86-64 host, with affinity to four virtual CPUs and an aggregate limit of 0.5 process CPU seconds per wall second. Four original-size inputs total 1,157,120 pixels: 640 × 448 text, 640 × 432 screen, 640 × 448 drawing and a 512 × 600 photograph. The text, screen and drawing inputs are authored corpus rasters. Every call uses quality 100.

Each table entry is the sum of four per-image medians, with one warmup and three measured repetitions per image. The worker count is additional threads; the caller also codes groups. Worker startup belongs to each job. These results describe this constrained x86 host; phone latency, browser scheduling, thermal performance and energy require target-device measurements.

Caller-thread CPU, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1174.4159.7154.1140.3
2418.2498.0422.4416.0
3574.0615.4487.6542.7
41045.21024.31031.21071.5
51959.11972.81940.12040.7
62501.92566.12553.32591.2
72640.92629.12621.12639.0
82577.12489.52540.62525.6
92507.52574.52583.52556.7

Aggregate process CPU, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1205.2192.6171.6147.2
2439.4523.5454.7434.9
3615.3696.6563.2621.0
41294.51190.41265.71298.2
52471.22437.12490.82631.1
63202.83201.13141.13262.8
73347.23287.23261.13340.6
83279.43151.43224.33211.4
93130.43252.43197.03239.7

Elapsed time under the CPU budget, milliseconds

EffortCaller only+1 worker+2 workers+4 workers
1390.0390.4328.9303.7
2900.81031.8915.5886.3
31244.01421.61141.11231.8
42603.32364.82526.82604.0
54962.44864.64976.55240.9
66511.46392.16330.96517.5
76631.96573.86509.26669.3
86569.06313.86456.16431.7
96246.56495.56365.16443.9

The benchmark records complete stream hashes, source module hashes, input hashes, individual CPU and wall samples, caller profiles and actual governor accounting. node bench/phone-budget.mjs --inputs=inputs.json --out=results --max-pixels=1157120 --efforts=1,2,3,4,5,6,7,8,9 --workers=0,1,2,4 --rounds=3 --cpu-share=0.5 --cpus=0-3 --profile runs the same declared work. The manifest is a JSON array; each entry names an id, a kind, width, height, the rgbaHash (SHA-256 of the raw RGBA bytes) and pixels, the path of those bytes relative to the manifest. Split the efforts into smaller invocations when limiting individual command duration.

Encoder payload

Encoder payloadMinified JS + WASM bytesgzip bytes
Rapier JXL core26,99711,408
libjxl via jxl-wasm 0.7.02,533,758961,538
@jsquash/jxl 1.3.01,388,572525,782
@squoosh-kit/jxl 0.2.101,371,789511,927

Executable payloads, gzip 9; external packages measured 2026-09-30. These are encoder download sizes. All comparisons, capabilities and measurement method.

API, options and errors · Imports, sizes and inlining · Machine-readable facts · Browser example · Worker example · Contributing

browser
encoder
hdr-image
image-compression
image-encoding
javascript
jpeg-xl
jpegxl
jpeg-xl-encoder
jxl
jxl-encoder
lossless-image-compression
lossless-images
lossy-compression
lossy-image-compression
offline-first
photo-compression
single-file
webassembly
zero-dependency

Project health

Latest release

2.6.0 · Oct 7, 2026

Releases this year

11