diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs index 17b1c77..38a35d4 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/APNGAnimationProvider.cs @@ -1,4 +1,4 @@ -// Copyright © 2017 Paddy Xu +// Copyright © 2018 Paddy Xu // // This file is part of QuickLook program. // @@ -16,105 +16,180 @@ // along with this program. If not, see . using System; +using System.Collections.Generic; +using System.Linq; +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; using LibAPNG; +using QuickLook.Common.ExtensionMethods; namespace QuickLook.Plugin.ImageViewer.AnimatedImage { - internal class APNGAnimationProvider : IAnimationProvider + internal class APNGAnimationProvider : AnimationProvider { - public void GetAnimator(ObjectAnimationUsingKeyFrames animator, string path) + private readonly List _frames; + private readonly List _renderedFrames; + private ImageMagickProvider _imageMagickProvider; + private int _lastEffecitvePreviousPreviousFrameIndex; + + public APNGAnimationProvider(string path, Dispatcher uiDispatcher) : base(path, uiDispatcher) { var decoder = new APNGBitmap(path); if (decoder.IsSimplePNG) { - new ImageMagickProvider().GetAnimator(animator, path); + _imageMagickProvider = new ImageMagickProvider(path, uiDispatcher); return; } - var clock = TimeSpan.Zero; - var header = decoder.IHDRChunk; - Frame currentFrame = null; - BitmapSource currentRenderedFrame = null; - BitmapSource previousStateRenderedFrame = null; - foreach (var nextFrame in decoder.Frames) + _frames = new List(decoder.Frames.Length); + _renderedFrames = new List(decoder.Frames.Length); + Enumerable.Repeat(0, decoder.Frames.Length).ForEach(_ => _renderedFrames.Add(null)); + + Animator = new Int32AnimationUsingKeyFrames {RepeatBehavior = RepeatBehavior.Forever}; + + var wallclock = TimeSpan.Zero; + + for (var i = 0; i < decoder.Frames.Length; i++) { - var nextRenderedFrame = MakeNextFrame(header, nextFrame, currentFrame, currentRenderedFrame, - previousStateRenderedFrame); + var frame = decoder.Frames[i]; - var delay = TimeSpan.FromSeconds( - (double) nextFrame.fcTLChunk.DelayNum / - (nextFrame.fcTLChunk.DelayDen == 0 ? 100 : nextFrame.fcTLChunk.DelayDen)); + _frames.Add(new FrameInfo(decoder.IHDRChunk, frame)); - animator.KeyFrames.Add(new DiscreteObjectKeyFrame(nextRenderedFrame, clock)); - clock += delay; - - // the "previous state" of a "DisposeOpPrevious" frame is its previous frame, so we do not record it - if (currentFrame != null && currentFrame.fcTLChunk.DisposeOp != DisposeOps.APNGDisposeOpPrevious) - previousStateRenderedFrame = currentRenderedFrame; - currentRenderedFrame = nextRenderedFrame; - currentFrame = nextFrame; + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(i, KeyTime.FromTimeSpan(wallclock))); + wallclock += _frames[i].Delay; } - - animator.Duration = clock; - animator.RepeatBehavior = RepeatBehavior.Forever; } - private static BitmapSource MakeNextFrame(IHDRChunk header, Frame nextFrame, Frame currentFrame, - BitmapSource currentRenderedFrame, BitmapSource previousStateRenderedFrame) + public override Task GetRenderedFrame(int index) { - var fullRect = new Rect(0, 0, header.Width, header.Height); - var frameRect = new Rect(nextFrame.fcTLChunk.XOffset, nextFrame.fcTLChunk.YOffset, - nextFrame.fcTLChunk.Width, nextFrame.fcTLChunk.Height); + if (_imageMagickProvider != null) + return _imageMagickProvider.GetRenderedFrame(index); + + if (_renderedFrames[index] != null) + return new Task(() => _renderedFrames[index]); + + return new Task(() => + { + var rendered = Render(index); + _renderedFrames[index] = rendered; + + return rendered; + }); + } + + public override void Dispose() + { + if (_imageMagickProvider != null) + { + _imageMagickProvider.Dispose(); + _imageMagickProvider = null; + return; + } + + _frames.Clear(); + _renderedFrames.Clear(); + } + + private BitmapSource Render(int index) + { + var currentFrame = _frames[index]; + FrameInfo previousFrame = null; + BitmapSource previousRendered = null; + BitmapSource previousPreviousRendered = null; + + if (index > 0) + { + if (_renderedFrames[index - 1] == null) + _renderedFrames[index - 1] = Render(index - 1); + + previousFrame = _frames[index - 1]; + previousRendered = _renderedFrames[index - 1]; + } + + // when saying APNGDisposeOpPrevious, we need to find the last frame not having APNGDisposeOpPrevious. + // Only [index-2] is not correct here since that frame may also have APNGDisposeOpPrevious. + if (index > 1) + previousPreviousRendered = _renderedFrames[_lastEffecitvePreviousPreviousFrameIndex]; + if (_frames[index].DisposeOp != DisposeOps.APNGDisposeOpPrevious) + _lastEffecitvePreviousPreviousFrameIndex = Math.Max(_lastEffecitvePreviousPreviousFrameIndex, index); - var fs = nextFrame.GetBitmapSource(); var visual = new DrawingVisual(); using (var context = visual.RenderOpen()) { // protect region - if (nextFrame.fcTLChunk.BlendOp == BlendOps.APNGBlendOpSource) + if (currentFrame.BlendOp == BlendOps.APNGBlendOpSource) { var freeRegion = new CombinedGeometry(GeometryCombineMode.Xor, - new RectangleGeometry(fullRect), - new RectangleGeometry(frameRect)); + new RectangleGeometry(currentFrame.FrameRect), + new RectangleGeometry(currentFrame.FrameRect)); context.PushOpacityMask( new DrawingBrush(new GeometryDrawing(Brushes.Transparent, null, freeRegion))); } - if (currentFrame != null && currentRenderedFrame != null) - switch (currentFrame.fcTLChunk.DisposeOp) + if (previousFrame != null) + switch (previousFrame.DisposeOp) { case DisposeOps.APNGDisposeOpNone: - // restore currentRenderedFrame - if (currentRenderedFrame != null) context.DrawImage(currentRenderedFrame, fullRect); + if (previousRendered != null) + context.DrawImage(previousRendered, currentFrame.FullRect); break; case DisposeOps.APNGDisposeOpPrevious: - // restore previousStateRenderedFrame - if (previousStateRenderedFrame != null) - context.DrawImage(previousStateRenderedFrame, fullRect); + if (previousPreviousRendered != null) + context.DrawImage(previousPreviousRendered, currentFrame.FullRect); break; case DisposeOps.APNGDisposeOpBackground: // do nothing break; } - // unprotect region and draw the next frame - if (nextFrame.fcTLChunk.BlendOp == BlendOps.APNGBlendOpSource) + // unprotect region and draw current frame + if (currentFrame.BlendOp == BlendOps.APNGBlendOpSource) context.Pop(); - context.DrawImage(fs, frameRect); + context.DrawImage(currentFrame.Pixels, currentFrame.FrameRect); } var bitmap = new RenderTargetBitmap( - header.Width, header.Height, - Math.Floor(fs.DpiX), Math.Floor(fs.DpiY), + (int) currentFrame.FullRect.Width, (int) currentFrame.FullRect.Height, + Math.Floor(currentFrame.Pixels.DpiX), Math.Floor(currentFrame.Pixels.DpiY), PixelFormats.Pbgra32); bitmap.Render(visual); + + bitmap.Freeze(); return bitmap; } + + private class FrameInfo + { + public readonly BlendOps BlendOp; + public readonly TimeSpan Delay; + public readonly DisposeOps DisposeOp; + public readonly Rect FrameRect; + public readonly Rect FullRect; + public readonly BitmapSource Pixels; + + public FrameInfo(IHDRChunk header, Frame frame) + { + FullRect = new Rect(0, 0, header.Width, header.Height); + FrameRect = new Rect(frame.fcTLChunk.XOffset, frame.fcTLChunk.YOffset, + frame.fcTLChunk.Width, frame.fcTLChunk.Height); + + BlendOp = frame.fcTLChunk.BlendOp; + DisposeOp = frame.fcTLChunk.DisposeOp; + + Pixels = frame.GetBitmapSource(); + Pixels.Freeze(); + + Delay = TimeSpan.FromSeconds((double) frame.fcTLChunk.DelayNum / + (frame.fcTLChunk.DelayDen == 0 + ? 100 + : frame.fcTLChunk.DelayDen)); + } + } } } \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs index 8a107ed..ab785a1 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimatedImage.cs @@ -27,9 +27,12 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage public class AnimatedImage : Image, IDisposable { private AnimationProvider _animation; + private bool _disposing; public void Dispose() { + _disposing = true; + BeginAnimation(AnimationFrameIndexProperty, null); Source = null; @@ -37,15 +40,6 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage _animation = null; } - private static void LoadFullImage(DependencyObject obj, DependencyPropertyChangedEventArgs ev) - { - if (!(obj is AnimatedImage instance)) - return; - - instance._animation = LoadFullImageCore((Uri) ev.NewValue, instance.Dispatcher); - instance.BeginAnimation(AnimationFrameIndexProperty, instance._animation.Animator); - } - private static AnimationProvider LoadFullImageCore(Uri path, Dispatcher uiDispatcher) { byte[] sign; @@ -55,14 +49,14 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage sign = reader.BaseStream.Length < 4 ? new byte[] {0, 0, 0, 0} : reader.ReadBytes(4); } - AnimationProvider provider = null; + AnimationProvider provider; if (sign[0] == 'G' && sign[1] == 'I' && sign[2] == 'F' && sign[3] == '8') provider = new GifAnimationProvider(path.LocalPath, uiDispatcher); - //else if (sign[0] == 0x89 && sign[1] == 'P' && sign[2] == 'N' && sign[3] == 'G') - // provider = new APNGAnimationProvider(); - //else - // provider = new ImageMagickProvider(); + else if (sign[0] == 0x89 && sign[1] == 'P' && sign[2] == 'N' && sign[3] == 'G') + provider = new APNGAnimationProvider(path.LocalPath, uiDispatcher); + else + provider = new ImageMagickProvider(path.LocalPath, uiDispatcher); return provider; } @@ -106,8 +100,9 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage //var thumbnail = instance.Meta?.GetThumbnail(true); //instance.Source = thumbnail; - LoadFullImage(obj, ev); + instance._animation = LoadFullImageCore((Uri) ev.NewValue, instance.Dispatcher); + instance.BeginAnimation(AnimationFrameIndexProperty, instance._animation.Animator); instance.AnimationFrameIndex = 0; } @@ -116,9 +111,25 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage if (!(obj is AnimatedImage instance)) return; - var image = instance._animation.GetRenderedFrame((int) ev.NewValue); - //if (!ReferenceEquals(instance.Source, image)) - instance.Source = image; + 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. + { + 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 dee9104..d27dd2f 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/AnimationProvider.cs @@ -16,8 +16,9 @@ // along with this program. If not, see . using System; -using System.Windows.Media; +using System.Threading.Tasks; using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; using System.Windows.Threading; namespace QuickLook.Plugin.ImageViewer.AnimatedImage @@ -34,10 +35,10 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage public string Path { get; } - public Int32Animation Animator { get; protected set; } + public Int32AnimationUsingKeyFrames Animator { get; protected set; } public abstract void Dispose(); - public abstract ImageSource GetRenderedFrame(int index); + 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 c8f40d9..0bf79c4 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/GifAnimationProvider.cs @@ -17,8 +17,7 @@ using System; using System.Drawing; -using System.Windows; -using System.Windows.Media; +using System.Threading.Tasks; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Threading; @@ -37,10 +36,11 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage _frame = (Bitmap) Image.FromFile(path); _frameSource = _frame.ToBitmapSource(); - Animator = new Int32Animation(0, 1, new Duration(TimeSpan.FromMilliseconds(50))) - { - RepeatBehavior = RepeatBehavior.Forever - }; + 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(10)))); + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(2, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(20)))); } public override void Dispose() @@ -55,7 +55,7 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage _frameSource = null; } - public override ImageSource GetRenderedFrame(int index) + public override Task GetRenderedFrame(int index) { if (!_isPlaying) { @@ -63,7 +63,7 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage ImageAnimator.Animate(_frame, OnFrameChanged); } - return _frameSource; + return new Task(() => _frameSource); } private void OnFrameChanged(object sender, EventArgs e) @@ -72,4 +72,4 @@ namespace QuickLook.Plugin.ImageViewer.AnimatedImage _frameSource = _frame.ToBitmapSource(); } } -} +} \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs index 85ff3a0..fb21677 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/AnimatedImage/ImageMagickProvider.cs @@ -1,4 +1,4 @@ -// Copyright © 2017 Paddy Xu +// Copyright © 2018 Paddy Xu // // This file is part of QuickLook program. // @@ -16,25 +16,56 @@ // along with this program. If not, see . using System; -using System.Windows; +using System.Threading.Tasks; using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; +using System.Windows.Threading; using ImageMagick; +using QuickLook.Plugin.ImageViewer.Exiv2; namespace QuickLook.Plugin.ImageViewer.AnimatedImage { - internal class ImageMagickProvider : IAnimationProvider + internal class ImageMagickProvider : AnimationProvider { - public void GetAnimator(ObjectAnimationUsingKeyFrames animator, string path) - { - using (var image = new MagickImage(path)) - { - image.AddProfile(ColorProfile.SRGB); - image.Density = new Density(Math.Floor(image.Density.X), Math.Floor(image.Density.Y)); - image.AutoOrient(); + private readonly string _path; + private readonly BitmapSource _thumbnail; - animator.KeyFrames.Add(new DiscreteObjectKeyFrame(image.ToBitmapSource(), TimeSpan.Zero)); - animator.Duration = Duration.Forever; - } + public ImageMagickProvider(string path, Dispatcher uiDispatcher) : base(path, uiDispatcher) + { + _path = path; + _thumbnail = new Meta(path).GetThumbnail(true); + + Animator = new Int32AnimationUsingKeyFrames(); + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(0, + KeyTime.FromTimeSpan(TimeSpan.Zero))); // thumbnail/full image + + if (_thumbnail != null) + Animator.KeyFrames.Add(new DiscreteInt32KeyFrame(1, + KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.20)))); // full image + } + + public override Task GetRenderedFrame(int index) + { + // the first image is always returns synchronously. + if (index == 0 && _thumbnail != null) return new Task(() => _thumbnail); + + return new Task(() => + { + using (var image = new MagickImage(_path)) + { + image.AddProfile(ColorProfile.SRGB); + image.Density = new Density(Math.Floor(image.Density.X), Math.Floor(image.Density.Y)); + image.AutoOrient(); + + var bs = image.ToBitmapSource(); + bs.Freeze(); + return bs; + } + }); + } + + public override void Dispose() + { } } } \ No newline at end of file diff --git a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml index 5f79c01..9bcece0 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml +++ b/QuickLook.Plugin/QuickLook.Plugin.ImageViewer/ImagePanel.xaml @@ -24,23 +24,25 @@