rapier-jxl 1.1.16

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/modular.mjs ADDED
@@ -0,0 +1,156 @@
1
+ // Rapier's JPEG XL encoder: the modular sub-bitstream. MIT (LICENSE).
2
+ // Trees, transforms and channel residuals as the specification's modular decoder reads them. A channel is coded
3
+ // through the tree leaf it lands on: prediction, then `PackSigned(residual)` as a hybrid-integer token; runs of
4
+ // zero residuals of eight or more become one zero and an LZ77 copy of length run-1 at distance 1.
5
+ import {packSigned, floorLog2} from './bits.mjs';
6
+ import {buildCode, writeHistograms, uintConfig, countToken, hybridToken} from './prefix.mjs';
7
+
8
+ export const PREDICTOR = Object.freeze({zero: 0, left: 1, top: 2, average0: 3, select: 4, gradient: 5, weighted: 6,
9
+ topRight: 7, topLeft: 8, leftLeft: 9, average1: 10, average2: 11, average3: 12, average4: 13});
10
+
11
+ export const LZ77 = Object.freeze({minSymbol: 224, minLength: 7, lengthConfig: uintConfig(4)});
12
+ export const RESIDUAL_CONFIG = uintConfig(0);
13
+ export const ALPHABET = LZ77.minSymbol + 33;
14
+
15
+ export function leaf(predictor, offset = 0, multiplier = 1) { return {predictor, offset, multiplier, context: -1}; }
16
+ export function split(property, splitval, left, right) { return {property, splitval, left, right}; }
17
+
18
+ // One leaf per channel, split on the channel property (0): channels above the split value go left.
19
+ export function channelTree(leaves) {
20
+ const build = (lo, hi) => {
21
+ if (lo === hi) return leaves[lo];
22
+ const mid = (lo + hi) >> 1;
23
+ return split(0, mid, build(mid + 1, hi), build(lo, mid));
24
+ };
25
+ return build(0, leaves.length - 1);
26
+ }
27
+
28
+ // Writes the tree with its own histogram bundle (six contexts, one histogram) and numbers the leaves in the
29
+ // decoder's order. Returns the leaves in that order.
30
+ export function writeTree(w, root) {
31
+ const queue = [root], tokens = [], leaves = [];
32
+ while (queue.length) {
33
+ const node = queue.shift();
34
+ if (node.left) { tokens.push(node.property + 1, packSigned(node.splitval)); queue.push(node.left, node.right); continue; }
35
+ node.context = leaves.length; leaves.push(node);
36
+ const mulLog = node.multiplier === 1 ? 0 : 31 - Math.clz32(node.multiplier & -node.multiplier);
37
+ tokens.push(0, node.predictor, packSigned(node.offset), mulLog, (node.multiplier >> mulLog) - 1);
38
+ }
39
+ const freqs = new Uint32Array(64);
40
+ for (const value of tokens) countToken(RESIDUAL_CONFIG, value, freqs);
41
+ const code = buildCode(freqs);
42
+ writeHistograms(w, {contextMap: new Uint8Array(6), histograms: [{config: RESIDUAL_CONFIG, code}]});
43
+ const t = [0, 0, 0];
44
+ for (const value of tokens) { hybridToken(RESIDUAL_CONFIG, value, t); w.write(code.lengths[t[0]], code.codes[t[0]]); if (t[1]) w.write(t[1], t[2]); }
45
+ return leaves;
46
+ }
47
+
48
+ export function writeTransform(w, transform) {
49
+ const beginC = [[3, 0], [6, 8], [10, 72], [13, 1096]];
50
+ if (transform.type === 'rct') { w.write(2, 0); w.writeU32(beginC, transform.beginC); w.writeU32([[0, 6], [2, 0], [4, 2], [6, 10]], transform.rctType ?? 6); }
51
+ else if (transform.type === 'palette') {
52
+ w.write(2, 1); w.writeU32(beginC, transform.beginC);
53
+ w.writeU32([[0, 1], [0, 3], [0, 4], [13, 1]], transform.numC);
54
+ w.writeU32([[8, 0], [10, 256], [12, 1280], [16, 5376]], transform.nbColors);
55
+ w.writeU32([[0, 0], [8, 1], [10, 257], [16, 1281]], transform.nbDeltas ?? 0);
56
+ w.write(4, transform.predictor ?? 0);
57
+ } else if (transform.type === 'squeeze') {
58
+ w.write(2, 2);
59
+ const params = transform.params ?? [];
60
+ w.writeU32([[0, 0], [4, 1], [6, 9], [8, 41]], params.length);
61
+ for (const p of params) {
62
+ w.write(1, p.horizontal ? 1 : 0); w.write(1, p.inPlace ? 1 : 0);
63
+ w.writeU32(beginC, p.beginC); w.writeU32([[0, 1], [0, 2], [0, 3], [4, 4]], p.numC);
64
+ }
65
+ } else throw new Error('unknown transform ' + transform.type);
66
+ }
67
+
68
+ // The group header: whether the global tree applies, the (default) weighted predictor header, the transforms.
69
+ export function writeModularHeader(w, {useGlobalTree = true, transforms = []} = {}) {
70
+ w.write(1, useGlobalTree ? 1 : 0);
71
+ w.write(1, 1);
72
+ w.writeU32([[0, 0], [0, 1], [4, 2], [8, 18]], transforms.length);
73
+ for (const transform of transforms) writeTransform(w, transform);
74
+ }
75
+
76
+ // Splits on the stream property (1): one subtree per section, for sections whose channels differ. `sections` are
77
+ // {streamId, tree} sorted by stream id; a stream above the split value goes left.
78
+ export function streamTree(sections) {
79
+ const build = (lo, hi) => {
80
+ if (lo === hi) return sections[lo].tree;
81
+ const mid = (lo + hi) >> 1;
82
+ return split(1, sections[mid].streamId, build(mid + 1, hi), build(lo, mid));
83
+ };
84
+ return build(0, sections.length - 1);
85
+ }
86
+
87
+ // The channel histogram bundle after a tree: histogram 0 holds the LZ77 distance (one symbol, 1: distance one);
88
+ // histogram i+1 is `freqs[i]`, and every ordered leaf names its histogram through `histogramOf(leaf)`.
89
+ export function writeChannelHistograms(w, orderedLeaves, freqs, histogramOf = leaf => leaf.context) {
90
+ const contextMap = new Uint8Array(orderedLeaves.length + 1);
91
+ for (const leaf of orderedLeaves) contextMap[leaf.context] = histogramOf(leaf) + 1;
92
+ const distance = new Uint32Array(2); distance[1] = 1;
93
+ const histograms = [{config: RESIDUAL_CONFIG, code: buildCode(distance)}];
94
+ for (const f of freqs) histograms.push({config: RESIDUAL_CONFIG, code: buildCode(f)});
95
+ writeHistograms(w, {lz77: LZ77, contextMap, histograms});
96
+ return histograms;
97
+ }
98
+
99
+ // Codes one channel plane (Int32Array, width*height) through `leaf`. Counting when `w` is null: `target` is the
100
+ // leaf's token histogram (Uint32Array(ALPHABET)). Writing otherwise: `target` is the leaf's prefix code. Lossy
101
+ // leaves (multiplier above one) replace the plane's values by the decoder's reconstruction as they go.
102
+ export function codeChannel(w, target, plane, width, height, leaf) {
103
+ const predictor = leaf.predictor, offset = leaf.offset, multiplier = leaf.multiplier;
104
+ const config = RESIDUAL_CONFIG, lengthConfig = LZ77.lengthConfig, t = [0, 0, 0];
105
+ const lengths = w ? target.lengths : null, codes = w ? target.codes : null;
106
+ let run = 0;
107
+ const emit = value => {
108
+ if (!w) { countToken(config, value, target); return; }
109
+ hybridToken(config, value, t);
110
+ w.write(lengths[t[0]], codes[t[0]]);
111
+ if (t[1]) w.write(t[1], t[2]);
112
+ };
113
+ const flush = () => {
114
+ if (!run) return;
115
+ if (run >= LZ77.minLength + 1) {
116
+ emit(0);
117
+ const count = run - LZ77.minLength - 1;
118
+ if (!w) countToken(lengthConfig, count, target, LZ77.minSymbol);
119
+ else { hybridToken(lengthConfig, count, t); const symbol = LZ77.minSymbol + t[0]; w.write(lengths[symbol], codes[symbol]); if (t[1]) w.write(t[1], t[2]); }
120
+ } else for (let i = 0; i < run; i++) emit(0);
121
+ run = 0;
122
+ };
123
+ const residual = (index, pred) => {
124
+ const value = plane[index];
125
+ let r;
126
+ if (multiplier === 1) r = value - pred - offset;
127
+ else { r = Math.round((value - pred - offset) / multiplier); plane[index] = pred + offset + r * multiplier; }
128
+ if (r === 0) { run++; return; }
129
+ flush();
130
+ emit(packSigned(r));
131
+ };
132
+ for (let y = 0, index = 0; y < height; y++) {
133
+ for (let x = 0; x < width; x++, index++) {
134
+ let pred;
135
+ if (predictor === 5) {
136
+ const left = x ? plane[index - 1] : y ? plane[index - width] : 0;
137
+ const top = y ? plane[index - width] : left;
138
+ const topleft = x && y ? plane[index - width - 1] : left;
139
+ const grad = left + top - topleft, lo = left < top ? left : top, hi = left < top ? top : left;
140
+ pred = grad < lo ? lo : grad > hi ? hi : grad;
141
+ } else if (predictor === 0) pred = 0;
142
+ else if (predictor === 1) pred = x ? plane[index - 1] : y ? plane[index - width] : 0;
143
+ else if (predictor === 2) pred = y ? plane[index - width] : x ? plane[index - 1] : 0;
144
+ else if (predictor === 3) {
145
+ const left = x ? plane[index - 1] : y ? plane[index - width] : 0, top = y ? plane[index - width] : left;
146
+ pred = ((left + top) / 2) | 0;
147
+ } else if (predictor === 4) {
148
+ const left = x ? plane[index - 1] : y ? plane[index - width] : 0, top = y ? plane[index - width] : left;
149
+ const topleft = x && y ? plane[index - width - 1] : left, p = left + top - topleft;
150
+ pred = Math.abs(p - left) < Math.abs(p - top) ? left : top;
151
+ } else throw new Error('predictor ' + predictor + ' is not coded here');
152
+ residual(index, pred);
153
+ }
154
+ }
155
+ flush();
156
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "rapier-jxl",
3
+ "version": "1.1.16",
4
+ "description": "A JPEG XL encoder in pure JavaScript: lossless, lossy, and a JPEG carried whole. The encoder inside Rapier.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./index.mjs",
8
+ "exports": {
9
+ ".": "./index.mjs",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "*.mjs",
14
+ "README.md",
15
+ "AGENTS.md",
16
+ "LICENSE"
17
+ ],
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/jackskip22/rapier-jxl.git"
25
+ },
26
+ "homepage": "https://rapier.website",
27
+ "keywords": [
28
+ "jpeg xl",
29
+ "jxl",
30
+ "encoder",
31
+ "image",
32
+ "lossless",
33
+ "jpeg",
34
+ "transcode",
35
+ "markdown",
36
+ "rapier"
37
+ ]
38
+ }
package/prefix.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // Rapier's JPEG XL encoder: prefix codes and histogram bundles. MIT (LICENSE).
2
+ // The entropy code of every stream Rapier writes is a prefix (Huffman) code, never ANS: simpler, and within a
3
+ // percent or two of ANS on the pictures Rapier keeps. The bit layout follows the specification's Brotli-derived
4
+ // code header: a simple code for up to four symbols, else code lengths sent through a code-length code with the
5
+ // 16 (repeat) and 17 (zeros) run symbols.
6
+ import {floorLog2, ceilLog2} from './bits.mjs';
7
+
8
+ const CODE_LENGTH_ORDER = [1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15];
9
+ // The fixed code for code-length-code lengths 0..5 (bits written least-significant first).
10
+ const CLCL_BITS = [2, 4, 3, 2, 2, 4], CLCL_CODE = [0, 7, 3, 2, 1, 15];
11
+
12
+ // Huffman code lengths of at most `limit` bits: a complete code whenever two or more symbols are used.
13
+ export function codeLengths(freqs, limit) {
14
+ const lengths = new Uint8Array(freqs.length), used = [];
15
+ for (let i = 0; i < freqs.length; i++) if (freqs[i] > 0) used.push(i);
16
+ if (!used.length) return lengths;
17
+ if (used.length === 1) { lengths[used[0]] = 1; return lengths; }
18
+ for (let floor = 1; ; floor *= 2) {
19
+ // Flattening the small frequencies shortens the longest codes; the loop ends at a balanced tree at the latest.
20
+ const leaves = used.map(sym => ({w: Math.max(freqs[sym], floor), sym, l: null, r: null})).sort((a, b) => a.w - b.w || a.sym - b.sym);
21
+ const inner = [];
22
+ let li = 0, ii = 0;
23
+ const pop = () => li < leaves.length && (ii >= inner.length || leaves[li].w <= inner[ii].w) ? leaves[li++] : inner[ii++];
24
+ while ((leaves.length - li) + (inner.length - ii) > 1) { const a = pop(), b = pop(); inner.push({w: a.w + b.w, sym: -1, l: a, r: b}); }
25
+ const stack = [[pop(), 0]];
26
+ let deepest = 0;
27
+ while (stack.length) {
28
+ const [node, depth] = stack.pop();
29
+ if (node.sym >= 0) { lengths[node.sym] = depth; if (depth > deepest) deepest = depth; }
30
+ else stack.push([node.l, depth + 1], [node.r, depth + 1]);
31
+ }
32
+ if (deepest <= limit) return lengths;
33
+ }
34
+ }
35
+
36
+ // A run of `count`+3 repeats as chained run symbols: the decoder folds each new symbol's extra bits into the run
37
+ // so far, so the digits go out most significant first (as Brotli writes them).
38
+ function pushRun(tokens, extras, symbol, count, mask, shift) {
39
+ const digits = [];
40
+ for (;;) { digits.push(count & mask); count >>= shift; if (!count) break; count--; }
41
+ for (let i = digits.length - 1; i >= 0; i--) { tokens.push(symbol); extras.push(digits[i]); }
42
+ }
43
+
44
+ function reverseBits(value, count) { let out = 0; for (let i = 0; i < count; i++) { out = (out << 1) | (value & 1); value >>= 1; } return out; }
45
+
46
+ // Canonical codes (shorter codes first, then by symbol), already bit-reversed for least-significant-first writing.
47
+ export function canonicalCodes(lengths) {
48
+ const counts = new Uint32Array(17), next = new Uint32Array(17), codes = new Uint16Array(lengths.length);
49
+ for (const length of lengths) counts[length]++;
50
+ counts[0] = 0;
51
+ for (let length = 1, code = 0; length <= 16; length++) { code = (code + counts[length - 1]) << 1; next[length] = code; }
52
+ for (let i = 0; i < lengths.length; i++) if (lengths[i]) codes[i] = reverseBits(next[lengths[i]]++, lengths[i]);
53
+ return codes;
54
+ }
55
+
56
+ // A prefix code for a token histogram: `lengths`/`codes` per symbol, `alphabetSize` past the last used symbol, and
57
+ // the simple form's symbol order when four or fewer symbols are used. A histogram with no tokens has alphabet 1.
58
+ export function buildCode(freqs) {
59
+ let last = -1, count = 0;
60
+ for (let i = 0; i < freqs.length; i++) if (freqs[i] > 0) { last = i; count++; }
61
+ const alphabetSize = last + 1 || 1, lengths = new Uint8Array(alphabetSize), codes = new Uint16Array(alphabetSize);
62
+ if (count === 0) return {alphabetSize, lengths, codes, simple: null, treeSelect: 0};
63
+ if (count <= 4) {
64
+ // The decoder derives the lengths from the symbol order: the first symbol written takes the shortest code.
65
+ const byWeight = [];
66
+ for (let i = 0; i < freqs.length; i++) if (freqs[i] > 0) byWeight.push(i);
67
+ byWeight.sort((a, b) => freqs[b] - freqs[a] || a - b);
68
+ let simple, treeSelect = 0;
69
+ const assign = (sym, length, index) => { lengths[sym] = length; codes[sym] = index; };
70
+ if (count === 1) { simple = byWeight; assign(byWeight[0], 0, 0); }
71
+ else if (count === 2) { simple = [...byWeight].sort((a, b) => a - b); assign(simple[0], 1, 0); assign(simple[1], 1, 1); }
72
+ else if (count === 3) {
73
+ const rest = byWeight.slice(1).sort((a, b) => a - b);
74
+ simple = [byWeight[0], ...rest];
75
+ assign(simple[0], 1, 0); assign(rest[0], 2, 1); assign(rest[1], 2, 3);
76
+ } else {
77
+ const flat = [...byWeight].sort((a, b) => a - b);
78
+ const flatCost = 2 * flat.reduce((sum, sym) => sum + freqs[sym], 0);
79
+ const deepRest = byWeight.slice(2).sort((a, b) => a - b);
80
+ const deepCost = freqs[byWeight[0]] + 2 * freqs[byWeight[1]] + 3 * (freqs[deepRest[0]] + freqs[deepRest[1]]);
81
+ if (deepCost < flatCost) {
82
+ treeSelect = 1; simple = [byWeight[0], byWeight[1], ...deepRest];
83
+ assign(simple[0], 1, 0); assign(simple[1], 2, 1); assign(deepRest[0], 3, 3); assign(deepRest[1], 3, 7);
84
+ } else {
85
+ simple = flat;
86
+ assign(flat[0], 2, 0); assign(flat[1], 2, 2); assign(flat[2], 2, 1); assign(flat[3], 2, 3);
87
+ }
88
+ }
89
+ return {alphabetSize, lengths, codes, simple, treeSelect};
90
+ }
91
+ const full = codeLengths(freqs.subarray ? freqs.subarray(0, alphabetSize) : freqs.slice(0, alphabetSize), 15);
92
+ lengths.set(full);
93
+ codes.set(canonicalCodes(lengths));
94
+ return {alphabetSize, lengths, codes, simple: null, treeSelect: 0};
95
+ }
96
+
97
+ // The code header. An alphabet of one symbol has no header at all (the decoder reads none).
98
+ export function writePrefixCode(w, code) {
99
+ if (code.alphabetSize <= 1) return;
100
+ if (code.simple) {
101
+ const bits = floorLog2(code.alphabetSize - 1) + 1;
102
+ w.write(2, 1); w.write(2, code.simple.length - 1);
103
+ for (const sym of code.simple) w.write(bits, sym);
104
+ if (code.simple.length === 4) w.write(1, code.treeSelect);
105
+ return;
106
+ }
107
+ // Run-length tokens over the lengths up to the last used symbol, as Brotli writes them.
108
+ const lengths = code.lengths, tokens = [], extras = [];
109
+ let last = lengths.length - 1;
110
+ while (last > 0 && !lengths[last]) last--;
111
+ let previous = 8;
112
+ for (let i = 0; i <= last;) {
113
+ const value = lengths[i];
114
+ let run = 1;
115
+ while (i + run <= last && lengths[i + run] === value) run++;
116
+ if (value === 0) {
117
+ if (run < 3) for (let k = 0; k < run; k++) { tokens.push(0); extras.push(0); }
118
+ else pushRun(tokens, extras, 17, run - 3, 7, 3);
119
+ } else {
120
+ let repeats = run;
121
+ if (value !== previous) { tokens.push(value); extras.push(0); repeats--; previous = value; }
122
+ if (repeats < 3) for (let k = 0; k < repeats; k++) { tokens.push(value); extras.push(0); }
123
+ else pushRun(tokens, extras, 16, repeats - 3, 3, 2);
124
+ }
125
+ i += run;
126
+ }
127
+ code.tokens = tokens; code.extras = extras;
128
+ const freqs = new Uint32Array(18);
129
+ for (const token of tokens) freqs[token]++;
130
+ let distinct = 0;
131
+ for (const f of freqs) if (f) distinct++;
132
+ // A single token kind would be an incomplete code-length code: a second, unused symbol completes it.
133
+ if (distinct === 1) freqs[tokens[0] ? 0 : 1] = freqs[tokens[0]];
134
+ const clcl = codeLengths(freqs, 5), clCodes = canonicalCodes(clcl);
135
+ let count = 18;
136
+ while (count > 0 && !clcl[CODE_LENGTH_ORDER[count - 1]]) count--;
137
+ w.write(2, 0);
138
+ for (let i = 0; i < count; i++) { const length = clcl[CODE_LENGTH_ORDER[i]]; w.write(CLCL_BITS[length], CLCL_CODE[length]); }
139
+ for (let i = 0; i < tokens.length; i++) {
140
+ const token = tokens[i];
141
+ w.write(clcl[token], clCodes[token]);
142
+ if (token === 16) w.write(2, extras[i]); else if (token === 17) w.write(3, extras[i]);
143
+ }
144
+ }
145
+
146
+ // Hybrid integer configuration: values below 2^split are tokens; above, the top bit's position, the `msb` bits
147
+ // under it and the `lsb` lowest bits ride in the token, the bits between them raw. Rapier writes 0,0,0 for residuals (every bit below the top one raw)
148
+ // and 4,0,0 for run lengths, as libjxl's fast lossless path does.
149
+ export function uintConfig(split, msb = 0, lsb = 0) { return {split, msb, lsb, splitToken: 1 << split}; }
150
+
151
+ export function writeUintConfig(w, config, logAlphabetSize = 15) {
152
+ w.write(ceilLog2(logAlphabetSize + 1), config.split);
153
+ if (config.split === logAlphabetSize) return;
154
+ w.write(ceilLog2(config.split + 1), config.msb);
155
+ w.write(ceilLog2(config.split - config.msb + 1), config.lsb);
156
+ }
157
+
158
+ // The token of a value under a configuration, with its raw bits: `out` receives [token, nbits, bits].
159
+ export function hybridToken(config, value, out) {
160
+ if (value < config.splitToken) { out[0] = value; out[1] = 0; out[2] = 0; return; }
161
+ const n = floorLog2(value), below = value - (1 << n), nbits = n - config.msb - config.lsb;
162
+ out[0] = config.splitToken + (((n - config.split) << (config.msb + config.lsb)) | ((below >> (n - config.msb)) << config.lsb) | (below & ((1 << config.lsb) - 1)));
163
+ out[1] = nbits; out[2] = (value >> config.lsb) & ((1 << nbits) - 1);
164
+ }
165
+
166
+ // Counts a value into a token histogram (the same split as hybridToken), `base` symbols in (LZ77 lengths sit at 224).
167
+ export function countToken(config, value, freqs, base = 0) {
168
+ if (value < config.splitToken) { freqs[base + value]++; return; }
169
+ const n = floorLog2(value), below = value - (1 << n);
170
+ freqs[base + config.splitToken + (((n - config.split) << (config.msb + config.lsb)) | ((below >> (n - config.msb)) << config.lsb) | (below & ((1 << config.lsb) - 1)))]++;
171
+ }
172
+
173
+ // Writes the value through a code and configuration (token bits, then the raw bits).
174
+ const scratch = [0, 0, 0];
175
+ export function writeHybrid(w, code, config, value) {
176
+ hybridToken(config, value, scratch);
177
+ w.write(code.lengths[scratch[0]], code.codes[scratch[0]]);
178
+ if (scratch[1]) w.write(scratch[1], scratch[2]);
179
+ }
180
+
181
+ // The context map: histogram indices below 8 in the simple form (up to three bits each); otherwise the entries go
182
+ // through their own one-histogram bundle, each index a direct token (split 8), no move-to-front.
183
+ export function writeContextMap(w, contextMap) {
184
+ let widest = 0;
185
+ for (const index of contextMap) widest = Math.max(widest, index);
186
+ const bits = widest ? ceilLog2(widest + 1) : 0;
187
+ if (bits <= 3) {
188
+ w.write(1, 1); w.write(2, bits);
189
+ if (bits) for (const index of contextMap) w.write(bits, index);
190
+ return;
191
+ }
192
+ w.write(1, 0); w.write(1, 0);
193
+ const config = uintConfig(8), length = uintConfig(4), minLength = 7, minSymbol = 224;
194
+ // A long map repeats itself: runs of one index of eight or more go out as the index and an LZ77 copy at distance one.
195
+ const runs = contextMap.length >= 64, freqs = new Uint32Array(runs ? minSymbol + 33 : widest + 1);
196
+ const pieces = [];
197
+ for (let i = 0; i < contextMap.length;) {
198
+ let run = 1;
199
+ while (runs && i + run < contextMap.length && contextMap[i + run] === contextMap[i]) run++;
200
+ if (run >= minLength + 1) { pieces.push([contextMap[i], run - 1]); freqs[contextMap[i]]++; countToken(length, run - 1 - minLength, freqs, minSymbol); i += run; }
201
+ else { for (let k = 0; k < run; k++) { pieces.push([contextMap[i], 0]); freqs[contextMap[i]]++; } i += run; }
202
+ }
203
+ const code = buildCode(freqs);
204
+ if (!runs) {
205
+ writeHistograms(w, {contextMap: new Uint8Array(1), histograms: [{config, code}]});
206
+ for (const [index] of pieces) writeHybrid(w, code, config, index);
207
+ return;
208
+ }
209
+ // No distance multiplier here, so the distance is the value plus one: the one distance symbol is 0.
210
+ const distance = new Uint32Array(1); distance[0] = 1;
211
+ writeHistograms(w, {lz77: {minSymbol, minLength, lengthConfig: length}, contextMap: new Uint8Array([1, 0]), histograms: [{config: uintConfig(0), code: buildCode(distance)}, {config, code}]});
212
+ const t = [0, 0, 0];
213
+ for (const [index, copy] of pieces) {
214
+ writeHybrid(w, code, config, index);
215
+ if (copy) { hybridToken(length, copy - minLength, t); const symbol = minSymbol + t[0]; w.write(code.lengths[symbol], code.codes[symbol]); if (t[1]) w.write(t[1], t[2]); }
216
+ }
217
+ }
218
+
219
+ // A histogram bundle: LZ77 parameters, the context map, then one prefix code per histogram (with its integer
220
+ // configuration and alphabet size). `contextMap[i]` names the histogram of context i; with LZ77 on, the last
221
+ // context is the distance context. Only the simple context map (histogram indices below 8) is needed here.
222
+ export function writeHistograms(w, {lz77 = null, contextMap, histograms}) {
223
+ w.write(1, lz77 ? 1 : 0);
224
+ if (lz77) {
225
+ w.writeU32([[0, 224], [0, 512], [0, 4096], [15, 8]], lz77.minSymbol);
226
+ w.writeU32([[0, 3], [0, 4], [2, 5], [8, 9]], lz77.minLength);
227
+ writeUintConfig(w, lz77.lengthConfig, 8);
228
+ }
229
+ if (contextMap.length > 1) writeContextMap(w, contextMap);
230
+ w.write(1, 1); // prefix codes
231
+ for (const histogram of histograms) writeUintConfig(w, histogram.config, 15);
232
+ for (const histogram of histograms) {
233
+ const size = histogram.code.alphabetSize - 1;
234
+ if (!size) w.write(1, 0);
235
+ else { const n = floorLog2(size); w.write(1, 1); w.write(4, n); w.write(n, size - (1 << n)); }
236
+ }
237
+ for (const histogram of histograms) writePrefixCode(w, histogram.code);
238
+ }
package/squeeze.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // Rapier's JPEG XL encoder: the Squeeze transform. MIT (LICENSE).
2
+ // The forward of the specification's Squeeze (libjxl's enc_squeeze): each step halves a channel into averages and
3
+ // residuals minus a smooth tendency; the decoder's default parameter list is regenerated here step for step,
4
+ // so the bitstream names no parameters (`num_squeezes` 0) and the channel order is the decoder's.
5
+
6
+ const average = (a, b) => (a + b + (a > b ? 1 : 0)) >> 1;
7
+
8
+ export function smoothTendency(B, a, n) {
9
+ let diff = 0;
10
+ if (B >= a && a >= n) {
11
+ diff = ((4 * B - 3 * n - a + 6) / 12) | 0;
12
+ if (diff - (diff & 1) > 2 * (B - a)) diff = 2 * (B - a) + 1;
13
+ if (diff + (diff & 1) > 2 * (a - n)) diff = 2 * (a - n);
14
+ } else if (B <= a && a <= n) {
15
+ diff = ((4 * B - 3 * n - a - 6) / 12) | 0;
16
+ if (diff + (diff & 1) < 2 * (B - a)) diff = 2 * (B - a) - 1;
17
+ if (diff - (diff & 1) < 2 * (a - n)) diff = 2 * (a - n);
18
+ }
19
+ return diff;
20
+ }
21
+
22
+ // A channel: {w, h, hshift, vshift, data: Int32Array}.
23
+ function squeezeH(ch) {
24
+ const w = ch.w, h = ch.h, ow = (w + 1) >> 1, rw = w - ow;
25
+ const out = new Int16Array(ow * h), res = new Int16Array(rw * h), p = ch.data;
26
+ for (let y = 0; y < h; y++) {
27
+ const row = y * w, orow = y * ow, rrow = y * rw;
28
+ for (let x = 0; x < rw; x++) {
29
+ const A = p[row + 2 * x], B = p[row + 2 * x + 1], avg = average(A, B);
30
+ out[orow + x] = avg;
31
+ let next = avg;
32
+ if (x + 1 < rw) next = average(p[row + 2 * x + 2], p[row + 2 * x + 3]);
33
+ else if (w & 1) next = p[row + 2 * x + 2];
34
+ const left = x > 0 ? p[row + 2 * x - 1] : avg;
35
+ res[rrow + x] = A - B - smoothTendency(left, avg, next);
36
+ }
37
+ if (w & 1) out[orow + ow - 1] = p[row + w - 1];
38
+ }
39
+ return [{w: ow, h, hshift: ch.hshift + 1, vshift: ch.vshift, data: out}, {w: rw, h, hshift: ch.hshift + 1, vshift: ch.vshift, data: res}];
40
+ }
41
+
42
+ function squeezeV(ch) {
43
+ const w = ch.w, h = ch.h, oh = (h + 1) >> 1, rh = h - oh;
44
+ const out = new Int16Array(w * oh), res = new Int16Array(w * rh), p = ch.data;
45
+ for (let y = 0; y < rh; y++) {
46
+ const row = 2 * y * w;
47
+ for (let x = 0; x < w; x++) {
48
+ const A = p[row + x], B = p[row + w + x], avg = average(A, B);
49
+ out[y * w + x] = avg;
50
+ let next = avg;
51
+ if (y + 1 < rh) next = average(p[row + 2 * w + x], p[row + 3 * w + x]);
52
+ else if (h & 1) next = p[row + 2 * w + x];
53
+ const top = y > 0 ? p[row - w + x] : avg;
54
+ res[y * w + x] = A - B - smoothTendency(top, avg, next);
55
+ }
56
+ }
57
+ if (h & 1) { const y = oh - 1; for (let x = 0; x < w; x++) out[y * w + x] = p[2 * y * w + x]; }
58
+ return [{w, h: oh, hshift: ch.hshift, vshift: ch.vshift + 1, data: out}, {w, h: rh, hshift: ch.hshift, vshift: ch.vshift + 1, data: res}];
59
+ }
60
+
61
+ // The decoder's default parameters for an image whose channels all start at full size.
62
+ export function defaultSqueezeParams(channels) {
63
+ const params = [], n = channels.length;
64
+ let w = channels[0].w, h = channels[0].h;
65
+ if (n > 2 && channels[1].w === w && channels[1].h === h) {
66
+ params.push({horizontal: true, inPlace: false, beginC: 1, numC: 2});
67
+ params.push({horizontal: false, inPlace: false, beginC: 1, numC: 2});
68
+ }
69
+ const wide = w > h;
70
+ if (!wide && h > 8) { params.push({horizontal: false, inPlace: true, beginC: 0, numC: n}); h = (h + 1) >> 1; }
71
+ while (w > 8 || h > 8) {
72
+ if (w > 8) { params.push({horizontal: true, inPlace: true, beginC: 0, numC: n}); w = (w + 1) >> 1; }
73
+ if (h > 8) { params.push({horizontal: false, inPlace: true, beginC: 0, numC: n}); h = (h + 1) >> 1; }
74
+ }
75
+ return params;
76
+ }
77
+
78
+ // Applies the parameters in order, returning the new channel list (the decoder's order) and, per channel, the
79
+ // squeeze `level` (steps taken) and whether it holds residuals.
80
+ export function forwardSqueeze(channels, params = defaultSqueezeParams(channels)) {
81
+ const list = channels.map(ch => ({...ch, level: 0, residual: false}));
82
+ for (const p of params) {
83
+ const endC = p.beginC + p.numC - 1, offset = p.inPlace ? endC + 1 : list.length;
84
+ for (let c = p.beginC; c <= endC; c++) {
85
+ const [low, res] = (p.horizontal ? squeezeH : squeezeV)(list[c]);
86
+ const level = list[c].level + 1;
87
+ list[c] = {...low, level, residual: list[c].residual, component: list[c].component};
88
+ list.splice(offset + (c - p.beginC), 0, {...res, level, residual: true, component: list[c].component});
89
+ }
90
+ }
91
+ return list;
92
+ }