Skip to content

Commit 47bc999

Browse files
matmenclaude
andcommitted
faster GIF decoding
- Frame indices become RGBA through a table of the 256 colors (transparent and missing entries resolved once), written as one 32-bit value per pixel instead of four bytes after a table bounds check. - LZW strings are written without checking every byte against the end of the image, unless the string runs past it. The results are bit-identical: still and animated decoding (and the GIF and APNG encoders on the decoded frames) checked against master on 1593 images. The benchmark's 101 frame GIF decodes about 25% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 4cb4753 commit 47bc999

2 files changed

Lines changed: 27 additions & 17 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
TGA, text chunks for PNG and APNG. Like PNGs, all files name ImageScript and its version as the software and
2525
include the creation time by default; `{software: ''}` leaves the software out.
2626

27+
### Performance
28+
29+
- GIF decoding is about 25% faster, with identical results: indices become colors through a lookup table, LZW
30+
strings are written without bounds checks unless they run past the image.
31+
2732
### Deprecated
2833

2934
- **Animations as arrays**: `Animation` (and `GIF`) extend `Array`, so `animation[0]`, `animation.length` and

‎src/codecs/gif.ts‎

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -272,21 +272,18 @@ export function decode(data: Uint8Array, onlyFirstFrame = false): GIFData {
272272
const pixels = new Uint8Array(fw * fh * 4);
273273
const rows = interlaced ? interlacedRows(fh) : null;
274274

275+
// the RGBA value of every index: transparent stays 0, indices past the color table are opaque black
276+
const colors = new Uint8Array(256 * 4);
277+
for (let index = 0; index < 256; index++) {
278+
if (index === transparentIndex) continue;
279+
if (table && index * 3 + 2 < table.length) colors.set(table.subarray(index * 3, index * 3 + 3), index * 4);
280+
colors[index * 4 + 3] = 0xff;
281+
}
282+
const lookup = new Uint32Array(colors.buffer), out = new Uint32Array(pixels.buffer);
283+
275284
for (let row = 0; row < fh; row++) {
276-
const targetRow = rows ? rows[row] : row;
277-
278-
for (let column = 0; column < fw; column++) {
279-
const index = indices[row * fw + column];
280-
if (index === transparentIndex) continue;
281-
282-
const o = (targetRow * fw + column) * 4;
283-
if (table && index * 3 + 2 < table.length) {
284-
pixels[o] = table[index * 3];
285-
pixels[o + 1] = table[index * 3 + 1];
286-
pixels[o + 2] = table[index * 3 + 2];
287-
}
288-
pixels[o + 3] = 0xff;
289-
}
285+
const target = (rows ? rows[row] : row) * fw, source = row * fw;
286+
for (let column = 0; column < fw; column++) out[target + column] = lookup[indices[source + column]];
290287
}
291288

292289
frames.push({ x, y, width: fw, height: fh, delay, dispose, pixels });
@@ -393,9 +390,17 @@ function lzwDecode(data: Uint8Array, minCodeSize: number, pixelCount: number): U
393390
const length = lengths[entry];
394391
const end = Math.min(out + length, pixelCount);
395392
let c = entry;
396-
for (let i = out + length - 1; i >= out; i--) {
397-
if (i < end) output[i] = suffix[c];
398-
c = prefix[c];
393+
if (end === out + length) {
394+
for (let i = end - 1; i >= out; i--) {
395+
output[i] = suffix[c];
396+
c = prefix[c];
397+
}
398+
} else {
399+
// the string runs past the image: only its start is written
400+
for (let i = out + length - 1; i >= out; i--) {
401+
if (i < end) output[i] = suffix[c];
402+
c = prefix[c];
403+
}
399404
}
400405

401406
let written = end - out;

0 commit comments

Comments
 (0)