Skip to content

feat: implement indexed PNG (color type 3) decoding support#1

Merged
nnmrts merged 3 commits intofeat-indexed-pngfrom
copilot/add-indexed-png-support
Mar 10, 2026
Merged

feat: implement indexed PNG (color type 3) decoding support#1
nnmrts merged 3 commits intofeat-indexed-pngfrom
copilot/add-indexed-png-support

Conversation

Copy link

Copilot AI commented Mar 10, 2026

  • Explore codebase to understand PNG implementation
  • Modify png_base.ts: update unfilterAndConvert to support indexed color (PLTE palette lookup, sub-byte bit depths 1/2/4/8)
  • Modify png.ts: parse PLTE and tRNS chunks, build RGBA palette, pass to decoder
  • Modify apng.ts: parse PLTE and tRNS chunks, pass palette through frame decoder
  • Add 4 tests for indexed PNG decoding (8-bit, 8-bit with tRNS transparency, 4-bit, 1-bit)
  • Update CHANGELOG.md
  • Fix scanline length for grayscale (color type 0) sub-byte bit depths
  • Add PLTE validation in png.ts and apng.ts: mandatory for type 3, length multiple of 3, ≤256 entries, valid bitDepth (1/2/4/8)
  • Replace silent fallback in png_base.ts with clear error when palette is missing for color type 3
  • Add palette index bounds check in png_base.ts with descriptive error
  • Add 3 negative tests: missing PLTE, PLTE length not divisible by 3, out-of-range palette index

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: nnmrts <20396367+nnmrts@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for indexed PNG color type 3 feat: implement indexed PNG (color type 3) decoding support Mar 10, 2026
@nnmrts nnmrts requested a review from Copilot March 10, 2026 01:29
@nnmrts nnmrts marked this pull request as ready for review March 10, 2026 01:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds decode support for PNG/APNG indexed color (color type 3) by parsing palette chunks and expanding packed palette indices into RGBA output, including sub-byte bit depths and per-entry transparency.

Changes:

  • Extend unfilterAndConvert to handle indexed scanlines (1/2/4/8-bit) and apply palette lookup into RGBA.
  • Parse PLTE/tRNS in PNG/APNG decoders and thread a combined RGBA palette into the scanline conversion step.
  • Add new unit tests covering indexed PNG variants and document the feature in the changelog.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/formats/png_base.ts Adds indexed-palette conversion logic and adjusts scanline sizing/filter bpp for sub-byte indexed data.
src/formats/png.ts Parses PLTE/tRNS, builds RGBA palette, passes it into unfilter/convert for PNG decode.
src/formats/apng.ts Mirrors palette parsing/building and threads palette into APNG frame decode.
test/formats/png.test.ts Adds fixture builder + tests for indexed PNG (8-bit, tRNS, 4-bit, 1-bit).
CHANGELOG.md Notes indexed PNG decoding support additions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +116 to +120
if (colorType === 3 && plte) {
const numColors = plte.length / 3;
palette = new Uint8Array(numColors * 4);
for (let i = 0; i < numColors; i++) {
palette[i * 4] = plte[i * 3];
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PLTE/tRNS handling needs basic validation for correctness and robustness: (1) for color type 3, PLTE is mandatory—if missing, decoding should fail with a clear error; (2) PLTE length must be a multiple of 3 and no more than 256 entries (768 bytes); and (3) the indexed bitDepth should be restricted to 1/2/4/8. Without these checks, malformed inputs can produce silent corruption or large/invalid palette allocations.

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +182
if (colorType === 3 && plte) {
const numColors = plte.length / 3;
palette = new Uint8Array(numColors * 4);
for (let i = 0; i < numColors; i++) {
palette[i * 4] = plte[i * 3];
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as PNG: when building the palette, validate PLTE (multiple of 3, <= 256 entries) and enforce that indexed images (color type 3) must have a PLTE chunk and a valid indexed bitDepth (1/2/4/8). This avoids silent corruption and potential memory issues from oversized/malformed palettes.

Copilot uses AI. Check for mistakes.
Comment on lines +362 to +375
test("PNG: indexed color (type 3) - 8-bit depth, no transparency", async () => {
const format = new PNGFormat();

// 2x2 image with 4 distinct palette colors
const palette: [number, number, number][] = [
[255, 0, 0], // index 0: red
[0, 255, 0], // index 1: green
[0, 0, 255], // index 2: blue
[255, 255, 0], // index 3: yellow
];
const indices = [0, 1, 2, 3]; // row-major: [red, green, blue, yellow]

const pngData = await buildIndexedPNG(2, 2, 8, palette, null, indices);
const decoded = await format.decode(pngData);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new indexed-PNG tests cover positive cases, but there are no negative tests for malformed indexed images (e.g., color type 3 with missing PLTE, PLTE length not divisible by 3, or pixel indices that exceed the palette size). Adding a couple of assertRejects cases would help lock in spec-compliant error handling for the new code paths.

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +91
// For indexed color with sub-byte bit depths, scanline bytes != width * bytesPerPixel
const scanlineLength = colorType === 3 && bitDepth < 8
? Math.ceil(width * bitDepth / 8)
: width * bytesPerPixel;

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanlineLength is only corrected for indexed (color type 3) sub-byte images. Valid grayscale PNGs (color type 0) can also have bit depths 1/2/4, but with the current calculation (width * bytesPerPixel) the decoder will read too many bytes per row and misalign subsequent scanlines. Consider computing scanline bytes generically as ceil(width * bitsPerPixel / 8) for any packed format (at least color types 0 and 3), or explicitly rejecting unsupported bit-depth/color-type combinations before unfiltering.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +156
} else {
// No palette: treat index as grayscale value
rgba[outIdx] = index;
rgba[outIdx + 1] = index;
rgba[outIdx + 2] = index;
rgba[outIdx + 3] = 255;
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For indexed PNGs (color type 3), the palette is required by the PNG spec. The current behavior silently falls back to treating the index as a grayscale value when palette is missing, which will produce incorrect output for otherwise-valid inputs and can hide malformed files. It would be better to throw a clear error when colorType === 3 and palette is undefined (ideally earlier during chunk parsing).

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +149
if (palette) {
rgba[outIdx] = palette[index * 4];
rgba[outIdx + 1] = palette[index * 4 + 1];
rgba[outIdx + 2] = palette[index * 4 + 2];
rgba[outIdx + 3] = palette[index * 4 + 3];
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Palette lookup does not validate that the decoded index is within the palette range. If the image references an out-of-range index, reads like palette[index * 4] will yield undefined and end up as 0, producing incorrect pixels instead of reporting a malformed PNG. Add a bounds check (e.g., index < palette.length / 4) and throw a descriptive error when violated.

Copilot uses AI. Check for mistakes.
@nnmrts
Copy link
Owner

nnmrts commented Mar 10, 2026

@copilot apply changes based on the comments in this thread

…dback

Co-authored-by: nnmrts <20396367+nnmrts@users.noreply.github.com>
Copy link
Author

Copilot AI commented Mar 10, 2026

@copilot apply changes based on the comments in this thread

Applied all review changes in commit 9ab72f3:

  • png_base.ts: Fixed scanline length calculation to also handle grayscale (color type 0) sub-byte bit depths using Math.ceil(width * bitDepth / 8). Replaced the silent grayscale fallback for missing palette with a clear error. Added palette index bounds check that throws a descriptive error on out-of-range access.
  • png.ts / apng.ts: Added PLTE validation for color type 3: throws if PLTE is missing, if its length isn't a multiple of 3, if it exceeds 256 entries, or if the bit depth isn't 1/2/4/8.
  • test/formats/png.test.ts: Added 3 assertRejects negative tests covering: missing PLTE chunk, PLTE length not divisible by 3, and out-of-range palette index.

@nnmrts nnmrts changed the base branch from main to feat-indexed-png March 10, 2026 01:59
@nnmrts nnmrts merged commit 071bcc6 into feat-indexed-png Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants