| File: System\Drawing\ImageInfo.cs | Web Access |
| Project: src\winforms\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Drawing.Imaging; namespace System.Drawing; /// <summary> /// Animates one or more images that have time-based frames. /// </summary> public sealed partial class ImageAnimator { /// <summary> /// ImageAnimator nested helper class used to store extra image state info. /// </summary> private sealed class ImageInfo { private const int PropertyTagFrameDelay = 0x5100; private const int PropertyTagLoopCount = 0x5101; private int _frame; private short _loop; private readonly int _frameCount; private readonly short _loopCount; private readonly long[]? _frameEndTimes; private readonly long _totalAnimationTime; private long _frameTimer; public ImageInfo(Image image) { Image = image; Animated = CanAnimate(image); _frameEndTimes = null; if (!Animated) { _frameCount = 1; return; } _frameCount = image.GetFrameCount(FrameDimension.Time); Imaging.PropertyItem? frameDelayItem = image.GetPropertyItem(PropertyTagFrameDelay); // If the image does not have a frame delay, we just return 0. if (frameDelayItem is not null) { // Convert the frame delay from byte[] to int byte[] values = frameDelayItem.Value!; // 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) { if (i >= values.Length) { i = 0; } // Frame delays are stored in 1/100ths of a second; convert to milliseconds while accumulating // Per spec, a frame delay can be 0 which is treated as a single animation tick int delay = BitConverter.ToInt32(values, i) * 10; lastEndTime += delay > 0 ? delay : AnimationDelayMS; // Guard against overflows if (lastEndTime < _totalAnimationTime) { lastEndTime = _totalAnimationTime; } else { _totalAnimationTime = lastEndTime; } _frameEndTimes[f] = lastEndTime; } } Imaging.PropertyItem? loopCountItem = image.GetPropertyItem(PropertyTagLoopCount); if (loopCountItem is not 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; } } /// <summary> /// Whether the image supports animation. /// </summary> public bool Animated { get; } /// <summary> /// The current frame has changed but the image has not yet been updated. /// </summary> public bool FrameDirty { get; private set; } public EventHandler? FrameChangedHandler { get; set; } /// <summary> /// The total animation time of the image in milliseconds, or <value>0</value> if not animated. /// </summary> private long TotalAnimationTime => Animated ? _totalAnimationTime : 0; /// <summary> /// Whether animation should progress, respecting the image's animation support /// and if there are animation frames or loops remaining. /// </summary> private bool ShouldAnimate => TotalAnimationTime > 0 && (_loopCount == 0 || _loop <= _loopCount); /// <summary> /// Advance the animation by the specified number of milliseconds. If the advancement progresses beyond the /// end time of the current Frame, <see cref="FrameChangedHandler"/> will be called. Subscribed handlers often /// use that event to call <see cref="UpdateFrames(Image)"/>.. /// </summary> /// <remarks> /// <para> /// If the animation progresses beyond the end of the image's total animation time, /// the animation will loop. /// </para> /// <para> /// This animation does not respect a GIF's specified number of animation repeats; /// instead, animations loop indefinitely. /// </para> /// </remarks> /// <param name="milliseconds">The number of milliseconds to advance the animation by</param> public void AdvanceAnimationBy(long milliseconds) { if (!ShouldAnimate) { return; } 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); } } /// <summary> /// The image this object wraps. /// </summary> internal Image Image { get; } /// <summary> /// Selects the current frame as the active frame in the image. /// </summary> internal void UpdateFrame() { if (FrameDirty) { Image.SelectActiveFrame(FrameDimension.Time, _frame); FrameDirty = false; } } /// <summary> /// Raises the FrameChanged event. /// </summary> private void OnFrameChanged(EventArgs e) => FrameChangedHandler?.Invoke(Image, e); } }