Skip to content

Commit 8df7364

Browse files
matmenclaude
andcommitted
JPEG XL: faster EPF, RGBA output, 2D DCTs and LZ77 reads
The edge-preserving filter computes its sigma factors per 8x8 block and its tap offsets per row instead of per pixel. The RGBA output divides by alpha only for premultiplied images. 2D DCTs transform the columns in place instead of transposing squares. LZ77 reads past a copy take a small, inlinable path, and the gradient and zero predictor channels of single leaf trees decode in their own small loops. The output is identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
1 parent fa52f06 commit 8df7364

5 files changed

Lines changed: 125 additions & 98 deletions

File tree

‎src/codecs/jxl/color.ts‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { ImageHeader, TF_PQ, TF_HLG, PRI_BT2100, PRI_P3, EXTRA_ALPHA, EXTRA_SPOT
77
import { toSRGB, hdrToLinear, ToneMapping, REFERENCE_WHITE } from '../hdr.ts';
88
import type { Planes } from './frame.ts';
99

10+
// a sample (0-1) as a byte
11+
const byte = (v: number): number => v <= 0 ? 0 : v >= 1 ? 255 : Math.round(v * 255);
12+
1013
// linear light to sRGB like libjxl (TF_SRGB::EncodedFromDisplay, lib/jxl/cms/transfer_functions-inl.h): a rational
1114
// polynomial of the square root instead of the power, within 5e-7
1215
const [P0, P1, P2, P3, P4] = [-5.135152395e-04, 5.287254571e-03, 3.903842876e-01, 1.474205315e+00, 7.352629620e-01].map(Math.fround);
@@ -119,16 +122,23 @@ export class ColorOutput {
119122
out = toSRGB(rgb, this.toneMapping);
120123
} else {
121124
out = new Uint8Array(4 * n);
122-
const byte = (v: number): number => v <= 0 ? 0 : v >= 1 ? 255 : Math.round(v * 255);
123-
for (let i = 0; i < n; i++) {
124-
const a = premultiplied ? Math.max(alpha![i], 2 ** -26) : 1;
125-
out[4 * i] = byte(r[i] / a);
126-
out[4 * i + 1] = byte(g[i] / a);
127-
out[4 * i + 2] = byte(b[i] / a);
128-
out[4 * i + 3] = 255;
125+
if (premultiplied) {
126+
for (let i = 0, o = 0; i < n; i++, o += 4) {
127+
const a = Math.max(alpha![i], 2 ** -26);
128+
out[o] = byte(r[i] / a);
129+
out[o + 1] = byte(g[i] / a);
130+
out[o + 2] = byte(b[i] / a);
131+
}
132+
} else {
133+
for (let i = 0, o = 0; i < n; i++, o += 4) {
134+
out[o] = byte(r[i]);
135+
out[o + 1] = byte(g[i]);
136+
out[o + 2] = byte(b[i]);
137+
}
129138
}
130139
}
131-
if (alpha) for (let i = 0; i < n; i++) out[4 * i + 3] = alpha[i] <= 0 ? 0 : alpha[i] >= 1 ? 255 : Math.round(alpha[i] * 255);
140+
if (alpha) for (let i = 0, o = 3; i < n; i++, o += 4) out[o] = byte(alpha[i]);
141+
else for (let o = 3; o < out.length; o += 4) out[o] = 255;
132142
return out;
133143
}
134144
}

‎src/codecs/jxl/dct.ts‎

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function dct(io: Float64Array, o: number, n: number, scratch: Float64Arra
9797
}
9898
}
9999

100-
const work = { row: new Float64Array(256), scratch: new Float64Array(256), col: new Float64Array(256 * 2) };
100+
const work = { row: new Float64Array(256), scratch: new Float64Array(256) };
101101

102102
const SEC8 = secHalf(8);
103103

@@ -205,40 +205,7 @@ export function dct2d(b: Block, inverse: boolean): void {
205205
return;
206206
}
207207
for (let y = 0; y < h; y++) rowDct(y, w);
208-
// (the square blocks of the size of the shorter side transposed, then the columns transformed as rows)
209-
const size = Math.min(w, h);
210-
const transposeSquares = (): void => {
211-
for (let by = 0; by < h; by += size) {
212-
for (let bx = 0; bx < w; bx += size) {
213-
for (let dy = 0; dy < size; dy++) {
214-
for (let dx = dy + 1; dx < size; dx++) {
215-
const p = at(bx + dx, by + dy), q = at(bx + dy, by + dx), t = buf[p];
216-
buf[p] = buf[q];
217-
buf[q] = t;
218-
}
219-
}
220-
}
221-
}
222-
};
223-
transposeSquares();
224-
if (size === h) {
225-
for (let y = 0; y < h; y++) for (let x0 = 0; x0 < w; x0 += h) rowDct(y, h, x0);
226-
} else {
227-
// (rows of w values, h / w of them make a column)
228-
const col = work.col, scratch = work.scratch;
229-
for (let y = 0; y < w; y++) {
230-
for (let k = 0; k < h / size; k++) {
231-
const p = at(0, y + k * size);
232-
for (let x = 0; x < w; x++) col[k * w + x] = buf[p + x];
233-
}
234-
dct(col, 0, h, scratch, 0, inverse);
235-
for (let k = 0; k < h / size; k++) {
236-
const p = at(0, y + k * size);
237-
for (let x = 0; x < w; x++) buf[p + x] = col[k * w + x];
238-
}
239-
}
240-
}
241-
transposeSquares();
208+
for (let x = 0; x < w; x++) colDct(x, h);
242209
}
243210

244211
// the DCT scale factors of the LF of large varblocks (for c up to 32 of a 256 point DCT)

‎src/codecs/jxl/entropy.ts‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ export class EntropyDecoder {
454454
const n = (config.splitExponent - inToken + ((token - config.split) >>> inToken)) & 31;
455455
const low = token & ((1 << config.lsb) - 1);
456456
const high = (token >>> config.lsb) & ((1 << config.msb) - 1) | 1 << config.msb;
457+
// (in integers while it fits in 31 bits)
458+
if (n + config.msb + config.lsb < 31) return ((high << n | r.bits(n)) << config.lsb) | low;
457459
return ((high * 2 ** n + r.bits(n)) * 2 ** config.lsb + low) >>> 0;
458460
}
459461

@@ -464,28 +466,33 @@ export class EntropyDecoder {
464466

465467
readClustered(r: BitReader, cluster: number, distMultiplier = 0): number {
466468
if (!this.lz77) return this.uint(r, this.configs[cluster], this.symbol(r, cluster));
469+
// (small, so that it's inlined: the rest of a copy, else readLz77)
470+
if (this.toCopy === 0) return this.readLz77(r, cluster, distMultiplier);
471+
const window = this.window as Uint32Array, v = window[this.copyPos++ & WINDOW_MASK];
472+
this.toCopy--;
473+
window[this.decoded++ & WINDOW_MASK] = v;
474+
return v;
475+
}
476+
477+
// a value or the start of a copy, with LZ77
478+
private readLz77(r: BitReader, cluster: number, distMultiplier: number): number {
467479
const window = this.window ??= new Uint32Array(1 << 20);
468480
let v: number;
469-
if (this.toCopy > 0) {
481+
const token = this.symbol(r, cluster);
482+
if (token >= this.minSymbol) {
483+
if (this.decoded === 0) throw new Error('JPEG XL: LZ77 copy before any value');
484+
const distCluster = this.clusters[this.clusters.length - 1];
485+
this.toCopy = this.uint(r, this.lenConfig as IntegerConfig, token - this.minSymbol) + this.minLength;
486+
let distance = this.uint(r, this.configs[distCluster], this.symbol(r, distCluster));
487+
if (distMultiplier) {
488+
if (distance < 120) distance = Math.max(SPECIAL_DISTANCES[distance * 2] + distMultiplier * SPECIAL_DISTANCES[distance * 2 + 1] - 1, 0);
489+
else distance -= 120;
490+
}
491+
distance = Math.min(Math.min(distance, WINDOW_MASK) + 1, this.decoded);
492+
this.copyPos = this.decoded - distance;
470493
v = window[this.copyPos++ & WINDOW_MASK];
471494
this.toCopy--;
472-
} else {
473-
const token = this.symbol(r, cluster);
474-
if (token >= this.minSymbol) {
475-
if (this.decoded === 0) throw new Error('JPEG XL: LZ77 copy before any value');
476-
const distCluster = this.clusters[this.clusters.length - 1];
477-
this.toCopy = this.uint(r, this.lenConfig as IntegerConfig, token - this.minSymbol) + this.minLength;
478-
let distance = this.uint(r, this.configs[distCluster], this.symbol(r, distCluster));
479-
if (distMultiplier) {
480-
if (distance < 120) distance = Math.max(SPECIAL_DISTANCES[distance * 2] + distMultiplier * SPECIAL_DISTANCES[distance * 2 + 1] - 1, 0);
481-
else distance -= 120;
482-
}
483-
distance = Math.min(Math.min(distance, WINDOW_MASK) + 1, this.decoded);
484-
this.copyPos = this.decoded - distance;
485-
v = window[this.copyPos++ & WINDOW_MASK];
486-
this.toCopy--;
487-
} else v = this.uint(r, this.configs[cluster], token);
488-
}
495+
} else v = this.uint(r, this.configs[cluster], token);
489496
window[this.decoded++ & WINDOW_MASK] = v;
490497
return v;
491498
}

‎src/codecs/jxl/filters.ts‎

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -62,43 +62,74 @@ function outputRow(cur: Float32Array[], out: Float32Array[], y: number, w: numbe
6262
// (rows 2 away from the edges: the taps without mirroring)
6363
const innerRow = rows[0] === (y - 2) * w && rows[4] === (y + 2) * w;
6464
const row = y * w;
65-
for (let x = 0; x < w; x++) {
66-
const i = row + x;
67-
const sg = sigma ? sigma[sigmaRow + (x >> 3)] : sigmaModular;
65+
// per tap pair: the offsets of +u in the sums and in the row
66+
const sumOff = TAPS.subarray(0, n), srcOff = TAPS.subarray(8, 8 + n);
67+
for (let k = 0; k < n; k++) {
68+
sumOff[k] = u[2 * k + 1] * pw + u[2 * k];
69+
srcOff[k] = u[2 * k + 1] * w + u[2 * k];
70+
}
71+
for (let x0 = 0; x0 < w; x0 += 8) {
72+
const x1 = Math.min(x0 + 8, w);
73+
const sg = sigma ? sigma[sigmaRow + (x0 >> 3)] : sigmaModular;
6874
if (sg < 0.3) {
69-
o0[i] = i0[i];
70-
o1[i] = i1[i];
71-
o2[i] = i2[i];
75+
o0.set(i0.subarray(row + x0, row + x1), row + x0);
76+
o1.set(i1.subarray(row + x0, row + x1), row + x0);
77+
o2.set(i2.subarray(row + x0, row + x1), row + x0);
7278
continue;
7379
}
74-
const sm = yBorder || (x & 7) === 0 || (x & 7) === 7 ? stepMul * borderMul : stepMul;
75-
const negInvSigma = 6.6 * (Math.SQRT1_2 - 1) / sg * sm;
76-
let sumW = 1, sum0 = i0[i], sum1 = i1[i], sum2 = i2[i];
77-
const p = p0 + x, inner = innerRow && x >= 2 && x < w - 2;
78-
for (let k = 0; k < n; k++) {
79-
const ux = u[2 * k], uy = u[2 * k + 1], sum = sums[k];
80-
let weight = 1 + sum[p] * negInvSigma;
81-
if (weight > 0) {
82-
const t = inner ? i + uy * w + ux : rows[2 + uy] + xs[x + 5 + ux];
83-
sumW += weight;
84-
sum0 += weight * i0[t];
85-
sum1 += weight * i1[t];
86-
sum2 += weight * i2[t];
87-
}
88-
weight = 1 + sum[p - uy * pw - ux] * negInvSigma;
89-
if (weight > 0) {
90-
const t = inner ? i - uy * w - ux : rows[2 - uy] + xs[x + 5 - ux];
91-
sumW += weight;
92-
sum0 += weight * i0[t];
93-
sum1 += weight * i1[t];
94-
sum2 += weight * i2[t];
80+
// (the block's columns 0 and 7, and its rows 0 and 7, are weighted by borderMul)
81+
const borderInvSigma = 6.6 * (Math.SQRT1_2 - 1) / sg * (stepMul * borderMul), innerInvSigma = 6.6 * (Math.SQRT1_2 - 1) / sg * stepMul;
82+
for (let x = x0; x < x1; x++) {
83+
const i = row + x;
84+
const negInvSigma = yBorder || (x & 7) === 0 || (x & 7) === 7 ? borderInvSigma : innerInvSigma;
85+
let sumW = 1, sum0 = i0[i], sum1 = i1[i], sum2 = i2[i];
86+
const p = p0 + x;
87+
if (innerRow && x >= 2 && x < w - 2) {
88+
for (let k = 0; k < n; k++) {
89+
const sum = sums[k], so = sumOff[k], to = srcOff[k];
90+
let weight = 1 + sum[p] * negInvSigma;
91+
if (weight > 0) {
92+
sumW += weight;
93+
sum0 += weight * i0[i + to];
94+
sum1 += weight * i1[i + to];
95+
sum2 += weight * i2[i + to];
96+
}
97+
weight = 1 + sum[p - so] * negInvSigma;
98+
if (weight > 0) {
99+
sumW += weight;
100+
sum0 += weight * i0[i - to];
101+
sum1 += weight * i1[i - to];
102+
sum2 += weight * i2[i - to];
103+
}
104+
}
105+
} else {
106+
for (let k = 0; k < n; k++) {
107+
const ux = u[2 * k], uy = u[2 * k + 1], sum = sums[k];
108+
let weight = 1 + sum[p] * negInvSigma;
109+
if (weight > 0) {
110+
const t = rows[2 + uy] + xs[x + 5 + ux];
111+
sumW += weight;
112+
sum0 += weight * i0[t];
113+
sum1 += weight * i1[t];
114+
sum2 += weight * i2[t];
115+
}
116+
weight = 1 + sum[p - uy * pw - ux] * negInvSigma;
117+
if (weight > 0) {
118+
const t = rows[2 - uy] + xs[x + 5 - ux];
119+
sumW += weight;
120+
sum0 += weight * i0[t];
121+
sum1 += weight * i1[t];
122+
sum2 += weight * i2[t];
123+
}
124+
}
95125
}
126+
o0[i] = sum0 / sumW;
127+
o1[i] = sum1 / sumW;
128+
o2[i] = sum2 / sumW;
96129
}
97-
o0[i] = sum0 / sumW;
98-
o1[i] = sum1 / sumW;
99-
o2[i] = sum2 / sumW;
100130
}
101131
}
132+
const TAPS = new Int32Array(16);
102133

103134
/**
104135
* The edge-preserving filter on 3 w x h channels

‎src/codecs/jxl/modular.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,23 @@ export function decodeModular(r: BitReader, image: ModularImage, stream: number,
647647
if (undo) image.undo();
648648
}
649649

650+
// the channels of a tree with a single leaf (small functions, so that the entropy decoding is inlined): no prediction
651+
function decodeZero(r: BitReader, dec: EntropyDecoder, cluster: number, data: Int32Array, mult: number, offset: number, distMultiplier: number): void {
652+
for (let i = 0; i < data.length; i++) data[i] = (Math.imul(unpackSigned(dec.readClustered(r, cluster, distMultiplier)), mult) + offset) | 0;
653+
}
654+
655+
// the gradient predictor (with the first row and column from the left and top)
656+
function decodeGradient(r: BitReader, dec: EntropyDecoder, cluster: number, data: Int32Array, w: number, h: number, distMultiplier: number): void {
657+
for (let x = 0; x < w; x++) data[x] = (unpackSigned(dec.readClustered(r, cluster, distMultiplier)) + (x ? data[x - 1] : 0)) | 0;
658+
for (let y = 1, i = w; y < h; y++) {
659+
data[i] = (unpackSigned(dec.readClustered(r, cluster, distMultiplier)) + data[i - w]) | 0;
660+
i++;
661+
for (let x = 1; x < w; x++, i++) {
662+
data[i] = (unpackSigned(dec.readClustered(r, cluster, distMultiplier)) + clampedGradient(data[i - w], data[i - 1], data[i - w - 1])) | 0;
663+
}
664+
}
665+
}
666+
650667
function decodeChannel(r: BitReader, dec: EntropyDecoder, globalTree: Tree, image: ModularImage, chan: number, stream: number, distMultiplier: number): void {
651668
const c = image.channels[chan], { w, h } = c, data = c.data;
652669
const t = specialize(globalTree, chan, stream);
@@ -670,16 +687,11 @@ function decodeChannel(r: BitReader, dec: EntropyDecoder, globalTree: Tree, imag
670687
data.fill((Math.imul(unpackSigned(single), mult) + offset) | 0);
671688
return;
672689
}
673-
for (let i = 0; i < w * h; i++) data[i] = (Math.imul(unpackSigned(dec.readClustered(r, cluster, distMultiplier)), mult) + offset) | 0;
690+
decodeZero(r, dec, cluster, data, mult, offset, distMultiplier);
674691
return;
675692
}
676693
if (pred === 5 && mult === 1 && offset === 0) {
677-
for (let y = 0, i = 0; y < h; y++) {
678-
for (let x = 0; x < w; x++, i++) {
679-
const left = x ? data[i - 1] : y ? data[i - w] : 0, top = y ? data[i - w] : left, topleft = x && y ? data[i - w - 1] : left;
680-
data[i] = (unpackSigned(dec.readClustered(r, cluster, distMultiplier)) + clampedGradient(top, left, topleft)) | 0;
681-
}
682-
}
694+
decodeGradient(r, dec, cluster, data, w, h, distMultiplier);
683695
return;
684696
}
685697
}

0 commit comments

Comments
 (0)