Skip to content

Commit

Permalink
Merge pull request #179 from Meowski/develop
Browse files Browse the repository at this point in the history
Issue #114 - Slow gif animation
  • Loading branch information
d2phap committed Apr 9, 2017
2 parents f51f3f3 + f9e103c commit f2f93f6
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 9 deletions.
25 changes: 25 additions & 0 deletions Source/Components/ImageGlass.ImageBox/DefaultGifAnimator.cs
@@ -0,0 +1,25 @@
using System;
using System.Drawing;

namespace ImageGlass {
/// <summary>
/// This is a wrapper for the original System.Drawing animator. See <see cref="ImageAnimator"/>.
/// </summary>
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);
}
}
}
31 changes: 31 additions & 0 deletions Source/Components/ImageGlass.ImageBox/GifAnimator.cs
@@ -0,0 +1,31 @@
using System;
using System.Drawing;

namespace ImageGlass {
/// <summary>
/// Used to animate gifs.
/// </summary>
public interface GifAnimator
{
/// <summary>
/// Updates the time frame for this image.
/// </summary>
void UpdateFrames(Image image);

/// <summary>
/// Stops updating frames for the given image.
/// </summary>
void StopAnimate(Image image, EventHandler eventHandler);

/// <summary>
/// Animates the given image.
/// </summary>
void Animate(Image image, EventHandler onFrameChangedHandler);

/// <summary>
/// Determines whether an image can be animated.
/// </summary>
/// <returns> true if the given image can be animated, otherwise false. </returns>
bool CanAnimate(Image image);
}
}
207 changes: 207 additions & 0 deletions 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 {

/// <summary>
/// <p> Implements GifAnimator with the potential to offer a timer resolution of 10ms,
/// the fastest a GIF can animate. </p>
/// <p> 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. </p>
/// </summary>
public class HighResolutionGifAnimator : GifAnimator {
#region STATIC
private static int ourMinTickTimeInMilliseconds;
private static readonly ConcurrentDictionary<Image, GifImageData> ourImageState;

/// <summary>
/// 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).
/// </summary>
/// <param name="value"> Ideally should be a multiple of 10. </param>
/// <returns>The actual tick value that will be used</returns>
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;
}

/// <summary>
/// Given a delay amount, return either the minimum tick or delay, whichever is greater.
/// </summary>
/// <returns> the time to sleep during a tick in milliseconds </returns>
private static int getSleepAmountInMilliseconds(int delayInMilliseconds) {
return Math.Max(ourMinTickTimeInMilliseconds, delayInMilliseconds);
}

static HighResolutionGifAnimator() {
ourMinTickTimeInMilliseconds = 20;
ourImageState = new ConcurrentDictionary<Image, GifImageData>();
}
#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;
}
}
}
}
}
38 changes: 29 additions & 9 deletions Source/Components/ImageGlass.ImageBox/ImageBox.cs
Expand Up @@ -97,7 +97,7 @@ public virtual BorderStyle BorderStyle
/// </summary>
public bool CanAnimate
{
get { return ImageAnimator.CanAnimate(Image); }
get { return Animator.CanAnimate(Image); }
}

#endregion
Expand Down Expand Up @@ -143,6 +143,8 @@ protected virtual void OnBorderStyleChanged(EventArgs e)

private bool _allowZoom;

private GifAnimator _animator;

private bool _autoCenter;

private bool _autoPan;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1124,6 +1128,22 @@ public virtual bool AllowZoom
}
}

/// <summary>
/// Handles animating gif images
/// </summary>
public GifAnimator Animator {
set {
if (Image != null && IsAnimating) {
StopAnimating();
}
_animator = value;
// Mimick Image property behavior
OnImageChanged(EventArgs.Empty);
}

get { return _animator; }
}

/// <summary>
/// Gets or sets a value indicating whether the image is centered where possible.
/// </summary>
Expand Down Expand Up @@ -1315,7 +1335,7 @@ public virtual Image Image
// disable animations
if (IsAnimating)
{
ImageAnimator.StopAnimate(Image, OnFrameChangedHandler);
Animator.StopAnimate(Image, OnFrameChangedHandler);
}

_image = value;
Expand Down Expand Up @@ -1980,7 +2000,7 @@ public void StopAnimating()
{
if (!IsAnimating)
return;
ImageAnimator.StopAnimate(Image, OnFrameChangedHandler);
Animator.StopAnimate(Image, OnFrameChangedHandler);
IsAnimating = false;
}

Expand All @@ -1994,7 +2014,7 @@ public void StartAnimating()

try
{
ImageAnimator.Animate(Image, OnFrameChangedHandler);
Animator.Animate(Image, OnFrameChangedHandler);
IsAnimating = true;
}
catch (Exception) { }
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Expand Up @@ -46,6 +46,9 @@
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="DefaultGifAnimator.cs" />
<Compile Include="GifAnimator.cs" />
<Compile Include="HighResolutionGifAnimator.cs" />
<Compile Include="ImageBox.cs">
<SubType>Component</SubType>
</Compile>
Expand Down

0 comments on commit f2f93f6

Please sign in to comment.