Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 3.2.0

### New

- **Metadata in every format**: `encodeJPEG()`, `encodeWebP()`, `encodeTGA()` and, for animations, `encodeGIF()`,
`encodeAPNG()` and `encodeWebP()` take the same metadata as `encode()` (now called `Metadata`). Each format stores
the fields it has a place for: Exif and a comment for JPEG, Exif for WebP, comments for GIF, the extension area for
TGA, text chunks for PNG and APNG. Like PNGs, all files name ImageScript and its version as the software and
include the creation time by default; `{software: ''}` leaves the software out.

### Deprecated

- **`PNGMetadata`**: renamed to `Metadata`, the old name stays an alias until 4.0.

### Fixes

- TGA encoding: run length encoded packets end with the row, as TGA 2.0 requires. Before, packets could span rows,
which some readers reject (Pillow fails with "buffer overrun").
- PNG text metadata: text that isn't Latin-1 (like `©` is, but `Ⓒ` or emoji aren't) is written into `iTXt` chunks as
UTF-8 instead of `tEXt`, where readers showed it garbled; text chunks come before the image data, where all readers
look for them.
- An invalid `creationTime` throws a `RangeError` instead of writing "Invalid Date".

## 3.1.0

### New
Expand Down
12 changes: 7 additions & 5 deletions src/codecs/tga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ export function encode(rgba: Uint8Array, width: number, height: number): Uint8Ar
if (rgba.byteOffset % 4) rgba = rgba.slice(0, count * 4);
const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count);

// worst case: a header byte per 128 raw pixels
const out = new Uint8Array(18 + count * bpp + Math.ceil(count / 128) + 26);
// worst case: a header byte per 128 raw pixels of a row
const out = new Uint8Array(18 + count * bpp + height * Math.ceil(width / 128) + 26);
out[2] = 10; // RLE truecolor
out[12] = width & 255; out[13] = width >> 8;
out[14] = height & 255; out[15] = height >> 8;
Expand All @@ -192,9 +192,11 @@ export function encode(rgba: Uint8Array, width: number, height: number): Uint8Ar
};

for (let i = 0; i < count;) {
// a run of at least two equal pixels becomes a run packet, otherwise collect raw pixels until the next run
// a run of at least two equal pixels becomes a run packet, otherwise collect raw pixels until the next run;
// packets end with the row (TGA 2.0 requires it, Pillow rejects packets across rows)
const end = (Math.floor(i / width) + 1) * width;
let run = 1;
while (run < 128 && i + run < count && pixels[i + run] === pixels[i]) run++;
while (run < 128 && i + run < end && pixels[i + run] === pixels[i]) run++;
if (run >= 2) {
out[o++] = 128 | run - 1;
put(i);
Expand All @@ -203,7 +205,7 @@ export function encode(rgba: Uint8Array, width: number, height: number): Uint8Ar
}

let raw = 1;
while (raw < 128 && i + raw < count && !(i + raw + 1 < count && pixels[i + raw] === pixels[i + raw + 1])) raw++;
while (raw < 128 && i + raw < end && !(i + raw + 1 < end && pixels[i + raw] === pixels[i + raw + 1])) raw++;
out[o++] = raw - 1;
for (let k = 0; k < raw; k++) put(i + k);
i += raw;
Expand Down
13 changes: 12 additions & 1 deletion tests/tga.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,23 @@ const rgba = (image, x, y) => image.getRGBAAt(x, y).join();
for (const image of [
new Image(50, 7).fill((x, y) => Image.rgbaToColor(x * 5, y * 30, (x >> 2) * 40, x % 3 ? 255 : 100)),
new Image(300, 2).fill(0x336699ff),
new Image(1, 1).fill(0x00000000)
new Image(1, 1).fill(0x00000000),
// a header per pixel: one pixel rows, alternating
new Image(1, 300).fill((x, y) => y & 1 ? 0xffffffff : 0x000000ff)
]) {
const encoded = await image.encodeTGA();
if (ImageType.getType(encoded) !== 'tga') panic('encoded TGA not recognized');
const back = await Image.decode(encoded);
if (!Buffer.from(back.bitmap).equals(Buffer.from(image.bitmap))) panic(`TGA: ${image.width}x${image.height} didn't round trip`);

// packets don't cross rows
const bpp = encoded[16] / 8;
for (let o = 18, pixel = 0; pixel < image.width * image.height;) {
const n = (encoded[o] & 127) + 1;
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`);
o += 1 + (encoded[o] & 128 ? bpp : n * bpp);
pixel += n;
}
}
const opaque = await new Image(4, 4).fill(0xff0000ff).encodeTGA();
if (opaque[16] !== 24) panic('TGA: opaque images should be stored with 24 bits');
Expand Down
Loading