From dacdff4be2662c0e746846ddf24064448c192ae0 Mon Sep 17 00:00:00 2001 From: deeferentleeg Date: Sat, 18 Jul 2026 19:16:43 +0200 Subject: [PATCH] Fix resizing animated WebP breaking animation (#258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResizeImageAsync used a single MagickImage to read the file, which only reads the first frame of a multi-frame image. For animated WebP (and animated GIF), this discarded all subsequent frames and produced a static image on resize. Detect multi-frame images with the existing ImageAnalyzer.IsAnimated() helper and route them through a new ResizeAnimatedImageAsync method that uses MagickImageCollection: Coalesce, resize each frame, then write the collection back — preserving animation. Single-frame images continue through the original MagickImage path unchanged. --- .../ImageDecoding/SaveImageFileHelper.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) 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