rapier-jxl 1.1.19 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -24
- package/ENCODER-COMPARISON.md +164 -47
- package/README.md +104 -100
- package/admit.mjs +56 -0
- package/bits.mjs +33 -11
- package/effort.d.mts +11 -0
- package/effort.min.mjs +2 -0
- package/effort.mjs +217 -0
- package/entropy.mjs +16 -1
- package/frame.mjs +40 -48
- package/index.d.mts +23 -0
- package/index.mjs +30 -83
- package/jfif.mjs +407 -0
- package/jpeg.d.mts +15 -0
- package/jpeg.min.mjs +2 -2
- package/jpeg.mjs +34 -378
- package/lossless.mjs +52 -50
- package/lossy.mjs +47 -21
- package/modular.mjs +16 -17
- package/package.json +30 -21
- package/photo.d.mts +9 -0
- package/photo.min.mjs +2 -2
- package/photo.mjs +88 -46
- package/prefix.mjs +15 -19
- package/public/examples/browser.html +39 -0
- package/public/examples/worker.mjs +42 -0
- package/rapier-jxl.min.mjs +2 -2
- package/sizes.json +181 -75
- package/vardct.mjs +40 -37
- package/weighted.mjs +79 -0
- package/writer.mjs +17 -0
- package/lossless.min.mjs +0 -2
package/AGENTS.md
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
# For an agent adding Rapier JXL to an app
|
|
2
2
|
|
|
3
|
-
1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
3.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this evidence does not prove every possible input. Keep original pixels until encoding and storage succeed.
|
|
23
|
-
7. Show the result only where the browser decodes JPEG XL (`image/jxl` in `<picture>` with a fallback, or a
|
|
24
|
-
feature test on a one-pixel stream); keep the original where it does not.
|
|
3
|
+
1. `npm install rapier-jxl`, or copy one file from this repository's root: `rapier-jxl.min.mjs` (the core),
|
|
4
|
+
`effort.min.mjs`, `jpeg.min.mjs` or `photo.min.mjs`. No build.
|
|
5
|
+
2. `import {encode} from 'rapier-jxl'` (or `rapier-jxl/min`), `transcode` from `rapier-jxl/jpeg`, `encodePhoto`
|
|
6
|
+
from `rapier-jxl/photo`. For smaller lossless files at more time, `encode` from `rapier-jxl/effort` with
|
|
7
|
+
`{effort: 3}`.
|
|
8
|
+
3. Pixels: `encode(rgba, width, height, {quality})`, straight RGBA row by row as `getImageData` gives it; from a
|
|
9
|
+
Display P3 canvas, add `colorSpace: 'display-p3'`. Quality 100 (the default) is lossless, 1 to 99 lossy, alpha
|
|
10
|
+
exact at every quality. Photographs:
|
|
11
|
+
`encodePhoto(rgba, width, height, {quality: 90})`. The answer is a `Uint8Array` of codestream bytes: save it
|
|
12
|
+
as `.jxl` or in a `Blob` of type `image/jxl`.
|
|
13
|
+
4. A JPEG: `transcode(jpegBytes)` gives `{bytes, width, height, orientation}` without decoding. On `JXL_JPEG`,
|
|
14
|
+
decode it and call `encodePhoto` on the pixels.
|
|
15
|
+
5. Anything larger than an icon runs in a worker. Calls are synchronous, so cancel by terminating the worker, or
|
|
16
|
+
loop over the door's twin (`encodeSteps`, `transcodeSteps`, `encodePhotoSteps`) and leave the loop. A
|
|
17
|
+
transferred buffer is gone from the sender: copy first if you may retry.
|
|
18
|
+
6. The error codes are `JXL_INPUT`, `JXL_DIMENSIONS`, `JXL_SIZE`, `JXL_MEMORY` and `JXL_JPEG`, no others;
|
|
19
|
+
`JXL_DIMENSIONS` is past the door's own `LIMITS` (24 million pixels for the core and `effort`, 40 million for
|
|
20
|
+
`photo`, 64 million for a carried JPEG). Keep the original pixels until the encode and its storage succeed.
|
|
21
|
+
7. Show the result only where the browser decodes JPEG XL (`<picture>` with a fallback, or a one-pixel feature test).
|
|
25
22
|
|
|
26
|
-
Nothing here reads files, fetches
|
|
27
|
-
|
|
23
|
+
Nothing here reads files, fetches or touches the DOM: it runs in a worker, Node or Deno alike. Publish the version
|
|
24
|
+
you took and its licence; do not vendor a minified copy under another name.
|
package/ENCODER-COMPARISON.md
CHANGED
|
@@ -1,68 +1,185 @@
|
|
|
1
1
|
# Encoder payloads measured on 30 September 2026
|
|
2
2
|
|
|
3
|
-
Rapier JXL is the smallest JavaScript
|
|
4
|
-
|
|
5
|
-
subsets of JPEG XL. In particular, Rapier's JPEG carrier preserves coefficients but cannot reconstruct the original
|
|
6
|
-
JPEG file; libjxl's reversible JPEG mode can. Larger codecs also offer features Rapier does not, including HDR,
|
|
7
|
-
ICC profiles, animation, perceptual search and more compression tools.
|
|
3
|
+
Rapier JXL is the smallest JavaScript or WebAssembly JPEG XL encoder among the published payloads measured here.
|
|
4
|
+
A bounded survey, not proof of a global minimum; the encoders differ in what they cover.
|
|
8
5
|
|
|
9
6
|
| Encoder / entry | Version | Minified JS + WASM bytes | gzip bytes | Included capability |
|
|
10
7
|
| --- | --- | ---: | ---: | --- |
|
|
11
|
-
| Rapier JXL core |
|
|
12
|
-
| Rapier JXL
|
|
13
|
-
| Rapier JXL JPEG |
|
|
14
|
-
| Rapier JXL photo |
|
|
8
|
+
| Rapier JXL core | 2.0.0 | 19,495 | 8,475 | 8-bit lossless RGBA, lossy modular with exact alpha |
|
|
9
|
+
| Rapier JXL effort door | 2.0.0 | 25,229 | 10,606 | The core, and the weighted predictor searched for lossless |
|
|
10
|
+
| Rapier JXL JPEG door | 2.0.0 | 29,592 | 12,533 | JPEG coefficients, orientation; no JPEG reconstruction |
|
|
11
|
+
| Rapier JXL photo door | 2.0.0 | 25,646 | 10,804 | 8-bit photographic VarDCT, exact alpha; q100 lossless |
|
|
15
12
|
| [jSquash](https://github.com/jamsinclair/jSquash/tree/main/packages/jxl) | 1.3.0 | 1,388,572 | 525,782 | 8-bit lossless/lossy RGBA |
|
|
16
|
-
| [Discourse's jSquash package](https://www.npmjs.com/package/@discourse/jxl) | 1.3.0 | 1,388,572 | 525,782 |
|
|
17
|
-
| [Lacinak's jSquash fork](https://github.com/kelaci/jSquash) | 1.3.0-kelaci.0 | 2,071,514 | 844,866 |
|
|
13
|
+
| [Discourse's jSquash package](https://www.npmjs.com/package/@discourse/jxl) | 1.3.0 | 1,388,572 | 525,782 | The same encoder bytes as jSquash |
|
|
14
|
+
| [Lacinak's jSquash fork](https://github.com/kelaci/jSquash) | 1.3.0-kelaci.0 | 2,071,514 | 844,866 | High bit-depth input options |
|
|
18
15
|
| [squoosh-kit](https://github.com/bnowak008/squoosh-kit/tree/main/packages/jxl) | 0.2.10 | 1,371,789 | 511,927 | 8-bit lossless/lossy RGBA |
|
|
19
|
-
| [icodec](https://github.com/Kaciras/icodec) | 0.6.0 | 2,474,954 | 900,957 | 8
|
|
20
|
-
| [Cornerstone libjxl](https://github.com/cornerstonejs/codecs/tree/main/packages/libjxl) | 1.1.1 | 2,575,008 | 936,242 | 1
|
|
16
|
+
| [icodec](https://github.com/Kaciras/icodec) | 0.6.0 | 2,474,954 | 900,957 | 8 to 16-bit lossless/lossy pixels |
|
|
17
|
+
| [Cornerstone libjxl](https://github.com/cornerstonejs/codecs/tree/main/packages/libjxl) | 1.1.1 | 2,575,008 | 936,242 | 1 to 16-bit grayscale/RGB; DICOM-oriented API |
|
|
21
18
|
| [jxl-wasm](https://github.com/saschanaz/jxl-wasm) | 0.7.0 | 2,533,758 | 961,538 | libjxl CLI, pixels and reversible JPEG |
|
|
22
|
-
| [jpeg-to-jxl](https://github.com/ChefJulio/jpeg-to-jxl) | 0.2.0 | 2,732,858 | 1,063,406 | Reversible JPEG recompression
|
|
19
|
+
| [jpeg-to-jxl](https://github.com/ChefJulio/jpeg-to-jxl) | 0.2.0 | 2,732,858 | 1,063,406 | Reversible JPEG recompression, with decode |
|
|
23
20
|
| [Squoosh library](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) | 0.5.3 | 1,615,596 | 539,210 | Shared multi-codec JavaScript and one JXL encoder WASM |
|
|
24
21
|
|
|
25
|
-
Bytes are decimal
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The jSquash, fork, squoosh-kit and icodec rows count the low-level encoder factory and its WASM. Optional
|
|
32
|
-
convenience wrappers, threaded variants, workers and decoders are excluded. This favors those alternatives;
|
|
33
|
-
it does not pretend the entire npm package is needed to encode one picture. The Squoosh library has shared
|
|
34
|
-
JavaScript, so that file is included whole. The JPEG-to-JXL package also exposes reverse conversion from the
|
|
35
|
-
same WASM; it is not an encoder-only custom rebuild. Size measurements do not establish the other packages'
|
|
36
|
-
correctness or compression quality.
|
|
22
|
+
Bytes are decimal. JavaScript is minified with Terser 5.51.2 (two compress passes, mangling, licence comments
|
|
23
|
+
kept), WASM is unchanged, each resource is gzipped alone at level 9, then summed: delivery bytes, not tarball
|
|
24
|
+
sizes. Every file, URL, npm integrity and SHA-256 is in [`public/encoder-sizes.json`](public/encoder-sizes.json).
|
|
25
|
+
The jSquash, fork, squoosh-kit and icodec rows count the encoder factory and its WASM only, without wrappers,
|
|
26
|
+
workers or decoders, which favours them. Size says nothing about correctness or compression quality.
|
|
37
27
|
|
|
38
28
|
## Reproduce
|
|
39
29
|
|
|
40
|
-
In a checkout of this public repository:
|
|
41
|
-
|
|
42
30
|
```sh
|
|
43
31
|
npm install --ignore-scripts
|
|
44
32
|
node public/measure-encoders.mjs .encoder-size-cache encoder-sizes.json
|
|
45
33
|
```
|
|
46
34
|
|
|
47
|
-
The script fetches
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
The script fetches the pinned npm archives in `public/encoders.json`, checks their integrity, reads the listed
|
|
36
|
+
members in memory and minifies them. It never runs downloaded code. Rapier's own bytes come from its stager.
|
|
37
|
+
|
|
38
|
+
## Quality at matched bytes, 30 September 2026
|
|
39
|
+
|
|
40
|
+
A small encoder does not mean small or good pictures. This measures Rapier's readable source against the
|
|
41
|
+
[libjxl v0.12.0 reference](https://github.com/libjxl/libjxl/releases/tag/v0.12.0) (`cjxl`, `djxl` and
|
|
42
|
+
`butteraugli_main` from the official Linux x86-64 static archive, SHA-256
|
|
43
|
+
`5318a1ea40adad76d023e0c17a03d4627f8282f83cb2b575e69be8e74f1ff456`) on eleven inputs: four synthetic
|
|
44
|
+
textures, the bundled Grace Hopper JPEG, three generated drawings and three generated paintings. It is not a
|
|
45
|
+
representative collection. Each Rapier stream sets a byte budget; the native encoder, at effort 7 with its mode
|
|
46
|
+
left to `cjxl`, is searched over distances for the largest stream within that budget (a gap of at most 0.5%
|
|
47
|
+
counts as matched). Where native distance 0 already fits, the row is marked **†**: an exact stream in fewer
|
|
48
|
+
bytes, not a match. Both outputs are decoded by the same pinned `djxl` to 8-bit sRGB; alpha is byte-exact in
|
|
49
|
+
every selected decode. PSNR and RGB SSIM are computed over the three RGB channels (single-scale SSIM, 11 × 11
|
|
50
|
+
Gaussian, no downsampling); alpha-bearing inputs are matted on white for the table. Butteraugli is the
|
|
51
|
+
reference tool at 80 nits, the worse of the white and black mattes for alpha inputs. Higher PSNR and SSIM and
|
|
52
|
+
lower Butteraugli are that metric's preference, not a human verdict. **R / N** is Rapier / native.
|
|
53
|
+
|
|
54
|
+
### The photo door
|
|
55
|
+
|
|
56
|
+
Native is within 0.402% of the budget in all 20 rows, and better on PSNR and SSIM in 19 of 20 and on
|
|
57
|
+
Butteraugli in 19 of 20; the exceptions are Grace q99 (Rapier's PSNR and SSIM) and lit surface q99 (Rapier's
|
|
58
|
+
Butteraugli).
|
|
59
|
+
|
|
60
|
+
| Input · Rapier q | Rapier bytes | Native bytes | Native d | PSNR dB R / N ↑ | RGB SSIM R / N ↑ | Butteraugli R / N ↓ |
|
|
61
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
62
|
+
| Wood grain · 50 | 6,351 | 6,335 | 5.061 | 33.459 / 37.501 | 0.833394 / 0.920358 | 7.9959 / 3.8476 |
|
|
63
|
+
| Wood grain · 80 | 11,640 | 11,632 | 2.246 | 36.866 / 39.628 | 0.900685 / 0.942440 | 4.0035 / 1.8251 |
|
|
64
|
+
| Wood grain · 90 | 18,713 | 18,713 | 1.226 | 38.910 / 40.727 | 0.929675 / 0.953458 | 2.3318 / 1.5172 |
|
|
65
|
+
| Wood grain · 99 | 76,863 | 76,842 | 0.327 | 45.530 / 47.954 | 0.983684 / 0.991316 | 0.9762 / 0.6128 |
|
|
66
|
+
| Landscape · 50 | 4,729 | 4,710 | 4.944 | 34.594 / 38.053 | 0.852275 / 0.908469 | 8.5642 / 3.4545 |
|
|
67
|
+
| Landscape · 80 | 7,932 | 7,921 | 2.438 | 37.305 / 39.502 | 0.891356 / 0.921732 | 4.1410 / 2.1015 |
|
|
68
|
+
| Landscape · 90 | 12,725 | 12,723 | 1.508 | 39.050 / 40.389 | 0.912417 / 0.931579 | 3.1272 / 1.6892 |
|
|
69
|
+
| Landscape · 99 | 64,221 | 64,192 | 0.377 | 45.545 / 47.492 | 0.977931 / 0.986445 | 1.0067 / 0.6164 |
|
|
70
|
+
| Folded texture · 50 | 3,563 | 3,552 | 5.427 | 35.334 / 38.643 | 0.839685 / 0.908679 | 7.2341 / 4.0082 |
|
|
71
|
+
| Folded texture · 80 | 6,525 | 6,514 | 2.468 | 38.175 / 40.176 | 0.894762 / 0.927122 | 3.6812 / 2.2637 |
|
|
72
|
+
| Folded texture · 90 | 10,046 | 10,046 | 1.668 | 39.618 / 40.820 | 0.917830 / 0.934923 | 2.2918 / 1.6726 |
|
|
73
|
+
| Folded texture · 99 | 57,111 | 57,111 | 0.412 | 45.611 / 47.539 | 0.979148 / 0.986739 | 0.9172 / 0.7164 |
|
|
74
|
+
| Lit surface · 50 | 2,825 | 2,825 | 2.909 | 38.241 / 40.292 | 0.891825 / 0.915090 | 4.4182 / 2.4656 |
|
|
75
|
+
| Lit surface · 80 | 4,107 | 4,107 | 2.314 | 39.474 / 40.530 | 0.903290 / 0.918372 | 3.1575 / 2.1072 |
|
|
76
|
+
| Lit surface · 90 | 5,465 | 5,453 | 1.988 | 40.214 / 40.738 | 0.912497 / 0.921402 | 2.2827 / 1.9999 |
|
|
77
|
+
| Lit surface · 99 | 47,155 | 47,152 | 0.466 | 45.664 / 46.636 | 0.975682 / 0.980349 | 0.9476 / 1.1255 |
|
|
78
|
+
| Grace Hopper · 50 | 15,990 | 15,982 | 5.451 | 29.546 / 30.676 | 0.778970 / 0.807254 | 6.6973 / 4.5990 |
|
|
79
|
+
| Grace Hopper · 80 | 34,419 | 34,409 | 2.338 | 33.675 / 34.477 | 0.862229 / 0.899859 | 3.8410 / 3.0344 |
|
|
80
|
+
| Grace Hopper · 90 | 56,543 | 56,519 | 1.110 | 38.605 / 38.944 | 0.950222 / 0.970007 | 2.3300 / 1.4546 |
|
|
81
|
+
| Grace Hopper · 99 | 110,542 | 110,533 | 0.173 | 49.948 / 47.182 | 0.996348 / 0.995977 | 0.4889 / 0.3406 |
|
|
82
|
+
|
|
83
|
+
### The core's lossy modular on drawings and paintings
|
|
84
|
+
|
|
85
|
+
Fourteen rows are matched within 0.127%. In the ten **†** rows an exact native stream fits in 3.052% to
|
|
86
|
+
51.144% fewer bytes. Native SSIM and Butteraugli are better in all 24 rows; PSNR favours Rapier for paint-0001
|
|
87
|
+
and paint-0002 at q80 and q90.
|
|
88
|
+
|
|
89
|
+
| Input · Rapier q | Rapier bytes | Native bytes | Native d | PSNR dB R / N ↑ | RGB SSIM R / N ↑ | Butteraugli R / N ↓ |
|
|
90
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
91
|
+
| recipe-0000 · 50 | 14,972 | 14,953 | 1.102 | 37.276 / 45.992 | 0.989901 / 0.996956 | 3.9870 / 1.0352 |
|
|
92
|
+
| recipe-0000 · 80 | 20,051 | 19,439† | 0 | 42.493 / ∞ | 0.995389 / 1.000000 | 2.6508 / 0.0000 |
|
|
93
|
+
| recipe-0000 · 90 | 25,232 | 19,439† | 0 | 46.614 / ∞ | 0.997777 / 1.000000 | 1.4840 / 0.0000 |
|
|
94
|
+
| recipe-0000 · 99 | 39,788 | 19,439† | 0 | 58.188 / ∞ | 0.999803 / 1.000000 | 0.5505 / 0.0000 |
|
|
95
|
+
| recipe-0001 · 50 | 31,284 | 31,279 | 1.642 | 44.663 / 51.571 | 0.998111 / 0.999252 | 3.6761 / 0.9379 |
|
|
96
|
+
| recipe-0001 · 80 | 37,226 | 35,491† | 0 | 49.928 / ∞ | 0.999293 / 1.000000 | 2.1718 / 0.0000 |
|
|
97
|
+
| recipe-0001 · 90 | 43,439 | 35,491† | 0 | 53.998 / ∞ | 0.999699 / 1.000000 | 0.7681 / 0.0000 |
|
|
98
|
+
| recipe-0001 · 99 | 60,225 | 35,491† | 0 | 63.163 / ∞ | 0.999953 / 1.000000 | 0.2608 / 0.0000 |
|
|
99
|
+
| recipe-0002 · 50 | 18,746 | 18,736 | 1.651 | 34.484 / 40.065 | 0.978619 / 0.992176 | 5.0667 / 1.4144 |
|
|
100
|
+
| recipe-0002 · 80 | 27,036 | 27,025 | 0.749 | 39.856 / 43.976 | 0.990765 / 0.995802 | 2.5048 / 1.0876 |
|
|
101
|
+
| recipe-0002 · 90 | 35,330 | 35,306 | 0.425 | 44.044 / 46.482 | 0.995583 / 0.997393 | 1.7128 / 0.6129 |
|
|
102
|
+
| recipe-0002 · 99 | 59,624 | 36,102† | 0 | 55.523 / ∞ | 0.999602 / 1.000000 | 0.5762 / 0.0000 |
|
|
103
|
+
| paint-0000 · 50 | 45,505 | 45,498 | 1.336 | 36.020 / 40.143 | 0.975668 / 0.991663 | 4.7640 / 1.5576 |
|
|
104
|
+
| paint-0000 · 80 | 54,652 | 54,618 | 0.684 | 39.900 / 43.518 | 0.988270 / 0.995770 | 2.7126 / 0.9569 |
|
|
105
|
+
| paint-0000 · 90 | 65,877 | 65,843 | 0.289 | 43.809 / 47.922 | 0.994447 / 0.998212 | 1.5637 / 0.6630 |
|
|
106
|
+
| paint-0000 · 99 | 97,769 | 74,658† | 0 | 54.474 / ∞ | 0.999312 / 1.000000 | 0.5388 / 0.0000 |
|
|
107
|
+
| paint-0001 · 50 | 61,462 | 61,442 | 1.680 | 33.606 / 34.830 | 0.956089 / 0.972432 | 7.1021 / 1.8314 |
|
|
108
|
+
| paint-0001 · 80 | 75,929 | 75,926 | 0.830 | 37.751 / 37.312 | 0.980393 / 0.982498 | 3.5325 / 1.1308 |
|
|
109
|
+
| paint-0001 · 90 | 93,451 | 93,451 | 0.343 | 41.285 / 40.730 | 0.990186 / 0.990973 | 1.9406 / 0.8474 |
|
|
110
|
+
| paint-0001 · 99 | 143,736 | 124,100† | 0 | 51.793 / ∞ | 0.998622 / 1.000000 | 0.6179 / 0.0000 |
|
|
111
|
+
| paint-0002 · 50 | 81,312 | 81,304 | 2.020 | 32.334 / 34.083 | 0.937986 / 0.962754 | 5.7590 / 2.1400 |
|
|
112
|
+
| paint-0002 · 80 | 98,676 | 98,622 | 1.055 | 36.749 / 36.598 | 0.972358 / 0.976745 | 3.1658 / 1.3790 |
|
|
113
|
+
| paint-0002 · 90 | 119,652 | 119,651 | 0.472 | 40.420 / 39.711 | 0.986313 / 0.987143 | 1.8052 / 1.1247 |
|
|
114
|
+
| paint-0002 · 99 | 187,934 | 170,069† | 0 | 50.304 / ∞ | 0.997853 / 1.000000 | 0.5696 / 0.0000 |
|
|
115
|
+
|
|
116
|
+
### The JPEG carrier against reversible recompression
|
|
117
|
+
|
|
118
|
+
`transcode` against `cjxl --lossless_jpeg=1`. The contracts differ: Rapier carries the coefficients and the
|
|
119
|
+
orientation and cannot rebuild the JPEG file; native also stores the reconstruction data and rebuilt every
|
|
120
|
+
original exactly. So a smaller Rapier row is not a like-for-like win. Six inputs are tiny conformance fixtures;
|
|
121
|
+
Grace is the one photograph.
|
|
122
|
+
|
|
123
|
+
| JPEG input | Original bytes | Rapier carrier | Native reversible | JPEG rebuilt by native |
|
|
124
|
+
| --- | ---: | ---: | ---: | --- |
|
|
125
|
+
| grey-sequential.jpg | 141 | 117 | 177 | yes |
|
|
126
|
+
| quant-before-scan.jpg | 141 | 120 | 181 | yes |
|
|
127
|
+
| quant-after-scan.jpg | 210 | 120 | refused | no reconstruction data possible |
|
|
128
|
+
| colour-sequential.jpg | 925 | 499 | 662 | yes |
|
|
129
|
+
| colour-progressive-restarts.jpg | 1,367 | 464 | 820 | yes |
|
|
130
|
+
| decoder-difference.jpg | 925 | 499 | 662 | yes |
|
|
131
|
+
| reconstruction-difference.jpg | 925 | 510 | 676 | yes |
|
|
132
|
+
| grace-hopper.jpg | 86,089 | 60,543 | 58,773 | yes |
|
|
133
|
+
|
|
134
|
+
### Inputs and commands
|
|
135
|
+
|
|
136
|
+
Textures are the first four `synthetic-photo` seeds of `tools/corpus/jxl-images.mjs` at 512 × 384; drawings and
|
|
137
|
+
paintings the first three seeds of each kind at the same size, through the real Draw and Paint owners with the
|
|
138
|
+
bundled Geist fonts and pinned `@napi-rs/canvas` 0.1.100. Grace is the public-domain fixture in
|
|
139
|
+
`public/test/photo-corpus/`, decoded once with `ffmpeg -pix_fmt rgba` at its native size.
|
|
140
|
+
|
|
141
|
+
| Input | Dimensions | Seed | Pixels with alpha < 255 | RGBA SHA-256 |
|
|
142
|
+
| --- | ---: | ---: | ---: | --- |
|
|
143
|
+
| Wood grain | 512 × 384 | 3399228925 | 0 | `cfc2495231eb53c4d423ab5ea7692e3cafce6160f07cc51942b910a89d5ffab7` |
|
|
144
|
+
| Landscape | 512 × 384 | 1351084136 | 0 | `c6c33cc695b19538ec6780647e32fd4f22b24872292d22707f26ab9ad1a0cef2` |
|
|
145
|
+
| Folded texture | 512 × 384 | 3597906643 | 0 | `57a0f40e8e14eb0b9fe5286dbfec7bc7783b1e748c6888ea44791f088b0ea106` |
|
|
146
|
+
| Lit surface | 512 × 384 | 1549761854 | 0 | `d998747e1c427144424c4d59f6795f88a2d2983a341dcba54b5a6b346e4f0a0d` |
|
|
147
|
+
| Grace Hopper | 512 × 600 | — | 0 | `af6a4dc548da3797b814f4be1b4489effe658ad13ba842d839628d01ba3ee39f` |
|
|
148
|
+
| recipe-0000 | 512 × 384 | 2385324683 | 23,538 | `6d8240cac5db3c3db509125658ba32cb919537b113ae9f0877856fe6c091e67f` |
|
|
149
|
+
| recipe-0001 | 512 × 384 | 337179894 | 163,701 | `7a5acaf7274bbadfabaa351f0dd3a7931818a438424d17ae555a2c2f72338b9f` |
|
|
150
|
+
| recipe-0002 | 512 × 384 | 2584002401 | 20,928 | `08ce520f30a10897b4acbb630e8d39aeeb97d53e02b50ac721ad80260fd2484c` |
|
|
151
|
+
| paint-0000 | 512 × 384 | 744793156 | 194,804 | `ad9467838000b99434d7f7cb041111cd0bad8f64fe2511ce98d0d106ebd147a3` |
|
|
152
|
+
| paint-0001 | 512 × 384 | 2991615663 | 193,919 | `a3af498e21cade0969d0b3fa63118316a18601aa23bd1b35ffc561e70a123237` |
|
|
153
|
+
| paint-0002 | 512 × 384 | 943470874 | 191,708 | `d2c586460a91e5bcb4c213080f46e4945d674ed6920230f50b2ab396076ea3c1` |
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
cjxl INPUT.pam OUTPUT.jxl --distance=D --effort=7 --num_threads=0 \
|
|
157
|
+
--alpha_distance=0 --keep_invisible=1 --premultiply=0 --container=0 \
|
|
158
|
+
-x color_space=RGB_D65_SRG_Per_SRG --quiet
|
|
159
|
+
djxl INPUT.jxl OUTPUT.pam --bits_per_sample=8 --color_space=RGB_D65_SRG_Per_SRG --num_threads=0 --quiet
|
|
160
|
+
butteraugli_main REFERENCE.ppm DECODED.ppm --intensity_target 80
|
|
161
|
+
cjxl INPUT.jpg OUTPUT.jxl --lossless_jpeg=1 --effort=7 --num_threads=0 --quiet
|
|
162
|
+
djxl OUTPUT.jxl RECONSTRUCTED.jpg --reconstruct_jpeg --num_threads=0 --quiet
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
From `repo/` in the development checkout, `tools/probes/jxl-quality.mjs --section=photo|modular|jpeg` makes
|
|
166
|
+
the inputs, encodes, runs the bracket search and writes `photo.json`, `modular.json` and `jpeg.json`;
|
|
167
|
+
`tools/probes/jxl-quality-metrics.py` computes the metrics (Python 3.12, NumPy 2.3, SciPy 1.17). The full
|
|
168
|
+
candidate pools, byte brackets, hashes and metric components are in the development receipts.
|
|
51
169
|
|
|
52
170
|
## Found but not ranked
|
|
53
171
|
|
|
54
|
-
| Project |
|
|
55
|
-
| --- | --- |
|
|
56
|
-
| [jxl-oxide-wasm](https://github.com/tirr-c/jxl-oxide-wasm) 0.12.6
|
|
57
|
-
| [jxl.js](https://github.com/niutech/jxl.js), jxl-rs-polyfill, TurboJXL, PureJsImage's
|
|
58
|
-
| [libjxl](https://github.com/libjxl/libjxl/blob/main/doc/building_wasm.md)
|
|
59
|
-
| [jixel](https://github.com/awxkee/jixel)
|
|
60
|
-
| [libjxl-tiny](https://github.com/libjxl/libjxl-tiny)
|
|
61
|
-
| [Hydrium](https://github.com/Traneptora/hydrium)
|
|
62
|
-
| [Imazen jxl-encoder](https://github.com/imazen/jxl-encoder)
|
|
63
|
-
| webcvt's jsquash-jxl adapter |
|
|
64
|
-
| sharp
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
smaller usable encoder is published, it belongs in this table and the claim must change.
|
|
172
|
+
| Project | Why not |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| [jxl-oxide-wasm](https://github.com/tirr-c/jxl-oxide-wasm) 0.12.6 (1,707,757 bytes, 612,548 gzip) | Decoder only |
|
|
175
|
+
| [jxl.js](https://github.com/niutech/jxl.js), jxl-rs-polyfill, TurboJXL, PureJsImage's codec | Decoders |
|
|
176
|
+
| [libjxl](https://github.com/libjxl/libjxl/blob/main/doc/building_wasm.md) WASM build | No published encoder payload; the libjxl-based packages above stand for it |
|
|
177
|
+
| [jixel](https://github.com/awxkee/jixel) `d84b45ff` | No published WASM |
|
|
178
|
+
| [libjxl-tiny](https://github.com/libjxl/libjxl-tiny) `8eae1817` | No published WASM |
|
|
179
|
+
| [Hydrium](https://github.com/Traneptora/hydrium) `45227f35` | Native executables only |
|
|
180
|
+
| [Imazen jxl-encoder](https://github.com/imazen/jxl-encoder) `0d79a23e` | No published WASM |
|
|
181
|
+
| webcvt's jsquash-jxl adapter | The jSquash encoder, already measured |
|
|
182
|
+
| sharp, libvips, the `cjxl` npm wrapper | Native binaries |
|
|
183
|
+
|
|
184
|
+
Searched: npm's `jpeg-xl`, `jpegxl`, `jxl` and `jixel` results, upstream repositories, the web. A smaller usable
|
|
185
|
+
encoder published later belongs in this table, and the claim changes with it.
|
package/README.md
CHANGED
|
@@ -1,69 +1,54 @@
|
|
|
1
1
|
# Rapier JXL
|
|
2
2
|
|
|
3
|
-
A JPEG XL encoder in pure JavaScript
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
- **Photographs
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
reconstruction data (the JPEG file cannot be rebuilt from the stream), ICC, Exif beyond the orientation, XMP.
|
|
18
|
-
|
|
19
|
-
The retained corpus and seeded fuzz cases are decoded through jxl-oxide and native libjxl (below).
|
|
3
|
+
A JPEG XL encoder in pure JavaScript: one file, 19.5 kB, 8.5 kB gzipped, no WebAssembly, no
|
|
4
|
+
dependencies, MIT. The encoder inside [Rapier](https://rapier.website), published on its own. The smallest
|
|
5
|
+
JavaScript or WebAssembly JPEG XL encoder among the payloads [we measured](ENCODER-COMPARISON.md).
|
|
6
|
+
|
|
7
|
+
- **Lossless.** Every pixel back as it went in: 8-bit grey, grey with alpha, RGB, RGBA.
|
|
8
|
+
- **Lossy**, quality 1 to 99, for flat-colour rasters (screenshots, pixel art, scanned line art). Alpha stays exact.
|
|
9
|
+
A picture of few colours is written exact when that is smaller.
|
|
10
|
+
- **Smaller exact pictures, slower**: `rapier-jxl/effort`, the same `encode` with `{effort: 2}` or `3`, each never
|
|
11
|
+
larger than the effort below. Effort 1, its default, is the core's bytes.
|
|
12
|
+
- **Photographs**: `rapier-jxl/photo`, DCT8 compression with exact alpha, a door of its own.
|
|
13
|
+
- **A JPEG carried as its coefficients**: `rapier-jxl/jpeg`, one call, no decode, the way libjxl transcodes. Not
|
|
14
|
+
carried: the reconstruction data (the JPEG file cannot be rebuilt), the ICC bytes, Exif beyond the orientation, XMP.
|
|
15
|
+
- **sRGB or Display P3.** `{colorSpace: 'display-p3'}` declares a wide-gamut canvas's samples. A JPEG's profile is
|
|
16
|
+
read by what it does, not what it says: sRGB and Display P3 are carried and declared.
|
|
20
17
|
|
|
21
18
|
## Use it
|
|
22
19
|
|
|
23
20
|
```js
|
|
24
|
-
import {encode
|
|
21
|
+
import {encode} from 'rapier-jxl';
|
|
22
|
+
import {transcode} from 'rapier-jxl/jpeg';
|
|
23
|
+
import {encodePhoto} from 'rapier-jxl/photo';
|
|
25
24
|
|
|
26
|
-
// Pixels from a canvas (straight RGBA, row by row) to a JPEG XL codestream.
|
|
27
25
|
const {data, width, height} = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
28
26
|
const exact = encode(data, width, height); // lossless
|
|
29
27
|
const small = encode(data, width, height, {quality: 80}); // lossy
|
|
28
|
+
const photo = encodePhoto(data, width, height); // quality 90; 100 is exact
|
|
30
29
|
const blob = new Blob([small], {type: 'image/jxl'});
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
const jpeg = new Uint8Array(await file.arrayBuffer());
|
|
34
|
-
const {bytes, width: w, height: h, orientation} = transcode(jpeg);
|
|
31
|
+
const {bytes, width: w, height: h, orientation} = transcode(new Uint8Array(await file.arrayBuffer()));
|
|
35
32
|
```
|
|
36
33
|
|
|
37
|
-
`encode(data, width, height, {quality = 100})` takes
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
shown (swapped when the Exif orientation turns it), and the orientation is kept in the JPEG XL header.
|
|
34
|
+
`encode(data, width, height, {quality = 100})` takes straight RGBA bytes, row by row, and returns a `Uint8Array`
|
|
35
|
+
holding a bare JPEG XL codestream; `encodePhoto` takes the same. `transcode(jpeg)` returns
|
|
36
|
+
`{bytes, width, height, orientation}`: the size as shown, the orientation kept in the header.
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`encodeLossyRGBA`, `encodePhotoRGBA` and `transcode`; the raw
|
|
48
|
-
`encodeLossless`, `encodeLossy`, `transcodeJPEG`, `parseJPEG` and `inspectPixels` beneath them take input the
|
|
49
|
-
checked entries have already admitted and may throw plain errors on anything else.
|
|
50
|
-
|
|
51
|
-
```js
|
|
52
|
-
import {encodePhotoRGBA} from 'rapier-jxl/photo';
|
|
53
|
-
const photo = encodePhotoRGBA(data, width, height, {quality: 90});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
The photo entry defaults to quality 90; quality 100 is exact lossless. Its 1–99 range controls photographic
|
|
57
|
-
quantisation, while `encode` retains its artwork-oriented modular path. Quality numbers do not promise
|
|
58
|
-
identical PSNR between codecs or images. Neither lossy entry promises fewer bytes than lossless for every input.
|
|
38
|
+
Doors: `rapier-jxl` (the core), `rapier-jxl/effort`, `rapier-jxl/jpeg`, `rapier-jxl/photo`, each readable, so a
|
|
39
|
+
bundler carries their shared modules once; `rapier-jxl/min` is the core as one minified file. `rapier-jxl/writer`
|
|
40
|
+
gives a module's author the layers beneath the doors (readable only; they change only with the major version).
|
|
41
|
+
TypeScript declarations sit beside each door, and a worker and a page are under `public/examples/`. Quality numbers
|
|
42
|
+
are not the same fidelity across encoders or pictures, and lossy is not always smaller than lossless.
|
|
59
43
|
|
|
60
44
|
### In a worker
|
|
61
45
|
|
|
62
|
-
Encoding is synchronous
|
|
46
|
+
Encoding is synchronous. Run it off the main thread:
|
|
63
47
|
|
|
64
48
|
```js
|
|
65
49
|
// jxl-worker.mjs
|
|
66
|
-
import {encode
|
|
50
|
+
import {encode} from 'rapier-jxl';
|
|
51
|
+
import {transcode} from 'rapier-jxl/jpeg';
|
|
67
52
|
self.onmessage = ({data: {id, op, ...ask}}) => {
|
|
68
53
|
try {
|
|
69
54
|
const out = op === 'transcode' ? transcode(ask.jpeg) : {bytes: encode(ask.data, ask.width, ask.height, {quality: ask.quality})};
|
|
@@ -72,67 +57,86 @@ self.onmessage = ({data: {id, op, ...ask}}) => {
|
|
|
72
57
|
};
|
|
73
58
|
```
|
|
74
59
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
To cancel, terminate the worker and drop its request id. Keep the input in the caller if you may retry.
|
|
61
|
+
|
|
62
|
+
Each door has a twin that does the same work in steps: `encodeSteps`, `transcodeSteps`, `encodePhotoSteps` return a
|
|
63
|
+
job, `for (const done of job)` runs one group of one pass per step (`done` is the fraction, the last exactly 1), and
|
|
64
|
+
`job.bytes` is the stream after the loop, the same bytes the door writes. Leaving the loop cancels, so a worker can
|
|
65
|
+
take messages and report progress between steps (`public/examples/worker.mjs`). `job.hurry = true` (the example's
|
|
66
|
+
`deadline` sets it) ends the effort door's search at its next step with the smallest stream written so far, never
|
|
67
|
+
larger than effort 1's.
|
|
80
68
|
|
|
81
69
|
### Limits and errors
|
|
82
70
|
|
|
83
|
-
One picture at a time, at most 16,384 pixels
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
One picture at a time, at most 16,384 pixels a side and a 16 MiB stream. Each door's `LIMITS` sets its pixels by its
|
|
72
|
+
memory, so that none needs more at its limit than the core at its own: 24 million for the core and `effort` (a lossy
|
|
73
|
+
picture holds its planes whole, 15.7 bytes a pixel at its peak besides the input), 40 million for `photo` (6.5), and
|
|
74
|
+
64 million for a JPEG that `jpeg` carries (3.3 at 4:2:0, 6.4 at 4:4:4), so a phone's 24 and 48 megapixel
|
|
75
|
+
photographs are carried. Arguments are checked before any work. A refusal is an `Error` whose `code` is `JXL_INPUT`,
|
|
76
|
+
`JXL_DIMENSIONS`, `JXL_SIZE`, `JXL_MEMORY` or `JXL_JPEG` (arithmetic coding, 12-bit, lossless, CMYK, a DNL height, a
|
|
77
|
+
colour profile other than sRGB or Display P3, or a JPEG cut short: decode it and encode the pixels instead).
|
|
78
|
+
|
|
79
|
+
### From 1.x
|
|
80
|
+
|
|
81
|
+
2.0.0 is the package's own version (1.x took Rapier's), and the core is pixels only:
|
|
82
|
+
|
|
83
|
+
- `transcode` is in `rapier-jxl/jpeg`, and `encodePhotoRGBA` is `encodePhoto` in `rapier-jxl/photo`.
|
|
84
|
+
- `encodeLosslessRGBA(data, width, height)` and `rapier-jxl/lossless` are `encode(data, width, height)`;
|
|
85
|
+
`encodeLossyRGBA(data, width, height, quality)` is `encode(data, width, height, {quality})`.
|
|
86
|
+
- `encodeLossless`, `encodeLossy`, `inspectPixels`, `parseJPEG` and `transcodeJPEG` are in `rapier-jxl/writer`.
|
|
87
|
+
- A JPEG's colour profile is read by what it does: Display P3 (an iPhone's) is carried and declared, where 1.x
|
|
88
|
+
refused it; a profile of lookup tables is refused, even one named sRGB.
|
|
89
|
+
- Lossless streams and carried JPEGs are 1.x's bytes. Lossy streams of a picture wider or taller than 256 pixels,
|
|
90
|
+
or of a colour picture one pixel wide or high, and every photo stream changed where Chrome's decoder (jxl-rs
|
|
91
|
+
0.7.4) misread a valid stream; each decodes to the same pixels as before through jxl-oxide and libjxl.
|
|
92
|
+
|
|
93
|
+
New: `rapier-jxl/effort`, each door's twin in steps with `hurry`, `colorSpace: 'display-p3'`, each door's own
|
|
94
|
+
`LIMITS`.
|
|
89
95
|
|
|
90
96
|
## Sizes
|
|
91
97
|
|
|
92
|
-
| file | bytes | gzip | Brotli |
|
|
93
|
-
| --- | ---: | ---: | ---: |
|
|
94
|
-
| `rapier-jxl.min.mjs`, core |
|
|
95
|
-
| `
|
|
96
|
-
| `jpeg.min.mjs
|
|
97
|
-
| `photo.min.mjs
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
36 JPEG forms remain the acceptance baseline. These checks cover their inputs, not every possible codestream
|
|
133
|
-
or every decoder implementation.
|
|
98
|
+
| file | bytes | gzip | Brotli | added to the core, gzip |
|
|
99
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
100
|
+
| `rapier-jxl.min.mjs`, the core: `encode` | 19,495 | 8,475 | 7,494 | |
|
|
101
|
+
| `effort.min.mjs`: `encode` with effort | 25,229 | 10,606 | 9,351 | 2,131 |
|
|
102
|
+
| `jpeg.min.mjs`: `transcode` | 29,592 | 12,533 | 11,072 | 7,673 |
|
|
103
|
+
| `photo.min.mjs`: `encodePhoto` | 25,646 | 10,804 | 9,537 | 4,242 |
|
|
104
|
+
| every door in one bundle | 47,499 | 19,228 | 16,977 | |
|
|
105
|
+
| all readable modules | 136,580 | 42,707 | | |
|
|
106
|
+
|
|
107
|
+
Exact bytes of release 2.0.0's files, measured by the script that stages this repository; `sizes.json`
|
|
108
|
+
carries their hashes and tools (terser 5.51.2, Node v22.22.2; gzip 9, Brotli 11). Each minified file stands alone
|
|
109
|
+
and is proved at staging to write the same bytes as its readable source; the last column is what a door adds to a
|
|
110
|
+
bundle that already holds the core. `effort`'s `encode` is the core's at effort 1, so it takes the core's place, in
|
|
111
|
+
that column and in the bundle of every door.
|
|
112
|
+
|
|
113
|
+
What it writes: bare codestreams, 8-bit, prefix codes (never ANS), one frame, no preview, animation, ICC (sRGB or
|
|
114
|
+
Display P3 is declared), XYB, chroma-from-luma or filters. Lossless in modular mode, a palette of up to 2,048
|
|
115
|
+
colours weighed against direct coding by actual length, groups of 256, reversible YCoCg, a predictor chosen per
|
|
116
|
+
channel; at effort 2 and 3 also the weighted predictor, its contexts split by its own error. Lossy through Squeeze
|
|
117
|
+
with exact alpha. Carried JPEGs in VarDCT with the JPEG's own tables. The photo door
|
|
118
|
+
writes DCT8 coefficients for the same writer.
|
|
119
|
+
|
|
120
|
+
## Checked
|
|
121
|
+
|
|
122
|
+
Tests and seeded structure-aware fuzzing in `public/test/`: `npm test`, decoded through
|
|
123
|
+
[jxl-oxide](https://github.com/tirr-c/jxl-oxide) 0.12.6 and FFmpeg's native libjxl (a missing native decoder is
|
|
124
|
+
reported, and fails in CI). Exact pixels and alpha where promised, fidelity where relevant, the accepted JPEG
|
|
125
|
+
forms, refusal of malformed input. The same input writes the same bytes in every JavaScript engine: nothing that
|
|
126
|
+
decides a byte uses a function engines round differently, and `public/test/bytes.test.mjs` holds the streams' hashes
|
|
127
|
+
under Node and Bun. The 30 September run put 2,000,000 JPEG mutations and 32,768 pixel cases
|
|
128
|
+
through both decoders. The two decoders differ by one RGB unit on some JPEG and photo streams (floating-point
|
|
129
|
+
reconstruction), kept in `public/test/seeds/`; alpha and modular output are exact. With a C compiler and libjxl
|
|
130
|
+
headers, `npm run fuzz:scale -- --out fuzz-run --workers 4` repeats the fixed budget.
|
|
131
|
+
|
|
132
|
+
## Why
|
|
133
|
+
|
|
134
|
+
Rapier keeps pictures inside Markdown, and its standard says a raster picture in Markdown is JPEG XL: exact where
|
|
135
|
+
it must be, small where it may be. Drawings stay SVG. An editor carrying an encoder offline in a small page needed
|
|
136
|
+
one this size, and none existed. The standard is at [rapier.website](https://rapier.website); for an agent, see
|
|
137
|
+
`AGENTS.md`.
|
|
134
138
|
|
|
135
139
|
## Licence
|
|
136
140
|
|
|
137
|
-
MIT, copyright rapier.website. The design follows
|
|
138
|
-
|
|
141
|
+
MIT, copyright rapier.website. The design follows ISO/IEC 18181 and libjxl's encoders, whose sources were read;
|
|
142
|
+
none of their code is here.
|