Skip to content

Commit 5bb9034

Browse files
committed
WebP lossless encoder: single symbol codes >= 256 take zero bits
A prefix code with one used symbol that can't be a simple code (a length code, >= 256) is written as a normal code with one non-zero length. Decoders read such a code with zero bits per symbol, the encoder wrote one, so every following bit was off by one. Only prefix groups (since 3.7.0) produce such histograms. It broke lossless WebPs and the alpha of lossy ones, which most optimized animations have. Fixes #82 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
1 parent c8c1b0b commit 5bb9034

3 files changed

Lines changed: 29 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 3.7.1
4+
5+
### Fixes
6+
7+
- Lossless WebPs, and lossy ones with alpha (most optimized animations), could be undecodable since 3.7.0 (#82): a
8+
prefix group whose code had a single symbol of 256 or more (a length code) took a bit per symbol instead of none.
9+
310
## 3.7.0
411

512
### Features

‎src/codecs/vp8l.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -684,8 +684,9 @@ function writeCode(w: BitWriter, counts: ArrayLike<number>): Code {
684684
return { lengths, codes: canonicalCodes(lengths) };
685685
}
686686

687+
// a single symbol >= 256 can't be a simple code, a normal code with one non-zero length takes zero bits per symbol
688+
// too (then the code length 1 it gets is only transmitted)
687689
const code = buildEncodeCode(counts);
688-
// a normal code needs at least two symbols with non-zero length, which is given here
689690

690691
// run length encode the code lengths: 16 repeats the previous non-zero length, 17/18 are runs of zeros
691692
const tokens: number[] = [], extras: number[] = [];
@@ -743,7 +744,7 @@ function writeCode(w: BitWriter, counts: ArrayLike<number>): Code {
743744
else if (t === 18) w.write(extras[i], 7);
744745
}
745746

746-
return code;
747+
return used.length === 1 ? { lengths: code.lengths, codes: new Uint16Array(0) } : code;
747748
}
748749

749750
// prefix symbol and extra bits of a length or distance value (>= 1)

‎tests/regressions.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,25 @@ const rgba = (image, x, y) => Image.colorToRGBA(image.getPixelAt(x, y));
535535
assert(crc32(data) === zlib.crc32(data), 'crc32: wrong for a long buffer');
536536
}
537537

538+
// a VP8L prefix code with a single symbol >= 256 (here: a prefix group whose green alphabet only has one length
539+
// code) takes zero bits per symbol, the encoder wrote one (#82). Lossy WebPs code their alpha as VP8L too
540+
{
541+
for (const [colors, width, height, rows, start] of [[16, 128, 128, 8, 2], [4, 128, 256, 16, 5], [2, 256, 600, 64, 3]]) {
542+
let seed = start;
543+
const random = () => (seed = seed * 1103515245 + 12345 >>> 0) / 2 ** 32;
544+
// noisy rows on top and a noisy strip in the bottom left, the rest transparent
545+
const image = new Image(width, height).fill((x, y) => {
546+
const c = y < rows || y > height * 2 / 3 && x < 64 ? random() * colors | 0 : 0;
547+
return c ? Image.rgbaToColor(c * 37, c * 11, c, 255) : 0;
548+
});
549+
for (const options of [{}, {quality: 85}]) {
550+
const decoded = await Image.decode(await image.encodeWebP(options)).catch(err => assert(false, `WebP ${JSON.stringify(options)} of the ${width}x${height} mask: ${err.message}`));
551+
for (let i = 3; i < image.bitmap.length; i += 4)
552+
assert(decoded.bitmap[i] === image.bitmap[i], `WebP ${JSON.stringify(options)} of the ${width}x${height} mask: wrong alpha`);
553+
}
554+
}
555+
}
556+
538557
// damaged fonts (truncated, bytes changed) render or throw a font error, quickly: no runaway memory or time (contours
539558
// with decreasing end points, composite glyphs containing themselves, huge COLR clip boxes)
540559
{

0 commit comments

Comments
 (0)