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.
What's wrong
ProcessDirectoryloads each file withImage.Load<Rgba32>(file)(IconHelper/IconHelper.cs:131), which decodes every frame of a multi-frame image (animated GIF, APNG, animated WebP). The pipeline inProcessImagethen treats the frames inconsistently:image.Mutate(...)calls (theBlackWhite()at line 205, and the crop/pad/resize inCropSquareAndResizeat line 369) are applied to all frames.FindBrightestOpaqueValueandFlattenToCoverageAndMeasureBoundsuseimage.ProcessPixelRows(...)(lines 240 and 308), which onImage<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
BlackWhitegreyscale output with its original alpha, and is then cropped to frame 0's bounding box. Finallyimage.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:Run:
iconhelper -i in -o out -c "#FF0000" -s 32Actual:
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:
(either in
ProcessDirectoryafterImage.Load, or at the top ofProcessImageso tests can drive it). Processing each frame with a shared bounding box is the larger alternative.Acceptance criteria:
ProcessImageTestsbuilds a two-frameImage<Rgba32>, runsProcessImage, and asserts onimage.Frames.Countand the pixels of each remaining frame.