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/AGENTS.md +18 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/bits.mjs +71 -0
- package/entropy.mjs +99 -0
- package/frame.mjs +100 -0
- package/index.mjs +77 -0
- package/jpeg.mjs +260 -0
- package/lossless.mjs +131 -0
- package/lossy.mjs +125 -0
- package/modular.mjs +156 -0
- package/package.json +38 -0
- package/prefix.mjs +238 -0
- package/squeeze.mjs +92 -0
- package/vardct.mjs +216 -0
package/jpeg.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Rapier's JPEG XL encoder: reading a JPEG's coefficients. MIT (LICENSE).
|
|
2
|
+
// A JPEG is not decoded to pixels here: its quantised DCT coefficients are recovered from the Huffman-coded scans
|
|
3
|
+
// (baseline, extended sequential and progressive, 8-bit, grey or three components) so they can be carried into a
|
|
4
|
+
// JPEG XL frame as they are. Anything else (arithmetic coding, 12-bit, lossless, hierarchical, four components, a
|
|
5
|
+
// height left to a DNL marker) is refused with JXL_JPEG and the picture takes the ordinary path instead.
|
|
6
|
+
|
|
7
|
+
export const ZIGZAG = [0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28,
|
|
8
|
+
35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63];
|
|
9
|
+
|
|
10
|
+
export function jpegError(message) { return Object.assign(new Error('JPEG: ' + message), {code: 'JXL_JPEG'}); }
|
|
11
|
+
|
|
12
|
+
// A 16-bit lookup: entry = (code length << 8) | symbol, 0 for no code.
|
|
13
|
+
function buildHuffman(counts, symbols) {
|
|
14
|
+
const lookup = new Uint16Array(65536);
|
|
15
|
+
let code = 0, k = 0;
|
|
16
|
+
for (let length = 1; length <= 16; length++) {
|
|
17
|
+
for (let i = 0; i < counts[length - 1]; i++) {
|
|
18
|
+
const symbol = symbols[k++], start = code << (16 - length), n = 1 << (16 - length);
|
|
19
|
+
if (start + n > 65536) throw jpegError('bad Huffman table');
|
|
20
|
+
lookup.fill((length << 8) | symbol, start, start + n);
|
|
21
|
+
code++;
|
|
22
|
+
}
|
|
23
|
+
code <<= 1;
|
|
24
|
+
}
|
|
25
|
+
return lookup;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseJPEG(bytes) {
|
|
29
|
+
if (!(bytes.length > 4) || bytes[0] !== 0xFF || bytes[1] !== 0xD8) throw jpegError('not a JPEG');
|
|
30
|
+
const quant = [], huffman = [[], []];
|
|
31
|
+
let frame = null, restartInterval = 0, adobeTransform = -1, jfif = false, orientation = 1, pos = 2;
|
|
32
|
+
const u16 = at => (bytes[at] << 8) | bytes[at + 1];
|
|
33
|
+
for (;;) {
|
|
34
|
+
if (pos >= bytes.length) { if (frame && frame.scans) break; throw jpegError('truncated'); }
|
|
35
|
+
if (bytes[pos] !== 0xFF) throw jpegError('marker expected');
|
|
36
|
+
while (bytes[pos] === 0xFF) pos++;
|
|
37
|
+
const marker = bytes[pos++];
|
|
38
|
+
if (marker === 0xD9) break;
|
|
39
|
+
if (marker === 0xD8) throw jpegError('a second start');
|
|
40
|
+
if ((marker >= 0xD0 && marker <= 0xD7) || marker === 0x01) continue;
|
|
41
|
+
if (pos + 2 > bytes.length) throw jpegError('truncated');
|
|
42
|
+
const length = u16(pos), segment = pos + 2, end = pos + length;
|
|
43
|
+
if (length < 2 || end > bytes.length) throw jpegError('bad segment');
|
|
44
|
+
pos = end;
|
|
45
|
+
if (marker === 0xDB) {
|
|
46
|
+
for (let at = segment; at < end;) {
|
|
47
|
+
const pq = bytes[at] >> 4, tq = bytes[at] & 15;
|
|
48
|
+
if (pq > 1 || tq > 3) throw jpegError('bad quantisation table');
|
|
49
|
+
at++;
|
|
50
|
+
const table = new Int32Array(64);
|
|
51
|
+
for (let k = 0; k < 64; k++) { table[ZIGZAG[k]] = pq ? u16(at) : bytes[at]; at += pq ? 2 : 1; }
|
|
52
|
+
quant[tq] = table;
|
|
53
|
+
}
|
|
54
|
+
} else if (marker === 0xC4) {
|
|
55
|
+
for (let at = segment; at < end;) {
|
|
56
|
+
const tc = bytes[at] >> 4, th = bytes[at] & 15;
|
|
57
|
+
if (tc > 1 || th > 3) throw jpegError('bad Huffman table');
|
|
58
|
+
at++;
|
|
59
|
+
const counts = bytes.subarray(at, at + 16);
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (const c of counts) total += c;
|
|
62
|
+
huffman[tc][th] = buildHuffman(counts, bytes.subarray(at + 16, at + 16 + total));
|
|
63
|
+
at += 16 + total;
|
|
64
|
+
}
|
|
65
|
+
} else if (marker === 0xC0 || marker === 0xC1 || marker === 0xC2) {
|
|
66
|
+
if (frame) throw jpegError('two frames');
|
|
67
|
+
if (bytes[segment] !== 8) throw jpegError('only 8-bit samples');
|
|
68
|
+
const height = u16(segment + 1), width = u16(segment + 3), count = bytes[segment + 5];
|
|
69
|
+
if (!width || !height) throw jpegError('a height left to a DNL marker');
|
|
70
|
+
if (count !== 1 && count !== 3) throw jpegError('only grey or three components');
|
|
71
|
+
const components = [];
|
|
72
|
+
for (let i = 0, at = segment + 6; i < count; i++, at += 3) {
|
|
73
|
+
const h = bytes[at + 1] >> 4, v = bytes[at + 1] & 15;
|
|
74
|
+
if (h < 1 || h > 4 || v < 1 || v > 4) throw jpegError('bad sampling');
|
|
75
|
+
components.push({id: bytes[at], h, v, tq: bytes[at + 2], pred: 0});
|
|
76
|
+
}
|
|
77
|
+
const hmax = Math.max(...components.map(c => c.h)), vmax = Math.max(...components.map(c => c.v));
|
|
78
|
+
const mcusX = Math.ceil(width / (8 * hmax)), mcusY = Math.ceil(height / (8 * vmax));
|
|
79
|
+
for (const c of components) {
|
|
80
|
+
c.blocksW = Math.ceil(Math.ceil(width * c.h / hmax) / 8); c.blocksH = Math.ceil(Math.ceil(height * c.v / vmax) / 8);
|
|
81
|
+
c.stride = mcusX * c.h; c.rows = mcusY * c.v;
|
|
82
|
+
c.coeffs = new Int16Array(c.stride * c.rows * 64);
|
|
83
|
+
}
|
|
84
|
+
frame = {progressive: marker === 0xC2, width, height, components, hmax, vmax, mcusX, mcusY, scans: 0};
|
|
85
|
+
} else if (marker === 0xC3 || (marker >= 0xC5 && marker <= 0xC7) || (marker >= 0xC9 && marker <= 0xCB) || (marker >= 0xCD && marker <= 0xCF)) {
|
|
86
|
+
throw jpegError('lossless, hierarchical or arithmetic coding');
|
|
87
|
+
} else if (marker === 0xDD) restartInterval = u16(segment);
|
|
88
|
+
else if (marker === 0xDC) throw jpegError('a DNL marker');
|
|
89
|
+
else if (marker === 0xDA) {
|
|
90
|
+
if (!frame) throw jpegError('a scan before the frame');
|
|
91
|
+
const n = bytes[segment], scan = [];
|
|
92
|
+
let at = segment + 1;
|
|
93
|
+
for (let i = 0; i < n; i++, at += 2) {
|
|
94
|
+
const component = frame.components.find(c => c.id === bytes[at]);
|
|
95
|
+
if (!component) throw jpegError('a scan of an unknown component');
|
|
96
|
+
scan.push({component, dc: huffman[0][bytes[at + 1] >> 4], ac: huffman[1][bytes[at + 1] & 15]});
|
|
97
|
+
}
|
|
98
|
+
const ss = bytes[at], se = bytes[at + 1], ah = bytes[at + 2] >> 4, al = bytes[at + 2] & 15;
|
|
99
|
+
pos = decodeScan(bytes, end, frame, scan, ss, se, ah, al, restartInterval);
|
|
100
|
+
frame.scans++;
|
|
101
|
+
} else if (marker === 0xE0) {
|
|
102
|
+
if (bytes[segment] === 0x4A && bytes[segment + 1] === 0x46 && bytes[segment + 2] === 0x49 && bytes[segment + 3] === 0x46) jfif = true;
|
|
103
|
+
} else if (marker === 0xE1) {
|
|
104
|
+
if (orientation === 1 && bytes[segment] === 0x45 && bytes[segment + 1] === 0x78 && bytes[segment + 2] === 0x69 && bytes[segment + 3] === 0x66) orientation = exifOrientation(bytes, segment + 6, end);
|
|
105
|
+
} else if (marker === 0xEE) {
|
|
106
|
+
if (length >= 14 && bytes[segment] === 0x41 && bytes[segment + 1] === 0x64 && bytes[segment + 2] === 0x6F && bytes[segment + 3] === 0x62 && bytes[segment + 4] === 0x65) adobeTransform = bytes[segment + 11];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!frame || !frame.scans) throw jpegError('no picture');
|
|
110
|
+
for (const c of frame.components) { if (!quant[c.tq]) throw jpegError('a component without its quantisation table'); c.quant = quant[c.tq]; }
|
|
111
|
+
// Colour as libjxl reads it: JFIF means YCbCr; else an Adobe marker decides; else component ids spelling RGB.
|
|
112
|
+
let rgb = false;
|
|
113
|
+
if (frame.components.length === 3 && !jfif) {
|
|
114
|
+
if (adobeTransform >= 0) rgb = adobeTransform === 0;
|
|
115
|
+
else rgb = frame.components[0].id === 0x52 && frame.components[1].id === 0x47 && frame.components[2].id === 0x42;
|
|
116
|
+
}
|
|
117
|
+
return {width: frame.width, height: frame.height, components: frame.components, progressive: frame.progressive, ycbcr: !rgb, orientation};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The Exif orientation tag (0x0112) of the first image directory, 1 when absent or unreadable.
|
|
121
|
+
function exifOrientation(bytes, start, end) {
|
|
122
|
+
const big = bytes[start] === 0x4D && bytes[start + 1] === 0x4D;
|
|
123
|
+
if (!big && !(bytes[start] === 0x49 && bytes[start + 1] === 0x49)) return 1;
|
|
124
|
+
const u16 = at => at + 2 > end ? 0 : big ? (bytes[at] << 8) | bytes[at + 1] : bytes[at] | (bytes[at + 1] << 8);
|
|
125
|
+
const u32 = at => at + 4 > end ? 0 : big ? ((bytes[at] << 24) | (bytes[at + 1] << 16) | (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0 : (bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16) | (bytes[at + 3] << 24)) >>> 0;
|
|
126
|
+
if (u16(start + 2) !== 42) return 1;
|
|
127
|
+
const ifd = start + u32(start + 4), entries = u16(ifd);
|
|
128
|
+
for (let i = 0, at = ifd + 2; i < entries && at + 12 <= end; i++, at += 12) {
|
|
129
|
+
if (u16(at) !== 0x0112) continue;
|
|
130
|
+
const value = u16(at + 8);
|
|
131
|
+
return value >= 1 && value <= 8 ? value : 1;
|
|
132
|
+
}
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function decodeScan(bytes, start, frame, scan, ss, se, ah, al, restartInterval) {
|
|
137
|
+
let pos = start, buffer = 0, count = 0, marker = false;
|
|
138
|
+
const fill = () => {
|
|
139
|
+
while (count <= 24) {
|
|
140
|
+
let byte = 0;
|
|
141
|
+
if (!marker) {
|
|
142
|
+
if (pos >= bytes.length) marker = true;
|
|
143
|
+
else {
|
|
144
|
+
byte = bytes[pos];
|
|
145
|
+
if (byte === 0xFF) {
|
|
146
|
+
const next = bytes[pos + 1];
|
|
147
|
+
if (next === 0) pos += 2;
|
|
148
|
+
else if (next === 0xFF) { pos++; continue; }
|
|
149
|
+
else { marker = true; byte = 0; }
|
|
150
|
+
} else pos++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
buffer = ((buffer << 8) | byte) >>> 0; count += 8;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const receive = n => { if (!n) return 0; fill(); const value = (buffer >>> (count - n)) & ((1 << n) - 1); count -= n; return value; };
|
|
157
|
+
const extend = (value, n) => value < (1 << (n - 1)) ? value - (1 << n) + 1 : value;
|
|
158
|
+
const decode = table => {
|
|
159
|
+
if (!table) throw jpegError('a scan without its Huffman table');
|
|
160
|
+
fill();
|
|
161
|
+
const entry = table[(buffer >>> (count - 16)) & 0xFFFF];
|
|
162
|
+
if (!entry) throw jpegError('bad Huffman code');
|
|
163
|
+
count -= entry >> 8;
|
|
164
|
+
return entry & 255;
|
|
165
|
+
};
|
|
166
|
+
const restart = () => {
|
|
167
|
+
buffer = 0; count = 0; marker = false;
|
|
168
|
+
while (pos < bytes.length && !(bytes[pos] === 0xFF && bytes[pos + 1] >= 0xD0 && bytes[pos + 1] <= 0xD7)) pos++;
|
|
169
|
+
if (pos >= bytes.length) throw jpegError('a restart marker missing');
|
|
170
|
+
pos += 2;
|
|
171
|
+
for (const s of scan) s.component.pred = 0;
|
|
172
|
+
eobrun = 0;
|
|
173
|
+
};
|
|
174
|
+
let eobrun = 0;
|
|
175
|
+
const single = scan.length === 1, progressive = frame.progressive;
|
|
176
|
+
const baseline = (s, coeffs, at) => {
|
|
177
|
+
const t = decode(s.dc), diff = t ? extend(receive(t), t) : 0;
|
|
178
|
+
s.component.pred += diff;
|
|
179
|
+
coeffs[at] = s.component.pred;
|
|
180
|
+
for (let k = 1; k < 64;) {
|
|
181
|
+
const rs = decode(s.ac), r = rs >> 4, size = rs & 15;
|
|
182
|
+
if (!size) { if (r === 15) { k += 16; continue; } break; }
|
|
183
|
+
k += r;
|
|
184
|
+
if (k > 63) throw jpegError('coefficients past the block');
|
|
185
|
+
coeffs[at + ZIGZAG[k]] = extend(receive(size), size);
|
|
186
|
+
k++;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const dcFirst = (s, coeffs, at) => {
|
|
190
|
+
const t = decode(s.dc), diff = t ? extend(receive(t), t) : 0;
|
|
191
|
+
s.component.pred += diff;
|
|
192
|
+
coeffs[at] = s.component.pred << al;
|
|
193
|
+
};
|
|
194
|
+
const dcRefine = (s, coeffs, at) => { if (receive(1)) coeffs[at] |= 1 << al; };
|
|
195
|
+
const acFirst = (s, coeffs, at) => {
|
|
196
|
+
if (eobrun > 0) { eobrun--; return; }
|
|
197
|
+
for (let k = ss; k <= se;) {
|
|
198
|
+
const rs = decode(s.ac), r = rs >> 4, size = rs & 15;
|
|
199
|
+
if (!size) {
|
|
200
|
+
if (r < 15) { eobrun = (1 << r) - 1 + (r ? receive(r) : 0); break; }
|
|
201
|
+
k += 16; continue;
|
|
202
|
+
}
|
|
203
|
+
k += r;
|
|
204
|
+
if (k > 63) throw jpegError('coefficients past the block');
|
|
205
|
+
coeffs[at + ZIGZAG[k]] = extend(receive(size), size) * (1 << al);
|
|
206
|
+
k++;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const acRefine = (s, coeffs, at) => {
|
|
210
|
+
const p1 = 1 << al, m1 = -1 << al;
|
|
211
|
+
let k = ss;
|
|
212
|
+
if (eobrun <= 0) {
|
|
213
|
+
for (; k <= se; k++) {
|
|
214
|
+
const rs = decode(s.ac);
|
|
215
|
+
let r = rs >> 4, value = 0;
|
|
216
|
+
const size = rs & 15;
|
|
217
|
+
if (size) { if (size !== 1) throw jpegError('a bad refinement code'); value = receive(1) ? p1 : m1; }
|
|
218
|
+
else if (r !== 15) { eobrun = 1 << r; if (r) eobrun += receive(r); break; }
|
|
219
|
+
for (; k <= se; k++) {
|
|
220
|
+
const z = at + ZIGZAG[k];
|
|
221
|
+
if (coeffs[z] !== 0) { if (receive(1) && (coeffs[z] & p1) === 0) coeffs[z] += coeffs[z] >= 0 ? p1 : m1; }
|
|
222
|
+
else if (--r < 0) break;
|
|
223
|
+
}
|
|
224
|
+
if (value && k <= se) coeffs[at + ZIGZAG[k]] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (eobrun > 0) {
|
|
228
|
+
for (; k <= se; k++) {
|
|
229
|
+
const z = at + ZIGZAG[k];
|
|
230
|
+
if (coeffs[z] !== 0 && receive(1) && (coeffs[z] & p1) === 0) coeffs[z] += coeffs[z] >= 0 ? p1 : m1;
|
|
231
|
+
}
|
|
232
|
+
eobrun--;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const block = !progressive ? baseline : ss === 0 ? (ah === 0 ? dcFirst : dcRefine) : (ah === 0 ? acFirst : acRefine);
|
|
236
|
+
if (progressive && ss > 0 && !single) throw jpegError('an interleaved AC scan');
|
|
237
|
+
if (progressive && (se > 63 || ss > se)) throw jpegError('a bad spectral selection');
|
|
238
|
+
for (const s of scan) s.component.pred = 0;
|
|
239
|
+
const first = scan[0].component;
|
|
240
|
+
const total = single ? first.blocksW * first.blocksH : frame.mcusX * frame.mcusY;
|
|
241
|
+
for (let unit = 0; unit < total; unit++) {
|
|
242
|
+
if (restartInterval && unit > 0 && unit % restartInterval === 0) restart();
|
|
243
|
+
if (single) {
|
|
244
|
+
const by = (unit / first.blocksW) | 0, bx = unit - by * first.blocksW;
|
|
245
|
+
block(scan[0], first.coeffs, (by * first.stride + bx) * 64);
|
|
246
|
+
} else {
|
|
247
|
+
const my = (unit / frame.mcusX) | 0, mx = unit - my * frame.mcusX;
|
|
248
|
+
for (const s of scan) {
|
|
249
|
+
const c = s.component;
|
|
250
|
+
for (let v = 0; v < c.v; v++) for (let h = 0; h < c.h; h++) block(s, c.coeffs, ((my * c.v + v) * c.stride + mx * c.h + h) * 64);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Past the scan's data to the next marker.
|
|
255
|
+
while (pos < bytes.length) {
|
|
256
|
+
if (bytes[pos] === 0xFF && bytes[pos + 1] !== 0 && bytes[pos + 1] !== 0xFF && !(bytes[pos + 1] >= 0xD0 && bytes[pos + 1] <= 0xD7)) break;
|
|
257
|
+
pos++;
|
|
258
|
+
}
|
|
259
|
+
return pos;
|
|
260
|
+
}
|
package/lossless.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Rapier's JPEG XL encoder: lossless pictures. MIT (LICENSE).
|
|
2
|
+
// The design of libjxl's fast lossless path, written for Rapier: 8-bit grey, grey+alpha, RGB or RGBA; an opaque
|
|
3
|
+
// alpha is dropped and a grey picture keeps one channel; up to 512 colours become a palette; colour goes through
|
|
4
|
+
// the reversible YCoCg transform; every channel is predicted by the clamped gradient and coded with one prefix
|
|
5
|
+
// code per channel, zero runs as LZ77 copies; groups of 256x256 pixels, each an independent section.
|
|
6
|
+
import {BitWriter} from './bits.mjs';
|
|
7
|
+
import {writeImageHeader, writeModularFrameHeader, groupLayout, assembleCodestream, GROUP_DIM} from './frame.mjs';
|
|
8
|
+
import {PREDICTOR, ALPHABET, leaf, channelTree, writeTree, writeModularHeader, writeChannelHistograms, codeChannel} from './modular.mjs';
|
|
9
|
+
|
|
10
|
+
const MAX_PALETTE = 512;
|
|
11
|
+
|
|
12
|
+
// What the picture is: which channels it needs and, when few colours, its palette.
|
|
13
|
+
export function inspectPixels(rgba, width, height, {palette: wantPalette = true} = {}) {
|
|
14
|
+
const pixels = width * height;
|
|
15
|
+
let opaque = true, grey = true;
|
|
16
|
+
for (let i = 0, p = 0; p < pixels; p++, i += 4) {
|
|
17
|
+
if (rgba[i + 3] !== 255) { opaque = false; if (!grey) break; }
|
|
18
|
+
if (rgba[i] !== rgba[i + 1] || rgba[i] !== rgba[i + 2]) { grey = false; if (!opaque) break; }
|
|
19
|
+
}
|
|
20
|
+
const colour = grey ? 1 : 3, alpha = !opaque, channels = colour + (alpha ? 1 : 0);
|
|
21
|
+
// The packed colour of a pixel: channel c in byte c, as the palette rows are laid out.
|
|
22
|
+
const key = i => grey
|
|
23
|
+
? (alpha ? rgba[i] + rgba[i + 3] * 256 : rgba[i])
|
|
24
|
+
: rgba[i] + rgba[i + 1] * 256 + rgba[i + 2] * 65536 + (alpha ? rgba[i + 3] * 16777216 : 0);
|
|
25
|
+
const seen = new Map();
|
|
26
|
+
let palette = null;
|
|
27
|
+
for (let i = 0, p = 0; wantPalette && p < pixels; p++, i += 4) {
|
|
28
|
+
const k = key(i);
|
|
29
|
+
if (k === 0 || seen.has(k)) continue;
|
|
30
|
+
seen.set(k, 1);
|
|
31
|
+
if (seen.size >= MAX_PALETTE) break;
|
|
32
|
+
}
|
|
33
|
+
if (wantPalette && seen.size < MAX_PALETTE) {
|
|
34
|
+
const entries = [...seen.keys()];
|
|
35
|
+
const byte = (k, c) => Math.floor(k / 256 ** c) % 256;
|
|
36
|
+
let lowest = 255, highest = 0;
|
|
37
|
+
for (const k of entries) { const g = byte(k, 1); if (g < lowest) lowest = g; if (g > highest) highest = g; }
|
|
38
|
+
// A grey ramp without holes gains nothing from a palette.
|
|
39
|
+
const ramp = colour === 1 && !alpha && (() => { let lo = 255, hi = 0; for (const k of entries) { if (k < lo) lo = k; if (k > hi) hi = k; } return hi - lo < entries.length * 1.4; })();
|
|
40
|
+
if (!ramp) {
|
|
41
|
+
const weight = k => (0.299 * byte(k, 0) + 0.587 * byte(k, 1) + 0.114 * byte(k, 2) + 0.01) * (channels === 4 ? byte(k, 3) : 1);
|
|
42
|
+
entries.sort((a, b) => weight(a) - weight(b));
|
|
43
|
+
const index = new Map([[0, 0]]);
|
|
44
|
+
entries.forEach((k, at) => index.set(k, at + 1));
|
|
45
|
+
palette = {colours: [0, ...entries], index, key, byte};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {colour, alpha, channels, palette};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function encodeLossless(rgba, width, height, {shape = inspectPixels(rgba, width, height)} = {}) {
|
|
52
|
+
const {channels, palette} = shape;
|
|
53
|
+
const layout = groupLayout(width, height);
|
|
54
|
+
const streamChannels = palette ? 2 : channels; // the palette's meta channel and the index channel
|
|
55
|
+
const leaves = Array.from({length: streamChannels}, () => leaf(PREDICTOR.gradient));
|
|
56
|
+
const tree = channelTree(leaves);
|
|
57
|
+
const transforms = palette
|
|
58
|
+
? [{type: 'palette', beginC: 0, numC: channels, nbColors: palette.colours.length}]
|
|
59
|
+
: channels >= 3 ? [{type: 'rct', beginC: 0, rctType: 6}] : [];
|
|
60
|
+
|
|
61
|
+
// Channel planes of a group rectangle, computed straight from the pixels.
|
|
62
|
+
const planes = Array.from({length: streamChannels}, () => new Int32Array(GROUP_DIM * GROUP_DIM));
|
|
63
|
+
const fill = (x0, y0, w, h) => {
|
|
64
|
+
for (let y = 0; y < h; y++) {
|
|
65
|
+
let i = ((y0 + y) * width + x0) * 4, at = y * w;
|
|
66
|
+
for (let x = 0; x < w; x++, i += 4, at++) {
|
|
67
|
+
if (palette) { planes[0][at] = palette.index.get(palette.key(i)); continue; }
|
|
68
|
+
if (channels >= 3) {
|
|
69
|
+
const r = rgba[i], g = rgba[i + 1], b = rgba[i + 2];
|
|
70
|
+
const co = r - b, tmp = b + (co >> 1), cg = g - tmp;
|
|
71
|
+
planes[0][at] = tmp + (cg >> 1); planes[1][at] = co; planes[2][at] = cg;
|
|
72
|
+
if (channels === 4) planes[3][at] = rgba[i + 3];
|
|
73
|
+
} else { planes[0][at] = rgba[i]; if (channels === 2) planes[1][at] = rgba[i + 3]; }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// The palette meta channel: one row per channel, one column per colour.
|
|
78
|
+
let paletteRows = null;
|
|
79
|
+
if (palette) {
|
|
80
|
+
const n = palette.colours.length;
|
|
81
|
+
paletteRows = new Int32Array(n * channels);
|
|
82
|
+
palette.colours.forEach((k, at) => { for (let c = 0; c < channels; c++) paletteRows[c * n + at] = palette.byte(k, c); });
|
|
83
|
+
}
|
|
84
|
+
// The image channels of a section: in the global section they follow the meta channel; in a group they start at 0.
|
|
85
|
+
const pictureLeaf = c => leaves[palette ? (layout.single ? 1 : 0) : c];
|
|
86
|
+
const pictureChannels = palette ? 1 : channels;
|
|
87
|
+
|
|
88
|
+
const forGroups = visit => {
|
|
89
|
+
for (let gy = 0; gy < layout.groupsY; gy++) for (let gx = 0; gx < layout.groupsX; gx++) {
|
|
90
|
+
const x0 = gx * GROUP_DIM, y0 = gy * GROUP_DIM, w = Math.min(GROUP_DIM, width - x0), h = Math.min(GROUP_DIM, height - y0);
|
|
91
|
+
fill(x0, y0, w, h);
|
|
92
|
+
visit(gy * layout.groupsX + gx, w, h);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Pass one: token histograms per leaf.
|
|
97
|
+
const freqs = leaves.map(() => new Uint32Array(ALPHABET)), freqOf = l => freqs[leaves.indexOf(l)];
|
|
98
|
+
if (paletteRows) codeChannel(null, freqOf(leaves[0]), paletteRows, palette.colours.length, channels, leaves[0]);
|
|
99
|
+
forGroups((g, w, h) => { for (let c = 0; c < pictureChannels; c++) codeChannel(null, freqOf(pictureLeaf(c)), planes[c], w, h, pictureLeaf(c)); });
|
|
100
|
+
|
|
101
|
+
// Pass two: the sections.
|
|
102
|
+
const header = new BitWriter(256);
|
|
103
|
+
writeImageHeader(header, width, height, shape.colour, shape.alpha);
|
|
104
|
+
writeModularFrameHeader(header, {alpha: shape.alpha});
|
|
105
|
+
const global = new BitWriter(4096);
|
|
106
|
+
global.write(1, 1); // default DC quantisation
|
|
107
|
+
global.write(1, 1); // a global tree
|
|
108
|
+
const ordered = writeTree(global, tree);
|
|
109
|
+
const histograms = writeChannelHistograms(global, ordered, freqs, l => leaves.indexOf(l));
|
|
110
|
+
const codeOf = l => histograms[leaves.indexOf(l) + 1].code;
|
|
111
|
+
writeModularHeader(global, {useGlobalTree: true, transforms});
|
|
112
|
+
if (paletteRows) codeChannel(global, codeOf(leaves[0]), paletteRows, palette.colours.length, channels, leaves[0]);
|
|
113
|
+
const sections = [];
|
|
114
|
+
if (layout.single) {
|
|
115
|
+
forGroups((g, w, h) => { for (let c = 0; c < pictureChannels; c++) codeChannel(global, codeOf(pictureLeaf(c)), planes[c], w, h, pictureLeaf(c)); });
|
|
116
|
+
global.zeroPadToByte();
|
|
117
|
+
sections.push(global.finish());
|
|
118
|
+
} else {
|
|
119
|
+
global.zeroPadToByte();
|
|
120
|
+
sections.push(global.finish());
|
|
121
|
+
for (let i = 0; i < layout.dcGroupsX * layout.dcGroupsY + 1; i++) sections.push(new Uint8Array(0));
|
|
122
|
+
forGroups((g, w, h) => {
|
|
123
|
+
const section = new BitWriter(w * h * pictureChannels + 64);
|
|
124
|
+
writeModularHeader(section, {useGlobalTree: true, transforms: []});
|
|
125
|
+
for (let c = 0; c < pictureChannels; c++) codeChannel(section, codeOf(pictureLeaf(c)), planes[c], w, h, pictureLeaf(c));
|
|
126
|
+
section.zeroPadToByte();
|
|
127
|
+
sections.push(section.finish());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return assembleCodestream(header, sections);
|
|
131
|
+
}
|
package/lossy.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Rapier's JPEG XL encoder: lossy pictures. MIT (LICENSE).
|
|
2
|
+
// Lossy modular as libjxl encodes it: the reversible YCoCg transform, the Squeeze transform to its default
|
|
3
|
+
// depth, then each squeezed channel quantised by a step that halves with every level (chroma coarser than luma),
|
|
4
|
+
// carried as the multiplier of that channel's tree leaf so the decoder scales the residuals back. The finest
|
|
5
|
+
// low-pass image is kept exact. Quality maps to libjxl's distance (90 is 1.0).
|
|
6
|
+
import {BitWriter} from './bits.mjs';
|
|
7
|
+
import {writeImageHeader, writeModularFrameHeader, groupLayout, assembleCodestream, GROUP_DIM, DC_GROUP_DIM} from './frame.mjs';
|
|
8
|
+
import {PREDICTOR, ALPHABET, leaf, channelTree, streamTree, writeTree, writeModularHeader, writeChannelHistograms, codeChannel} from './modular.mjs';
|
|
9
|
+
import {forwardSqueeze} from './squeeze.mjs';
|
|
10
|
+
import {inspectPixels} from './lossless.mjs';
|
|
11
|
+
|
|
12
|
+
const LUMA_STEPS = [163.84, 81.92, 40.96, 20.48, 10.24, 5.12, 2.56, 1.28, 0.64, 0.32, 0.16, 0.08, 0.04, 0.02, 0.01, 0.005];
|
|
13
|
+
const CHROMA_STEPS = [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.5, 0.5, 0.5, 0.5];
|
|
14
|
+
const QUALITY_FACTOR = 0.35, LUMA_FACTOR = 1.1;
|
|
15
|
+
|
|
16
|
+
// libjxl's JxlEncoderDistanceFromQuality.
|
|
17
|
+
export function distanceFromQuality(quality) {
|
|
18
|
+
return quality >= 100 ? 0 : quality >= 30 ? 0.1 + (100 - quality) * 0.09 : 53 / 3000 * quality * quality - 23 / 20 * quality + 25;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The quantisation step of a squeezed channel: `component` 0 luma, 1-2 chroma, 3 an extra channel.
|
|
22
|
+
export function quantiserFor(component, hshift, vshift, distance) {
|
|
23
|
+
let shift = Math.min(16, hshift + vshift);
|
|
24
|
+
if (shift > 0) shift--;
|
|
25
|
+
// An extra channel (alpha) is quantised at half the colour steps: its edges fringe before colour does.
|
|
26
|
+
const base = component === 3 ? 0.125 * distance : 0.25 * distance ** 1.2;
|
|
27
|
+
const q = component === 1 || component === 2 ? base * QUALITY_FACTOR * CHROMA_STEPS[shift] : base * QUALITY_FACTOR * LUMA_FACTOR * LUMA_STEPS[shift];
|
|
28
|
+
return Math.max(1, Math.floor(q));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function encodeLossy(rgba, width, height, {quality = 90, shape = inspectPixels(rgba, width, height, {palette: false})} = {}) {
|
|
32
|
+
const distance = distanceFromQuality(quality);
|
|
33
|
+
if (!(distance > 0)) throw new Error('lossy encoding needs a quality below 100');
|
|
34
|
+
const {colour, alpha, channels: count} = shape;
|
|
35
|
+
const pixels = width * height;
|
|
36
|
+
// Full planes in the transformed colour space.
|
|
37
|
+
const planes = Array.from({length: count}, () => new Int16Array(pixels));
|
|
38
|
+
for (let i = 0, p = 0; p < pixels; p++, i += 4) {
|
|
39
|
+
if (colour === 3) {
|
|
40
|
+
const r = rgba[i], g = rgba[i + 1], b = rgba[i + 2], co = r - b, tmp = b + (co >> 1), cg = g - tmp;
|
|
41
|
+
planes[0][p] = tmp + (cg >> 1); planes[1][p] = co; planes[2][p] = cg;
|
|
42
|
+
} else planes[0][p] = rgba[i];
|
|
43
|
+
if (alpha) planes[count - 1][p] = rgba[i + 3];
|
|
44
|
+
}
|
|
45
|
+
const channels = planes.map((data, c) => ({w: width, h: height, hshift: 0, vshift: 0, data, component: colour === 3 ? c : c === 0 ? 0 : 3}));
|
|
46
|
+
const squeezed = forwardSqueeze(channels);
|
|
47
|
+
const predictorOf = ch => ch.residual ? PREDICTOR.zero : PREDICTOR.gradient;
|
|
48
|
+
const quantiserOf = ch => quantiserFor(ch.component, ch.hshift, ch.vshift, distance);
|
|
49
|
+
const transforms = [...(colour === 3 ? [{type: 'rct', beginC: 0, rctType: 6}] : []), {type: 'squeeze', params: []}];
|
|
50
|
+
|
|
51
|
+
// The sections and the channel pieces each holds, in the decoder's order: the global section takes the channels
|
|
52
|
+
// up to the first wider or taller than a group; DC groups take the rest with both shifts of three or more, AC
|
|
53
|
+
// groups the rest; a piece is the group's rectangle at the channel's scale. A channel's number inside a section
|
|
54
|
+
// is its place among that section's pieces, so every section gets its own subtree under the stream property.
|
|
55
|
+
const layout = groupLayout(width, height), dcGroups = layout.dcGroupsX * layout.dcGroupsY, acGroups = layout.groupsX * layout.groupsY;
|
|
56
|
+
let globalCount = 0;
|
|
57
|
+
while (globalCount < squeezed.length && squeezed[globalCount].w <= GROUP_DIM && squeezed[globalCount].h <= GROUP_DIM) globalCount++;
|
|
58
|
+
const rectOf = (ch, x0, y0, dim) => {
|
|
59
|
+
const x = x0 >> ch.hshift, y = y0 >> ch.vshift;
|
|
60
|
+
return {x, y, w: Math.min(ch.w, x + (dim >> ch.hshift)) - x, h: Math.min(ch.h, y + (dim >> ch.vshift)) - y};
|
|
61
|
+
};
|
|
62
|
+
const sections = [{index: 0, streamId: 0, pieces: []}];
|
|
63
|
+
for (let c = 0; c < globalCount; c++) sections[0].pieces.push({c, rect: {x: 0, y: 0, w: squeezed[c].w, h: squeezed[c].h}});
|
|
64
|
+
if (!layout.single) {
|
|
65
|
+
for (let g = 0; g < dcGroups; g++) {
|
|
66
|
+
const gx = g % layout.dcGroupsX, gy = (g / layout.dcGroupsX) | 0, section = {index: 1 + g, streamId: 1 + dcGroups + g, pieces: []};
|
|
67
|
+
for (let c = globalCount; c < squeezed.length; c++) {
|
|
68
|
+
const ch = squeezed[c];
|
|
69
|
+
if (Math.min(ch.hshift, ch.vshift) < 3) continue;
|
|
70
|
+
const rect = rectOf(ch, gx * DC_GROUP_DIM, gy * DC_GROUP_DIM, DC_GROUP_DIM);
|
|
71
|
+
if (rect.w > 0 && rect.h > 0) section.pieces.push({c, rect});
|
|
72
|
+
}
|
|
73
|
+
sections.push(section);
|
|
74
|
+
}
|
|
75
|
+
for (let g = 0; g < acGroups; g++) {
|
|
76
|
+
const gx = g % layout.groupsX, gy = (g / layout.groupsX) | 0, section = {index: 2 + dcGroups + g, streamId: 1 + 3 * dcGroups + 17 + g, pieces: []};
|
|
77
|
+
for (let c = globalCount; c < squeezed.length; c++) {
|
|
78
|
+
const ch = squeezed[c];
|
|
79
|
+
if (Math.min(ch.hshift, ch.vshift) > 2) continue;
|
|
80
|
+
const rect = rectOf(ch, gx * GROUP_DIM, gy * GROUP_DIM, GROUP_DIM);
|
|
81
|
+
if (rect.w > 0 && rect.h > 0) section.pieces.push({c, rect});
|
|
82
|
+
}
|
|
83
|
+
sections.push(section);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const section of sections) for (const piece of section.pieces) {
|
|
87
|
+
const ch = squeezed[piece.c];
|
|
88
|
+
piece.leaf = leaf(predictorOf(ch), 0, quantiserOf(ch)); piece.leaf.channel = piece.c;
|
|
89
|
+
}
|
|
90
|
+
const withPieces = sections.filter(section => section.pieces.length);
|
|
91
|
+
const tree = withPieces.length === 1 ? channelTree(withPieces[0].pieces.map(p => p.leaf))
|
|
92
|
+
: streamTree(withPieces.map(section => ({streamId: section.streamId, tree: channelTree(section.pieces.map(p => p.leaf))})));
|
|
93
|
+
|
|
94
|
+
const scratch = new Int16Array(GROUP_DIM * GROUP_DIM);
|
|
95
|
+
const dataOf = piece => {
|
|
96
|
+
const ch = squeezed[piece.c], rect = piece.rect;
|
|
97
|
+
if (rect.w === ch.w && rect.h === ch.h) return ch.data;
|
|
98
|
+
for (let y = 0; y < rect.h; y++) scratch.set(ch.data.subarray((rect.y + y) * ch.w + rect.x, (rect.y + y) * ch.w + rect.x + rect.w), y * rect.w);
|
|
99
|
+
return scratch;
|
|
100
|
+
};
|
|
101
|
+
const freqs = squeezed.map(() => new Uint32Array(ALPHABET));
|
|
102
|
+
for (const section of sections) for (const piece of section.pieces) codeChannel(null, freqs[piece.c], dataOf(piece), piece.rect.w, piece.rect.h, piece.leaf);
|
|
103
|
+
|
|
104
|
+
const header = new BitWriter(256);
|
|
105
|
+
writeImageHeader(header, width, height, colour, alpha);
|
|
106
|
+
writeModularFrameHeader(header, {alpha});
|
|
107
|
+
// Section order: DC global, DC groups, AC global (empty), AC groups.
|
|
108
|
+
const writers = Array.from({length: layout.single ? 1 : 2 + dcGroups + acGroups}, () => null);
|
|
109
|
+
const global = writers[0] = new BitWriter(65536);
|
|
110
|
+
global.write(1, 1); // default DC quantisation
|
|
111
|
+
global.write(1, 1); // a global tree
|
|
112
|
+
const ordered = writeTree(global, tree);
|
|
113
|
+
const histograms = writeChannelHistograms(global, ordered, freqs, l => l.channel);
|
|
114
|
+
writeModularHeader(global, {useGlobalTree: true, transforms});
|
|
115
|
+
for (const section of sections) {
|
|
116
|
+
let w = writers[section.index];
|
|
117
|
+
if (!w) {
|
|
118
|
+
w = writers[section.index] = new BitWriter(4096);
|
|
119
|
+
if (section.pieces.length) writeModularHeader(w, {useGlobalTree: true, transforms: []});
|
|
120
|
+
}
|
|
121
|
+
for (const piece of section.pieces) codeChannel(w, histograms[piece.c + 1].code, dataOf(piece), piece.rect.w, piece.rect.h, piece.leaf);
|
|
122
|
+
}
|
|
123
|
+
const out = writers.map(w => w || new BitWriter(16));
|
|
124
|
+
return assembleCodestream(header, out.map(w => { w.zeroPadToByte(); return w.finish(); }));
|
|
125
|
+
}
|