Skip to content

Animated inputs (GIF/APNG/WebP) produce an animated PNG whose frames after the first are not recoloured #119

Description

@matt-edmondson

What's wrong

ProcessDirectory loads each file with Image.Load<Rgba32>(file) (IconHelper/IconHelper.cs:131), which decodes every frame of a multi-frame image (animated GIF, APNG, animated WebP). The pipeline in ProcessImage then treats the frames inconsistently:

  • image.Mutate(...) calls (the BlackWhite() at line 205, and the crop/pad/resize in CropSquareAndResize at line 369) are applied to all frames.
  • FindBrightestOpaqueValue and FlattenToCoverageAndMeasureBounds use image.ProcessPixelRows(...) (lines 240 and 308), which on Image<TPixel> only touches the root frame.

So only frame 0 gets the coverage treatment (flat target colour, brightness folded into alpha). Every other frame is left as the raw BlackWhite greyscale output with its original alpha, and is then cropped to frame 0's bounding box. Finally image.SaveAsPng(outputFilePath, Encoder) (line 141) with ImageSharp 3.1 encodes all frames, so the output is an animated PNG containing the unprocessed frames.

Failure scenario

Input directory containing anim.gif, 64x64, two frames:

  • frame 0: transparent with a 20x20 opaque white square at (20,20)
  • frame 1: transparent with a 55x55 opaque white square at (5,5)

Run: iconhelper -i in -o out -c "#FF0000" -s 32

Actual:

Processing .../in/anim.gif...
Done. 1 file(s) written.   (exit 0)

out/anim.png: 20x20 frames=2 format=PNG
 frame0: center=Rgba32(255, 0, 0, 255) corner=Rgba32(255, 0, 0, 255)
 frame1: center=Rgba32(0, 0, 0, 255)   corner=Rgba32(0, 0, 0, 255)

Frame 1 is fully opaque black instead of a red coverage mask, and it was cropped to frame 0's bounds (its larger artwork is cut off). Any APNG-aware consumer (browsers, many image viewers) will animate the output and flash an opaque black square. The run reports success and exits 0.

Expected: a single-frame PNG coverage mask in the target colour (the tool's documented output), or every frame processed consistently. Nothing in the README or CLAUDE.md documents multi-frame input as unsupported.

How verified

Built the tool from HEAD, generated the two-frame GIF above with ImageSharp 3.1.12 (the version pinned in Directory.Packages.props), ran the built tool against it, and reloaded the output with ImageSharp to print frame count and pixel values (shown above).

Suggested fix / acceptance criteria

The simplest fix, matching the tool's purpose (static icons), is to reduce the image to its root frame right after loading:

while (image.Frames.Count > 1)
{
    image.Frames.RemoveFrame(image.Frames.Count - 1);
}

(either in ProcessDirectory after Image.Load, or at the top of ProcessImage so tests can drive it). Processing each frame with a shared bounding box is the larger alternative.

Acceptance criteria:

  • A multi-frame input produces an output PNG with exactly one frame (or, if all frames are kept, every frame is a coverage mask in the target colour with consistent bounds).
  • A regression test in ProcessImageTests builds a two-frame Image<Rgba32>, runs ProcessImage, and asserts on image.Frames.Count and the pixels of each remaining frame.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions