Skip to content

Commit 31f9b3f

Browse files
matmenclaude
andcommitted
merge the TGA run length fix
Both change the TGA output buffer size: a packet header per 128 pixels of a row, plus the extension area. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
2 parents 6f3631b + fafc004 commit 31f9b3f

2 files changed

Lines changed: 19 additions & 6 deletions

File tree

‎src/codecs/tga.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ export function encode(rgba: Uint8Array, width: number, height: number, extensio
216216
if (rgba.byteOffset % 4) rgba = rgba.slice(0, count * 4);
217217
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);
218218

219-
// worst case: a header byte per 128 raw pixels
220-
const out = new Uint8Array(18 + count * bpp + Math.ceil(count / 128) + 495 + 26);
219+
// worst case: a header byte per 128 raw pixels of a row, the extension area and the footer
220+
const out = new Uint8Array(18 + count * bpp + height * Math.ceil(width / 128) + 495 + 26);
221221
out[2] = 10; // RLE truecolor
222222
out[12] = width & 255; out[13] = width >> 8;
223223
out[14] = height & 255; out[15] = height >> 8;
@@ -234,9 +234,11 @@ export function encode(rgba: Uint8Array, width: number, height: number, extensio
234234
};
235235

236236
for (let i = 0; i < count;) {
237-
// a run of at least two equal pixels becomes a run packet, otherwise collect raw pixels until the next run
237+
// a run of at least two equal pixels becomes a run packet, otherwise collect raw pixels until the next run;
238+
// packets end with the row (TGA 2.0 requires it, Pillow rejects packets across rows)
239+
const end = (Math.floor(i / width) + 1) * width;
238240
let run = 1;
239-
while (run < 128 && i + run < count && pixels[i + run] === pixels[i]) run++;
241+
while (run < 128 && i + run < end && pixels[i + run] === pixels[i]) run++;
240242
if (run >= 2) {
241243
out[o++] = 128 | run - 1;
242244
put(i);
@@ -245,7 +247,7 @@ export function encode(rgba: Uint8Array, width: number, height: number, extensio
245247
}
246248

247249
let raw = 1;
248-
while (raw < 128 && i + raw < count && !(i + raw + 1 < count && pixels[i + raw] === pixels[i + raw + 1])) raw++;
250+
while (raw < 128 && i + raw < end && !(i + raw + 1 < end && pixels[i + raw] === pixels[i + raw + 1])) raw++;
249251
out[o++] = raw - 1;
250252
for (let k = 0; k < raw; k++) put(i + k);
251253
i += raw;

‎tests/tga.js‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,23 @@ const rgba = (image, x, y) => image.getRGBAAt(x, y).join();
5555
for (const image of [
5656
new Image(50, 7).fill((x, y) => Image.rgbaToColor(x * 5, y * 30, (x >> 2) * 40, x % 3 ? 255 : 100)),
5757
new Image(300, 2).fill(0x336699ff),
58-
new Image(1, 1).fill(0x00000000)
58+
new Image(1, 1).fill(0x00000000),
59+
// a header per pixel: one pixel rows, alternating
60+
new Image(1, 300).fill((x, y) => y & 1 ? 0xffffffff : 0x000000ff)
5961
]) {
6062
const encoded = await image.encodeTGA();
6163
if (ImageType.getType(encoded) !== 'tga') panic('encoded TGA not recognized');
6264
const back = await Image.decode(encoded);
6365
if (!Buffer.from(back.bitmap).equals(Buffer.from(image.bitmap))) panic(`TGA: ${image.width}x${image.height} didn't round trip`);
66+
67+
// packets don't cross rows
68+
const bpp = encoded[16] / 8;
69+
for (let o = 18, pixel = 0; pixel < image.width * image.height;) {
70+
const n = (encoded[o] & 127) + 1;
71+
if (Math.floor(pixel / image.width) !== Math.floor((pixel + n - 1) / image.width)) panic(`TGA: ${image.width}x${image.height} has a packet across rows`);
72+
o += 1 + (encoded[o] & 128 ? bpp : n * bpp);
73+
pixel += n;
74+
}
6475
}
6576
const opaque = await new Image(4, 4).fill(0xff0000ff).encodeTGA();
6677
if (opaque[16] !== 24) panic('TGA: opaque images should be stored with 24 bits');

0 commit comments

Comments
 (0)