diff --git a/src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs b/src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs index 125086b65..92124b652 100644 --- a/src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs +++ b/src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs @@ -225,6 +225,15 @@ public static async Task ResizeImageAsync(FileInfo fileInfo, uint width, u return false; } + // Animated images (e.g. animated WebP, animated GIF) contain multiple frames. + // Reading them with a single MagickImage loses all but the first frame, producing + // a static image. Detect multi-frame images and resize the full collection instead. + if (ImageAnalyzer.IsAnimated(fileInfo)) + { + return await ResizeAnimatedImageAsync(fileInfo, width, height, quality, percentage, destination, ext) + .ConfigureAwait(false); + } + var magick = new MagickImage { ColorSpace = ColorSpace.Transparent @@ -341,4 +350,88 @@ public static async Task ResizeImageAsync(FileInfo fileInfo, uint width, u return true; } + + /// + /// Resizes a multi-frame (animated) image by processing every frame through a + /// , preserving animation timing and disposal. + /// + /// + /// Uses Coalesce() to expand frame diffs into full frames before resizing, mirroring + /// the pattern recommended by the Magick.NET documentation for animated images. + /// + private static async Task ResizeAnimatedImageAsync(FileInfo fileInfo, uint width, uint height, + uint? quality, Percentage? percentage, string? destination, string? ext) + { + try + { + using var collection = new MagickImageCollection(); + + if (fileInfo.Length < 2147483648) + { + await collection.ReadAsync(fileInfo.FullName).ConfigureAwait(false); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + collection.Read(fileInfo.FullName); + } + + if (quality.HasValue) + { + foreach (var frame in collection) + { + frame.Quality = quality.Value; + } + } + + // Expand frame diffs into full frames so each frame can be resized independently. + collection.Coalesce(); + + foreach (var frame in collection) + { + if (percentage is not null) + { + frame.Resize(percentage.Value); + } + else + { + frame.Resize(width, height); + } + } + + var outputPath = destination ?? fileInfo.FullName; + + if (ext is not null) + { + outputPath = Path.ChangeExtension(outputPath, ext); + var format = Path.GetExtension(ext).ToLowerInvariant() switch + { + ".gif" => MagickFormat.Gif, + ".webp" => MagickFormat.WebP, + ".png" => MagickFormat.Png, + ".jpeg" or ".jpg" => MagickFormat.Jpeg, + _ => collection[0].Format + }; + foreach (var frame in collection) + { + frame.Format = format; + } + } + + var dir = Path.GetDirectoryName(outputPath); + if (dir is not null && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + await collection.WriteAsync(outputPath).ConfigureAwait(false); + } + catch (MagickException e) + { + DebugHelper.LogDebug(nameof(SaveImageFileHelper), nameof(ResizeAnimatedImageAsync), e); + return false; + } + + return true; + } } \ No newline at end of file