diff --git a/IconHelper.Test/ProcessImageTests.cs b/IconHelper.Test/ProcessImageTests.cs index c74152c..d36616f 100644 --- a/IconHelper.Test/ProcessImageTests.cs +++ b/IconHelper.Test/ProcessImageTests.cs @@ -376,4 +376,26 @@ public void PreservesTransparencyOfTheSourceArtwork() Assert.AreEqual(0, image[15, 15].A, "The transparent notch should survive processing."); Assert.AreEqual(255, image[2, 2].A, "The surrounding artwork should remain opaque."); } + + [TestMethod] + public void ReducesAnAnimatedInputToItsFirstFrame() + { + // An animated GIF, APNG or WebP decodes to one frame per animation step. Only the root frame + // used to get the coverage pass, so the rest were written out as an animated PNG of raw + // greyscale, cropped to the root frame's bounds. + using Image image = TestImages.Blank(64, 64); + TestImages.FillRect(image, 20, 20, 20, 20, OpaqueWhite); + using (Image second = TestImages.Blank(64, 64)) + { + TestImages.FillRect(second, 5, 5, 55, 55, OpaqueWhite); + image.Frames.AddFrame(second.Frames.RootFrame); + } + + IconHelper.ProcessImage(image, Color.FromBytes(255, 0, 0), 32, 0); + + Assert.AreEqual(1, image.Frames.Count, "The output should be a single static frame."); + Assert.AreEqual(20, image.Width, "The bounds should come from the first frame's artwork."); + Assert.AreEqual(new Rgba32(255, 0, 0, 255), image[10, 10]); + Assert.AreEqual(new Rgba32(255, 0, 0, 255), image[0, 0]); + } } diff --git a/IconHelper/IconHelper.cs b/IconHelper/IconHelper.cs index 56f9478..d5730ad 100644 --- a/IconHelper/IconHelper.cs +++ b/IconHelper/IconHelper.cs @@ -159,12 +159,21 @@ internal static BatchResult ProcessDirectory(Arguments args, Color color) /// Reduces an icon to a coverage mask: every pixel carries the flat target colour and the shape /// lives entirely in the alpha channel. The result is trimmed of its transparent margins, centred /// on a square canvas and scaled down to at most pixels. + /// An animated input is reduced to its first frame, since the output is a static icon. /// The image is mutated in place. /// internal static void ProcessImage(Image image, Color color, int size, int padding) { Ensure.NotNull(image); + // An animated GIF, APNG or WebP loads with one frame per step. Mutate applies to every frame, + // but ProcessPixelRows below reads and writes the root frame alone, so the other frames would + // skip the coverage pass and be saved as an animated PNG of raw greyscale. Drop them first. + while (image.Frames.Count > 1) + { + image.Frames.RemoveFrame(image.Frames.Count - 1); + } + // The semantic Color stores linear channels as doubles. Encode to sRGB bytes once here rather // than per pixel, both for speed and so the pass below stays plain byte arithmetic. (byte colorR, byte colorG, byte colorB, byte _) = color.ToBytes();