From 94838625f15328a7f1247151aebe98fe2afb2c04 Mon Sep 17 00:00:00 2001 From: ema Date: Mon, 30 Dec 2024 17:00:18 +0800 Subject: [PATCH] Support Animated WebP #1024 #1324 Limitations: Only supports x64 systems --- .../Providers/ImageMagickProvider.cs | 93 +++---- .../AnimatedImage/Providers/WebPProvider.cs | 228 ++++++++++++++++++ .../QuickLook.Plugin.ImageViewer/Plugin.cs | 5 +- .../QuickLook.Plugin.ImageViewer.csproj | 1 + 4 files changed, 280 insertions(+), 47 deletions(-) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/WebPProvider.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ImageMagickProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ImageMagickProvider.cs index d06dcb320..17872d6f6 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ImageMagickProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/ImageMagickProvider.cs @@ -75,74 +75,75 @@ public override Task GetThumbnail(Size renderSize) } public override Task GetRenderedFrame(int index) + { + return new Task(GetRenderedFrame); + } + + protected virtual BitmapSource GetRenderedFrame() { var fullSize = Meta.GetSize(); + var settings = new MagickReadSettings + { + BackgroundColor = MagickColors.None, + Defines = new DngReadDefines + { + OutputColor = DngOutputColor.SRGB, + UseCameraWhiteBalance = true, + DisableAutoBrightness = false + } + }; - return new Task(() => + try { - var settings = new MagickReadSettings + using (MagickImageCollection layers = new MagickImageCollection(Path.LocalPath, settings)) { - BackgroundColor = MagickColors.None, - Defines = new DngReadDefines + IMagickImage mi; + // Only flatten multi-layer gimp xcf files. + if (Path.LocalPath.ToLower().EndsWith(".xcf") && layers.Count > 1) { - OutputColor = DngOutputColor.SRGB, - UseCameraWhiteBalance = true, - DisableAutoBrightness = false + // Flatten crops layers to canvas + mi = layers.Flatten(MagickColor.FromRgba(0, 0, 0, 0)); } - }; - - try - { - using (MagickImageCollection layers = new MagickImageCollection(Path.LocalPath, settings)) + else { - IMagickImage mi; - // Only flatten multi-layer gimp xcf files. - if (Path.LocalPath.ToLower().EndsWith(".xcf") && layers.Count > 1) - { - // Flatten crops layers to canvas - mi = layers.Flatten(MagickColor.FromRgba(0, 0, 0, 0)); - } - else - { - mi = layers[0]; - } - if (SettingHelper.Get("UseColorProfile", false, "QuickLook.Plugin.ImageViewer")) + mi = layers[0]; + } + if (SettingHelper.Get("UseColorProfile", false, "QuickLook.Plugin.ImageViewer")) + { + if (mi.ColorSpace == ColorSpace.RGB || mi.ColorSpace == ColorSpace.sRGB || mi.ColorSpace == ColorSpace.scRGB) { - if (mi.ColorSpace == ColorSpace.RGB || mi.ColorSpace == ColorSpace.sRGB || mi.ColorSpace == ColorSpace.scRGB) - { - mi.SetProfile(ColorProfile.SRGB); - if (ContextObject.ColorProfileName != null) - mi.SetProfile(new ColorProfile(ContextObject.ColorProfileName)); // map to monitor color - } + mi.SetProfile(ColorProfile.SRGB); + if (ContextObject.ColorProfileName != null) + mi.SetProfile(new ColorProfile(ContextObject.ColorProfileName)); // map to monitor color } + } - mi.AutoOrient(); + mi.AutoOrient(); - if (mi.Width != (int)fullSize.Width || mi.Height != (int)fullSize.Height) - mi.Resize((uint)fullSize.Width, (uint)fullSize.Height); + if (mi.Width != (int)fullSize.Width || mi.Height != (int)fullSize.Height) + mi.Resize((uint)fullSize.Width, (uint)fullSize.Height); - mi.Density = new Density(DisplayDeviceHelper.DefaultDpi * DisplayDeviceHelper.GetCurrentScaleFactor().Horizontal, - DisplayDeviceHelper.DefaultDpi * DisplayDeviceHelper.GetCurrentScaleFactor().Vertical); + mi.Density = new Density(DisplayDeviceHelper.DefaultDpi * DisplayDeviceHelper.GetCurrentScaleFactor().Horizontal, + DisplayDeviceHelper.DefaultDpi * DisplayDeviceHelper.GetCurrentScaleFactor().Vertical); - var img = mi.ToBitmapSourceWithDensity(); + var img = mi.ToBitmapSourceWithDensity(); - img.Freeze(); - return img; - } + img.Freeze(); + return img; } - catch (Exception e) - { - ProcessHelper.WriteLog(e.ToString()); - return null!; - } - }); + } + catch (Exception e) + { + ProcessHelper.WriteLog(e.ToString()); + return null!; + } } public override void Dispose() { } - private static TransformedBitmap RotateAndScaleThumbnail(BitmapImage image, Orientation orientation, + protected static TransformedBitmap RotateAndScaleThumbnail(BitmapImage image, Orientation orientation, Size fullSize) { var swap = false; diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/WebPProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/WebPProvider.cs new file mode 100644 index 000000000..b8bc80ce9 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/Providers/WebPProvider.cs @@ -0,0 +1,228 @@ +// Copyright © 2024 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using ImageGlass.Base.Photoing.Codecs; +using ImageGlass.WebP; +using ImageMagick; +using ImageMagick.Formats; +using QuickLook.Common.Helpers; +using QuickLook.Common.Plugin; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace QuickLook.Plugin.ImageViewer.AnimatedImage.Providers; + +internal class WebPProvider : ImageMagickProvider +{ + private bool _isPlaying; + + public WebPProvider(Uri path, MetaProvider meta, ContextObject contextObject) : base(path, meta, contextObject) + { + } + + public override Task GetRenderedFrame(int index) + { + return new Task(() => + { + var settings = new MagickReadSettings + { + BackgroundColor = MagickColors.None, + Defines = new DngReadDefines + { + OutputColor = DngOutputColor.SRGB, + UseCameraWhiteBalance = true, + DisableAutoBrightness = false + } + }; + + try + { + // Unfortunately we only support Animated WebP on x64 platforms + if (Environment.Is64BitProcess) + { + var layers = MagickImageInfo.ReadCollection(Path.LocalPath); + int count = layers.Count(); + + // Animated WebP image + if (count > 1) + { + return AnimatedWebP(Path.LocalPath); + } + else + { + return base.GetRenderedFrame(); + } + } + + return base.GetRenderedFrame(); + } + catch (Exception e) + { + ProcessHelper.WriteLog(e.ToString()); + return null!; + } + }); + } + + public override void Dispose() + { + _isPlaying = false; + base.Dispose(); + } + + private BitmapSource AnimatedWebP(string fileName) + { + using var webp = new WebPWrapper(); + + var aniWebP = webp.AnimLoad(fileName); + var frames = aniWebP.Select(frame => + { + var duration = frame.Duration > 0 ? frame.Duration : 100; + var bitmap = frame.Bitmap; + + return new AnimatedImgFrame(frame.Bitmap, (uint)duration); + }); + + var animatedImg = new AnimatedImg(frames, frames.Count()); + + var writeableBitmap = Application.Current.Dispatcher.Invoke(() => + { + var frame = animatedImg.Frames.ElementAt(0); + var bitmap = (Bitmap)frame.Bitmap; + return bitmap.ToWriteableBitmap(); + }); + + _isPlaying = true; + _ = Task.Factory.StartNew(() => + { + while (_isPlaying) + { + foreach (var frame in animatedImg.Frames) + { + if (!_isPlaying) break; + + writeableBitmap.Dispatcher.Invoke(() => + { + var bitmap = (Bitmap)frame.Bitmap; + bitmap.CopyToWriteableBitmap(writeableBitmap); + }); + + Thread.Sleep((int)frame.Duration.TotalMilliseconds); + } + } + + animatedImg?.Dispose(); + animatedImg = null; + }, TaskCreationOptions.LongRunning); + + return writeableBitmap; + } +} + +file static class Extension +{ + public static WriteableBitmap ToWriteableBitmap(this Bitmap bitmap) + { + if (bitmap == null) throw new ArgumentNullException(nameof(bitmap)); + + var pixelFormat = bitmap.PixelFormat; + var width = bitmap.Width; + var height = bitmap.Height; + + var wpfPixelFormat = pixelFormat switch + { + System.Drawing.Imaging.PixelFormat.Format32bppArgb => PixelFormats.Bgra32, + System.Drawing.Imaging.PixelFormat.Format24bppRgb => PixelFormats.Bgr24, + _ => throw new NotSupportedException($"Unsupported PixelFormat: {pixelFormat}") + }; + + var writeableBitmap = new WriteableBitmap(width, height, 96, 96, wpfPixelFormat, null); + + var bitmapData = bitmap.LockBits( + new Rectangle(0, 0, width, height), + ImageLockMode.ReadOnly, + pixelFormat); + + try + { + writeableBitmap.Lock(); + unsafe + { + Buffer.MemoryCopy( + source: bitmapData.Scan0.ToPointer(), + destination: writeableBitmap.BackBuffer.ToPointer(), + destinationSizeInBytes: writeableBitmap.BackBufferStride * height, + sourceBytesToCopy: bitmapData.Stride * height); + } + + writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + } + finally + { + bitmap.UnlockBits(bitmapData); + writeableBitmap.Unlock(); + } + + return writeableBitmap; + } + + public static void CopyToWriteableBitmap(this Bitmap bitmap, WriteableBitmap writeableBitmap) + { + var pixelFormat = bitmap.PixelFormat; + var width = bitmap.Width; + var height = bitmap.Height; + + var wpfPixelFormat = pixelFormat switch + { + System.Drawing.Imaging.PixelFormat.Format32bppArgb => PixelFormats.Bgra32, + System.Drawing.Imaging.PixelFormat.Format24bppRgb => PixelFormats.Bgr24, + _ => throw new NotSupportedException($"Unsupported PixelFormat: {pixelFormat}") + }; + + var bitmapData = bitmap.LockBits( + new Rectangle(0, 0, width, height), + ImageLockMode.ReadOnly, + pixelFormat); + + try + { + writeableBitmap.Lock(); + unsafe + { + Buffer.MemoryCopy( + source: bitmapData.Scan0.ToPointer(), + destination: writeableBitmap.BackBuffer.ToPointer(), + destinationSizeInBytes: writeableBitmap.BackBufferStride * height, + sourceBytesToCopy: bitmapData.Stride * height); + } + + writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + } + finally + { + bitmap.UnlockBits(bitmapData); + writeableBitmap.Unlock(); + } + } +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs index bc7ee52f7..ecdfa7386 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs @@ -1,4 +1,4 @@ -// Copyright © 2018 Paddy Xu +// Copyright © 2024 QL-Win Contributors // // This file is part of QuickLook program. // @@ -76,6 +76,9 @@ public void Init() AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair([".icns"], typeof(IcnsProvider))); + AnimatedImage.AnimatedImage.Providers.Add( + new KeyValuePair([".webp"], + typeof(WebPProvider))); AnimatedImage.AnimatedImage.Providers.Add( new KeyValuePair(["*"], typeof(ImageMagickProvider))); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj index 2f9ab42cd..d4f14f577 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/QuickLook.Plugin.ImageViewer.csproj @@ -55,6 +55,7 @@ + all