diff --git a/src/libraries/System.Drawing.Common/src/Resources/Strings.resx b/src/libraries/System.Drawing.Common/src/Resources/Strings.resx index 1b718caf5ad6c..969bd00ec5e7a 100644 --- a/src/libraries/System.Drawing.Common/src/Resources/Strings.resx +++ b/src/libraries/System.Drawing.Common/src/Resources/Strings.resx @@ -281,9 +281,6 @@ Value of '{1}' is not valid for '{0}'. '{0}' should be greater than or equal to {2} and less than or equal to {3}. - - Frame is not valid. Frame must be between 0 and FrameCount. - Win32 handle that was passed to {0} is not valid or is the wrong type. diff --git a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj index 403cad31cdf5b..f62446cca0b4b 100644 --- a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj +++ b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj @@ -29,8 +29,10 @@ + + @@ -194,8 +196,6 @@ - - @@ -344,7 +344,6 @@ - diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Unix.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Unix.cs deleted file mode 100644 index a8a03c47fa444..0000000000000 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Unix.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// -// System.Drawing.ImageAnimator.cs -// -// Authors: -// Dennis Hayes (dennish@Raytek.com) -// Sanjay Gupta (gsanjay@novell.com) -// Sebastien Pouliot -// -// (C) 2002 Ximian, Inc -// Copyright (C) 2004,2006-2007 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Drawing.Imaging; -using System.Threading; - -namespace System.Drawing -{ - - internal sealed class AnimateEventArgs : EventArgs - { - - private int frameCount; - private int activeFrame; - private Thread? thread; - - public AnimateEventArgs(Image image) - { - frameCount = image.GetFrameCount(FrameDimension.Time); - } - - public Thread? RunThread - { - get { return thread; } - set { thread = value; } - } - - public int GetNextFrame() - { - if (activeFrame < frameCount - 1) - activeFrame++; - else - activeFrame = 0; - - return activeFrame; - } - } - - public sealed class ImageAnimator - { - - private static Hashtable ht = Hashtable.Synchronized(new Hashtable()); - - private ImageAnimator() - { - } - - public static void Animate(Image image, EventHandler onFrameChangedHandler) - { - // must be non-null and contain animation time frames - if (!CanAnimate(image)) - return; - - // is animation already in progress ? - if (ht.ContainsKey(image)) - return; - - PropertyItem item = image.GetPropertyItem(0x5100)!; // FrameDelay in libgdiplus - byte[] value = item.Value!; - int[] delay = new int[(value.Length >> 2)]; - for (int i = 0, n = 0; i < value.Length; i += 4, n++) - { - int d = BitConverter.ToInt32(value, i) * 10; - // follow worse case (Opera) see http://news.deviantart.com/article/27613/ - delay[n] = d < 100 ? 100 : d; - } - - AnimateEventArgs aea = new AnimateEventArgs(image); - WorkerThread wt = new WorkerThread(onFrameChangedHandler, aea, delay); - Thread thread = new Thread(new ThreadStart(wt.LoopHandler)); - thread.IsBackground = true; - aea.RunThread = thread; - ht.Add(image, aea); - thread.Start(); - } - - public static bool CanAnimate([NotNullWhen(true)] Image? image) - { - if (image == null) - return false; - - int n = image.FrameDimensionsList.Length; - if (n < 1) - return false; - - for (int i = 0; i < n; i++) - { - if (image.FrameDimensionsList[i].Equals(FrameDimension.Time.Guid)) - { - return (image.GetFrameCount(FrameDimension.Time) > 1); - } - } - return false; - } - - public static void StopAnimate(Image image, EventHandler onFrameChangedHandler) - { - if (image == null) - return; - - if (ht.ContainsKey(image)) - { - AnimateEventArgs evtArgs = (AnimateEventArgs)ht[image]!; -#pragma warning disable SYSLIB0006 // https://github.com/dotnet/runtime/issues/39405 - evtArgs.RunThread!.Abort(); -#pragma warning restore SYSLIB0006 - ht.Remove(image); - } - } - - public static void UpdateFrames() - { - foreach (Image? image in ht.Keys) - UpdateImageFrame(image!); - } - - - public static void UpdateFrames(Image image) - { - if (image == null) - return; - - if (ht.ContainsKey(image)) - UpdateImageFrame(image); - } - - // this method avoid checks that aren't requied for UpdateFrames() - private static void UpdateImageFrame(Image image) - { - AnimateEventArgs aea = (AnimateEventArgs)ht[image]!; - image.SelectActiveFrame(FrameDimension.Time, aea.GetNextFrame()); - } - } - - internal sealed class WorkerThread - { - - private EventHandler frameChangeHandler; - private AnimateEventArgs animateEventArgs; - private int[] delay; - - public WorkerThread(EventHandler frmChgHandler, AnimateEventArgs aniEvtArgs, int[] delay) - { - frameChangeHandler = frmChgHandler; - animateEventArgs = aniEvtArgs; - this.delay = delay; - } - - public void LoopHandler() - { - int n = 0; - while (true) - { - Thread.Sleep(delay[n++]); - frameChangeHandler(null, animateEventArgs); - if (n == delay.Length) - n = 0; - } - } - } -} diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Windows.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.cs similarity index 93% rename from src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Windows.cs rename to src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.cs index 4f518384c550f..eb3fb8ef1605d 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.Windows.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/ImageAnimator.cs @@ -96,7 +96,7 @@ private ImageAnimator() /// public static void UpdateFrames(Image image) { - if (!s_anyFrameDirty || image == null || s_imageInfoList == null) + if (image == null || s_imageInfoList == null) { return; } @@ -130,10 +130,10 @@ public static void UpdateFrames(Image image) imageInfo.UpdateFrame(); } } + foundImage = true; } - - if (imageInfo.FrameDirty) + else if (imageInfo.FrameDirty) { foundDirty = true; } @@ -161,6 +161,7 @@ public static void UpdateFrames() { return; } + if (t_threadWriterLockWaitCount > 0) { // Cannot acquire reader lock at this time, frames update will be missed. @@ -179,6 +180,7 @@ public static void UpdateFrames() imageInfo.UpdateFrame(); } } + s_anyFrameDirty = false; } finally @@ -257,7 +259,7 @@ public static void Animate(Image image, EventHandler onFrameChangedHandler) // if (s_animationThread == null) { - s_animationThread = new Thread(new ThreadStart(AnimateImages50ms)); + s_animationThread = new Thread(new ThreadStart(AnimateImages)); s_animationThread.Name = nameof(ImageAnimator); s_animationThread.IsBackground = true; s_animationThread.Start(); @@ -370,7 +372,6 @@ public static void StopAnimate(Image image, EventHandler onFrameChangedHandler) } } - /// /// Worker thread procedure which implements the main animation loop. /// NOTE: This is the ONLY code the worker thread executes, keeping it in one method helps better understand @@ -378,13 +379,21 @@ public static void StopAnimate(Image image, EventHandler onFrameChangedHandler) /// WARNING: Also, this is the only place where ImageInfo objects (not the contained image object) are modified, /// so no access synchronization is required to modify them. /// - private static void AnimateImages50ms() + private static void AnimateImages() { Debug.Assert(s_imageInfoList != null, "Null images list"); + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); while (true) { - // Acquire reader-lock to access imageInfoList, elemens in the list can be modified w/o needing a writer-lock. + Thread.Sleep(40); + + // Because Thread.Sleep is not accurate, capture how much time has actually elapsed during the animation + long timeElapsed = stopwatch.ElapsedMilliseconds; + stopwatch.Restart(); + + // Acquire reader-lock to access imageInfoList, elements in the list can be modified w/o needing a writer-lock. // Observe that we don't need to check if the thread is waiting or a writer lock here since the thread this // method runs in never acquires a writer lock. s_rwImgListLock.AcquireReaderLock(Timeout.Infinite); @@ -394,24 +403,9 @@ private static void AnimateImages50ms() { ImageInfo imageInfo = s_imageInfoList[i]; - // Frame delay is measured in 1/100ths of a second. This thread - // sleeps for 50 ms = 5/100ths of a second between frame updates, - // so we increase the frame delay count 5/100ths of a second - // at a time. - // - imageInfo.FrameTimer += 5; - if (imageInfo.FrameTimer >= imageInfo.FrameDelay(imageInfo.Frame)) + if (imageInfo.Animated) { - imageInfo.FrameTimer = 0; - - if (imageInfo.Frame + 1 < imageInfo.FrameCount) - { - imageInfo.Frame++; - } - else - { - imageInfo.Frame = 0; - } + imageInfo.AdvanceAnimationBy(timeElapsed); if (imageInfo.FrameDirty) { @@ -424,8 +418,6 @@ private static void AnimateImages50ms() { s_rwImgListLock.ReleaseReaderLock(); } - - Thread.Sleep(50); } } } diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageInfo.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/ImageInfo.cs index d9e299ee16dfe..24b041395580a 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/ImageInfo.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/ImageInfo.cs @@ -18,21 +18,24 @@ public sealed partial class ImageAnimator private sealed class ImageInfo { private const int PropertyTagFrameDelay = 0x5100; + private const int PropertyTagLoopCount = 0x5101; private readonly Image _image; private int _frame; + private short _loop; private readonly int _frameCount; + private readonly short _loopCount; private bool _frameDirty; private readonly bool _animated; private EventHandler? _onFrameChangedHandler; - private readonly int[] _frameDelay; - private int _frameTimer; + private readonly long[]? _frameEndTimes; + private long _frameTimer; public ImageInfo(Image image) { _image = image; _animated = ImageAnimator.CanAnimate(image); - _frameDelay = null!; // guaranteed to be initialized by the final check + _frameEndTimes = null; if (_animated) { @@ -41,28 +44,52 @@ public ImageInfo(Image image) PropertyItem? frameDelayItem = image.GetPropertyItem(PropertyTagFrameDelay); // If the image does not have a frame delay, we just return 0. - // if (frameDelayItem != null) { // Convert the frame delay from byte[] to int - // byte[] values = frameDelayItem.Value!; - Debug.Assert(values.Length == 4 * FrameCount, "PropertyItem has invalid value byte array"); - _frameDelay = new int[FrameCount]; - for (int i = 0; i < FrameCount; ++i) + + // On Windows, we get the frame delays for every frame. On Linux, we only get the first frame delay. + // We handle this by treating the frame delays as a repeating sequence, asserting that the sequence + // is fully repeatable to match the frame count. + Debug.Assert(values.Length % 4 == 0, "PropertyItem has an invalid value byte array. It should have a length evenly divisible by 4 to represent ints."); + Debug.Assert(_frameCount % (values.Length / 4) == 0, "PropertyItem has invalid value byte array. The FrameCount should be evenly divisible by a quarter of the byte array's length."); + + _frameEndTimes = new long[_frameCount]; + long lastEndTime = 0; + + for (int f = 0, i = 0; f < _frameCount; ++f, i += 4) { - _frameDelay[i] = values[i * 4] + 256 * values[i * 4 + 1] + 256 * 256 * values[i * 4 + 2] + 256 * 256 * 256 * values[i * 4 + 3]; + if (i >= values.Length) + { + i = 0; + } + + // Frame delays are stored in 1/100ths of a second; convert to milliseconds while accumulating + _frameEndTimes[f] = (lastEndTime += (BitConverter.ToInt32(values, i) * 10)); } } + + PropertyItem? loopCountItem = image.GetPropertyItem(PropertyTagLoopCount); + + if (loopCountItem != null) + { + // The loop count is a short where 0 = infinite, and a positive value indicates the + // number of times to loop. The animation will be shown 1 time more than the loop count. + byte[] values = loopCountItem.Value!; + + Debug.Assert(values.Length == sizeof(short), "PropertyItem has an invalid byte array. It should represent a single short value."); + _loopCount = BitConverter.ToInt16(values); + } + else + { + _loopCount = 0; + } } else { _frameCount = 1; } - if (_frameDelay == null) - { - _frameDelay = new int[FrameCount]; - } } /// @@ -77,36 +104,7 @@ public bool Animated } /// - /// The current frame. - /// - public int Frame - { - get - { - return _frame; - } - set - { - if (_frame != value) - { - if (value < 0 || value >= FrameCount) - { - throw new ArgumentException(SR.InvalidFrame, nameof(value)); - } - - if (Animated) - { - _frame = value; - _frameDirty = true; - - OnFrameChanged(EventArgs.Empty); - } - } - } - } - - /// - /// The current frame has not been updated. + /// The current frame has changed but the image has not yet been updated. /// public bool FrameDirty { @@ -129,33 +127,68 @@ public EventHandler? FrameChangedHandler } /// - /// The number of frames in the image. + /// The total animation time of the image, in milliseconds. /// - public int FrameCount - { - get - { - return _frameCount; - } - } + private long TotalAnimationTime => Animated ? _frameEndTimes![_frameCount - 1] : 0; /// - /// The delay associated with the frame at the specified index. + /// Whether animation should progress, respecting the image's animation support + /// and if there are animation frames or loops remaining. /// - public int FrameDelay(int frame) - { - return _frameDelay![frame]; - } + private bool ShouldAnimate => Animated ? (_loopCount == 0 || _loop <= _loopCount) : false; - internal int FrameTimer + /// + /// Advance the animation by the specified number of milliseconds. If the advancement + /// progresses beyond the end time of the current Frame, + /// will be called. Subscribed handlers often use that event to call + /// . + /// + /// If the animation progresses beyond the end of the image's total animation time, + /// the animation will loop. + /// + /// + /// + /// This animation does not respect a GIF's specified number of animation repeats; + /// instead, animations loop indefinitely. + /// + /// The number of milliseconds to advance the animation by + public void AdvanceAnimationBy(long milliseconds) { - get - { - return _frameTimer; - } - set + if (ShouldAnimate) { - _frameTimer = value; + int oldFrame = _frame; + _frameTimer += milliseconds; + + if (_frameTimer > TotalAnimationTime) + { + _loop += (short)Math.DivRem(_frameTimer, TotalAnimationTime, out long newTimer); + _frameTimer = newTimer; + + if (!ShouldAnimate) + { + // If we've finished looping, then freeze onto the last frame + _frame = _frameCount - 1; + _frameTimer = TotalAnimationTime; + } + else if (_frame > 0 && _frameTimer < _frameEndTimes![_frame - 1]) + { + // If the loop put us before the current frame (which is common) + // then reset back to the first frame. We will then progress + // forward again from there (below). + _frame = 0; + } + } + + while (_frameTimer > _frameEndTimes![_frame]) + { + _frame++; + } + + if (_frame != oldFrame) + { + _frameDirty = true; + OnFrameChanged(EventArgs.Empty); + } } } @@ -177,7 +210,7 @@ internal void UpdateFrame() { if (_frameDirty) { - _image.SelectActiveFrame(FrameDimension.Time, Frame); + _image.SelectActiveFrame(FrameDimension.Time, _frame); _frameDirty = false; } } diff --git a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj index 426780cb891e1..1b9175f3eb5e1 100644 --- a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj +++ b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj @@ -67,6 +67,8 @@ + + diff --git a/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimator.ManualTests.cs b/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimator.ManualTests.cs new file mode 100644 index 0000000000000..63cb4e3f9329e --- /dev/null +++ b/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimator.ManualTests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using Xunit; + +namespace System.Drawing.Tests +{ + public class ImageAnimatorManualTests + { + public static bool ManualTestsEnabled => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MANUAL_TESTS")); + public static string OutputFolder = Path.Combine(Environment.CurrentDirectory, "ImageAnimatorManualTests", DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")); + + // To run these tests, change the working directory to src/libraries/System.Drawing.Common, + // set the `MANUAL_TESTS` environment variable to any non-empty value, and run + // `dotnet test --filter "ImageAnimatorManualTests" + + [ConditionalFact(Helpers.IsDrawingSupported, nameof(ManualTestsEnabled), Timeout = 75_000)] + public void AnimateAndCaptureFrames() + { + // This test animates the test gifs that we have and waits 60 seconds + // for the animations to progress. As the frame change events occur, we + // capture snapshots of the current frame, essentially extracting the + // frames from the GIF. + + // The animation should progress at the expected pace to stay synchronized + // with the wall clock, and the animated timer images show the time duration + // within the image itself, so this can be manually verified for accuracy. + + // The captured frames are stored in the `artifacts/bin/System.Drawing.Common.Tests` + // folder for each configuration, and then under an `ImageAnimatorManualTests` folder + // with a timestamped folder under that. Each animation image gets its own folder too. + + string[] images = new string[] + { + "animated-timer-1fps-repeat-2.gif", + "animated-timer-1fps-repeat-infinite.gif", + "animated-timer-10fps-repeat-2.gif", + "animated-timer-10fps-repeat-infinite.gif", + "animated-timer-100fps-repeat-2.gif", + "animated-timer-100fps-repeat-infinite.gif", + }; + + Dictionary handlers = new(); + Dictionary frameIndexes = new(); + Dictionary bitmaps = new(); + + Stopwatch stopwatch = new(); + + foreach (var imageName in images) + { + string testOutputFolder = Path.Combine(OutputFolder, Path.GetFileNameWithoutExtension(imageName)); + Directory.CreateDirectory(testOutputFolder); + frameIndexes[imageName] = 0; + + handlers[imageName] = new EventHandler(new Action((object o, EventArgs e) => + { + Bitmap animation = (Bitmap)o; + ImageAnimator.UpdateFrames(animation); + + // We save captures using jpg so that: + // a) The images don't get saved as animated gifs again, and just a single frame is saved + // b) Saving pngs in this test on Linux was leading to sporadic GDI+ errors; Jpeg is more reliable + string timestamp = stopwatch.ElapsedMilliseconds.ToString("000000"); + animation.Save(Path.Combine(testOutputFolder, $"{++frameIndexes[imageName]}_{timestamp}.jpg"), ImageFormat.Jpeg); + })); + + bitmaps[imageName] = new(Helpers.GetTestBitmapPath(imageName)); + ImageAnimator.Animate(bitmaps[imageName], handlers[imageName]); + } + + stopwatch.Start(); + Thread.Sleep(60_000); + + foreach (var imageName in images) + { + ImageAnimator.StopAnimate(bitmaps[imageName], handlers[imageName]); + bitmaps[imageName].Dispose(); + } + } + } +} diff --git a/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimatorTests.cs b/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimatorTests.cs new file mode 100644 index 0000000000000..49925d4763151 --- /dev/null +++ b/src/libraries/System.Drawing.Common/tests/System/Drawing/ImageAnimatorTests.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.Drawing.Tests +{ + public class ImageAnimatorTests + { + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_WithNothingAnimating() + { + ImageAnimator.UpdateFrames(); + } + + [ConditionalTheory(Helpers.IsDrawingSupported)] + [InlineData("1bit.png")] + [InlineData("48x48_one_entry_1bit.ico")] + [InlineData("81773-interlaced.gif")] + public void CanAnimate_ReturnsFalse_ForNonAnimatedImages(string imageName) + { + using (var image = new Bitmap(Helpers.GetTestBitmapPath(imageName))) + { + Assert.False(ImageAnimator.CanAnimate(image)); + } + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void Animate_Succeeds_ForNonAnimatedImages_WithNothingAnimating() + { + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.Animate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void Animate_Succeeds_ForNonAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.Animate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_ForNonAnimatedImages_WithNothingAnimating() + { + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.UpdateFrames(image); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_ForNonAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.UpdateFrames(image); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void StopAnimate_Succeeds_ForNonAnimatedImages_WithNothingAnimating() + { + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.StopAnimate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void StopAnimate_Succeeds_ForNonAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + + var image = new Bitmap(Helpers.GetTestBitmapPath("1bit.png")); + ImageAnimator.StopAnimate(image, (object o, EventArgs e) => { }); + } + + [ConditionalTheory(Helpers.IsDrawingSupported)] + [InlineData("animated-timer-1fps-repeat-2.gif")] + [InlineData("animated-timer-1fps-repeat-infinite.gif")] + [InlineData("animated-timer-10fps-repeat-2.gif")] + [InlineData("animated-timer-10fps-repeat-infinite.gif")] + [InlineData("animated-timer-100fps-repeat-2.gif")] + [InlineData("animated-timer-100fps-repeat-infinite.gif")] + public void CanAnimate_ReturnsTrue_ForAnimatedImages(string imageName) + { + using (var image = new Bitmap(Helpers.GetTestBitmapPath(imageName))) + { + Assert.True(ImageAnimator.CanAnimate(image)); + } + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void Animate_Succeeds_ForAnimatedImages_WithNothingAnimating() + { + var image = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void Animate_Succeeds_ForAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + + var image = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-infinite.gif")); + ImageAnimator.Animate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_ForAnimatedImages_WithNothingAnimating() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.UpdateFrames(animatedImage); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + ImageAnimator.UpdateFrames(); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void UpdateFrames_Succeeds_ForAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + ImageAnimator.UpdateFrames(animatedImage); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void StopAnimate_Succeeds_ForAnimatedImages_WithNothingAnimating() + { + var image = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.StopAnimate(image, (object o, EventArgs e) => { }); + } + + [ConditionalFact(Helpers.IsDrawingSupported)] + public void StopAnimate_Succeeds_ForAnimatedImages_WithCurrentAnimations() + { + var animatedImage = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-2.gif")); + ImageAnimator.Animate(animatedImage, (object o, EventArgs e) => { }); + + var image = new Bitmap(Helpers.GetTestBitmapPath("animated-timer-100fps-repeat-infinite.gif")); + ImageAnimator.StopAnimate(animatedImage, (object o, EventArgs e) => { }); + ImageAnimator.StopAnimate(image, (object o, EventArgs e) => { }); + } + } +}