Skip to content

Commit 03d8c56

Browse files
matmenclaude
andcommitted
JPEG: upsample chroma a row at a time when decoding
The output loop now asks each subsampled component to upsample the row it needs into a row buffer (same fancy upsampling arithmetic), and unscaled components are read from their IDCT plane directly instead of being copied into a full-size plane first: about 6 MB less for a 1920x1080 4:2:0 image. Same pixels for 392 JPEGs (tests, samples); CPU time (node --single-threaded) -2% (4:4:4) to +1% (4:2:2), i.e. neutral. Tried and dropped: libjpeg's zero row test in the IDCT's row pass (slightly slower here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GAqbnFSNaYDBGD1Km2
1 parent fe2b0ef commit 03d8c56

2 files changed

Lines changed: 67 additions & 55 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
### Performance
66

7-
- `encodeJPEG()` is 15-25% faster with the same output: the chroma planes are converted and subsampled two rows at a
8-
time (instead of full-size planes first, 16 MB less for a 1920x1080 image), and the entropy coding counts and
7+
- `encodeJPEG()` is about 20% faster with the same output: the chroma planes are converted and subsampled two rows at
8+
a time (instead of full-size planes first, 16 MB less for a 1920x1080 image), and the entropy coding counts and
99
writes symbols in two specialized passes.
10+
- JPEG decoding upsamples chroma a row at a time while converting to RGB, and uses unscaled planes as they are,
11+
instead of first building full-size copies of all of them (about 6 MB less for a 1920x1080 image). Same output and
12+
speed.
1013

1114
## 3.12.1
1215

‎src/codecs/jpeg.ts‎

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -670,78 +670,85 @@ function buildPlane(component: Component): { plane: Uint8Array, stride: number }
670670
return { plane, stride };
671671
}
672672

673-
// upsamples a component to full resolution, following libjpeg's fancy upsampling
674-
function upsample(component: Component, frame: Frame): { samples: Uint8Array, stride: number } {
673+
// a component at full resolution, following libjpeg's fancy upsampling: the plane itself if it isn't subsampled, else
674+
// a row buffer that fill(y) upsamples output row y into
675+
interface Samples {
676+
samples: Uint8Array;
677+
stride: number;
678+
fill?: (y: number) => void;
679+
}
680+
681+
function upsample(component: Component, frame: Frame): Samples {
675682
const { plane, stride } = buildPlane(component);
676683
const hf = frame.hmax / component.h;
677684
const vf = frame.vmax / component.v;
678685
const w = component.width, h = component.height;
679-
const W = Math.ceil(w * hf), H = Math.ceil(h * vf);
680-
const out = new Uint8Array(W * H);
686+
const W = Math.ceil(w * hf);
687+
const out = new Uint8Array(W);
681688
const fancy = w > 2;
682689

683-
if (hf === 1 && vf === 1) {
684-
for (let y = 0; y < h; y++) out.set(plane.subarray(y * stride, y * stride + w), y * W);
685-
} else if (hf === 2 && vf === 1 && fancy) {
686-
for (let y = 0; y < h; y++) {
687-
const i = y * stride, o = y * W;
688-
out[o] = plane[i];
689-
out[o + 1] = plane[i] * 3 + plane[i + 1] + 2 >> 2;
690+
if (hf === 1 && vf === 1) return { samples: plane, stride };
691+
692+
if (hf === 2 && vf === 1 && fancy) return {
693+
samples: out, stride: 0, fill: y => {
694+
const i = y * stride;
695+
out[0] = plane[i];
696+
out[1] = plane[i] * 3 + plane[i + 1] + 2 >> 2;
690697
for (let x = 1; x < w - 1; x++) {
691698
const v = plane[i + x] * 3;
692-
out[o + 2 * x] = v + plane[i + x - 1] + 1 >> 2;
693-
out[o + 2 * x + 1] = v + plane[i + x + 1] + 2 >> 2;
699+
out[2 * x] = v + plane[i + x - 1] + 1 >> 2;
700+
out[2 * x + 1] = v + plane[i + x + 1] + 2 >> 2;
694701
}
695-
out[o + 2 * w - 2] = plane[i + w - 1] * 3 + plane[i + w - 2] + 1 >> 2;
696-
out[o + 2 * w - 1] = plane[i + w - 1];
702+
out[2 * w - 2] = plane[i + w - 1] * 3 + plane[i + w - 2] + 1 >> 2;
703+
out[2 * w - 1] = plane[i + w - 1];
697704
}
698-
} else if (hf === 1 && vf === 2 && fancy) {
699-
for (let y = 0; y < h; y++) {
700-
const i = y * stride;
701-
const above = Math.max(0, y - 1) * stride;
702-
const below = Math.min(h - 1, y + 1) * stride;
703-
for (let x = 0; x < w; x++) {
704-
const v = plane[i + x] * 3;
705-
out[2 * y * W + x] = v + plane[above + x] + 1 >> 2;
706-
out[(2 * y + 1) * W + x] = v + plane[below + x] + 2 >> 2;
705+
};
706+
707+
if (hf === 1 && vf === 2 && fancy) return {
708+
samples: out, stride: 0, fill: y => {
709+
const i = (y >> 1) * stride;
710+
if (y & 1) {
711+
const below = Math.min(h - 1, (y >> 1) + 1) * stride;
712+
for (let x = 0; x < w; x++) out[x] = plane[i + x] * 3 + plane[below + x] + 2 >> 2;
713+
} else {
714+
const above = Math.max(0, (y >> 1) - 1) * stride;
715+
for (let x = 0; x < w; x++) out[x] = plane[i + x] * 3 + plane[above + x] + 1 >> 2;
707716
}
708717
}
709-
} else if (hf === 2 && vf === 2 && fancy) {
710-
for (let y = 0; y < h; y++) {
711-
const i = y * stride;
712-
713-
for (let pass = 0; pass < 2; pass++) {
714-
const other = (pass === 0 ? Math.max(0, y - 1) : Math.min(h - 1, y + 1)) * stride;
715-
const o = (2 * y + pass) * W;
718+
};
716719

717-
let thisSum = plane[i] * 3 + plane[other];
718-
let nextSum = plane[i + 1] * 3 + plane[other + 1];
719-
out[o] = thisSum * 4 + 8 >> 4;
720-
out[o + 1] = thisSum * 3 + nextSum + 7 >> 4;
721-
let lastSum = thisSum;
722-
thisSum = nextSum;
720+
if (hf === 2 && vf === 2 && fancy) return {
721+
samples: out, stride: 0, fill: y => {
722+
const i = (y >> 1) * stride;
723+
const other = (y & 1 ? Math.min(h - 1, (y >> 1) + 1) : Math.max(0, (y >> 1) - 1)) * stride;
723724

724-
for (let x = 1; x < w - 1; x++) {
725-
nextSum = plane[i + x + 1] * 3 + plane[other + x + 1];
726-
out[o + 2 * x] = thisSum * 3 + lastSum + 8 >> 4;
727-
out[o + 2 * x + 1] = thisSum * 3 + nextSum + 7 >> 4;
728-
lastSum = thisSum;
729-
thisSum = nextSum;
730-
}
725+
let thisSum = plane[i] * 3 + plane[other];
726+
let nextSum = plane[i + 1] * 3 + plane[other + 1];
727+
out[0] = thisSum * 4 + 8 >> 4;
728+
out[1] = thisSum * 3 + nextSum + 7 >> 4;
729+
let lastSum = thisSum;
730+
thisSum = nextSum;
731731

732-
out[o + 2 * w - 2] = thisSum * 3 + lastSum + 8 >> 4;
733-
out[o + 2 * w - 1] = thisSum * 4 + 7 >> 4;
732+
for (let x = 1; x < w - 1; x++) {
733+
nextSum = plane[i + x + 1] * 3 + plane[other + x + 1];
734+
out[2 * x] = thisSum * 3 + lastSum + 8 >> 4;
735+
out[2 * x + 1] = thisSum * 3 + nextSum + 7 >> 4;
736+
lastSum = thisSum;
737+
thisSum = nextSum;
734738
}
739+
740+
out[2 * w - 2] = thisSum * 3 + lastSum + 8 >> 4;
741+
out[2 * w - 1] = thisSum * 4 + 7 >> 4;
735742
}
736-
} else {
737-
// plain replication for every other sampling ratio
738-
for (let y = 0; y < H; y++) {
743+
};
744+
745+
// plain replication for every other sampling ratio
746+
return {
747+
samples: out, stride: 0, fill: y => {
739748
const i = Math.min(h - 1, y / vf | 0) * stride;
740-
for (let x = 0; x < W; x++) out[y * W + x] = plane[i + Math.min(w - 1, x / hf | 0)];
749+
for (let x = 0; x < W; x++) out[x] = plane[i + Math.min(w - 1, x / hf | 0)];
741750
}
742-
}
743-
744-
return { samples: out, stride: W };
751+
};
745752
}
746753

747754
// libjpeg's fixed point YCbCr -> RGB tables
@@ -787,6 +794,7 @@ function output(frame: Frame, adobe: { transform: number } | null, jfif: boolean
787794
// (YCbCr a pixel at a time as a 32-bit word, little endian)
788795
const words = littleEndian ? new Uint32Array(pixels.buffer) : null;
789796
for (let y = 0, o = 0; y < height; y++, o = y * width * 4) {
797+
for (const p of planes) p.fill?.(y);
790798
const r0 = y * p0.stride, r1 = p1 ? y * p1.stride : r0, r2 = p2 ? y * p2.stride : r0;
791799
if (grey) for (let x = 0; x < width; x++, o += 4) {
792800
pixels[o] = pixels[o + 1] = pixels[o + 2] = s0[r0 + x];
@@ -811,6 +819,7 @@ function output(frame: Frame, adobe: { transform: number } | null, jfif: boolean
811819
}
812820

813821
for (let y = 0, o = 0; y < height; y++) {
822+
for (const p of planes) p.fill?.(y);
814823
const r0 = y * p0.stride, r1 = y * p1.stride, r2 = y * p2.stride, r3 = y * p3.stride;
815824

816825
for (let x = 0; x < width; x++, o += 4) {

0 commit comments

Comments
 (0)