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/vardct.mjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Rapier's JPEG XL encoder: a JPEG carried into a VarDCT frame. MIT (LICENSE).
|
|
2
|
+
// The JPEG's quantised DCT coefficients become the frame's coefficients without loss, the way libjxl transcodes
|
|
3
|
+
// a JPEG: its quantisation tables as raw dequantisation matrices, its DC values as the DC image, its colour
|
|
4
|
+
// (YCbCr, or RGB) and chroma subsampling kept, every block an 8x8 DCT, no filters, no smoothing, no chroma from
|
|
5
|
+
// luma. Only the entropy coding changes: JPEG XL's contexts over the coefficients, with prefix codes.
|
|
6
|
+
import {BitWriter, float16Bits, packSigned, ceilLog2} from './bits.mjs';
|
|
7
|
+
import {writeImageHeader, assembleCodestream, GROUP_DIM} from './frame.mjs';
|
|
8
|
+
import {PREDICTOR, ALPHABET, leaf, channelTree, writeTree, writeModularHeader, writeChannelHistograms, codeChannel} from './modular.mjs';
|
|
9
|
+
import {writeContextMap, writeHistograms} from './prefix.mjs';
|
|
10
|
+
import {TokenCounts, buildTokenCoding} from './entropy.mjs';
|
|
11
|
+
import {parseJPEG, jpegError} from './jpeg.mjs';
|
|
12
|
+
|
|
13
|
+
const NONZERO_BUCKETS = 37, ZERO_DENSITY_CONTEXTS = 458, ORDERS = 13;
|
|
14
|
+
const COEFF_FREQ_CONTEXT = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
|
|
15
|
+
23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30];
|
|
16
|
+
const COEFF_NUM_NONZERO_CONTEXT = [0, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123, 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180,
|
|
17
|
+
180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206,
|
|
18
|
+
206, 206, 206, 206, 206, 206, 206, 206];
|
|
19
|
+
|
|
20
|
+
// The frame's natural coefficient order for an 8x8 block (the zigzag over the frame's transposed layout), each
|
|
21
|
+
// entry already turned into the JPEG's natural index of that coefficient.
|
|
22
|
+
const SCAN = (() => {
|
|
23
|
+
const order = new Uint8Array(64);
|
|
24
|
+
let cur = 1;
|
|
25
|
+
for (let i = 0; i < 8; i++) for (let j = 0; j <= i; j++) {
|
|
26
|
+
let x = j, y = i - j;
|
|
27
|
+
if (i % 2) [x, y] = [y, x];
|
|
28
|
+
order[x < 1 && y < 1 ? 0 : cur++] = y * 8 + x;
|
|
29
|
+
}
|
|
30
|
+
for (let ip = 7; ip > 0; ip--) { const i = ip - 1; for (let j = 0; j <= i; j++) { let x = 7 - (i - j), y = 7 - j; if (i % 2) [x, y] = [y, x]; order[cur++] = y * 8 + x; } }
|
|
31
|
+
// frame index n = u*8 + v holds the JPEG's coefficient v*8 + u.
|
|
32
|
+
return order.map(n => ((n & 7) << 3) | (n >> 3));
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
const zeroDensityContext = (left, k, prev) => (COEFF_NUM_NONZERO_CONTEXT[left] + COEFF_FREQ_CONTEXT[k]) * 2 + prev;
|
|
36
|
+
|
|
37
|
+
// Block contexts as libjxl chooses them for a JPEG: luma blocks in up to eight buckets of their quantised DC (more
|
|
38
|
+
// buckets for larger, finer pictures), chroma in about half as many, one chroma context for a grey picture.
|
|
39
|
+
function blockContexts(luma, thresholdsWanted, grey) {
|
|
40
|
+
const thresholds = [];
|
|
41
|
+
if (thresholdsWanted > 0) {
|
|
42
|
+
const counts = new Uint32Array(2048);
|
|
43
|
+
let total = 0;
|
|
44
|
+
for (const value of luma) { counts[Math.min(2047, Math.max(0, value + 1024))]++; total++; }
|
|
45
|
+
let cumulative = 0, cut = total / (thresholdsWanted + 1);
|
|
46
|
+
for (let j = 0; j < 2048 && thresholds.length < thresholdsWanted; j++) {
|
|
47
|
+
cumulative += counts[j];
|
|
48
|
+
if (cumulative > cut) { thresholds.push(j - 1025); cut = total * (thresholds.length + 1) / (thresholdsWanted + 1); }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const numDc = thresholds.length + 1, map = new Uint8Array(3 * ORDERS * numDc);
|
|
52
|
+
for (let i = 0; i < numDc; i++) for (let ord = 0; ord < ORDERS; ord++) {
|
|
53
|
+
map[(0 * ORDERS + ord) * numDc + i] = i;
|
|
54
|
+
map[(1 * ORDERS + ord) * numDc + i] = grey ? numDc : numDc + (i >> 1);
|
|
55
|
+
map[(2 * ORDERS + ord) * numDc + i] = grey ? numDc : numDc + ((numDc - 1) >> 1) + 1 + (i >> 1);
|
|
56
|
+
}
|
|
57
|
+
let numCtxs = 0;
|
|
58
|
+
for (const v of map) numCtxs = Math.max(numCtxs, v + 1);
|
|
59
|
+
const bucketOf = value => { let b = 0; for (const t of thresholds) if (value > t) b++; return b; };
|
|
60
|
+
return {thresholds, numDc, map, numCtxs, bucketOf,
|
|
61
|
+
contextOf: (c, bucket) => map[((c < 2 ? c ^ 1 : 2) * ORDERS) * numDc + bucket],
|
|
62
|
+
nonZero: (predicted, blockCtx) => (Math.min(predicted, 64) < 8 ? Math.min(predicted, 64) : 4 + (Math.min(predicted, 64) >> 1)) * numCtxs + blockCtx,
|
|
63
|
+
zeroDensityOffset: blockCtx => numCtxs * NONZERO_BUCKETS + ZERO_DENSITY_CONTEXTS * blockCtx,
|
|
64
|
+
contexts: numCtxs * (NONZERO_BUCKETS + ZERO_DENSITY_CONTEXTS)};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A modular stream with its own tree: one leaf per channel, the given predictor, counted then written.
|
|
68
|
+
function writeModularStream(w, planes, predictor) {
|
|
69
|
+
const leaves = planes.map(() => leaf(predictor));
|
|
70
|
+
const freqs = leaves.map(() => new Uint32Array(ALPHABET));
|
|
71
|
+
planes.forEach((p, i) => codeChannel(null, freqs[i], p.data, p.w, p.h, leaves[i]));
|
|
72
|
+
writeModularHeader(w, {useGlobalTree: false, transforms: []});
|
|
73
|
+
const ordered = writeTree(w, channelTree(leaves));
|
|
74
|
+
const histograms = writeChannelHistograms(w, ordered, freqs, l => leaves.indexOf(l));
|
|
75
|
+
planes.forEach((p, i) => codeChannel(w, histograms[i + 1].code, p.data, p.w, p.h, leaves[i]));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function transcodeJPEG(bytes, jpeg = parseJPEG(bytes)) {
|
|
79
|
+
const {width, height, components} = jpeg, grey = components.length === 1;
|
|
80
|
+
const ycbcr = jpeg.ycbcr || grey;
|
|
81
|
+
const map = grey ? [0, 0, 0] : ycbcr ? [1, 0, 2] : [0, 1, 2]; // frame channel (X, Y, B) to JPEG component
|
|
82
|
+
const comps = map.map(i => components[i]);
|
|
83
|
+
const log2 = v => v === 1 ? 0 : v === 2 ? 1 : -1;
|
|
84
|
+
const rawH = comps.map(c => log2(c.h)), rawV = comps.map(c => log2(c.v));
|
|
85
|
+
if (rawH.includes(-1) || rawV.includes(-1)) throw jpegError('sampling factors other than one or two');
|
|
86
|
+
const maxhs = Math.max(...rawH), maxvs = Math.max(...rawV);
|
|
87
|
+
const hshift = c => maxhs - rawH[c], vshift = c => maxvs - rawV[c];
|
|
88
|
+
if (!ycbcr && (maxhs || maxvs)) throw jpegError('an RGB JPEG with subsampling');
|
|
89
|
+
const xsizeBlocks = Math.ceil(width / (8 << maxhs)) << maxhs, ysizeBlocks = Math.ceil(height / (8 << maxvs)) << maxvs;
|
|
90
|
+
const groupsX = Math.ceil(width / GROUP_DIM), groupsY = Math.ceil(height / GROUP_DIM), numGroups = groupsX * groupsY;
|
|
91
|
+
const dcGroupsX = Math.ceil(xsizeBlocks / 256), dcGroupsY = Math.ceil(ysizeBlocks / 256), numDcGroups = dcGroupsX * dcGroupsY;
|
|
92
|
+
const single = numGroups === 1;
|
|
93
|
+
// A block's coefficients: the JPEG's block (bx, by) at the channel's scale; chroma of a grey picture is empty.
|
|
94
|
+
const blockAt = (c, bx, by) => { const comp = comps[c]; if (grey && c !== 1) return -1; return ((by * comp.stride) + bx) * 64; };
|
|
95
|
+
|
|
96
|
+
// Every luma block's quantised DC, for the block context buckets.
|
|
97
|
+
const lumaDc = new Int32Array(xsizeBlocks * ysizeBlocks);
|
|
98
|
+
for (let by = 0; by < ysizeBlocks; by++) for (let bx = 0; bx < xsizeBlocks; bx++) lumaDc[by * xsizeBlocks + bx] = comps[1].coeffs[blockAt(1, bx, by)];
|
|
99
|
+
const lumaQuant = comps[1].quant, quantSum = lumaQuant[8] + lumaQuant[16] + lumaQuant[24] + lumaQuant[32] + lumaQuant[40];
|
|
100
|
+
const blocksTotal = xsizeBlocks * ysizeBlocks;
|
|
101
|
+
const wanted = blocksTotal < 256 ? 0 : Math.max(1, Math.min(7, ceilLog2(blocksTotal) - ceilLog2(quantSum) - 7));
|
|
102
|
+
let contexts = blockContexts(lumaDc, wanted, grey);
|
|
103
|
+
|
|
104
|
+
const header = new BitWriter(256);
|
|
105
|
+
writeImageHeader(header, width, height, grey ? 1 : 3, false, {orientation: jpeg.orientation});
|
|
106
|
+
// The frame header: VarDCT, adaptive DC smoothing skipped, the colour and subsampling of the JPEG.
|
|
107
|
+
header.write(1, 0); header.write(2, 0); header.write(1, 0);
|
|
108
|
+
header.write(2, 2); header.write(8, 128 - 17); // flags: kSkipAdaptiveDCSmoothing
|
|
109
|
+
header.write(1, ycbcr ? 1 : 0);
|
|
110
|
+
if (ycbcr) for (let c = 0; c < 3; c++) header.write(2, rawH[c] === 0 && rawV[c] === 0 ? 0 : rawH[c] === 1 && rawV[c] === 1 ? 1 : rawH[c] === 1 ? 2 : 3);
|
|
111
|
+
header.write(2, 0); // no upsampling
|
|
112
|
+
header.write(2, 0); // one pass
|
|
113
|
+
header.write(1, 0); // no custom size
|
|
114
|
+
header.write(2, 0); // replace
|
|
115
|
+
header.write(1, 1); // the last frame
|
|
116
|
+
header.write(2, 0); // no name
|
|
117
|
+
header.write(1, 0); header.write(1, 0); header.write(2, 0); header.write(2, 0); // loop filter: no gaborish, no EPF, no extensions
|
|
118
|
+
header.write(2, 0); // no extensions
|
|
119
|
+
|
|
120
|
+
// The AC tokens of every group, counted first and written second.
|
|
121
|
+
const nzeros = [0, 1, 2].map(() => new Int32Array(32 * 32));
|
|
122
|
+
const tokens = (g, emit) => {
|
|
123
|
+
const gx = g % groupsX, gy = (g / groupsX) | 0, bx0 = gx * 32, by0 = gy * 32;
|
|
124
|
+
const bw = Math.min(32, xsizeBlocks - bx0), bh = Math.min(32, ysizeBlocks - by0);
|
|
125
|
+
for (let by = 0; by < bh; by++) for (let bx = 0; bx < bw; bx++) for (const c of [1, 0, 2]) {
|
|
126
|
+
const hs = hshift(c), vs = vshift(c), sbx = bx >> hs, sby = by >> vs;
|
|
127
|
+
if ((sbx << hs) !== bx || (sby << vs) !== by) continue;
|
|
128
|
+
const at = blockAt(c, (bx0 >> hs) + sbx, (by0 >> vs) + sby), coeffs = comps[c].coeffs;
|
|
129
|
+
let count = 0;
|
|
130
|
+
if (at >= 0) for (let k = 1; k < 64; k++) if (coeffs[at + k]) count++;
|
|
131
|
+
const row = nzeros[c], predicted = sbx === 0 ? (sby === 0 ? 32 : row[(sby - 1) * 32]) : sby === 0 ? row[sby * 32 + sbx - 1] : (row[(sby - 1) * 32 + sbx] + row[sby * 32 + sbx - 1] + 1) >> 1;
|
|
132
|
+
row[sby * 32 + sbx] = count;
|
|
133
|
+
const blockCtx = contexts.contextOf(c, contexts.bucketOf(lumaDc[(by0 + by) * xsizeBlocks + bx0 + bx]));
|
|
134
|
+
emit(contexts.nonZero(predicted, blockCtx), count);
|
|
135
|
+
const offset = contexts.zeroDensityOffset(blockCtx);
|
|
136
|
+
let prev = count > 4 ? 0 : 1, left = count;
|
|
137
|
+
for (let k = 1; k < 64 && left; k++) {
|
|
138
|
+
const coeff = coeffs[at + SCAN[k]];
|
|
139
|
+
emit(offset + zeroDensityContext(left, k, prev), packSigned(coeff));
|
|
140
|
+
prev = coeff ? 1 : 0; left -= prev;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
// Bucketed contexts win on photographs and lose on small or flat pictures: both are counted and the cheaper kept.
|
|
145
|
+
const countWith = chosen => { contexts = chosen; const counts = new TokenCounts(chosen.contexts); for (let g = 0; g < numGroups; g++) tokens(g, (ctx, value) => counts.add(ctx, value)); return buildTokenCoding(counts); };
|
|
146
|
+
let coding = countWith(contexts);
|
|
147
|
+
if (wanted > 0) {
|
|
148
|
+
const bucketed = contexts, plain = blockContexts(lumaDc, 0, grey), plainCoding = countWith(plain);
|
|
149
|
+
if (plainCoding.bits <= coding.bits) coding = plainCoding; else contexts = bucketed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const writers = Array.from({length: single ? 1 : 2 + numDcGroups + numGroups}, () => null);
|
|
153
|
+
const section = index => writers[single ? 0 : index] || (writers[single ? 0 : index] = new BitWriter(4096));
|
|
154
|
+
|
|
155
|
+
// DC global: the DC quantisation of each channel, the quantizer at scale one, luma and chroma block contexts, no
|
|
156
|
+
// chroma from luma, no global modular tree.
|
|
157
|
+
const dc = section(0);
|
|
158
|
+
dc.write(1, 0);
|
|
159
|
+
// The DC step of each channel in the frame's units (the JPEG's DC quant over 8 times 255), stored times 128.
|
|
160
|
+
for (let c = 0; c < 3; c++) dc.write(16, float16Bits(comps[c].quant[0] / (255 * 8) * 128));
|
|
161
|
+
dc.write(2, 3); dc.write(16, 65536 - 8193);
|
|
162
|
+
dc.write(2, 1); dc.write(5, 0);
|
|
163
|
+
dc.write(1, 0); dc.write(4, 0);
|
|
164
|
+
dc.write(4, contexts.thresholds.length);
|
|
165
|
+
for (const t of contexts.thresholds) dc.writeU32([[4, 0], [8, 16], [16, 272], [32, 65808]], packSigned(t));
|
|
166
|
+
dc.write(4, 0); dc.write(4, 0);
|
|
167
|
+
writeContextMap(dc, contexts.map);
|
|
168
|
+
// Chroma from luma switched off in full: the default map carries a base luma-to-B ratio of one (meant for XYB),
|
|
169
|
+
// which a 4:4:4 or grey frame would apply to its Cr. Colour factor 84, both bases zero, no DC correlation.
|
|
170
|
+
dc.write(1, 0); dc.write(2, 0); dc.write(16, 0); dc.write(16, 0); dc.write(8, 128); dc.write(8, 128);
|
|
171
|
+
dc.write(1, 0);
|
|
172
|
+
|
|
173
|
+
// DC groups: the DC coefficients as a modular image (luma first), then the AC metadata: all blocks 8x8 DCT at
|
|
174
|
+
// quant one, no chroma-from-luma tiles, no sharpness.
|
|
175
|
+
for (let g = 0; g < numDcGroups; g++) {
|
|
176
|
+
const w = section(1 + g), gx = g % dcGroupsX, gy = (g / dcGroupsX) | 0;
|
|
177
|
+
const x0 = gx * 256, y0 = gy * 256, rw = Math.min(256, xsizeBlocks - x0), rh = Math.min(256, ysizeBlocks - y0);
|
|
178
|
+
w.write(2, 0);
|
|
179
|
+
// An RGB JPEG's level shift comes back through the DC (128 in pixel units, as libjxl adds it); YCbCr keeps its
|
|
180
|
+
// DC as it is, luma already centred and chroma zero-centred in the frame.
|
|
181
|
+
const planes = [1, 0, 2].map(c => {
|
|
182
|
+
const pw = rw >> hshift(c), ph = rh >> vshift(c), data = new Int32Array(pw * ph), lift = ycbcr ? 0 : Math.trunc(1024 / comps[c].quant[0]);
|
|
183
|
+
for (let y = 0; y < ph; y++) for (let x = 0; x < pw; x++) {
|
|
184
|
+
const at = blockAt(c, (x0 >> hshift(c)) + x, (y0 >> vshift(c)) + y);
|
|
185
|
+
data[y * pw + x] = at < 0 ? 0 : comps[c].coeffs[at] + lift;
|
|
186
|
+
}
|
|
187
|
+
return {w: pw, h: ph, data};
|
|
188
|
+
});
|
|
189
|
+
writeModularStream(w, planes, PREDICTOR.gradient);
|
|
190
|
+
const count = rw * rh, bits = ceilLog2(count);
|
|
191
|
+
if (bits) w.write(bits, count - 1);
|
|
192
|
+
const cw = (rw + 7) >> 3, ch = (rh + 7) >> 3;
|
|
193
|
+
writeModularStream(w, [
|
|
194
|
+
{w: cw, h: ch, data: new Int32Array(cw * ch)}, {w: cw, h: ch, data: new Int32Array(cw * ch)},
|
|
195
|
+
{w: count, h: 2, data: new Int32Array(count * 2)}, {w: rw, h: rh, data: new Int32Array(count)},
|
|
196
|
+
], PREDICTOR.zero);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// AC global: the JPEG's quantisation tables as the raw DCT8 matrices (transposed into the frame's layout), the
|
|
200
|
+
// other kinds from the library, one histogram set, the natural coefficient order, the AC histograms.
|
|
201
|
+
const ac = section(1 + numDcGroups);
|
|
202
|
+
ac.write(1, 0);
|
|
203
|
+
ac.write(3, 7); ac.write(16, float16Bits(1 / (8 * 255)));
|
|
204
|
+
writeModularStream(ac, [0, 1, 2].map(c => {
|
|
205
|
+
const data = new Int32Array(64), quant = comps[c].quant;
|
|
206
|
+
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) data[8 * x + y] = quant[8 * y + x];
|
|
207
|
+
return {w: 8, h: 8, data};
|
|
208
|
+
}), PREDICTOR.gradient);
|
|
209
|
+
for (let i = 1; i < 17; i++) ac.write(3, 0);
|
|
210
|
+
if (numGroups > 1) ac.write(ceilLog2(numGroups), 0);
|
|
211
|
+
ac.write(2, 2);
|
|
212
|
+
writeHistograms(ac, {contextMap: coding.contextMap, histograms: coding.histograms});
|
|
213
|
+
|
|
214
|
+
for (let g = 0; g < numGroups; g++) { const w = section(2 + numDcGroups + g); tokens(g, (ctx, value) => coding.write(w, ctx, value)); }
|
|
215
|
+
return assembleCodestream(header, writers.map(w => { const out = w || new BitWriter(16); out.zeroPadToByte(); return out.finish(); }));
|
|
216
|
+
}
|