Skip to content

Commit 4c4d887

Browse files
matmenclaude
andcommitted
faster PNG decoding and encoding
- The paeth predictor is computed without branches (bit masks pick a, b or c), in unfilter and filter: photos make the branches unpredictable. - 8-bit RGB images are converted to RGBA without the generic per-sample functions, interlaced 8-bit RGBA without a subarray per pixel. - Encoding: the pixel format scan stops once nothing is left to find out (no palette, not grey, not opaque), the conversion to RGB, grey and grey with alpha has a loop for each. The results are bit-identical: decoding, every encoder and animations checked against master on 1593 images. A 1920×1080 photo decodes about 25% faster (PNG written by ImageScript) and encodes about 10% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 6e1e182 commit 4c4d887

2 files changed

Lines changed: 46 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
strings are written without bounds checks unless they run past the image.
3131
- SVG rendering is about 15-20% faster, with identical results: groups with opacity, clip paths, masks and nested
3232
viewports are composited only where they painted.
33+
- PNG decoding is about 25% faster and encoding about 10% faster, with identical results: the paeth filter is
34+
computed without branches, 8-bit RGB images are converted without per-sample function calls.
3335

3436
### Deprecated
3537

‎src/codecs/png.ts‎

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ function filter(data: Uint8Array, rowBytes: number, height: number, bpp: number)
125125
for (let i = bpp; i < rowBytes; i++) {
126126
const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp];
127127
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
128-
out[o + i] = data[row + i] - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
128+
// the predictor without branches, see unfilter
129+
const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
130+
out[o + i] = data[row + i] - (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
129131
}
130132
}
131133

@@ -191,7 +193,7 @@ function pixelFormat(images: Uint8Array[]): PixelFormat {
191193

192194
// collect up to 257 distinct colors, runs of equal pixels are skipped cheaply
193195
let colors: Map<number, number> | null = new Map();
194-
for (const rgba of images) {
196+
scan: for (const rgba of images) {
195197
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, rgba.length >> 2);
196198
let previous = -1;
197199
for (let i = 0; i < pixels.length; i++) {
@@ -206,6 +208,8 @@ function pixelFormat(images: Uint8Array[]): PixelFormat {
206208
if (colors.size === 256) colors = null;
207209
else colors.set(value, colors.size);
208210
}
211+
// nothing left to find out: RGBA it is
212+
if (!colors && !opaque && !grey) break scan;
209213
}
210214
}
211215

@@ -253,16 +257,18 @@ function pixelFormat(images: Uint8Array[]): PixelFormat {
253257
if (channels === 4) return rgba;
254258
const count = width * height;
255259
const data = new Uint8Array(count * channels);
256-
for (let i = 0, o = 0; i < count; i++) {
257-
const s = i * 4;
258-
if (grey) data[o++] = rgba[s];
259-
else {
260-
data[o++] = rgba[s];
261-
data[o++] = rgba[s + 1];
262-
data[o++] = rgba[s + 2];
260+
if (channels === 3) {
261+
for (let s = 0, o = 0; o < data.length; s += 4, o += 3) {
262+
data[o] = rgba[s];
263+
data[o + 1] = rgba[s + 1];
264+
data[o + 2] = rgba[s + 2];
263265
}
264-
if (!opaque) data[o++] = rgba[s + 3];
265-
}
266+
} else if (channels === 2) {
267+
for (let s = 0, o = 0; o < data.length; s += 4, o += 2) {
268+
data[o] = rgba[s];
269+
data[o + 1] = rgba[s + 3];
270+
}
271+
} else for (let s = 0, o = 0; o < data.length; s += 4, o++) data[o] = rgba[s];
266272
return data;
267273
}
268274
};
@@ -747,7 +753,9 @@ function unfilter(data: Uint8Array, offset: number, height: number, rowBytes: nu
747753
for (let i = row + bpp; i < row + rowBytes; i++) {
748754
const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp];
749755
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
750-
out[i] = data[src + i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
756+
// without branches (photos make them unpredictable): all ones when a loses to b or c, when c beats b
757+
const notA = (pb - pa | pc - pa) >> 31, takeC = (pc - pb) >> 31;
758+
out[i] = data[src + i] + (a ^ (a ^ b ^ (b ^ c) & takeC) & notA);
751759
}
752760
}
753761
} else throw new Error(`PNG: invalid filter type ${filter}`);
@@ -824,6 +832,22 @@ function createConverter(colorType: number, depth: number, palette: Uint8Array |
824832
? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue)
825833
: null;
826834

835+
// 8-bit samples are bytes that need no scaling, read directly (the most common kind of opaque PNG)
836+
if (depth === 8) {
837+
const [kr, kg, kb] = key || [-1, -1, -1];
838+
return (pixels, rowBytes, w, h, out, place) => {
839+
for (let y = 0; y < h; y++) {
840+
for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 3, o += step) {
841+
const r = pixels[s], g = pixels[s + 1], b = pixels[s + 2];
842+
out[o] = r;
843+
out[o + 1] = g;
844+
out[o + 2] = b;
845+
out[o + 3] = r === kr && g === kg && b === kb ? 0 : 0xff;
846+
}
847+
}
848+
};
849+
}
850+
827851
return (pixels, rowBytes, w, h, out, place) => {
828852
for (let y = 0; y < h; y++) {
829853
const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes);
@@ -861,9 +885,14 @@ function createConverter(colorType: number, depth: number, palette: Uint8Array |
861885
// non-interlaced 8-bit RGBA maps 1:1 onto the output
862886
if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes) return out.set(pixels);
863887

864-
for (let y = 0; y < h; y++)
865-
for (let x = 0, o = place.row(y); x < w; x++, o += place.step)
866-
out.set(pixels.subarray(y * rowBytes + x * 4, y * rowBytes + x * 4 + 4), o);
888+
for (let y = 0; y < h; y++) {
889+
for (let x = 0, s = y * rowBytes, o = place.row(y), step = place.step; x < w; x++, s += 4, o += step) {
890+
out[o] = pixels[s];
891+
out[o + 1] = pixels[s + 1];
892+
out[o + 2] = pixels[s + 2];
893+
out[o + 3] = pixels[s + 3];
894+
}
895+
}
867896
};
868897
}
869898

0 commit comments

Comments
 (0)