diff --git a/Source/Components/ImageGlass.ImageBox/DefaultGifAnimator.cs b/Source/Components/ImageGlass.ImageBox/DefaultGifAnimator.cs new file mode 100755 index 000000000..259226752 --- /dev/null +++ b/Source/Components/ImageGlass.ImageBox/DefaultGifAnimator.cs @@ -0,0 +1,25 @@ +using System; +using System.Drawing; + +namespace ImageGlass { + /// + /// This is a wrapper for the original System.Drawing animator. See . + /// + public class DefaultGifAnimator : GifAnimator { + public void UpdateFrames(Image image) { + ImageAnimator.UpdateFrames(image); + } + + public void StopAnimate(Image image, EventHandler eventHandler) { + ImageAnimator.StopAnimate(image, eventHandler); + } + + public void Animate(Image image, EventHandler eventHandler) { + ImageAnimator.Animate(image, eventHandler); + } + + public bool CanAnimate(Image image) { + return ImageAnimator.CanAnimate(image); + } + } +} \ No newline at end of file diff --git a/Source/Components/ImageGlass.ImageBox/GifAnimator.cs b/Source/Components/ImageGlass.ImageBox/GifAnimator.cs new file mode 100755 index 000000000..9d258c0cb --- /dev/null +++ b/Source/Components/ImageGlass.ImageBox/GifAnimator.cs @@ -0,0 +1,31 @@ +using System; +using System.Drawing; + +namespace ImageGlass { + /// + /// Used to animate gifs. + /// + public interface GifAnimator + { + /// + /// Updates the time frame for this image. + /// + void UpdateFrames(Image image); + + /// + /// Stops updating frames for the given image. + /// + void StopAnimate(Image image, EventHandler eventHandler); + + /// + /// Animates the given image. + /// + void Animate(Image image, EventHandler onFrameChangedHandler); + + /// + /// Determines whether an image can be animated. + /// + /// true if the given image can be animated, otherwise false. + bool CanAnimate(Image image); + } +} diff --git a/Source/Components/ImageGlass.ImageBox/HighResolutionGifAnimator.cs b/Source/Components/ImageGlass.ImageBox/HighResolutionGifAnimator.cs new file mode 100755 index 000000000..3c59e1c4d --- /dev/null +++ b/Source/Components/ImageGlass.ImageBox/HighResolutionGifAnimator.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Concurrent; +using System.Drawing; +using System.Drawing.Imaging; +using System.Threading; + +namespace ImageGlass { + + /// + ///

Implements GifAnimator with the potential to offer a timer resolution of 10ms, + /// the fastest a GIF can animate.

+ ///

Each animated image is given its own thread + /// which is torn down with a corresponding call to StopAnimate or when the spawning + /// process dies. The default resolution is 20ms, as windows timers are by default limited + /// to a resolution of 15ms. Call setTickInMilliseconds to ask for a different rate, which + /// sets the fastest tick allowed for all HighResolutionAnimators.

+ ///
+ public class HighResolutionGifAnimator : GifAnimator { + #region STATIC + private static int ourMinTickTimeInMilliseconds; + private static readonly ConcurrentDictionary ourImageState; + + /// + /// Sets the tick for the animation thread. The thread may use a lower tick to ensure + /// the passed value is divisible by 10 (the gif format operates in units of 10 ms). + /// + /// Ideally should be a multiple of 10. + /// The actual tick value that will be used + public static int setTickTimeInMilliseconds(int value) { + // 10 is the minimum value, as a GIF's lowest tick rate is 10ms + // + int newTickValue = Math.Max(10, (value / 10) * 10); + ourMinTickTimeInMilliseconds = newTickValue; + return newTickValue; + } + + public static int getTickTimeInMilliseconds() { + return ourMinTickTimeInMilliseconds; + } + + /// + /// Given a delay amount, return either the minimum tick or delay, whichever is greater. + /// + /// the time to sleep during a tick in milliseconds + private static int getSleepAmountInMilliseconds(int delayInMilliseconds) { + return Math.Max(ourMinTickTimeInMilliseconds, delayInMilliseconds); + } + + static HighResolutionGifAnimator() { + ourMinTickTimeInMilliseconds = 20; + ourImageState = new ConcurrentDictionary(); + } + #endregion + + public void Animate(Image image, EventHandler onFrameChangedHandler) { + + if (!CanAnimate(image)) + return; + + if (ourImageState.ContainsKey(image)) + return; + + // AddOrUpdate has a race condition that could allow us to erroneously + // create multiple animation threads per image. To combat that + // we manually try to add entries ourself, and if it fails we + // kill the thread. + // + GifImageData toAdd = addFactory(image, onFrameChangedHandler); + if (!ourImageState.TryAdd(image, toAdd)) + Interlocked.Exchange(ref toAdd.myIsThreadDead, 1); + } + + private GifImageData addFactory(Image image, EventHandler eventHandler) { + GifImageData data; + lock (image) { + data = new GifImageData(image, eventHandler); + } + + Thread heartbeat = new Thread(() => { + int sleepTime = getSleepAmountInMilliseconds(data.getCurrentDelayInMilliseconds()); + Thread.Sleep(sleepTime); + while (data.threadIsNotDead()) { + data.handleUpdateTick(); + sleepTime = getSleepAmountInMilliseconds(data.getCurrentDelayInMilliseconds()); + Thread.Sleep(sleepTime); + } + }); + heartbeat.IsBackground = true; + heartbeat.Name = "heartbeat - HighResolutionAnimator"; + heartbeat.Start(); + return data; + } + + public void UpdateFrames(Image image) { + if (image == null) + return; + + GifImageData outData; + if (!ourImageState.TryGetValue(image, out outData)) + return; + + if (!outData.myIsDirty) + return; + + lock (image) { + outData.updateFrame(); + } + } + + public void StopAnimate(Image image, EventHandler eventHandler) { + if (image == null) + return; + + GifImageData outData; + if (ourImageState.TryRemove(image, out outData)) + Interlocked.Exchange(ref outData.myIsThreadDead, 1); + } + + // See if we have more than one frame in the time dimension. + // + public bool CanAnimate(Image image) { + if (image == null) + return false; + + lock (image) { + return imageHasTimeFrames(image); + } + } + + // image lock should be held + // + private bool imageHasTimeFrames(Image image) { + foreach (Guid guid in image.FrameDimensionsList) { + FrameDimension dimension = new FrameDimension(guid); + if (dimension.Equals(FrameDimension.Time)) + return image.GetFrameCount(FrameDimension.Time) > 1; + } + + return false; + } + + private class GifImageData { + private static readonly int FrameDelayTag = 0x5100; + + // image is used for identification in map + // + public int myIsThreadDead; + + private readonly Image myImage; + private readonly EventHandler myOnFrameChangedHandler; + private readonly int myNumFrames; + private readonly int[] myFrameDelaysInCentiseconds; + public bool myIsDirty; + private int myCurrentFrame; + + // image should be locked by caller + // + public GifImageData(Image image, EventHandler onFrameChangedHandler) { + myIsThreadDead = 0; + myImage = image; + // We should only be called if we already know we can be animated. Therefore this + // call is valid. + // + myNumFrames = image.GetFrameCount(FrameDimension.Time); + myFrameDelaysInCentiseconds = new int[myNumFrames]; + populateFrameDelays(image); + myCurrentFrame = 0; + myIsDirty = false; + myOnFrameChangedHandler = onFrameChangedHandler; + } + + public bool threadIsNotDead() { + return myIsThreadDead == 0; + } + + public void handleUpdateTick() { + myCurrentFrame = (myCurrentFrame + 1) % myNumFrames; + myIsDirty = true; + myOnFrameChangedHandler(myImage, EventArgs.Empty); + } + + public int getCurrentDelayInMilliseconds() { + return myFrameDelaysInCentiseconds[myCurrentFrame] * 10; + } + + public void updateFrame() { + myImage.SelectActiveFrame(FrameDimension.Time, myCurrentFrame); + } + + private void populateFrameDelays(Image image) { + byte[] frameDelays = image.GetPropertyItem(FrameDelayTag).Value; + for (int i = 0; i < myNumFrames; i++) { + myFrameDelaysInCentiseconds[i] = BitConverter.ToInt32(frameDelays, i * 4); + // Sometimes gifs have a zero frame delay, erroneously? + // These gifs seem to play differently depending on the program. + // I'll give them a default delay, as most gifs with 0 delay seem + // wayyyy to fast compared to other programs. + // + // 0.1 seconds appears to be chromes default setting... I'll use that + // + if (myFrameDelaysInCentiseconds[i] < 1) + myFrameDelaysInCentiseconds[i] = 10; + } + } + } + } +} \ No newline at end of file diff --git a/Source/Components/ImageGlass.ImageBox/ImageBox.cs b/Source/Components/ImageGlass.ImageBox/ImageBox.cs index 232d83ddb..ebf37dd72 100644 --- a/Source/Components/ImageGlass.ImageBox/ImageBox.cs +++ b/Source/Components/ImageGlass.ImageBox/ImageBox.cs @@ -97,7 +97,7 @@ public virtual BorderStyle BorderStyle /// public bool CanAnimate { - get { return ImageAnimator.CanAnimate(Image); } + get { return Animator.CanAnimate(Image); } } #endregion @@ -143,6 +143,8 @@ protected virtual void OnBorderStyleChanged(EventArgs e) private bool _allowZoom; + private GifAnimator _animator; + private bool _autoCenter; private bool _autoPan; @@ -229,6 +231,8 @@ public ImageBox() SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); SetStyle(ControlStyles.StandardDoubleClick, false); + _animator = new DefaultGifAnimator(); + _vScrollBar = new VScrollBar { Visible = false @@ -717,7 +721,7 @@ protected override void Dispose(bool disposing) if (IsAnimating) { // ReSharper disable once HeapView.DelegateAllocation - ImageAnimator.StopAnimate(Image, OnFrameChangedHandler); + Animator.StopAnimate(Image, OnFrameChangedHandler); } if (_hScrollBar != null) @@ -1124,6 +1128,22 @@ public virtual bool AllowZoom } } + /// + /// Handles animating gif images + /// + public GifAnimator Animator { + set { + if (Image != null && IsAnimating) { + StopAnimating(); + } + _animator = value; + // Mimick Image property behavior + OnImageChanged(EventArgs.Empty); + } + + get { return _animator; } + } + /// /// Gets or sets a value indicating whether the image is centered where possible. /// @@ -1315,7 +1335,7 @@ public virtual Image Image // disable animations if (IsAnimating) { - ImageAnimator.StopAnimate(Image, OnFrameChangedHandler); + Animator.StopAnimate(Image, OnFrameChangedHandler); } _image = value; @@ -1980,7 +2000,7 @@ public void StopAnimating() { if (!IsAnimating) return; - ImageAnimator.StopAnimate(Image, OnFrameChangedHandler); + Animator.StopAnimate(Image, OnFrameChangedHandler); IsAnimating = false; } @@ -1994,7 +2014,7 @@ public void StartAnimating() try { - ImageAnimator.Animate(Image, OnFrameChangedHandler); + Animator.Animate(Image, OnFrameChangedHandler); IsAnimating = true; } catch (Exception) { } @@ -3260,7 +3280,7 @@ protected virtual void DrawImage(Graphics g) // Animation. Thanks to teamalpha5441 for the contribution if (IsAnimating && !DesignMode) { - ImageAnimator.UpdateFrames(Image); + Animator.UpdateFrames(Image); } g.DrawImage(Image, GetImageViewPort(), GetSourceImageRegion(), GraphicsUnit.Pixel); @@ -3277,7 +3297,7 @@ protected virtual void DrawImage(Graphics g) { // stop the animation and reset to the first frame. IsAnimating = false; - ImageAnimator.StopAnimate(Image, OnFrameChangedHandler); + Animator.StopAnimate(Image, OnFrameChangedHandler); } g.PixelOffsetMode = currentPixelOffsetMode; @@ -3945,10 +3965,10 @@ protected virtual void OnImageChanged(EventArgs e) { //try //{ - // this.IsAnimating = ImageAnimator.CanAnimate(this.Image); + // this.IsAnimating = Animator.CanAnimate(this.Image); // if (this.IsAnimating) // { - // ImageAnimator.Animate(this.Image, this.OnFrameChangedHandler); + // Animator.Animate(this.Image, this.OnFrameChangedHandler); // } //} //catch (ArgumentException) diff --git a/Source/Components/ImageGlass.ImageBox/ImageGlass.ImageBox.csproj b/Source/Components/ImageGlass.ImageBox/ImageGlass.ImageBox.csproj index 3af332ccb..a7e10bf7d 100644 --- a/Source/Components/ImageGlass.ImageBox/ImageGlass.ImageBox.csproj +++ b/Source/Components/ImageGlass.ImageBox/ImageGlass.ImageBox.csproj @@ -46,6 +46,9 @@ + + + Component diff --git a/Source/Components/ImageGlass.Library/ImageGlass.Library.csproj b/Source/Components/ImageGlass.Library/ImageGlass.Library.csproj index 0a1fd6180..b24e7c5d3 100644 --- a/Source/Components/ImageGlass.Library/ImageGlass.Library.csproj +++ b/Source/Components/ImageGlass.Library/ImageGlass.Library.csproj @@ -77,6 +77,7 @@ + diff --git a/Source/Components/ImageGlass.Library/WinAPI/TimerAPI.cs b/Source/Components/ImageGlass.Library/WinAPI/TimerAPI.cs new file mode 100755 index 000000000..ae4705848 --- /dev/null +++ b/Source/Components/ImageGlass.Library/WinAPI/TimerAPI.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImageGlass.Library.WinAPI { + /// + /// Used to make requests for obtaining and setting timer resolution. + /// This is a global request shared by all processes on the computer. All + /// requests are revoked when this process ends. + /// + public static class TimerAPI { + // locks ourCurRequests + // + private static readonly object ourLock; + + [System.Runtime.InteropServices.DllImport("winmm.dll")] + private static extern int timeBeginPeriod(int msec); + + [System.Runtime.InteropServices.DllImport("winmm.dll")] + private static extern int timeEndPeriod(int msec); + + [System.Runtime.InteropServices.DllImport("winmm.dll")] + private static extern int timeGetDevCaps(ref TIMECAPS ptc, int cbtc); + + private static readonly int ourMinPeriod; + private static readonly int ourMaxPeriod; + private static readonly List ourCurRequests; + + [StructLayout(LayoutKind.Sequential)] + private struct TIMECAPS { + public int periodMin; + public int periodMax; + } + + static TimerAPI() { + ourLock = new object(); + + TIMECAPS tc = new TIMECAPS(); + timeGetDevCaps(ref tc, Marshal.SizeOf(tc)); + ourMinPeriod = tc.periodMin; + ourMaxPeriod = tc.periodMax; + ourCurRequests = new List(); + } + + /// + /// Request a rate from the system clock. + /// + /// the time in milliseconds + /// true if we succesfully acquired a clock of + /// the given rate, otherwise returns false. + public static bool TimeBeginPeriod(int timeInMilliseconds) { + if (timeInMilliseconds < ourMinPeriod || timeInMilliseconds > ourMaxPeriod) + return false; + + bool successfullyRequestedPeriod; + lock (ourLock) { + successfullyRequestedPeriod = timeBeginPeriod(timeInMilliseconds) == 0; + if (successfullyRequestedPeriod) + ourCurRequests.Add(timeInMilliseconds); + } + + return successfullyRequestedPeriod; + } + + /// + /// Revoke request for a rate from the system clock. + /// + /// the time in milliseconds + /// true if we revoked a previous request, otherwise returns false + public static bool TimeEndPeriod(int timeInMilliseconds) { + bool successfullyEndedPeriod; + lock (ourLock) { + successfullyEndedPeriod = ourCurRequests.Remove(timeInMilliseconds) && timeEndPeriod(timeInMilliseconds) == 0; + } + + return successfullyEndedPeriod; + } + + /// + /// Determines whether the current rate has already been requested. + /// + /// the time in milliseconds + public static bool HasRequestedRateAlready(int timeInMilliseconds) { + bool hasRequestedAlready; + lock (ourLock) { + hasRequestedAlready = ourCurRequests.Contains(timeInMilliseconds); + } + + return hasRequestedAlready; + } + + /// + /// Determines whether a rate at least as fast as the given has been requested + /// + /// the time in milliseconds + public static bool HasRequestedRateAtLeastAsFastAs(int timeInMilliseconds) { + bool result; + lock (ourLock) { + result = ourCurRequests.Exists(elt => elt <= timeInMilliseconds); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Source/ImageGlass/frmMain.cs b/Source/ImageGlass/frmMain.cs index d79de13ef..2aad8c5c0 100644 --- a/Source/ImageGlass/frmMain.cs +++ b/Source/ImageGlass/frmMain.cs @@ -35,6 +35,7 @@ using System.Drawing.Imaging; using ImageGlass.Theme; using System.Threading.Tasks; +using ImageGlass.Library.WinAPI; namespace ImageGlass { @@ -1201,6 +1202,22 @@ private void LoadToolbarIcons(Theme.Theme t) btnMenu.Image = t.ToolbarIcons.Menu.Image; } + /// + /// If true is passed, try to use a 10ms system clock for animating GIFs, otherwise + /// use the default animator. + /// + private void CheckAnimationClock(bool isUsingFasterClock) { + if (isUsingFasterClock) { + if (!TimerAPI.HasRequestedRateAtLeastAsFastAs(10) && TimerAPI.TimeBeginPeriod(10)) + HighResolutionGifAnimator.setTickTimeInMilliseconds(10); + picMain.Animator = new HighResolutionGifAnimator(); + } + else { + if (TimerAPI.HasRequestedRateAlready(10)) + TimerAPI.TimeEndPeriod(10); + picMain.Animator = new DefaultGifAnimator(); + } + } /// /// Load app configurations @@ -1506,6 +1523,9 @@ private void frmMain_Load(object sender, EventArgs e) LoadConfig(); Application.DoEvents(); + //Try to use a faster image clock for animating GIFs + CheckAnimationClock(true); + //Load image from param LoadFromParams(Environment.GetCommandLineArgs()); }