Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #179 from Meowski/develop
Issue #114 - Slow gif animation
- Loading branch information
Showing
8 changed files
with
420 additions
and
9 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
Source/Components/ImageGlass.ImageBox/DefaultGifAnimator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
207
Source/Components/ImageGlass.ImageBox/HighResolutionGifAnimator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.