HMCL-dev/HMCL · error · PngIntegrityException

Unsupported disposeOp at frame

Error message

Unsupported disposeOp  at frame 

What it means

Thrown by ImageUtils.toImage as PngIntegrityException when an APNG frame's dispose_op is not 0 (NONE), 1 (BACKGROUND), or 2 (PREVIOUS) — the only operations defined by the APNG spec. Indicates a corrupt or invalid fcTL chunk.

Solutions

  1. Re-encode or re-download the APNG — valid files never contain dispose_op > 2
  2. Catch PngIntegrityException and use the PNG's static default image instead of the animation
  3. Validate fcTL fields at parse time and surface a clear 'corrupt APNG' error
  4. Run the file through apngcheck/ffmpeg to confirm integrity before decoding

Example fix

// before
Image anim = ImageUtils.createAnimationFromAPNGApngStream(stream);
// after
Image anim;
try {
    anim = ImageUtils.createAnimationFromAPNGApngStream(stream);
} catch (PngIntegrityException e) {
    anim = new Image(stream); // static fallback
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (control.disposeOp < 0 || control.disposeOp > 2) {
    throw new IllegalArgumentException("Non-spec disposeOp: " + control.disposeOp);
}

Type guard

static boolean hasKnownDisposeOp(PngFrameControl c) {
    return c.disposeOp >= 0 && c.disposeOp <= 2; // NONE, BACKGROUND, PREVIOUS
}

Try / catch

try {
    animation = ImageUtils.toImage(sequence, ...);
} catch (PngIntegrityException e) {
    animation = null;
    useStaticFrame();
}

Prevention

When it happens

Trigger: Decoding an APNG whose fcTL dispose_op byte holds a value > 2, typically from file corruption or a deliberately malformed image.

Common situations: Damaged downloads; corrupted game/resource-pack assets; adversarial PNG files in untrusted content.

Related errors


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

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/ui/image/ImageUtils.java:699

                durations[frameIndex] = durationsMills;
            }

            switch (control.disposeOp) {
                case 0:  // APNG_DISPOST_OP_NONE
                    System.arraycopy(currentFrameBuffer, 0, buffer, 0, currentFrameBuffer.length);
                    break;
                case 1: // APNG_DISPOSE_OP_BACKGROUND
                    for (int row = 0; row < control.height; row++) {
                        int fromIndex = (control.yOffset + row) * width + control.xOffset;
                        Arrays.fill(buffer, fromIndex, fromIndex + control.width, 0);
                    }
                    break;
                case 2: // APNG_DISPOSE_OP_PREVIOUS
                    // Do nothing, keep the previous frame.
                    break;
                default:
                    throw new PngIntegrityException("Unsupported disposeOp " + control.disposeOp + " at frame " + frameIndex);
            }
        }

        @Nullable PngAnimationControl animationControl = sequence.getAnimationControl();
        int cycleCount;
        if (animationControl != null) {
            cycleCount = animationControl.numPlays();
            if (cycleCount == 0)
                cycleCount = Timeline.INDEFINITE;
        } else {
            cycleCount = Timeline.INDEFINITE;
        }

        if (doScale)
            return new AnimationImageImpl(targetWidth, targetHeight, framePixels, durations, cycleCount);
        else
            return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
    }

View on GitHub (pinned to 24702dc5a0)