Skip to content

Fix PNG decoding of empty data chunks and short palettes, and use big-endian 16-bit tRNS - #3228

Merged
JimBobSquarePants merged 5 commits into
SixLabors:mainfrom
jafin:jf/png-chunk-split-and-transparency
Oct 27, 2026
Merged

JimBobSquarePants merged 5 commits into
SixLabors:mainfrom
jafin:jf/png-chunk-split-and-transparency

Conversation

@jafin

@jafin jafin commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull requests open
  • I have followed the existing coding patterns and practices
  • I have provided test coverage for my change

Description

This fixes three PNG defects: empty image-data chunks prematurely ended decompression, palettes shorter than one RGB entry could be indexed outside their storage, and 16-bit transparency markers used the wrong byte order.

The decoder skips valid empty IDAT payloads and sequence-only fdAT chunks without ending the compressed stream. Chunk parsing and CRC checks still use the existing reader. Only consecutive IDAT payloads belong to the default image's compressed stream; frame-data chunks return to the outer chunk loop for their framing checks.

Indexed images require a complete palette entry. That requirement is checked before allocating pixels and outside image-data recovery. fdAT chunks must also contain their four-byte sequence number. Short fdAT chunks encountered during decompression or Identify return through the existing pending-chunk path, so the outer loop rejects them in every integrity mode.

Both encoding and decoding now use big-endian 16-bit tRNS samples, as required by the PNG specification.

Integrity policy

  • Corrupt CRCs on empty IDAT and sequence-only fdAT chunks throw in Strict and IgnoreAncillary modes.
  • IgnoreImageData permits those CRC errors while retaining exact decoded pixels.
  • Missing indexed palettes and missing frame-data sequence numbers remain fatal in every mode.

Test inputs

All regression inputs are committed files, including the chunk layouts, corrupt CRCs, malformed palettes, truncated frame chunks and 16-bit transparency samples. Decoder tests do not construct, compress or rewrite input images. Encoder tests also load their input images from files. Six input-building helpers were removed. Offline verification checked all 25 variant files for chunk bounds, CRC classification and payload contents; Pillow independently confirmed unchanged pixels for all 12 valid rechunked PNG/APNG files.

Verification

Ten of the original fifteen regression cases fail on current main before the fix. Review tests also exposed suppressed missing-palette errors and accepted short frame-data chunks.

After correction, all 463 PNG tests pass in Release on .NET 10. This includes exact static and animated pixel comparisons, corrupt empty-chunk CRCs, malformed frame chunks at the start and during decompression, palette bounds, and independent checks of stored transparency bytes. The existing whole-image comparer checks every frame without cloning them for comparison.

The branch includes current main. No tolerances or reference images changed.

Fixes #3240

…big-endian 16-bit tRNS

Zero-length IDAT and fdAT chunks are valid, but the data chunk readers
returned their length of 0, which the inflater treats as the end of the
compressed stream. An empty chunk inside the zlib header, or two in a row,
broke decoding. The readers now skip empty chunks.

A PLTE chunk shorter than one entry produced an empty color table, which
the palette scanline processor indexed outside of. It is now treated as
a missing palette, so decoding throws InvalidImageContentException.

16-bit grayscale and truecolor tRNS samples are big-endian, like all PNG
multi-byte values, but were read and written little-endian.
Validate indexed palettes before allocating pixels and reject frame data without a complete sequence number in every integrity mode. Reuse the pending-chunk path to return malformed fdAT chunks from inflation to the outer chunk loop. Keep IDAT payloads separate from frame-data framing.

Extend existing chunk tests for CRC policies, empty frame payload layouts, short palettes and missing sequence numbers. Use the existing whole-image comparer instead of cloning every frame. All 463 PNG tests pass in Release on .NET 10.
Load a stored noise PNG and the existing APNG through TestFile instead of generating random pixels and encoding baseline images for every case. Remove the image generator and defer baseline decoding until a pixel comparison is required.
Store all chunk layouts, corrupt-CRC variants, short palettes, truncated frame chunks and asymmetric 16-bit transparency samples as input files. Tests load those files without constructing or compressing PNG input. Encoder tests also load their input images from files.

Remove six input-building helpers. Independently check every fixture for chunk bounds, CRC classification and payload contents; Pillow verifies unchanged pixels for all twelve valid rechunked inputs. All 463 PNG tests pass in Release on .NET 10.
@JimBobSquarePants
JimBobSquarePants merged commit ebb6430 into SixLabors:main Oct 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PNG: empty IDAT/fdAT chunks break decoding, a 1-2 byte PLTE reads outside the palette, and 16-bit tRNS uses the wrong byte order

2 participants