HMCL-dev/HMCL · error · PngIntegrityException

png spec: palette chunk length must be divisible by 3

Error message

png spec: palette chunk length must be divisible by 3: %d

What it means

This validation in readPaletteChunk rejects a PLTE chunk whose data length is not divisible by 3, mirroring PngPalette's own check. Palette entries are fixed 3-byte RGB triplets, so a bad length means the chunk is corrupt and the colour table cannot be constructed.

Solutions

  1. Re-encode the PNG with a compliant encoder to produce a valid PLTE.
  2. Run pngcheck on the file to confirm the chunk is malformed before decoding.
  3. Catch PngIntegrityException and treat the image as corrupt.

Example fix

// before
processChunk(PLTE, source, dataLength);
// after
if (isPaletteChunk(code) && dataLength % 3 != 0) {
    throw new PngIntegrityException("bad PLTE length " + dataLength);
}
processChunk(PLTE, source, dataLength);
Defensive patterns

Strategy: try-catch

Validate before calling

if (code == PLTE && dataLength % 3 != 0) throw new IllegalStateException("PLTE length must be multiple of 3");

Try / catch

try { reader.readPaletteChunk(source, dataLength); } catch (PngIntegrityException e) { failDecode("malformed PLTE"); }

Prevention

When it happens

Trigger: readChunk dispatches a PLTE chunk whose dataLength % 3 != 0, typically from file truncation or corruption.

Common situations: Corrupted downloads, images manipulated by buggy tools, fuzzed inputs.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/366c8593b63960cd. Report an issue: GitHub.

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/ui/image/apng/reader/DefaultPngChunkReader.java:150

            processor.processTransparency(paletteBytes, 0, dataLength);
        }
    }

    @Override
    public void readBackgroundChunk(PngSource source, int dataLength) throws IOException, PngException {
        if (!seenHeader) {
            throw new PngIntegrityException("bKGD chunk received before IHDR chunk");
        }
        // TODO
        //processor.processBackground(PngBackground.from(source, dataLength);
        source.skip(dataLength);
    }

    @Override
    public void readPaletteChunk(PngSource source, int dataLength) throws IOException, PngException {

        if (dataLength % 3 != 0) {
            throw new PngIntegrityException(String.format("png spec: palette chunk length must be divisible by 3: %d", dataLength));
        }
        // TODO: can check if colour type matches palette type, or if any palette received before (overkill?)

        if (source.supportsByteAccess()) {
            processor.processPalette(source.getBytes(), source.tell(), dataLength);
            source.skip(dataLength);
        } else {
            byte[] paletteBytes = new byte[dataLength];
            //ByteStreams.readFully(source.getBis(), paletteBytes);
            source.getDis().readFully(paletteBytes);
            processor.processPalette(paletteBytes, 0, dataLength);
        }
    }

    @Override
    public void readImageDataChunk(PngSource source, int dataLength) throws PngException, IOException {

        if (idatCount == 0 && apngSequenceExpect == 0) {

View on GitHub (pinned to 24702dc5a0)