HMCL-dev/HMCL · error · PngIntegrityException

bKGD chunk received before IHDR chunk

Error message

bKGD chunk received before IHDR chunk

What it means

This ordering guard in readBackgroundChunk fires when a bKGD chunk appears in the stream before the IHDR header chunk has been processed. The PNG spec requires IHDR to be the first chunk; a background chunk without a header means the stream is out of order or malformed, and the reader cannot interpret the background against an unknown colour type.

Solutions

  1. Ensure decoding starts at the file beginning so IHDR is processed first.
  2. Validate chunk ordering before feeding the stream to the reader (IHDR must be first chunk).
  3. Re-export the PNG with a compliant tool to fix chunk order.
  4. Catch PngIntegrityException and reject the malformed file.

Example fix

// before: feeding arbitrary chunk stream
reader.readChunk(source, code, len);
// after: require header first
if (!headerSeen) {
    throw new PngIntegrityException("bKGD before IHDR");
}
reader.readBackgroundChunk(source, len);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!headerSeen && chunkCodeIsBackground(code)) throw new IllegalStateException("bKGD before IHDR");

Try / catch

try { readChunks(stream); } catch (PngIntegrityException e) { if (e.getMessage().contains("before IHDR")) rejectMalformedOrdering(file); }

Prevention

When it happens

Trigger: A stream where bKGD appears before IHDR, e.g. a malformed file or a reader resumed mid-stream at the wrong offset.

Common situations: Corrupted or hand-assembled PNGs, chunk streams spliced by tools, resuming a partial decode without skipping to IHDR.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

//        if (dataLength % 3 != 0) {
//            throw new PngIntegrityException(String.format("png spec: palette chunk length must be divisible by 3: %d", dataLength));
//        }

        if (source.supportsByteAccess()) {
            processor.processTransparency(source.getBytes(), source.tell(), dataLength);
            source.skip(dataLength);
        } else {
            byte[] paletteBytes = new byte[dataLength];
            //ByteStreams.readFully(source.getBis(), paletteBytes);
            source.getDis().readFully(paletteBytes);
            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 {

View on GitHub (pinned to 24702dc5a0)