Skip to content

Commit 223a37f

Browse files
matmenclaude
andcommitted
faster lossless WebP encoding
- Hash chain candidates are skipped without measuring the match when they can't win: the chain runs from near to far, so a candidate no closer than the best one only wins by being longer, which it can't be if it differs right after the best length (zlib's check). - The left and above references are checked directly instead of through an array created for every pixel. - The per-channel helpers of the predictors (select, the clamped ones) and the residual cost are unrolled. The files are byte-identical: lossless encoding checked against master on 1593 images. Encoding a 1920×1080 photo is about 15% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 3e26325 commit 223a37f

2 files changed

Lines changed: 29 additions & 30 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
viewports are composited only where they painted.
3333
- PNG decoding is about 25% faster and encoding about 10% faster, with identical results: the paeth filter is
3434
computed without branches, 8-bit RGB images are converted without per-sample function calls.
35+
- Lossless WebP encoding is about 15% faster, with identical files: backward reference candidates that can't be
36+
longer are skipped, the per-channel predictor helpers are unrolled.
3537

3638
### Deprecated
3739

‎src/codecs/vp8l.ts‎

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -341,34 +341,29 @@ function decodeImage(reader: BitReader, width: number, height: number, isMain: b
341341
const average2 = (a: number, b: number): number => (((a ^ b) & 0xfefefefe) >>> 1) + (a & b) >>> 0;
342342

343343
// per channel (a + b - c) clamped to 0..255
344+
const clamp255 = (v: number): number => v < 0 ? 0 : v > 255 ? 255 : v;
345+
344346
function clampAddSubtractFull(a: number, b: number, c: number): number {
345-
let out = 0;
346-
for (let shift = 0; shift < 32; shift += 8) {
347-
const v = (a >>> shift & 255) + (b >>> shift & 255) - (c >>> shift & 255);
348-
out |= (v < 0 ? 0 : v > 255 ? 255 : v) << shift;
349-
}
350-
return out >>> 0;
347+
return (clamp255((a & 255) + (b & 255) - (c & 255))
348+
| clamp255((a >>> 8 & 255) + (b >>> 8 & 255) - (c >>> 8 & 255)) << 8
349+
| clamp255((a >>> 16 & 255) + (b >>> 16 & 255) - (c >>> 16 & 255)) << 16
350+
| clamp255((a >>> 24) + (b >>> 24) - (c >>> 24)) << 24) >>> 0;
351351
}
352352

353353
// per channel a + (a - b) / 2 clamped to 0..255 (C division, truncating)
354354
function clampAddSubtractHalf(a: number, b: number): number {
355-
let out = 0;
356-
for (let shift = 0; shift < 32; shift += 8) {
357-
const ac = a >>> shift & 255, d = ac - (b >>> shift & 255);
358-
const v = ac + (d / 2 | 0);
359-
out |= (v < 0 ? 0 : v > 255 ? 255 : v) << shift;
360-
}
361-
return out >>> 0;
355+
const a0 = a & 255, a1 = a >>> 8 & 255, a2 = a >>> 16 & 255, a3 = a >>> 24;
356+
return (clamp255(a0 + ((a0 - (b & 255)) / 2 | 0))
357+
| clamp255(a1 + ((a1 - (b >>> 8 & 255)) / 2 | 0)) << 8
358+
| clamp255(a2 + ((a2 - (b >>> 16 & 255)) / 2 | 0)) << 16
359+
| clamp255(a3 + ((a3 - (b >>> 24)) / 2 | 0)) << 24) >>> 0;
362360
}
363361

364362
function select(l: number, t: number, tl: number): number {
365-
let pl = 0, pt = 0;
366-
for (let shift = 0; shift < 32; shift += 8) {
367-
const lc = l >>> shift & 255, tc = t >>> shift & 255, tlc = tl >>> shift & 255;
368-
// distance of the estimate (l + t - tl) to l and to t
369-
pl += Math.abs(tc - tlc);
370-
pt += Math.abs(lc - tlc);
371-
}
363+
const tl0 = tl & 255, tl1 = tl >>> 8 & 255, tl2 = tl >>> 16 & 255, tl3 = tl >>> 24;
364+
// distance of the estimate (l + t - tl) to l and to t
365+
const pl = Math.abs((t & 255) - tl0) + Math.abs((t >>> 8 & 255) - tl1) + Math.abs((t >>> 16 & 255) - tl2) + Math.abs((t >>> 24) - tl3);
366+
const pt = Math.abs((l & 255) - tl0) + Math.abs((l >>> 8 & 255) - tl1) + Math.abs((l >>> 16 & 255) - tl2) + Math.abs((l >>> 24) - tl3);
372367
return pl < pt ? l : t;
373368
}
374369

@@ -807,13 +802,19 @@ function tokenize(pixels: Uint32Array, width: number, cacheBits: number): Stream
807802
let bestLength = 0, bestDistance = 0;
808803

809804
// the pixel to the left and the one above are the cheapest references
810-
for (const d of [1, width]) {
811-
if (d > i) continue;
812-
const l = matchLength(i, i - d);
813-
if (l > bestLength) { bestLength = l; bestDistance = d; }
805+
if (i >= 1) {
806+
const l = matchLength(i, i - 1);
807+
if (l > bestLength) { bestLength = l; bestDistance = 1; }
808+
}
809+
if (width <= i) {
810+
const l = matchLength(i, i - width);
811+
if (l > bestLength) { bestLength = l; bestDistance = width; }
814812
}
815813
if (i + 1 < n && bestLength < MAX_LENGTH) {
816814
for (let j = head[hash(i)], k = 0; j >= 0 && k < CHAIN && i - j <= WINDOW; j = chain[j], k++) {
815+
// the chain goes from near to far: a candidate no closer than the best one only wins by being
816+
// longer, which it can't be if it differs right after the best length
817+
if (i - j >= bestDistance && (i + bestLength >= n || pixels[j + bestLength] !== pixels[i + bestLength])) continue;
817818
const l = matchLength(i, j);
818819
// longer, or as long and closer
819820
if (l > bestLength || l === bestLength && l > 0 && i - j < bestDistance) { bestLength = l; bestDistance = i - j; }
@@ -902,12 +903,8 @@ const subPixels = (a: number, b: number): number => ((a | 0x00ff00ff) - (b & 0xf
902903

903904
// cost estimate of a residual: sum of the channels' distances to zero (as signed bytes)
904905
function residualCost(r: number): number {
905-
let cost = 0;
906-
for (let shift = 0; shift < 32; shift += 8) {
907-
const c = r >>> shift & 255;
908-
cost += c < 128 ? c : 256 - c;
909-
}
910-
return cost;
906+
const c0 = r & 255, c1 = r >>> 8 & 255, c2 = r >>> 16 & 255, c3 = r >>> 24;
907+
return (c0 < 128 ? c0 : 256 - c0) + (c1 < 128 ? c1 : 256 - c1) + (c2 < 128 ? c2 : 256 - c2) + (c3 < 128 ? c3 : 256 - c3);
911908
}
912909

913910
/**

0 commit comments

Comments
 (0)