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