|
|
@@ -13,699 +13,670 @@ using XamlAnimatedGif.Decoding;
|
|
|
using XamlAnimatedGif.Decompression;
|
|
|
using XamlAnimatedGif.Extensions;
|
|
|
|
|
|
-namespace XamlAnimatedGif
|
|
|
+namespace XamlAnimatedGif;
|
|
|
+
|
|
|
+public abstract class Animator : DependencyObject, IDisposable
|
|
|
{
|
|
|
- public abstract class Animator : DependencyObject, IDisposable
|
|
|
- {
|
|
|
- private static readonly Task CompletedTask = Task.FromResult(0);
|
|
|
- private readonly Stream _sourceStream;
|
|
|
- private readonly Uri _sourceUri;
|
|
|
- private readonly bool _isSourceStreamOwner;
|
|
|
- private readonly GifDataStream _metadata;
|
|
|
- private readonly Dictionary<int, GifPalette> _palettes;
|
|
|
- private readonly WriteableBitmap _bitmap;
|
|
|
- private readonly int _stride;
|
|
|
- private readonly byte[] _previousBackBuffer;
|
|
|
- private readonly byte[] _indexStreamBuffer;
|
|
|
- private readonly TimingManager _timingManager;
|
|
|
- private readonly bool _cacheFrameDataInMemory;
|
|
|
- private readonly byte[][][] _cachedFrameBytes;
|
|
|
- private TaskCompletionSource<bool> _frameLoadedEvent;
|
|
|
- private readonly object _lockObject;
|
|
|
-
|
|
|
- #region Constructor and factory methods
|
|
|
-
|
|
|
- internal Animator(Stream sourceStream, Uri sourceUri, GifDataStream metadata, RepeatBehavior repeatBehavior,
|
|
|
- bool cacheFrameDataInMemory, CancellationToken cancellationToken)
|
|
|
+ private readonly Stream _sourceStream;
|
|
|
+ private readonly Uri _sourceUri;
|
|
|
+ private readonly bool _isSourceStreamOwner;
|
|
|
+ private readonly GifDataStream _metadata;
|
|
|
+ private readonly Dictionary<int, GifPalette> _palettes;
|
|
|
+ private readonly WriteableBitmap _bitmap;
|
|
|
+ private readonly int _stride;
|
|
|
+ private readonly byte[] _previousBackBuffer;
|
|
|
+ private readonly byte[] _indexStreamBuffer;
|
|
|
+ private readonly TimingManager _timingManager;
|
|
|
+ private readonly bool _cacheFrameDataInMemory;
|
|
|
+ private readonly byte[][] _cachedFrameBytes;
|
|
|
+ private readonly Task _loadFramesDataTask;
|
|
|
+
|
|
|
+ #region Constructor and factory methods
|
|
|
+
|
|
|
+ internal Animator(Stream sourceStream, Uri sourceUri, GifDataStream metadata, RepeatBehavior repeatBehavior,
|
|
|
+ bool cacheFrameDataInMemory)
|
|
|
+ {
|
|
|
+ _sourceStream = sourceStream;
|
|
|
+ _sourceUri = sourceUri;
|
|
|
+ _isSourceStreamOwner = sourceUri != null; // stream opened from URI, should close it
|
|
|
+ _metadata = metadata;
|
|
|
+ _palettes = CreatePalettes(metadata);
|
|
|
+ _bitmap = CreateBitmap(metadata);
|
|
|
+ var desc = metadata.Header.LogicalScreenDescriptor;
|
|
|
+ _stride = 4 * ((desc.Width * 32 + 31) / 32);
|
|
|
+ _previousBackBuffer = new byte[desc.Height * _stride];
|
|
|
+ _indexStreamBuffer = CreateIndexStreamBuffer(metadata, _sourceStream);
|
|
|
+ _timingManager = CreateTimingManager(metadata, repeatBehavior);
|
|
|
+
|
|
|
+ _cacheFrameDataInMemory = cacheFrameDataInMemory;
|
|
|
+
|
|
|
+ if (cacheFrameDataInMemory)
|
|
|
{
|
|
|
- _sourceStream = sourceStream;
|
|
|
- _sourceUri = sourceUri;
|
|
|
- _isSourceStreamOwner = sourceUri != null; // stream opened from URI, should close it
|
|
|
- _metadata = metadata;
|
|
|
- _palettes = CreatePalettes(metadata);
|
|
|
- _bitmap = CreateBitmap(metadata);
|
|
|
- var desc = metadata.Header.LogicalScreenDescriptor;
|
|
|
- _stride = 4 * ((desc.Width * 32 + 31) / 32);
|
|
|
- _previousBackBuffer = new byte[desc.Height * _stride];
|
|
|
- _indexStreamBuffer = CreateIndexStreamBuffer(metadata, _sourceStream);
|
|
|
- _timingManager = CreateTimingManager(metadata, repeatBehavior);
|
|
|
-
|
|
|
- _cacheFrameDataInMemory = cacheFrameDataInMemory;
|
|
|
-
|
|
|
- if (cacheFrameDataInMemory)
|
|
|
- {
|
|
|
- _lockObject = new object();
|
|
|
- _frameLoadedEvent = new TaskCompletionSource<bool>();
|
|
|
- _cachedFrameBytes = new byte[_metadata.Frames.Count][][];
|
|
|
- Task.Run(() => LoadFrames(cancellationToken));
|
|
|
- }
|
|
|
+ _cachedFrameBytes = new byte[_metadata.Frames.Count][];
|
|
|
+ _loadFramesDataTask = Task.Run(LoadFrames);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- private async Task LoadFrames(CancellationToken cancellationToken)
|
|
|
+ private async Task LoadFrames()
|
|
|
+ {
|
|
|
+ var biggestFrameSize = 0L;
|
|
|
+ for (var frameIndex = 0; frameIndex < _metadata.Frames.Count; frameIndex++)
|
|
|
{
|
|
|
- var biggestFrameSize = 0L;
|
|
|
- for (var frameIndex = 0; frameIndex < _metadata.Frames.Count; frameIndex++)
|
|
|
- {
|
|
|
- var startPosition = _metadata.Frames[frameIndex].ImageData.CompressedDataStartOffset;
|
|
|
- var endPosition = _metadata.Frames.Count == frameIndex + 1
|
|
|
- ? _sourceStream.Length
|
|
|
- : _metadata.Frames[frameIndex + 1].ImageData.CompressedDataStartOffset - 1;
|
|
|
- var size = endPosition - startPosition;
|
|
|
- biggestFrameSize = Math.Max(size, biggestFrameSize);
|
|
|
- }
|
|
|
-
|
|
|
- byte[] indexCompressedBytes = new byte[biggestFrameSize];
|
|
|
- try
|
|
|
- {
|
|
|
- for (var frameIndex = 0; frameIndex < _metadata.Frames.Count; frameIndex++)
|
|
|
- {
|
|
|
- if (cancellationToken.IsCancellationRequested)
|
|
|
- {
|
|
|
- _frameLoadedEvent.SetCanceled();
|
|
|
- _frameLoadedEvent = null;
|
|
|
- return;
|
|
|
- }
|
|
|
- var frame = _metadata.Frames[frameIndex];
|
|
|
- var frameDesc = _metadata.Frames[frameIndex].Descriptor;
|
|
|
- await GetIndexBytesAsync(frameIndex, indexCompressedBytes);
|
|
|
- using var indexDecompressedStream =
|
|
|
- new LzwDecompressStream(indexCompressedBytes, frame.ImageData.LzwMinimumCodeSize);
|
|
|
- _cachedFrameBytes[frameIndex] = new byte[frame.Descriptor.Height][];
|
|
|
- for (var row = 0; row < frame.Descriptor.Height; row++)
|
|
|
- {
|
|
|
- _cachedFrameBytes[frameIndex][row] = new byte[frameDesc.Width];
|
|
|
- await indexDecompressedStream.ReadAllAsync(_cachedFrameBytes[frameIndex][row], 0, frameDesc.Width);
|
|
|
- }
|
|
|
- NotifyOfFrameLoaded();
|
|
|
- }
|
|
|
- }
|
|
|
- finally
|
|
|
- {
|
|
|
- _frameLoadedEvent?.SetResult(true);
|
|
|
- _frameLoadedEvent = null;
|
|
|
- }
|
|
|
+ var startPosition = _metadata.Frames[frameIndex].ImageData.CompressedDataStartOffset;
|
|
|
+ var endPosition = _metadata.Frames.Count == frameIndex + 1
|
|
|
+ ? _sourceStream.Length
|
|
|
+ : _metadata.Frames[frameIndex + 1].ImageData.CompressedDataStartOffset - 1;
|
|
|
+ var size = endPosition - startPosition;
|
|
|
+ biggestFrameSize = Math.Max(size, biggestFrameSize);
|
|
|
}
|
|
|
|
|
|
- internal static async Task<TAnimator> CreateAsyncCore<TAnimator>(
|
|
|
- Uri sourceUri,
|
|
|
- IProgress<int> progress,
|
|
|
- Func<Stream, GifDataStream, TAnimator> create)
|
|
|
- where TAnimator : Animator
|
|
|
+ var indexCompressedBytes = new byte[biggestFrameSize];
|
|
|
+ for (var frameIndex = 0; frameIndex < _metadata.Frames.Count; frameIndex++)
|
|
|
{
|
|
|
- var stream = await UriLoader.GetStreamFromUriAsync(sourceUri, progress);
|
|
|
- try
|
|
|
- {
|
|
|
- // ReSharper disable once AccessToDisposedClosure
|
|
|
- return await CreateAsyncCore(stream, metadata => create(stream, metadata));
|
|
|
- }
|
|
|
- catch
|
|
|
- {
|
|
|
- stream?.Dispose();
|
|
|
- throw;
|
|
|
- }
|
|
|
+ var frame = _metadata.Frames[frameIndex];
|
|
|
+ var frameDesc = _metadata.Frames[frameIndex].Descriptor;
|
|
|
+ await GetIndexBytesAsync(frameIndex, indexCompressedBytes);
|
|
|
+ await using var indexDecompressedStream =
|
|
|
+ new LzwDecompressStream(indexCompressedBytes, frame.ImageData.LzwMinimumCodeSize);
|
|
|
+ _cachedFrameBytes[frameIndex] = new byte[frameDesc.Width * frameDesc.Height];
|
|
|
+
|
|
|
+ await indexDecompressedStream.ReadAllAsync(_cachedFrameBytes[frameIndex], 0,
|
|
|
+ frameDesc.Width * frameDesc.Height);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- internal static async Task<TAnimator> CreateAsyncCore<TAnimator>(
|
|
|
- Stream sourceStream,
|
|
|
- Func<GifDataStream, TAnimator> create)
|
|
|
- where TAnimator : Animator
|
|
|
+ internal static async Task<TAnimator> CreateAsyncCore<TAnimator>(
|
|
|
+ Uri sourceUri,
|
|
|
+ IProgress<int> progress,
|
|
|
+ Func<Stream, GifDataStream, TAnimator> create)
|
|
|
+ where TAnimator : Animator
|
|
|
+ {
|
|
|
+ var stream = await UriLoader.GetStreamFromUriAsync(sourceUri);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // ReSharper disable once AccessToDisposedClosure
|
|
|
+ return await CreateAsyncCore(stream, metadata => create(stream, metadata));
|
|
|
+ }
|
|
|
+ catch
|
|
|
{
|
|
|
- if (!sourceStream.CanSeek)
|
|
|
- throw new ArgumentException("The stream is not seekable");
|
|
|
- sourceStream.Seek(0, SeekOrigin.Begin);
|
|
|
- var metadata = await GifDataStream.ReadAsync(sourceStream);
|
|
|
- return create(metadata);
|
|
|
+ stream?.Dispose();
|
|
|
+ throw;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- #endregion Constructor and factory methods
|
|
|
+ internal static async Task<TAnimator> CreateAsyncCore<TAnimator>(
|
|
|
+ Stream sourceStream,
|
|
|
+ Func<GifDataStream, TAnimator> create)
|
|
|
+ where TAnimator : Animator
|
|
|
+ {
|
|
|
+ if (!sourceStream.CanSeek)
|
|
|
+ throw new ArgumentException("The stream is not seekable");
|
|
|
+ sourceStream.Seek(0, SeekOrigin.Begin);
|
|
|
+ var metadata = await GifDataStream.ReadAsync(sourceStream);
|
|
|
+ return create(metadata);
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion Constructor and factory methods
|
|
|
|
|
|
- #region Animation
|
|
|
+ #region Animation
|
|
|
|
|
|
- public int FrameCount => _metadata.Frames.Count;
|
|
|
+ public int FrameCount => _metadata.Frames.Count;
|
|
|
|
|
|
- private bool _isStarted;
|
|
|
- private CancellationTokenSource _cancellationTokenSource;
|
|
|
+ private bool _isStarted;
|
|
|
+ private CancellationTokenSource _cancellationTokenSource;
|
|
|
|
|
|
- public async void Play()
|
|
|
+ public async void Play()
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- try
|
|
|
+ if (_timingManager.IsComplete)
|
|
|
{
|
|
|
- if (_timingManager.IsComplete)
|
|
|
- {
|
|
|
- _timingManager.Reset();
|
|
|
- _isStarted = false;
|
|
|
- }
|
|
|
-
|
|
|
- if (!_isStarted)
|
|
|
- {
|
|
|
- _cancellationTokenSource?.Dispose();
|
|
|
- _cancellationTokenSource = new CancellationTokenSource();
|
|
|
- _isStarted = true;
|
|
|
- OnAnimationStarted();
|
|
|
- if (_timingManager.IsPaused)
|
|
|
- _timingManager.Resume();
|
|
|
- await RunAsync(_cancellationTokenSource.Token);
|
|
|
- }
|
|
|
- else if (_timingManager.IsPaused)
|
|
|
- {
|
|
|
- _timingManager.Resume();
|
|
|
- }
|
|
|
+ _timingManager.Reset();
|
|
|
+ _isStarted = false;
|
|
|
}
|
|
|
- catch (OperationCanceledException)
|
|
|
+
|
|
|
+ if (!_isStarted)
|
|
|
{
|
|
|
+ _cancellationTokenSource?.Dispose();
|
|
|
+ _cancellationTokenSource = new CancellationTokenSource();
|
|
|
+ _isStarted = true;
|
|
|
+ OnAnimationStarted();
|
|
|
+ if (_timingManager.IsPaused)
|
|
|
+ _timingManager.Resume();
|
|
|
+ await RunAsync(_cancellationTokenSource.Token);
|
|
|
}
|
|
|
- catch (Exception ex)
|
|
|
+ else if (_timingManager.IsPaused)
|
|
|
{
|
|
|
- // ignore errors that might occur during Dispose
|
|
|
- if (!_disposing)
|
|
|
- OnError(ex, AnimationErrorKind.Rendering);
|
|
|
+ _timingManager.Resume();
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- private int _frameIndex;
|
|
|
-
|
|
|
- private async Task RunAsync(CancellationToken cancellationToken)
|
|
|
+ catch (OperationCanceledException)
|
|
|
{
|
|
|
- while (true)
|
|
|
- {
|
|
|
- cancellationToken.ThrowIfCancellationRequested();
|
|
|
- var timing = _timingManager.NextAsync(cancellationToken);
|
|
|
- var rendering = RenderFrameAsync(CurrentFrameIndex, cancellationToken);
|
|
|
- await Task.WhenAll(timing, rendering);
|
|
|
- if (!timing.Result)
|
|
|
- break;
|
|
|
- CurrentFrameIndex = (CurrentFrameIndex + 1) % FrameCount;
|
|
|
- }
|
|
|
}
|
|
|
-
|
|
|
- public void Pause()
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- _timingManager.Pause();
|
|
|
+ // ignore errors that might occur during Dispose
|
|
|
+ if (!_disposing)
|
|
|
+ OnError(ex, AnimationErrorKind.Rendering);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- public bool IsPaused => _timingManager.IsPaused;
|
|
|
+ private int _frameIndex;
|
|
|
|
|
|
- public bool IsComplete
|
|
|
+ private async Task RunAsync(CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ if (_loadFramesDataTask != null)
|
|
|
+ await _loadFramesDataTask;
|
|
|
+ while (true)
|
|
|
{
|
|
|
- get
|
|
|
- {
|
|
|
- if (_isStarted)
|
|
|
- return _timingManager.IsComplete;
|
|
|
- return false;
|
|
|
- }
|
|
|
+ cancellationToken.ThrowIfCancellationRequested();
|
|
|
+ var timing = _timingManager.NextAsync(cancellationToken);
|
|
|
+ var rendering = RenderFrameAsync(CurrentFrameIndex, cancellationToken);
|
|
|
+ await Task.WhenAll(timing, rendering);
|
|
|
+ if (!timing.Result)
|
|
|
+ break;
|
|
|
+ CurrentFrameIndex = (CurrentFrameIndex + 1) % FrameCount;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Pause()
|
|
|
+ {
|
|
|
+ _timingManager.Pause();
|
|
|
+ }
|
|
|
|
|
|
- public event EventHandler CurrentFrameChanged;
|
|
|
+ public bool IsPaused => _timingManager.IsPaused;
|
|
|
|
|
|
- protected virtual void OnCurrentFrameChanged()
|
|
|
+ public bool IsComplete
|
|
|
+ {
|
|
|
+ get
|
|
|
{
|
|
|
- CurrentFrameChanged?.Invoke(this, EventArgs.Empty);
|
|
|
+ if (_isStarted)
|
|
|
+ return _timingManager.IsComplete;
|
|
|
+ return false;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- public event EventHandler<AnimationStartedEventArgs> AnimationStarted;
|
|
|
+ public event EventHandler CurrentFrameChanged;
|
|
|
|
|
|
- protected virtual void OnAnimationStarted()
|
|
|
+ protected virtual void OnCurrentFrameChanged()
|
|
|
+ {
|
|
|
+ CurrentFrameChanged?.Invoke(this, EventArgs.Empty);
|
|
|
+ }
|
|
|
+
|
|
|
+ public event EventHandler<AnimationStartedEventArgs> AnimationStarted;
|
|
|
+
|
|
|
+ protected virtual void OnAnimationStarted()
|
|
|
+ {
|
|
|
+ AnimationStarted?.Invoke(this, new AnimationStartedEventArgs(AnimationSource));
|
|
|
+ }
|
|
|
+
|
|
|
+ public event EventHandler<AnimationCompletedEventArgs> AnimationCompleted;
|
|
|
+
|
|
|
+ protected virtual void OnAnimationCompleted()
|
|
|
+ {
|
|
|
+ AnimationCompleted?.Invoke(this, new AnimationCompletedEventArgs(AnimationSource));
|
|
|
+ }
|
|
|
+
|
|
|
+ public event EventHandler<AnimationErrorEventArgs> Error;
|
|
|
+
|
|
|
+ protected virtual void OnError(Exception ex, AnimationErrorKind kind)
|
|
|
+ {
|
|
|
+ Error?.Invoke(this, new AnimationErrorEventArgs(AnimationSource, ex, kind));
|
|
|
+ }
|
|
|
+
|
|
|
+ public int CurrentFrameIndex
|
|
|
+ {
|
|
|
+ get => _frameIndex;
|
|
|
+ private set
|
|
|
{
|
|
|
- AnimationStarted?.Invoke(this, new AnimationStartedEventArgs(AnimationSource));
|
|
|
+ _frameIndex = value;
|
|
|
+ OnCurrentFrameChanged();
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- public event EventHandler<AnimationCompletedEventArgs> AnimationCompleted;
|
|
|
+ private TimingManager CreateTimingManager(GifDataStream metadata, RepeatBehavior repeatBehavior)
|
|
|
+ {
|
|
|
+ var actualRepeatBehavior = GetActualRepeatBehavior(metadata, repeatBehavior);
|
|
|
|
|
|
- protected virtual void OnAnimationCompleted()
|
|
|
+ var manager = new TimingManager(actualRepeatBehavior);
|
|
|
+ foreach (var frame in metadata.Frames)
|
|
|
{
|
|
|
- AnimationCompleted?.Invoke(this, new AnimationCompletedEventArgs(AnimationSource));
|
|
|
+ manager.Add(GetFrameDelay(frame));
|
|
|
}
|
|
|
|
|
|
- public event EventHandler<AnimationErrorEventArgs> Error;
|
|
|
+ manager.Completed += TimingManagerCompleted;
|
|
|
+ return manager;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static RepeatBehavior GetActualRepeatBehavior(GifDataStream metadata, RepeatBehavior repeatBehavior)
|
|
|
+ {
|
|
|
+ return repeatBehavior == default
|
|
|
+ ? GetRepeatBehaviorFromGif(metadata)
|
|
|
+ : repeatBehavior;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected abstract RepeatBehavior GetSpecifiedRepeatBehavior();
|
|
|
+
|
|
|
+ private void TimingManagerCompleted(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ OnAnimationCompleted();
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion Animation
|
|
|
|
|
|
- protected virtual void OnError(Exception ex, AnimationErrorKind kind)
|
|
|
+ #region Rendering
|
|
|
+
|
|
|
+ private static WriteableBitmap CreateBitmap(GifDataStream metadata)
|
|
|
+ {
|
|
|
+ var desc = metadata.Header.LogicalScreenDescriptor;
|
|
|
+ var bitmap = new WriteableBitmap(desc.Width, desc.Height, 96, 96, PixelFormats.Bgra32, null);
|
|
|
+ return bitmap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Dictionary<int, GifPalette> CreatePalettes(GifDataStream metadata)
|
|
|
+ {
|
|
|
+ var palettes = new Dictionary<int, GifPalette>();
|
|
|
+ Color[] globalColorTable = null;
|
|
|
+ if (metadata.Header.LogicalScreenDescriptor.HasGlobalColorTable)
|
|
|
{
|
|
|
- Error?.Invoke(this, new AnimationErrorEventArgs(AnimationSource, ex, kind));
|
|
|
+ globalColorTable =
|
|
|
+ metadata.GlobalColorTable
|
|
|
+ .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
|
|
|
+ .ToArray();
|
|
|
}
|
|
|
|
|
|
- public int CurrentFrameIndex
|
|
|
+ for (var i = 0; i < metadata.Frames.Count; i++)
|
|
|
{
|
|
|
- get => _frameIndex;
|
|
|
- private set
|
|
|
+ var frame = metadata.Frames[i];
|
|
|
+ var colorTable = globalColorTable;
|
|
|
+ if (frame.Descriptor.HasLocalColorTable)
|
|
|
{
|
|
|
- _frameIndex = value;
|
|
|
- OnCurrentFrameChanged();
|
|
|
+ colorTable =
|
|
|
+ frame.LocalColorTable
|
|
|
+ .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
|
|
|
+ .ToArray();
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- private TimingManager CreateTimingManager(GifDataStream metadata, RepeatBehavior repeatBehavior)
|
|
|
- {
|
|
|
- var actualRepeatBehavior = GetActualRepeatBehavior(metadata, repeatBehavior);
|
|
|
-
|
|
|
- var manager = new TimingManager(actualRepeatBehavior);
|
|
|
- foreach (var frame in metadata.Frames)
|
|
|
+ int? transparencyIndex = null;
|
|
|
+ var gce = frame.GraphicControl;
|
|
|
+ if (gce is { HasTransparency: true })
|
|
|
{
|
|
|
- manager.Add(GetFrameDelay(frame));
|
|
|
+ transparencyIndex = gce.TransparencyIndex;
|
|
|
}
|
|
|
|
|
|
- manager.Completed += TimingManagerCompleted;
|
|
|
- return manager;
|
|
|
+ palettes[i] = new GifPalette(transparencyIndex, colorTable);
|
|
|
}
|
|
|
|
|
|
- private static RepeatBehavior GetActualRepeatBehavior(GifDataStream metadata, RepeatBehavior repeatBehavior)
|
|
|
- {
|
|
|
- return repeatBehavior == default
|
|
|
- ? GetRepeatBehaviorFromGif(metadata)
|
|
|
- : repeatBehavior;
|
|
|
- }
|
|
|
+ return palettes;
|
|
|
+ }
|
|
|
|
|
|
- protected abstract RepeatBehavior GetSpecifiedRepeatBehavior();
|
|
|
+ private static byte[] CreateIndexStreamBuffer(GifDataStream metadata, Stream stream)
|
|
|
+ {
|
|
|
+ // Find the size of the largest frame pixel data
|
|
|
+ // (ignoring the fact that we include the next frame's header)
|
|
|
|
|
|
- private void TimingManagerCompleted(object sender, EventArgs e)
|
|
|
+ var lastSize = stream.Length - metadata.Frames.Last().ImageData.CompressedDataStartOffset;
|
|
|
+ var maxSize = lastSize;
|
|
|
+ if (metadata.Frames.Count > 1)
|
|
|
{
|
|
|
- OnAnimationCompleted();
|
|
|
+ var sizes = metadata.Frames.Zip(metadata.Frames.Skip(1),
|
|
|
+ (f1, f2) => f2.ImageData.CompressedDataStartOffset - f1.ImageData.CompressedDataStartOffset);
|
|
|
+ maxSize = Math.Max(sizes.Max(), lastSize);
|
|
|
}
|
|
|
|
|
|
- #endregion Animation
|
|
|
+ // Need 4 extra bytes so that BitReader doesn't need to check the size for every read
|
|
|
+ return new byte[maxSize + 4];
|
|
|
+ }
|
|
|
+
|
|
|
+ private int _previousFrameIndex;
|
|
|
+ private GifFrame _previousFrame;
|
|
|
|
|
|
- #region Rendering
|
|
|
+ private async Task RenderFrameAsync(int frameIndex, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ if (frameIndex < 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var frame = _metadata.Frames[frameIndex];
|
|
|
+ var desc = frame.Descriptor;
|
|
|
+ var rect = GetFixedUpFrameRect(desc);
|
|
|
|
|
|
- private static WriteableBitmap CreateBitmap(GifDataStream metadata)
|
|
|
+ Stream indexStream = null;
|
|
|
+ if (!_cacheFrameDataInMemory)
|
|
|
{
|
|
|
- var desc = metadata.Header.LogicalScreenDescriptor;
|
|
|
- var bitmap = new WriteableBitmap(desc.Width, desc.Height, 96, 96, PixelFormats.Bgra32, null);
|
|
|
- return bitmap;
|
|
|
+ indexStream = await GetIndexStreamAsync(frame, cancellationToken);
|
|
|
}
|
|
|
|
|
|
- private static Dictionary<int, GifPalette> CreatePalettes(GifDataStream metadata)
|
|
|
+ await using (indexStream)
|
|
|
+ using (_bitmap.LockInScope())
|
|
|
{
|
|
|
- var palettes = new Dictionary<int, GifPalette>();
|
|
|
- Color[] globalColorTable = null;
|
|
|
- if (metadata.Header.LogicalScreenDescriptor.HasGlobalColorTable)
|
|
|
+ if (frameIndex < _previousFrameIndex)
|
|
|
+ ClearArea(_metadata.Header.LogicalScreenDescriptor);
|
|
|
+ else
|
|
|
+ DisposePreviousFrame(frame);
|
|
|
+
|
|
|
+ var bufferLength = 4 * rect.Width;
|
|
|
+ byte[] indexBuffer;
|
|
|
+ var lineBuffer = new byte[bufferLength];
|
|
|
+
|
|
|
+ var palette = _palettes[frameIndex];
|
|
|
+ var transparencyIndex = palette.TransparencyIndex ?? -1;
|
|
|
+
|
|
|
+ var rows = desc.Interlace
|
|
|
+ ? InterlacedRows(rect.Height)
|
|
|
+ : NormalRows(rect.Height);
|
|
|
+
|
|
|
+ if (!_cacheFrameDataInMemory)
|
|
|
{
|
|
|
- globalColorTable =
|
|
|
- metadata.GlobalColorTable
|
|
|
- .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
|
|
|
- .ToArray();
|
|
|
+ indexBuffer = new byte[desc.Width * desc.Height];
|
|
|
+ await indexStream.ReadAllAsync(indexBuffer, 0, indexBuffer.Length, cancellationToken);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ indexBuffer = _cachedFrameBytes[frameIndex];
|
|
|
}
|
|
|
|
|
|
- for (int i = 0; i < metadata.Frames.Count; i++)
|
|
|
+ foreach (var y in rows)
|
|
|
{
|
|
|
- var frame = metadata.Frames[i];
|
|
|
- var colorTable = globalColorTable;
|
|
|
- if (frame.Descriptor.HasLocalColorTable)
|
|
|
+ var offset = (desc.Top + y) * _stride + desc.Left * 4;
|
|
|
+
|
|
|
+ if (transparencyIndex >= 0)
|
|
|
{
|
|
|
- colorTable =
|
|
|
- frame.LocalColorTable
|
|
|
- .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
|
|
|
- .ToArray();
|
|
|
+ CopyFromBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
}
|
|
|
|
|
|
- int? transparencyIndex = null;
|
|
|
- var gce = frame.GraphicControl;
|
|
|
- if (gce is { HasTransparency: true })
|
|
|
+ for (var x = 0; x < rect.Width; x++)
|
|
|
{
|
|
|
- transparencyIndex = gce.TransparencyIndex;
|
|
|
+ var index = indexBuffer[x + y * desc.Width];
|
|
|
+ var i = 4 * x;
|
|
|
+ if (index != transparencyIndex)
|
|
|
+ {
|
|
|
+ WriteColor(lineBuffer, palette[index], i);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- palettes[i] = new GifPalette(transparencyIndex, colorTable);
|
|
|
+ CopyToBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
}
|
|
|
|
|
|
- return palettes;
|
|
|
+ _bitmap.AddDirtyRect(rect);
|
|
|
}
|
|
|
|
|
|
- private static byte[] CreateIndexStreamBuffer(GifDataStream metadata, Stream stream)
|
|
|
- {
|
|
|
- // Find the size of the largest frame pixel data
|
|
|
- // (ignoring the fact that we include the next frame's header)
|
|
|
+ _previousFrame = frame;
|
|
|
+ _previousFrameIndex = frameIndex;
|
|
|
+ }
|
|
|
|
|
|
- long lastSize = stream.Length - metadata.Frames.Last().ImageData.CompressedDataStartOffset;
|
|
|
- long maxSize = lastSize;
|
|
|
- if (metadata.Frames.Count > 1)
|
|
|
- {
|
|
|
- var sizes = metadata.Frames.Zip(metadata.Frames.Skip(1),
|
|
|
- (f1, f2) => f2.ImageData.CompressedDataStartOffset - f1.ImageData.CompressedDataStartOffset);
|
|
|
- maxSize = Math.Max(sizes.Max(), lastSize);
|
|
|
+ private static IEnumerable<int> NormalRows(int height)
|
|
|
+ {
|
|
|
+ return Enumerable.Range(0, height);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static IEnumerable<int> InterlacedRows(int height)
|
|
|
+ {
|
|
|
+ /*
|
|
|
+ * 4 passes:
|
|
|
+ * Pass 1: rows 0, 8, 16, 24...
|
|
|
+ * Pass 2: rows 4, 12, 20, 28...
|
|
|
+ * Pass 3: rows 2, 6, 10, 14...
|
|
|
+ * Pass 4: rows 1, 3, 5, 7...
|
|
|
+ * */
|
|
|
+ var passes = new[]
|
|
|
+ {
|
|
|
+ new { Start = 0, Step = 8 },
|
|
|
+ new { Start = 4, Step = 8 },
|
|
|
+ new { Start = 2, Step = 4 },
|
|
|
+ new { Start = 1, Step = 2 }
|
|
|
+ };
|
|
|
+ foreach (var pass in passes)
|
|
|
+ {
|
|
|
+ var y = pass.Start;
|
|
|
+ while (y < height)
|
|
|
+ {
|
|
|
+ yield return y;
|
|
|
+ y += pass.Step;
|
|
|
}
|
|
|
- // Need 4 extra bytes so that BitReader doesn't need to check the size for every read
|
|
|
- return new byte[maxSize + 4];
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- private int _previousFrameIndex;
|
|
|
- private GifFrame _previousFrame;
|
|
|
+ private static void CopyToBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
|
|
|
+ {
|
|
|
+ Marshal.Copy(buffer, 0, bitmap.BackBuffer + offset, length);
|
|
|
+ }
|
|
|
|
|
|
- private async Task RenderFrameAsync(int frameIndex, CancellationToken cancellationToken)
|
|
|
- {
|
|
|
- await GetWaitLoadSingleFrameTask();
|
|
|
- if (frameIndex < 0)
|
|
|
- return;
|
|
|
+ private static void CopyFromBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
|
|
|
+ {
|
|
|
+ Marshal.Copy(bitmap.BackBuffer + offset, buffer, 0, length);
|
|
|
+ }
|
|
|
|
|
|
- var frame = _metadata.Frames[frameIndex];
|
|
|
- var desc = frame.Descriptor;
|
|
|
- var rect = GetFixedUpFrameRect(desc);
|
|
|
+ private static void WriteColor(byte[] lineBuffer, Color color, int startIndex)
|
|
|
+ {
|
|
|
+ lineBuffer[startIndex] = color.B;
|
|
|
+ lineBuffer[startIndex + 1] = color.G;
|
|
|
+ lineBuffer[startIndex + 2] = color.R;
|
|
|
+ lineBuffer[startIndex + 3] = color.A;
|
|
|
+ }
|
|
|
|
|
|
- Stream indexStream = null;
|
|
|
- if (!_cacheFrameDataInMemory)
|
|
|
- {
|
|
|
- indexStream = await GetIndexStreamAsync(frame, cancellationToken);
|
|
|
- }
|
|
|
- using (indexStream)
|
|
|
- using (_bitmap.LockInScope())
|
|
|
+ private void DisposePreviousFrame(GifFrame currentFrame)
|
|
|
+ {
|
|
|
+ var pgce = _previousFrame?.GraphicControl;
|
|
|
+ if (pgce != null)
|
|
|
+ {
|
|
|
+ switch (pgce.DisposalMethod)
|
|
|
{
|
|
|
- if (frameIndex < _previousFrameIndex)
|
|
|
- ClearArea(_metadata.Header.LogicalScreenDescriptor);
|
|
|
- else
|
|
|
- DisposePreviousFrame(frame);
|
|
|
-
|
|
|
- int bufferLength = 4 * rect.Width;
|
|
|
- byte[] indexBuffer = new byte[desc.Width];
|
|
|
- byte[] lineBuffer = new byte[bufferLength];
|
|
|
-
|
|
|
- var palette = _palettes[frameIndex];
|
|
|
- int transparencyIndex = palette.TransparencyIndex ?? -1;
|
|
|
-
|
|
|
- var rows = desc.Interlace
|
|
|
- ? InterlacedRows(rect.Height)
|
|
|
- : NormalRows(rect.Height);
|
|
|
-
|
|
|
- foreach (int y in rows)
|
|
|
- {
|
|
|
- if (!_cacheFrameDataInMemory)
|
|
|
+ case GifFrameDisposalMethod.None:
|
|
|
+ case GifFrameDisposalMethod.DoNotDispose:
|
|
|
{
|
|
|
- await indexStream.ReadAllAsync(indexBuffer, 0, desc.Width, cancellationToken);
|
|
|
+ // Leave previous frame in place
|
|
|
+ break;
|
|
|
}
|
|
|
- else
|
|
|
+ case GifFrameDisposalMethod.RestoreBackground:
|
|
|
{
|
|
|
- indexBuffer = _cachedFrameBytes[frameIndex][y];
|
|
|
+ ClearArea(GetFixedUpFrameRect(_previousFrame.Descriptor));
|
|
|
+ break;
|
|
|
}
|
|
|
-
|
|
|
- int offset = (desc.Top + y) * _stride + desc.Left * 4;
|
|
|
-
|
|
|
- if (transparencyIndex >= 0)
|
|
|
+ case GifFrameDisposalMethod.RestorePrevious:
|
|
|
{
|
|
|
- CopyFromBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
+ CopyToBitmap(_previousBackBuffer, _bitmap, 0, _previousBackBuffer.Length);
|
|
|
+ var desc = _metadata.Header.LogicalScreenDescriptor;
|
|
|
+ var rect = new Int32Rect(0, 0, desc.Width, desc.Height);
|
|
|
+ _bitmap.AddDirtyRect(rect);
|
|
|
+ break;
|
|
|
}
|
|
|
-
|
|
|
- for (int x = 0; x < rect.Width; x++)
|
|
|
- {
|
|
|
- byte index = indexBuffer[x];
|
|
|
- int i = 4 * x;
|
|
|
- if (index != transparencyIndex)
|
|
|
- {
|
|
|
- WriteColor(lineBuffer, palette[index], i);
|
|
|
- }
|
|
|
- }
|
|
|
- CopyToBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
- }
|
|
|
- _bitmap.AddDirtyRect(rect);
|
|
|
}
|
|
|
-
|
|
|
- _previousFrame = frame;
|
|
|
- _previousFrameIndex = frameIndex;
|
|
|
}
|
|
|
|
|
|
- private void NotifyOfFrameLoaded()
|
|
|
+ var gce = currentFrame.GraphicControl;
|
|
|
+ if (gce is { DisposalMethod: GifFrameDisposalMethod.RestorePrevious })
|
|
|
{
|
|
|
- lock (_lockObject)
|
|
|
- {
|
|
|
- _frameLoadedEvent.SetResult(true);
|
|
|
- _frameLoadedEvent = new TaskCompletionSource<bool>();
|
|
|
- }
|
|
|
+ CopyFromBitmap(_previousBackBuffer, _bitmap, 0, _previousBackBuffer.Length);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- private Task GetWaitLoadSingleFrameTask()
|
|
|
- {
|
|
|
- if (_frameLoadedEvent == null)
|
|
|
- return CompletedTask; // avoiding lock statement if loading frames completed
|
|
|
- lock (_lockObject)
|
|
|
- {
|
|
|
- return _frameLoadedEvent.Task ?? CompletedTask;
|
|
|
- }
|
|
|
- }
|
|
|
+ private void ClearArea(IGifRect rect)
|
|
|
+ {
|
|
|
+ ClearArea(new Int32Rect(rect.Left, rect.Top, rect.Width, rect.Height));
|
|
|
+ }
|
|
|
|
|
|
- private static IEnumerable<int> NormalRows(int height)
|
|
|
+ private void ClearArea(Int32Rect rect)
|
|
|
+ {
|
|
|
+ var bufferLength = 4 * rect.Width;
|
|
|
+ var lineBuffer = new byte[bufferLength];
|
|
|
+ for (var y = 0; y < rect.Height; y++)
|
|
|
{
|
|
|
- return Enumerable.Range(0, height);
|
|
|
+ var offset = (rect.Y + y) * _stride + 4 * rect.X;
|
|
|
+ CopyToBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
}
|
|
|
|
|
|
- private static IEnumerable<int> InterlacedRows(int height)
|
|
|
- {
|
|
|
- /*
|
|
|
- * 4 passes:
|
|
|
- * Pass 1: rows 0, 8, 16, 24...
|
|
|
- * Pass 2: rows 4, 12, 20, 28...
|
|
|
- * Pass 3: rows 2, 6, 10, 14...
|
|
|
- * Pass 4: rows 1, 3, 5, 7...
|
|
|
- * */
|
|
|
- var passes = new[]
|
|
|
- {
|
|
|
- new { Start = 0, Step = 8 },
|
|
|
- new { Start = 4, Step = 8 },
|
|
|
- new { Start = 2, Step = 4 },
|
|
|
- new { Start = 1, Step = 2 }
|
|
|
- };
|
|
|
- foreach (var pass in passes)
|
|
|
- {
|
|
|
- int y = pass.Start;
|
|
|
- while (y < height)
|
|
|
- {
|
|
|
- yield return y;
|
|
|
- y += pass.Step;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ _bitmap.AddDirtyRect(new Int32Rect(rect.X, rect.Y, rect.Width, rect.Height));
|
|
|
+ }
|
|
|
|
|
|
- private static void CopyToBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
|
|
|
+ private async Task<Stream> GetIndexStreamAsync(GifFrame frame, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var data = frame.ImageData;
|
|
|
+ cancellationToken.ThrowIfCancellationRequested();
|
|
|
+ _sourceStream.Seek(data.CompressedDataStartOffset, SeekOrigin.Begin);
|
|
|
+ using (var ms = new MemoryStream(_indexStreamBuffer))
|
|
|
{
|
|
|
- Marshal.Copy(buffer, 0, bitmap.BackBuffer + offset, length);
|
|
|
+ await GifHelpers.CopyDataBlocksToStreamAsync(_sourceStream, ms, cancellationToken)
|
|
|
+ .ConfigureAwait(false);
|
|
|
}
|
|
|
|
|
|
- private static void CopyFromBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
|
|
|
- {
|
|
|
- Marshal.Copy(bitmap.BackBuffer + offset, buffer, 0, length);
|
|
|
- }
|
|
|
+ var lzwStream = new LzwDecompressStream(_indexStreamBuffer, data.LzwMinimumCodeSize);
|
|
|
+ return lzwStream;
|
|
|
+ }
|
|
|
|
|
|
- private static void WriteColor(byte[] lineBuffer, Color color, int startIndex)
|
|
|
- {
|
|
|
- lineBuffer[startIndex] = color.B;
|
|
|
- lineBuffer[startIndex + 1] = color.G;
|
|
|
- lineBuffer[startIndex + 2] = color.R;
|
|
|
- lineBuffer[startIndex + 3] = color.A;
|
|
|
- }
|
|
|
+ private async Task GetIndexBytesAsync(int frameIndex, byte[] buffer)
|
|
|
+ {
|
|
|
+ var startPosition = _metadata.Frames[frameIndex].ImageData.CompressedDataStartOffset;
|
|
|
|
|
|
- private void DisposePreviousFrame(GifFrame currentFrame)
|
|
|
- {
|
|
|
- var pgce = _previousFrame?.GraphicControl;
|
|
|
- if (pgce != null)
|
|
|
- {
|
|
|
- switch (pgce.DisposalMethod)
|
|
|
- {
|
|
|
- case GifFrameDisposalMethod.None:
|
|
|
- case GifFrameDisposalMethod.DoNotDispose:
|
|
|
- {
|
|
|
- // Leave previous frame in place
|
|
|
- break;
|
|
|
- }
|
|
|
- case GifFrameDisposalMethod.RestoreBackground:
|
|
|
- {
|
|
|
- ClearArea(GetFixedUpFrameRect(_previousFrame.Descriptor));
|
|
|
- break;
|
|
|
- }
|
|
|
- case GifFrameDisposalMethod.RestorePrevious:
|
|
|
- {
|
|
|
- CopyToBitmap(_previousBackBuffer, _bitmap, 0, _previousBackBuffer.Length);
|
|
|
- var desc = _metadata.Header.LogicalScreenDescriptor;
|
|
|
- var rect = new Int32Rect(0, 0, desc.Width, desc.Height);
|
|
|
- _bitmap.AddDirtyRect(rect);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ _sourceStream.Seek(startPosition, SeekOrigin.Begin);
|
|
|
+ using var memoryStream = new MemoryStream(buffer);
|
|
|
+ await GifHelpers.CopyDataBlocksToStreamAsync(_sourceStream, memoryStream).ConfigureAwait(false);
|
|
|
+ }
|
|
|
|
|
|
- var gce = currentFrame.GraphicControl;
|
|
|
- if (gce is { DisposalMethod: GifFrameDisposalMethod.RestorePrevious })
|
|
|
- {
|
|
|
- CopyFromBitmap(_previousBackBuffer, _bitmap, 0, _previousBackBuffer.Length);
|
|
|
- }
|
|
|
- }
|
|
|
+ internal BitmapSource Bitmap => _bitmap;
|
|
|
+
|
|
|
+ #endregion Rendering
|
|
|
+
|
|
|
+ #region Helper methods
|
|
|
|
|
|
- private void ClearArea(IGifRect rect)
|
|
|
+ private static TimeSpan GetFrameDelay(GifFrame frame)
|
|
|
+ {
|
|
|
+ var gce = frame.GraphicControl;
|
|
|
+ if (gce != null)
|
|
|
{
|
|
|
- ClearArea(new Int32Rect(rect.Left, rect.Top, rect.Width, rect.Height));
|
|
|
+ if (gce.Delay != 0)
|
|
|
+ return TimeSpan.FromMilliseconds(gce.Delay);
|
|
|
}
|
|
|
|
|
|
- private void ClearArea(Int32Rect rect)
|
|
|
- {
|
|
|
- int bufferLength = 4 * rect.Width;
|
|
|
- byte[] lineBuffer = new byte[bufferLength];
|
|
|
- for (int y = 0; y < rect.Height; y++)
|
|
|
- {
|
|
|
- int offset = (rect.Y + y) * _stride + 4 * rect.X;
|
|
|
- CopyToBitmap(lineBuffer, _bitmap, offset, bufferLength);
|
|
|
- }
|
|
|
+ return TimeSpan.FromMilliseconds(100);
|
|
|
+ }
|
|
|
|
|
|
- _bitmap.AddDirtyRect(new Int32Rect(rect.X, rect.Y, rect.Width, rect.Height));
|
|
|
- }
|
|
|
+ private static RepeatBehavior GetRepeatBehaviorFromGif(GifDataStream metadata)
|
|
|
+ {
|
|
|
+ if (metadata.RepeatCount == 0)
|
|
|
+ return RepeatBehavior.Forever;
|
|
|
+ return new RepeatBehavior(metadata.RepeatCount);
|
|
|
+ }
|
|
|
|
|
|
- private async Task<Stream> GetIndexStreamAsync(GifFrame frame, CancellationToken cancellationToken)
|
|
|
- {
|
|
|
- var data = frame.ImageData;
|
|
|
- cancellationToken.ThrowIfCancellationRequested();
|
|
|
- _sourceStream.Seek(data.CompressedDataStartOffset, SeekOrigin.Begin);
|
|
|
- using (var ms = new MemoryStream(_indexStreamBuffer))
|
|
|
- {
|
|
|
- await GifHelpers.CopyDataBlocksToStreamAsync(_sourceStream, ms, cancellationToken).ConfigureAwait(false);
|
|
|
- }
|
|
|
- var lzwStream = new LzwDecompressStream(_indexStreamBuffer, data.LzwMinimumCodeSize);
|
|
|
- return lzwStream;
|
|
|
- }
|
|
|
+ private Int32Rect GetFixedUpFrameRect(GifImageDescriptor desc)
|
|
|
+ {
|
|
|
+ var width = Math.Min(desc.Width, _bitmap.PixelWidth - desc.Left);
|
|
|
+ var height = Math.Min(desc.Height, _bitmap.PixelHeight - desc.Top);
|
|
|
+ return new Int32Rect(desc.Left, desc.Top, width, height);
|
|
|
+ }
|
|
|
|
|
|
- private async Task GetIndexBytesAsync(int frameIndex, byte[] buffer)
|
|
|
- {
|
|
|
- var startPosition = _metadata.Frames[frameIndex].ImageData.CompressedDataStartOffset;
|
|
|
+ #endregion Helper methods
|
|
|
|
|
|
- _sourceStream.Seek(startPosition, SeekOrigin.Begin);
|
|
|
- using var memoryStream = new MemoryStream(buffer);
|
|
|
- await GifHelpers.CopyDataBlocksToStreamAsync(_sourceStream, memoryStream).ConfigureAwait(false);
|
|
|
- }
|
|
|
+ #region Finalizer and Dispose
|
|
|
|
|
|
- internal BitmapSource Bitmap => _bitmap;
|
|
|
+ ~Animator()
|
|
|
+ {
|
|
|
+ Dispose(false);
|
|
|
+ }
|
|
|
|
|
|
- #endregion Rendering
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ Dispose(true);
|
|
|
+ GC.SuppressFinalize(this);
|
|
|
+ }
|
|
|
|
|
|
- #region Helper methods
|
|
|
+ private volatile bool _disposing;
|
|
|
+ private bool _disposed;
|
|
|
|
|
|
- private static TimeSpan GetFrameDelay(GifFrame frame)
|
|
|
+ protected virtual void Dispose(bool disposing)
|
|
|
+ {
|
|
|
+ if (!_disposed)
|
|
|
{
|
|
|
- var gce = frame.GraphicControl;
|
|
|
- if (gce != null)
|
|
|
+ _disposing = true;
|
|
|
+ if (_timingManager != null) _timingManager.Completed -= TimingManagerCompleted;
|
|
|
+ _cancellationTokenSource?.Cancel();
|
|
|
+ if (_isSourceStreamOwner)
|
|
|
{
|
|
|
- if (gce.Delay != 0)
|
|
|
- return TimeSpan.FromMilliseconds(gce.Delay);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ _sourceStream?.Dispose();
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ /* ignored */
|
|
|
+ }
|
|
|
}
|
|
|
- return TimeSpan.FromMilliseconds(100);
|
|
|
- }
|
|
|
|
|
|
- private static RepeatBehavior GetRepeatBehaviorFromGif(GifDataStream metadata)
|
|
|
- {
|
|
|
- if (metadata.RepeatCount == 0)
|
|
|
- return RepeatBehavior.Forever;
|
|
|
- return new RepeatBehavior(metadata.RepeatCount);
|
|
|
- }
|
|
|
-
|
|
|
- private Int32Rect GetFixedUpFrameRect(GifImageDescriptor desc)
|
|
|
- {
|
|
|
- int width = Math.Min(desc.Width, _bitmap.PixelWidth - desc.Left);
|
|
|
- int height = Math.Min(desc.Height, _bitmap.PixelHeight - desc.Top);
|
|
|
- return new Int32Rect(desc.Left, desc.Top, width, height);
|
|
|
+ _disposed = true;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- #endregion Helper methods
|
|
|
+ #endregion Finalizer and Dispose
|
|
|
|
|
|
- #region Finalizer and Dispose
|
|
|
+ public override string ToString()
|
|
|
+ {
|
|
|
+ var s = _sourceUri?.ToString() ?? _sourceStream.ToString();
|
|
|
+ return "GIF: " + s;
|
|
|
+ }
|
|
|
|
|
|
- ~Animator()
|
|
|
- {
|
|
|
- Dispose(false);
|
|
|
- }
|
|
|
+ private class GifPalette
|
|
|
+ {
|
|
|
+ private readonly Color[] _colors;
|
|
|
|
|
|
- public void Dispose()
|
|
|
+ public GifPalette(int? transparencyIndex, Color[] colors)
|
|
|
{
|
|
|
- Dispose(true);
|
|
|
- GC.SuppressFinalize(this);
|
|
|
+ TransparencyIndex = transparencyIndex;
|
|
|
+ _colors = colors;
|
|
|
}
|
|
|
|
|
|
- private volatile bool _disposing;
|
|
|
- private bool _disposed;
|
|
|
+ public int? TransparencyIndex { get; }
|
|
|
|
|
|
- protected virtual void Dispose(bool disposing)
|
|
|
- {
|
|
|
- if (!_disposed)
|
|
|
- {
|
|
|
- _disposing = true;
|
|
|
- if (_timingManager != null) _timingManager.Completed -= TimingManagerCompleted;
|
|
|
- _cancellationTokenSource?.Cancel();
|
|
|
- if (_isSourceStreamOwner)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- _sourceStream?.Dispose();
|
|
|
- }
|
|
|
- catch
|
|
|
- {
|
|
|
- /* ignored */
|
|
|
- }
|
|
|
- }
|
|
|
- _disposed = true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- #endregion Finalizer and Dispose
|
|
|
+ public Color this[int i] => _colors[i];
|
|
|
+ }
|
|
|
|
|
|
- public override string ToString()
|
|
|
+ internal async Task ShowFirstFrameAsync()
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- string s = _sourceUri?.ToString() ?? _sourceStream.ToString();
|
|
|
- return "GIF: " + s;
|
|
|
+ if (_loadFramesDataTask != null)
|
|
|
+ await _loadFramesDataTask;
|
|
|
+ await RenderFrameAsync(0, CancellationToken.None).ConfigureAwait(false);
|
|
|
+ CurrentFrameIndex = 0;
|
|
|
+ _timingManager.Pause();
|
|
|
}
|
|
|
-
|
|
|
- private class GifPalette
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- private readonly Color[] _colors;
|
|
|
-
|
|
|
- public GifPalette(int? transparencyIndex, Color[] colors)
|
|
|
- {
|
|
|
- TransparencyIndex = transparencyIndex;
|
|
|
- _colors = colors;
|
|
|
- }
|
|
|
-
|
|
|
- public int? TransparencyIndex { get; }
|
|
|
-
|
|
|
- public Color this[int i] => _colors[i];
|
|
|
+ OnError(ex, AnimationErrorKind.Rendering);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- internal async Task ShowFirstFrameAsync()
|
|
|
+ public async void Rewind()
|
|
|
+ {
|
|
|
+ CurrentFrameIndex = 0;
|
|
|
+ var isStopped = _timingManager.IsPaused || _timingManager.IsComplete;
|
|
|
+ _timingManager.Reset();
|
|
|
+ if (isStopped)
|
|
|
{
|
|
|
+ _timingManager.Pause();
|
|
|
+ _isStarted = false;
|
|
|
try
|
|
|
{
|
|
|
await RenderFrameAsync(0, CancellationToken.None);
|
|
|
- CurrentFrameIndex = 0;
|
|
|
- _timingManager.Pause();
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
OnError(ex, AnimationErrorKind.Rendering);
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- public async void Rewind()
|
|
|
- {
|
|
|
- CurrentFrameIndex = 0;
|
|
|
- bool isStopped = _timingManager.IsPaused || _timingManager.IsComplete;
|
|
|
- _timingManager.Reset();
|
|
|
- if (isStopped)
|
|
|
- {
|
|
|
- _timingManager.Pause();
|
|
|
- _isStarted = false;
|
|
|
- try
|
|
|
- {
|
|
|
- await RenderFrameAsync(0, CancellationToken.None);
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- OnError(ex, AnimationErrorKind.Rendering);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- protected abstract object AnimationSource { get; }
|
|
|
+ protected abstract object AnimationSource { get; }
|
|
|
|
|
|
- internal void OnRepeatBehaviorChanged()
|
|
|
- {
|
|
|
- if (_timingManager == null)
|
|
|
- return;
|
|
|
+ internal void OnRepeatBehaviorChanged()
|
|
|
+ {
|
|
|
+ if (_timingManager == null)
|
|
|
+ return;
|
|
|
|
|
|
- var newValue = GetSpecifiedRepeatBehavior();
|
|
|
- var newActualValue = GetActualRepeatBehavior(_metadata, newValue);
|
|
|
- if (_timingManager.RepeatBehavior == newActualValue)
|
|
|
- return;
|
|
|
+ var newValue = GetSpecifiedRepeatBehavior();
|
|
|
+ var newActualValue = GetActualRepeatBehavior(_metadata, newValue);
|
|
|
+ if (_timingManager.RepeatBehavior == newActualValue)
|
|
|
+ return;
|
|
|
|
|
|
- _timingManager.RepeatBehavior = newActualValue;
|
|
|
- Rewind();
|
|
|
- }
|
|
|
+ _timingManager.RepeatBehavior = newActualValue;
|
|
|
+ Rewind();
|
|
|
}
|
|
|
}
|