diff --git a/QuickLook.Common/ExtensionMethods/TypeExtensions.cs b/QuickLook.Common/ExtensionMethods/TypeExtensions.cs index f1f07eb..21d9c39 100644 --- a/QuickLook.Common/ExtensionMethods/TypeExtensions.cs +++ b/QuickLook.Common/ExtensionMethods/TypeExtensions.cs @@ -21,9 +21,9 @@ namespace QuickLook.Common.ExtensionMethods { public static class TypeExtensions { - public static T CreateInstance(this Type t) + public static T CreateInstance(this Type t, params object[] paramArray) { - return (T) Activator.CreateInstance(t); + return (T)Activator.CreateInstance(t, paramArray); } } } \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs index 8ed97cb..4be0da4 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs @@ -33,17 +33,16 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage { private readonly List _frames; private readonly List _renderedFrames; - private ImageMagickProvider _imageMagickProvider; + private NETImageProvider _imageMagickProvider; private int _lastEffecitvePreviousPreviousFrameIndex; - public APNGAnimationProvider(string path, NConvert meta, Dispatcher uiDispatcher) : base(path, meta, - uiDispatcher) + public APNGAnimationProvider(string path) : base(path) { var decoder = new APNGBitmap(path); if (decoder.IsSimplePNG) { - _imageMagickProvider = new ImageMagickProvider(path, meta, uiDispatcher); + _imageMagickProvider = new NETImageProvider(path); return; } @@ -66,6 +65,11 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage } } + public override Task GetThumbnail(Size size, Size fullSize) + { + throw new NotImplementedException(); + } + public override Task GetRenderedFrame(int index) { if (_imageMagickProvider != null) diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs index 5dfb834..81198f5 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs @@ -15,8 +15,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using QuickLook.Common.Annotations; +using QuickLook.Common.ExtensionMethods; +using QuickLook.Common.Plugin; using System; +using System.Collections.Generic; +using System.ComponentModel; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; @@ -25,9 +32,15 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage { public class AnimatedImage : Image, IDisposable { + // List> + public static List> Providers = new List>(); + private AnimationProvider _animation; private bool _disposing; + public event EventHandler ImageLoaded; + public event EventHandler DoZoomToFit; + public void Dispose() { _disposing = true; @@ -39,23 +52,12 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage _animation = null; } - private static AnimationProvider LoadFullImageCore(Uri path, NConvert meta, Dispatcher uiDispatcher) + private static AnimationProvider LoadFullImageCore(Uri path) { - byte[] sign; - using (var reader = - new BinaryReader(new FileStream(path.LocalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - sign = reader.BaseStream.Length < 4 ? new byte[] {0, 0, 0, 0} : reader.ReadBytes(4); - } + var ext = Path.GetExtension(path.LocalPath).ToLower(); + var type = Providers.First(p => p.Key.Contains(ext) || p.Key.Contains("*")).Value; - AnimationProvider provider; - - if (sign[0] == 'G' && sign[1] == 'I' && sign[2] == 'F' && sign[3] == '8') - provider = new GifAnimationProvider(path.LocalPath, meta, uiDispatcher); - else if (sign[0] == 0x89 && sign[1] == 'P' && sign[2] == 'N' && sign[3] == 'G') - provider = new APNGAnimationProvider(path.LocalPath, meta, uiDispatcher); - else - provider = new ImageMagickProvider(path.LocalPath, meta, uiDispatcher); + var provider = type.CreateInstance(path.LocalPath); return provider; } @@ -64,7 +66,7 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage public static readonly DependencyProperty AnimationFrameIndexProperty = DependencyProperty.Register("AnimationFrameIndex", typeof(int), typeof(AnimatedImage), - new UIPropertyMetadata(-1, AnimationFrameIndexChanged)); + new UIPropertyMetadata(-2, AnimationFrameIndexChanged)); public static readonly DependencyProperty AnimationUriProperty = DependencyProperty.Register("AnimationUri", typeof(Uri), typeof(AnimatedImage), @@ -73,24 +75,33 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage public static readonly DependencyProperty MetaProperty = DependencyProperty.Register("Meta", typeof(NConvert), typeof(AnimatedImage)); + public static readonly DependencyProperty ContextObjectProperty = + DependencyProperty.Register("ContextObject", typeof(ContextObject), typeof(AnimatedImage)); + public int AnimationFrameIndex { - get => (int) GetValue(AnimationFrameIndexProperty); + get => (int)GetValue(AnimationFrameIndexProperty); set => SetValue(AnimationFrameIndexProperty, value); } public Uri AnimationUri { - get => (Uri) GetValue(AnimationUriProperty); + get => (Uri)GetValue(AnimationUriProperty); set => SetValue(AnimationUriProperty, value); } public NConvert Meta { - private get => (NConvert) GetValue(MetaProperty); + private get => (NConvert)GetValue(MetaProperty); set => SetValue(MetaProperty, value); } + public ContextObject ContextObject + { + private get => (ContextObject)GetValue(ContextObjectProperty); + set => SetValue(ContextObjectProperty, value); + } + private static void AnimationUriChanged(DependencyObject obj, DependencyPropertyChangedEventArgs ev) { if (!(obj is AnimatedImage instance)) @@ -99,10 +110,10 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage //var thumbnail = instance.Meta?.GetThumbnail(true); //instance.Source = thumbnail; - instance._animation = LoadFullImageCore((Uri) ev.NewValue, instance.Meta, instance.Dispatcher); + instance._animation = LoadFullImageCore((Uri)ev.NewValue); instance.BeginAnimation(AnimationFrameIndexProperty, instance._animation.Animator); - instance.AnimationFrameIndex = 0; + instance.AnimationFrameIndex = -1; } private static void AnimationFrameIndexChanged(DependencyObject obj, DependencyPropertyChangedEventArgs ev) @@ -113,22 +124,32 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage if (instance._disposing) return; - var task = instance._animation.GetRenderedFrame((int) ev.NewValue); - if (instance.Source == null && (int) ev.NewValue == 0) // this is the first image. Run it synchronously. + if ((int)ev.NewValue == -1) // get thumbnail { + var task = instance._animation.GetThumbnail(instance.ContextObject.PreferredSize, instance.Meta.GetSize()); + + if (task != null) + { + task.ContinueWith(_ => instance.Dispatcher.Invoke(() => + { + instance.Source = _.Result; + instance.DoZoomToFit?.Invoke(instance, new EventArgs()); + instance.ImageLoaded?.Invoke(instance, new EventArgs()); + })); + task.Start(); + } + } + else // begin to loop in the animator + { + var task = instance._animation.GetRenderedFrame((int)ev.NewValue); + + task.ContinueWith(_ => instance.Dispatcher.Invoke(() => + { + instance.Source = _.Result; + })); task.Start(); - task.Wait(5000); } - - if (task.IsCompleted) - { - instance.Source = task.Result; - return; - } - - task.ContinueWith(t => { instance.Dispatcher.Invoke(() => instance.Source = t.Result); }); - task.Start(); } #endregion DependencyProperty diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs index 4154e14..62c7423 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs @@ -17,6 +17,7 @@ using System; using System.Threading.Tasks; +using System.Windows; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Threading; @@ -25,23 +26,19 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage { internal abstract class AnimationProvider : IDisposable { - protected AnimationProvider(string path, NConvert meta, Dispatcher uiDispatcher) + protected AnimationProvider(string path) { Path = path; - Meta = meta; - Dispatcher = uiDispatcher; } - public Dispatcher Dispatcher { get; } - public string Path { get; } - public NConvert Meta { get; } - public Int32AnimationUsingKeyFrames Animator { get; protected set; } public abstract void Dispose(); + public abstract Task GetThumbnail(Size size, Size fullSize); + public abstract Task GetRenderedFrame(int index); } } \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs index f0c1c89..065dc1e 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs @@ -18,6 +18,7 @@ using System; using System.Drawing; using System.Threading.Tasks; +using System.Windows; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Threading; @@ -27,50 +28,58 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage { internal class GifAnimationProvider : AnimationProvider { - private Bitmap _frame; - private BitmapSource _frameSource; + private Bitmap _fileHandle; + private BitmapSource _frame; private bool _isPlaying; - public GifAnimationProvider(string path, NConvert meta, Dispatcher uiDispatcher) : base(path, meta, - uiDispatcher) + public GifAnimationProvider(string path) : base(path) { - _frame = (Bitmap) Image.FromFile(path); - _frameSource = _frame.ToBitmapSource(); - - Animator = new Int32AnimationUsingKeyFrames {RepeatBehavior = RepeatBehavior.Forever}; + _fileHandle = (Bitmap)Image.FromFile(path); + Animator = new Int32AnimationUsingKeyFrames { RepeatBehavior = RepeatBehavior.Forever }; Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)))); - Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(50)))); - Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(2, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(100)))); + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(10)))); + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(2, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(20)))); } public override void Dispose() { - if (_frame == null) + if (_fileHandle == null) return; - ImageAnimator.StopAnimate(_frame, OnFrameChanged); - _frame.Dispose(); + ImageAnimator.StopAnimate(_fileHandle, OnFrameChanged); + _fileHandle.Dispose(); + _fileHandle = null; _frame = null; - _frameSource = null; + } + + public override Task GetThumbnail(System.Windows.Size size, System.Windows.Size fullSize) + { + return new Task(() => + { + return _fileHandle.ToBitmapSource(); + }); } public override Task GetRenderedFrame(int index) { - if (!_isPlaying) + return new Task(() => { - _isPlaying = true; - ImageAnimator.Animate(_frame, OnFrameChanged); - } + if (!_isPlaying) + { + _isPlaying = true; + ImageAnimator.Animate(_fileHandle, OnFrameChanged); + } - return new Task(() => _frameSource); + return _frame; + }); } private void OnFrameChanged(object sender, EventArgs e) { ImageAnimator.UpdateFrames(); - _frameSource = _frame.ToBitmapSource(); + _frame = _fileHandle.ToBitmapSource(); } } } diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs index cbfe3fa..3defb6e 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs @@ -17,36 +17,55 @@ using System; using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Threading; namespace QuickLook.Plugin.ImageViewer.AnimatedImage { - internal class ImageMagickProvider : AnimationProvider + internal class NETImageProvider : AnimationProvider { - public ImageMagickProvider(string path, NConvert meta, Dispatcher uiDispatcher) : base(path, meta, uiDispatcher) + public NETImageProvider(string path) : base(path) { Animator = new Int32AnimationUsingKeyFrames(); Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(0, - KeyTime.FromTimeSpan(TimeSpan.Zero))); // thumbnail/full image + KeyTime.FromTimeSpan(TimeSpan.Zero))); + } + + public override Task GetThumbnail(Size size, Size fullSize) + { + var factor = Math.Min(size.Width / 2 / fullSize.Width, size.Height / 2 / fullSize.Height); + var decode_width = fullSize.Width * factor; + + return new Task(() => + { + var img = new BitmapImage(); + img.BeginInit(); + img.UriSource = new Uri(Path); + img.CacheOption = BitmapCacheOption.OnLoad; + img.DecodePixelWidth = (int)Math.Floor(decode_width); + img.EndInit(); + + var scaled = new TransformedBitmap(img, new ScaleTransform(1d / factor, 1d / factor)); + scaled.Freeze(); + return scaled; + }); } public override Task GetRenderedFrame(int index) { return new Task(() => { - using (var ms = Meta.GetPngStream()) - { - var img = new BitmapImage(); - img.BeginInit(); - img.StreamSource = ms; - img.CacheOption = BitmapCacheOption.OnLoad; - img.EndInit(); - img.Freeze(); + var img = new BitmapImage(); + img.BeginInit(); + img.UriSource = new Uri(Path); + img.CacheOption = BitmapCacheOption.OnLoad; + img.EndInit(); - return img; - } + img.Freeze(); + return img; }); } diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml index 9bcece0..babe3b9 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml @@ -54,6 +54,7 @@ VerticalScrollBarVisibility="Auto" Focusable="False" IsManipulationEnabled="True"> diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml.cs index 86fbffa..ff27a20 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml.cs @@ -42,7 +42,8 @@ namespace QuickLook.Plugin.ImageViewer /// public partial class ImagePanel : UserControl, INotifyPropertyChanged, IDisposable { - private readonly ContextObject _context; + private ContextObject _contextObject; + private Visibility _backgroundVisibility = Visibility.Visible; private Point? _dragInitPos; private Uri _imageSource; @@ -74,6 +75,8 @@ namespace QuickLook.Plugin.ImageViewer buttonBackgroundColour.Click += OnBackgroundColourOnClick; SizeChanged += ImagePanel_SizeChanged; + viewPanelImage.DoZoomToFit += (sender, e) => DoZoomToFit(); + viewPanelImage.ImageLoaded += (sender, e) => ContextObject.IsBusy = false; viewPanel.PreviewMouseWheel += ViewPanel_PreviewMouseWheel; viewPanel.MouseLeftButtonDown += ViewPanel_MouseLeftButtonDown; @@ -86,11 +89,11 @@ namespace QuickLook.Plugin.ImageViewer internal ImagePanel(ContextObject context, NConvert meta) : this() { - _context = context; + ContextObject = context; Meta = meta; ShowMeta(); - Theme = _context.Theme; + Theme = ContextObject.Theme; } public bool ShowZoomLevelInfo @@ -106,10 +109,10 @@ namespace QuickLook.Plugin.ImageViewer public Themes Theme { - get => _context?.Theme ?? Themes.Dark; + get => ContextObject?.Theme ?? Themes.Dark; set { - _context.Theme = value; + ContextObject.Theme = value; OnPropertyChanged(); } } @@ -199,7 +202,7 @@ namespace QuickLook.Plugin.ImageViewer } if (ShowZoomLevelInfo) - ((Storyboard) zoomLevelInfo.FindResource("StoryboardShowZoomLevelInfo")).Begin(); + ((Storyboard)zoomLevelInfo.FindResource("StoryboardShowZoomLevelInfo")).Begin(); } } @@ -209,6 +212,7 @@ namespace QuickLook.Plugin.ImageViewer set { _imageSource = value; + OnPropertyChanged(); } } @@ -226,6 +230,16 @@ namespace QuickLook.Plugin.ImageViewer } } + public ContextObject ContextObject + { + get => _contextObject; + set + { + _contextObject = value; + OnPropertyChanged(); + } + } + public NConvert Meta { get => _meta; @@ -249,7 +263,7 @@ namespace QuickLook.Plugin.ImageViewer { Theme = Theme == Themes.Dark ? Themes.Light : Themes.Dark; - SettingHelper.Set("LastTheme", (int) Theme); + SettingHelper.Set("LastTheme", (int)Theme); } private void ShowMeta() @@ -264,7 +278,7 @@ namespace QuickLook.Plugin.ImageViewer || m.Item1 == "Thumbnail" || m.Item1 == "Exif comment") return; - textMeta.Inlines.Add(new Run(m.Item1) {FontWeight = FontWeights.SemiBold}); + textMeta.Inlines.Add(new Run(m.Item1) { FontWeight = FontWeights.SemiBold }); textMeta.Inlines.Add(": "); textMeta.Inlines.Add(m.Item2); textMeta.Inlines.Add("\r\n"); @@ -390,7 +404,7 @@ namespace QuickLook.Plugin.ImageViewer private void UpdateZoomToFitFactor() { - if (viewPanelImage.Source == null) + if (viewPanelImage?.Source == null) { ZoomToFitFactor = 1d; return; @@ -410,7 +424,7 @@ namespace QuickLook.Plugin.ImageViewer public void Zoom(double factor, bool suppressEvent = false, bool isToFit = false) { - if (viewPanelImage.Source == null) + if (viewPanelImage?.Source == null) return; // pause when fit width diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/NConvert.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/NConvert.cs index 45553b5..1c2e8c5 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/NConvert.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/NConvert.cs @@ -59,13 +59,13 @@ namespace QuickLook.Plugin.ImageViewer return _metaExif; } - public MemoryStream GetPngStream() + public MemoryStream GetPngStream(bool thumbnail) { var temp = Path.GetTempFileName(); File.Delete(temp); - // - var d = RunInternal($"-quiet -embedded_jpeg -out png -o \"{temp}\" \"{_path}\"", 10000); + var thumb = thumbnail ? "-embedded_jpeg" : ""; + var d = RunInternal($"-quiet {thumb} -out tiff -o \"{temp}\" \"{_path}\"", 10000); var ms = new MemoryStream(File.ReadAllBytes(temp)); diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs index a600e80..8d72dbf 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/Plugin.cs @@ -19,8 +19,10 @@ using System; using System.IO; using System.Linq; using System.Windows; +using System.Collections.Generic; using QuickLook.Common.Helpers; using QuickLook.Common.Plugin; +using QuickLook.Plugin.ImageViewer.AnimatedImage; namespace QuickLook.Plugin.ImageViewer { @@ -45,6 +47,9 @@ namespace QuickLook.Plugin.ImageViewer public void Init() { + AnimatedImage.AnimatedImage.Providers.Add(new KeyValuePair(new[] { ".apng", ".png" }, typeof(APNGAnimationProvider))); + AnimatedImage.AnimatedImage.Providers.Add(new KeyValuePair(new[] { ".gif" }, typeof(GifAnimationProvider))); + AnimatedImage.AnimatedImage.Providers.Add(new KeyValuePair(new[] { "*" }, typeof(NETImageProvider))); } public bool CanHandle(string path) @@ -76,9 +81,7 @@ namespace QuickLook.Plugin.ImageViewer ? $"{Path.GetFileName(path)}" : $"{Path.GetFileName(path)} ({size.Width}×{size.Height})"; - LoadImage(_ip, path); - - context.IsBusy = false; + _ip.ImageUriSource = new Uri(path); } public void Cleanup() @@ -86,10 +89,5 @@ namespace QuickLook.Plugin.ImageViewer _ip?.Dispose(); _ip = null; } - - private void LoadImage(ImagePanel ui, string path) - { - ui.ImageUriSource = new Uri(path); - } } } \ No newline at end of file diff --git a/QuickLook/ViewerWindow.xaml b/QuickLook/ViewerWindow.xaml index 9616b48..1819ba7 100644 --- a/QuickLook/ViewerWindow.xaml +++ b/QuickLook/ViewerWindow.xaml @@ -82,9 +82,6 @@ GlassVisibility="{Binding ContextObject.TitlebarBlurVisibility, ElementName=mainWindow, Converter={StaticResource BooleanToVisibilityConverter}}" NoiseVisibility="Visible" /> - - -