Bläddra i källkod

Animated GIF check

Ruben 2 år sedan
förälder
incheckning
35d3a81ac9
49 ändrade filer med 3228 tillägg och 45 borttagningar
  1. 6 0
      src/PicView.sln
  2. 5 0
      src/PicView/ChangeImage/ErrorHandling.cs
  3. 2 2
      src/PicView/ChangeImage/FastPic.cs
  4. 1 1
      src/PicView/ChangeImage/LoadPic.cs
  5. 43 37
      src/PicView/ChangeImage/UpdateImage.cs
  6. 7 5
      src/PicView/PicView.csproj
  7. 602 0
      src/XamlAnimatedGif/AnimationBehavior.cs
  8. 14 0
      src/XamlAnimatedGif/AnimationCompletedEventArgs.cs
  9. 27 0
      src/XamlAnimatedGif/AnimationErrorEventArgs.cs
  10. 14 0
      src/XamlAnimatedGif/AnimationStartedEventArgs.cs
  11. 711 0
      src/XamlAnimatedGif/Animator.cs
  12. 62 0
      src/XamlAnimatedGif/BrushAnimator.cs
  13. 39 0
      src/XamlAnimatedGif/CancellationExtensions.cs
  14. 51 0
      src/XamlAnimatedGif/Decoding/GifApplicationExtension.cs
  15. 26 0
      src/XamlAnimatedGif/Decoding/GifBlock.cs
  16. 10 0
      src/XamlAnimatedGif/Decoding/GifBlockKind.cs
  17. 21 0
      src/XamlAnimatedGif/Decoding/GifColor.cs
  18. 37 0
      src/XamlAnimatedGif/Decoding/GifCommentExtension.cs
  19. 98 0
      src/XamlAnimatedGif/Decoding/GifDataStream.cs
  20. 17 0
      src/XamlAnimatedGif/Decoding/GifDecoderException.cs
  21. 28 0
      src/XamlAnimatedGif/Decoding/GifExtension.cs
  22. 50 0
      src/XamlAnimatedGif/Decoding/GifFrame.cs
  23. 10 0
      src/XamlAnimatedGif/Decoding/GifFrameDisposalMethod.cs
  24. 54 0
      src/XamlAnimatedGif/Decoding/GifGraphicControlExtension.cs
  25. 39 0
      src/XamlAnimatedGif/Decoding/GifHeader.cs
  26. 114 0
      src/XamlAnimatedGif/Decoding/GifHelpers.cs
  27. 29 0
      src/XamlAnimatedGif/Decoding/GifImageData.cs
  28. 45 0
      src/XamlAnimatedGif/Decoding/GifImageDescriptor.cs
  29. 55 0
      src/XamlAnimatedGif/Decoding/GifLogicalScreenDescriptor.cs
  30. 69 0
      src/XamlAnimatedGif/Decoding/GifPlainTextExtension.cs
  31. 23 0
      src/XamlAnimatedGif/Decoding/GifTrailer.cs
  32. 10 0
      src/XamlAnimatedGif/Decoding/IGifRect.cs
  33. 17 0
      src/XamlAnimatedGif/Decoding/InvalidBlockSizeException.cs
  34. 18 0
      src/XamlAnimatedGif/Decoding/InvalidSignatureException.cs
  35. 18 0
      src/XamlAnimatedGif/Decoding/UnknownBlockTypeException.cs
  36. 18 0
      src/XamlAnimatedGif/Decoding/UnknownExtensionTypeException.cs
  37. 18 0
      src/XamlAnimatedGif/Decoding/UnsupportedGifVersionException.cs
  38. 56 0
      src/XamlAnimatedGif/Decompression/BitReader.cs
  39. 251 0
      src/XamlAnimatedGif/Decompression/LzwDecompressStream.cs
  40. 16 0
      src/XamlAnimatedGif/DownloadProgressEventArgs.cs
  41. 17 0
      src/XamlAnimatedGif/Extensions/BitArrayExtensions.cs
  42. 79 0
      src/XamlAnimatedGif/Extensions/StreamExtensions.cs
  43. 29 0
      src/XamlAnimatedGif/Extensions/WritableBitmapExtensions.cs
  44. 55 0
      src/XamlAnimatedGif/ImageAnimator.cs
  45. 13 0
      src/XamlAnimatedGif/Properties/Xmlns.cs
  46. 130 0
      src/XamlAnimatedGif/TimingManager.cs
  47. 134 0
      src/XamlAnimatedGif/UriLoader.cs
  48. 40 0
      src/XamlAnimatedGif/XamlAnimatedGif.csproj
  49. BIN
      src/XamlAnimatedGif/XamlAnimatedGif.snk

+ 6 - 0
src/PicView.sln

@@ -10,6 +10,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PicView", "PicView\PicView.
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PicView.Tools", "PicView.Tools\PicView.Tools.csproj", "{47DE1EC3-CD33-43E1-857F-4820C6AD16B6}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamlAnimatedGif", "XamlAnimatedGif\XamlAnimatedGif.csproj", "{1D4E804D-33E8-46CC-B4FC-AC6A84F5085A}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -24,6 +26,10 @@ Global
 		{47DE1EC3-CD33-43E1-857F-4820C6AD16B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{47DE1EC3-CD33-43E1-857F-4820C6AD16B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{47DE1EC3-CD33-43E1-857F-4820C6AD16B6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1D4E804D-33E8-46CC-B4FC-AC6A84F5085A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1D4E804D-33E8-46CC-B4FC-AC6A84F5085A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1D4E804D-33E8-46CC-B4FC-AC6A84F5085A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1D4E804D-33E8-46CC-B4FC-AC6A84F5085A}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 5 - 0
src/PicView/ChangeImage/ErrorHandling.cs

@@ -43,6 +43,11 @@ internal static class ErrorHandling
             ConfigureWindows.GetMainWindow.TitleText.Text = (string)Application.Current.Resources["UnexpectedError"];
             ConfigureWindows.GetMainWindow.TitleText.ToolTip = (string)Application.Current.Resources["UnexpectedError"];
             ConfigureWindows.GetMainWindow.MainImage.Cursor = Cursors.Arrow;
+
+            if (UC.GetSpinWaiter is { IsVisible: true })
+            {
+                UC.GetSpinWaiter.Visibility = Visibility.Collapsed;
+            }
         });
     }
 

+ 2 - 2
src/PicView/ChangeImage/FastPic.cs

@@ -59,7 +59,7 @@ internal static class FastPic
             }
         }
 
-        UpdateImage.UpdateImageValues(index, preLoadValue);
+        await UpdateImage.UpdateImageValuesAsync(index, preLoadValue).ConfigureAwait(false);
 
         _updateSource = false;
         await PreLoader.PreLoadAsync(index, Pics.Count).ConfigureAwait(false);
@@ -89,6 +89,6 @@ internal static class FastPic
         {
             await Task.Delay(10).ConfigureAwait(false);
         }
-        UpdateImage.UpdateImageValues(FolderIndex, preLoadValue);
+        await UpdateImage.UpdateImageValuesAsync(FolderIndex, preLoadValue).ConfigureAwait(false);
     }
 }

+ 1 - 1
src/PicView/ChangeImage/LoadPic.cs

@@ -418,7 +418,7 @@ internal static class LoadPic
             return; // Skip loading if user went to next value
         }
 
-        UpdateImage.UpdateImageValues(index, preLoadValue);
+        await UpdateImage.UpdateImageValuesAsync(index, preLoadValue).ConfigureAwait(false);
 
         if (ConfigureWindows.GetImageInfoWindow is { IsVisible: true })
             _ = ImageInfo.UpdateValuesAsync(preLoadValue.FileInfo).ConfigureAwait(false);

+ 43 - 37
src/PicView/ChangeImage/UpdateImage.cs

@@ -6,6 +6,8 @@ using PicView.SystemIntegration;
 using PicView.UILogic;
 using PicView.UILogic.Sizing;
 using PicView.UILogic.TransformImage;
+using System.IO;
+using System.Reflection;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Media.Imaging;
@@ -29,27 +31,19 @@ internal static class UpdateImage
     /// </summary>
     /// <param name="index"></param>
     /// <param name="preLoadValue"></param>
-    internal static void UpdateImageValues(int index, PreLoadValue preLoadValue)
+    internal static async Task UpdateImageValuesAsync(int index, PreLoadValue preLoadValue)
     {
         preLoadValue.BitmapSource ??= ImageFunctions.ImageErrorMessage();
-        var isGif = preLoadValue.FileInfo.Extension.Equals(".gif", StringComparison.OrdinalIgnoreCase);
 
-        ConfigureWindows.GetMainWindow.MainImage.Dispatcher.Invoke(() =>
+        await ConfigureWindows.GetMainWindow.MainImage.Dispatcher.InvokeAsync(() =>
         {
-            if (isGif) // Loads gif from XamlAnimatedGif if necessary
-            {
-                AnimationBehavior.SetSourceUri(ConfigureWindows.GetMainWindow.MainImage, new Uri(Pics?[index]));
-            }
-            else
-            {
-                ConfigureWindows.GetMainWindow.MainImage.Source = preLoadValue.BitmapSource;
-            }
+            ConfigureWindows.GetMainWindow.MainImage.Source = preLoadValue.BitmapSource;
         }, DispatcherPriority.Send);
 
         var titleString = TitleString(preLoadValue.BitmapSource.PixelWidth, preLoadValue.BitmapSource.PixelHeight,
             index, preLoadValue.FileInfo);
 
-        ConfigureWindows.GetMainWindow.Dispatcher.Invoke(() =>
+        await ConfigureWindows.GetMainWindow.Dispatcher.InvokeAsync(() =>
         {
             if (Rotation.RotationAngle is not 0)
             {
@@ -85,6 +79,15 @@ internal static class UpdateImage
             }
         }, DispatcherPriority.Send);
 
+        if (preLoadValue.FileInfo.Extension.Equals(".gif", StringComparison.OrdinalIgnoreCase))
+        {
+            var uri = new Uri(Pics?[index]);
+            await ConfigureWindows.GetMainWindow.MainImage.Dispatcher.InvokeAsync(() =>
+            {
+                AnimationBehavior.SetSourceUri(ConfigureWindows.GetMainWindow.MainImage, uri);
+            }, DispatcherPriority.Normal);
+        }
+
         if (GetToolTipMessage is { IsVisible: true })
             ConfigureWindows.GetMainWindow.Dispatcher.Invoke(() => GetToolTipMessage.Visibility = Visibility.Hidden);
     }
@@ -98,6 +101,11 @@ internal static class UpdateImage
     /// <param name="file"></param>
     internal static async Task UpdateImageAsync(string name, BitmapSource? bitmapSource, bool isGif = false, string? file = null)
     {
+        if (bitmapSource is null)
+        {
+            UnexpectedError();
+            return;
+        }
         if (GetPicGallery is not null)
         {
             await GetPicGallery.Dispatcher.InvokeAsync(() =>
@@ -106,6 +114,25 @@ internal static class UpdateImage
             }, DispatcherPriority.Send);
         }
 
+        await ConfigureWindows.GetMainWindow.Dispatcher.InvokeAsync(() =>
+        {
+            ToggleStartUpUC(true);
+
+            if (Settings.Default.ScrollEnabled)
+            {
+                ConfigureWindows.GetMainWindow.Scroller.ScrollToTop();
+            }
+
+            ConfigureWindows.GetMainWindow.MainImage.Source = bitmapSource;
+            SetTitleString(bitmapSource.PixelWidth, bitmapSource.PixelHeight, name);
+            FitImage(bitmapSource.PixelWidth, bitmapSource.PixelHeight);
+
+            if (GetSpinWaiter is { IsVisible: true })
+            {
+                GetSpinWaiter.Visibility = Visibility.Collapsed;
+            }
+        }, DispatcherPriority.Send);
+
         Size? imageSize = null;
         if (isGif)
         {
@@ -117,16 +144,9 @@ internal static class UpdateImage
             imageSize = ImageSizeFunctions.GetImageSize(file);
         }
 
-        await ConfigureWindows.GetMainWindow.Dispatcher.InvokeAsync(() =>
+        if (isGif)
         {
-            ToggleStartUpUC(true);
-
-            if (Settings.Default.ScrollEnabled)
-            {
-                ConfigureWindows.GetMainWindow.Scroller.ScrollToTop();
-            }
-
-            if (isGif)
+            await ConfigureWindows.GetMainWindow.Dispatcher.InvokeAsync(() =>
             {
                 if (imageSize.HasValue)
                 {
@@ -134,22 +154,8 @@ internal static class UpdateImage
                     SetTitleString((int)imageSize.Value.Width, (int)imageSize.Value.Height, name);
                 }
                 AnimationBehavior.SetSourceUri(ConfigureWindows.GetMainWindow.MainImage, new Uri(file));
-            }
-            else if (bitmapSource != null)
-            {
-                ConfigureWindows.GetMainWindow.MainImage.Source = bitmapSource;
-                SetTitleString(bitmapSource.PixelWidth, bitmapSource.PixelHeight, name);
-                FitImage(bitmapSource.PixelWidth, bitmapSource.PixelHeight);
-            }
-            else
-            {
-                UnexpectedError();
-            }
-            if (GetSpinWaiter is { IsVisible: true })
-            {
-                GetSpinWaiter.Visibility = Visibility.Collapsed;
-            }
-        }, DispatcherPriority.Send);
+            }, DispatcherPriority.Normal);
+        }
 
         CloseToolTipMessage();
         Pics?.Clear();

+ 7 - 5
src/PicView/PicView.csproj

@@ -181,11 +181,11 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="Autoupdater.NET.Official" Version="1.8.4" />
-    <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="13.3.0" />
-    <PackageReference Include="Magick.NET.SystemDrawing" Version="7.1.0">
+    <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="13.4.0" />
+    <PackageReference Include="Magick.NET.SystemDrawing" Version="7.2.0">
       <TreatAsUsed>true</TreatAsUsed>
     </PackageReference>
-    <PackageReference Include="Magick.NET.SystemWindowsMedia" Version="7.1.0" />
+    <PackageReference Include="Magick.NET.SystemWindowsMedia" Version="7.2.0" />
     <PackageReference Include="Microsoft-WindowsAPICodePack-Core" Version="1.1.5">
       <TreatAsUsed>true</TreatAsUsed>
     </PackageReference>
@@ -196,7 +196,6 @@
     </PackageReference>
     <PackageReference Include="SkiaSharp.Views.WPF" Version="2.88.6" />
     <PackageReference Include="System.Drawing.Common" Version="8.0.0-rc.2.23479.14" />
-    <PackageReference Include="XamlAnimatedGif" Version="2.2.0" />
   </ItemGroup>
   <ItemGroup>
     <Compile Update="Properties\Settings.Designer.cs">
@@ -317,6 +316,9 @@
     <None Remove="Themes\Resources\img\favicon.ico" />
     <None Remove="Themes\Resources\img\icon__Q6k_icon.ico" />
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\XamlAnimatedGif\XamlAnimatedGif.csproj" />
+  </ItemGroup>
   <ItemGroup>
     <Resource Include="Themes\Resources\img\icon__Q6k_icon.ico" />
   </ItemGroup>
@@ -336,7 +338,7 @@
     <AssemblyVersion>1.9.6</AssemblyVersion>
     <EnableNETAnalyzers>true</EnableNETAnalyzers>
     <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
-    <TargetFramework>net8.0-windows</TargetFramework>
+    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
     <Nullable>enable</Nullable>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <Description>Fast Picture Viewer with compact UI that can be hidden. </Description>

+ 602 - 0
src/XamlAnimatedGif/AnimationBehavior.cs

@@ -0,0 +1,602 @@
+using XamlAnimatedGif.Decoding;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Markup;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+using System.Threading;
+
+namespace XamlAnimatedGif
+{
+    public static class AnimationBehavior
+    {
+        #region Public attached properties and events
+
+        #region SourceUri
+
+        [AttachedPropertyBrowsableForType(typeof(Image))]
+        public static Uri GetSourceUri(Image image)
+        {
+            return (Uri)image.GetValue(SourceUriProperty);
+        }
+
+        public static void SetSourceUri(Image image, Uri value)
+        {
+            image.SetValue(SourceUriProperty, value);
+        }
+
+        public static readonly DependencyProperty SourceUriProperty =
+            DependencyProperty.RegisterAttached(
+              "SourceUri",
+              typeof(Uri),
+              typeof(AnimationBehavior),
+              new PropertyMetadata(
+                null,
+                SourceChanged));
+
+        #endregion SourceUri
+
+        #region SourceStream
+
+        [AttachedPropertyBrowsableForType(typeof(Image))]
+        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+        public static Stream GetSourceStream(DependencyObject obj)
+        {
+            return (Stream)obj.GetValue(SourceStreamProperty);
+        }
+
+        public static void SetSourceStream(DependencyObject obj, Stream value)
+        {
+            obj.SetValue(SourceStreamProperty, value);
+        }
+
+        public static readonly DependencyProperty SourceStreamProperty =
+            DependencyProperty.RegisterAttached(
+                "SourceStream",
+                typeof(Stream),
+                typeof(AnimationBehavior),
+                new PropertyMetadata(
+                    null,
+                    SourceChanged));
+
+        #endregion SourceStream
+
+        #region RepeatBehavior
+
+        [AttachedPropertyBrowsableForType(typeof(Image))]
+        public static RepeatBehavior GetRepeatBehavior(DependencyObject obj)
+        {
+            return (RepeatBehavior)obj.GetValue(RepeatBehaviorProperty);
+        }
+
+        public static void SetRepeatBehavior(DependencyObject obj, RepeatBehavior value)
+        {
+            obj.SetValue(RepeatBehaviorProperty, value);
+        }
+
+        public static readonly DependencyProperty RepeatBehaviorProperty =
+            DependencyProperty.RegisterAttached(
+              "RepeatBehavior",
+              typeof(RepeatBehavior),
+              typeof(AnimationBehavior),
+              new PropertyMetadata(
+                default(RepeatBehavior),
+                RepeatBehaviorChanged));
+
+        #endregion RepeatBehavior
+
+        #region CacheFramesInMemory
+
+        public static void SetCacheFramesInMemory(DependencyObject element, bool value)
+        {
+            element.SetValue(CacheFramesInMemoryProperty, value);
+        }
+
+        [AttachedPropertyBrowsableForType(typeof(Image))]
+        public static bool GetCacheFramesInMemory(DependencyObject element)
+        {
+            return (bool)element.GetValue(CacheFramesInMemoryProperty);
+        }
+
+        public static readonly DependencyProperty CacheFramesInMemoryProperty =
+            DependencyProperty.RegisterAttached(
+            "CacheFramesInMemory",
+            typeof(bool),
+            typeof(AnimationBehavior),
+            new PropertyMetadata(false, SourceChanged));
+
+        #endregion CacheFramesInMemory
+
+        #region AutoStart
+
+        [AttachedPropertyBrowsableForType(typeof(Image))]
+        public static bool GetAutoStart(DependencyObject obj)
+        {
+            return (bool)obj.GetValue(AutoStartProperty);
+        }
+
+        public static void SetAutoStart(DependencyObject obj, bool value)
+        {
+            obj.SetValue(AutoStartProperty, value);
+        }
+
+        public static readonly DependencyProperty AutoStartProperty =
+            DependencyProperty.RegisterAttached(
+                "AutoStart",
+                typeof(bool),
+                typeof(AnimationBehavior),
+                new PropertyMetadata(true));
+
+        #endregion AutoStart
+
+        #region AnimateInDesignMode
+
+        public static bool GetAnimateInDesignMode(DependencyObject obj)
+        {
+            return (bool)obj.GetValue(AnimateInDesignModeProperty);
+        }
+
+        public static void SetAnimateInDesignMode(DependencyObject obj, bool value)
+        {
+            obj.SetValue(AnimateInDesignModeProperty, value);
+        }
+
+        public static readonly DependencyProperty AnimateInDesignModeProperty =
+            DependencyProperty.RegisterAttached(
+                "AnimateInDesignMode",
+                typeof(bool),
+                typeof(AnimationBehavior),
+                new PropertyMetadata(
+                    false,
+                    AnimateInDesignModeChanged));
+
+        #endregion AnimateInDesignMode
+
+        #region Animator
+
+        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+        public static Animator GetAnimator(DependencyObject obj)
+        {
+            return (Animator)obj.GetValue(AnimatorProperty);
+        }
+
+        private static void SetAnimator(DependencyObject obj, Animator value)
+        {
+            obj.SetValue(AnimatorProperty, value);
+        }
+
+        public static readonly DependencyProperty AnimatorProperty =
+            DependencyProperty.RegisterAttached(
+                "Animator",
+                typeof(Animator),
+                typeof(AnimationBehavior),
+                new PropertyMetadata(null));
+
+        #endregion Animator
+
+        #region Error
+
+        public static readonly RoutedEvent ErrorEvent =
+            EventManager.RegisterRoutedEvent(
+                "Error",
+                RoutingStrategy.Bubble,
+                typeof(AnimationErrorEventHandler),
+                typeof(AnimationBehavior));
+
+        public static void AddErrorHandler(DependencyObject d, AnimationErrorEventHandler handler)
+        {
+            (d as UIElement)?.AddHandler(ErrorEvent, handler);
+        }
+
+        public static void RemoveErrorHandler(DependencyObject d, AnimationErrorEventHandler handler)
+        {
+            (d as UIElement)?.RemoveHandler(ErrorEvent, handler);
+        }
+
+        internal static void OnError(Image image, Exception exception, AnimationErrorKind kind)
+        {
+            image.RaiseEvent(new AnimationErrorEventArgs(image, exception, kind));
+        }
+
+        private static void AnimatorError(object sender, AnimationErrorEventArgs e)
+        {
+            var source = e.Source as UIElement;
+            source?.RaiseEvent(e);
+        }
+
+        #endregion Error
+
+        #region DownloadProgress
+
+        public static readonly RoutedEvent DownloadProgressEvent =
+            EventManager.RegisterRoutedEvent(
+                "DownloadProgress",
+                RoutingStrategy.Bubble,
+                typeof(DownloadProgressEventHandler),
+                typeof(AnimationBehavior));
+
+        public static void AddDownloadProgressHandler(DependencyObject d, DownloadProgressEventHandler handler)
+        {
+            (d as UIElement)?.AddHandler(DownloadProgressEvent, handler);
+        }
+
+        public static void RemoveDownloadProgressHandler(DependencyObject d, DownloadProgressEventHandler handler)
+        {
+            (d as UIElement)?.RemoveHandler(DownloadProgressEvent, handler);
+        }
+
+        internal static void OnDownloadProgress(Image image, int downloadPercentage)
+        {
+            image.RaiseEvent(new DownloadProgressEventArgs(image, downloadPercentage));
+        }
+
+        #endregion DownloadProgress
+
+        #region Loaded
+
+        public static readonly RoutedEvent LoadedEvent =
+            EventManager.RegisterRoutedEvent(
+                "Loaded",
+                RoutingStrategy.Bubble,
+                typeof(RoutedEventHandler),
+                typeof(AnimationBehavior));
+
+        public static void AddLoadedHandler(DependencyObject d, RoutedEventHandler handler)
+        {
+            (d as UIElement)?.AddHandler(LoadedEvent, handler);
+        }
+
+        public static void RemoveLoadedHandler(DependencyObject d, RoutedEventHandler handler)
+        {
+            (d as UIElement)?.RemoveHandler(LoadedEvent, handler);
+        }
+
+        private static void OnLoaded(Image sender)
+        {
+            sender.RaiseEvent(new RoutedEventArgs(LoadedEvent, sender));
+        }
+
+        #endregion Loaded
+
+        #region AnimationStarted
+
+        public static readonly RoutedEvent AnimationStartedEvent =
+            EventManager.RegisterRoutedEvent(
+                "AnimationStarted",
+                RoutingStrategy.Bubble,
+                typeof(AnimationStartedEventHandler),
+                typeof(AnimationBehavior));
+
+        public static void AddAnimationStartedHandler(DependencyObject d, AnimationStartedEventHandler handler)
+        {
+            (d as UIElement)?.AddHandler(AnimationStartedEvent, handler);
+        }
+
+        public static void RemoveAnimationStartedHandler(DependencyObject d, AnimationStartedEventHandler handler)
+        {
+            (d as UIElement)?.RemoveHandler(AnimationStartedEvent, handler);
+        }
+
+        private static void AnimatorAnimationStarted(object sender, AnimationStartedEventArgs e)
+        {
+            (e.Source as Image)?.RaiseEvent(e);
+        }
+
+        #endregion AnimationStarted
+
+        #region AnimationCompleted
+
+        public static readonly RoutedEvent AnimationCompletedEvent =
+            EventManager.RegisterRoutedEvent(
+                "AnimationCompleted",
+                RoutingStrategy.Bubble,
+                typeof(AnimationCompletedEventHandler),
+                typeof(AnimationBehavior));
+
+        public static void AddAnimationCompletedHandler(DependencyObject d, AnimationCompletedEventHandler handler)
+        {
+            (d as UIElement)?.AddHandler(AnimationCompletedEvent, handler);
+        }
+
+        public static void RemoveAnimationCompletedHandler(DependencyObject d, AnimationCompletedEventHandler handler)
+        {
+            (d as UIElement)?.RemoveHandler(AnimationCompletedEvent, handler);
+        }
+
+        private static void AnimatorAnimationCompleted(object sender, AnimationCompletedEventArgs e)
+        {
+            (e.Source as Image)?.RaiseEvent(e);
+        }
+
+        #endregion AnimationCompleted
+
+        #endregion Public attached properties and events
+
+        #region Private attached properties
+
+        private static int GetSeqNum(DependencyObject obj)
+        {
+            return (int)obj.GetValue(SeqNumProperty);
+        }
+
+        private static void SetSeqNum(DependencyObject obj, int value)
+        {
+            obj.SetValue(SeqNumProperty, value);
+        }
+
+        private static readonly DependencyProperty SeqNumProperty =
+            DependencyProperty.RegisterAttached("SeqNum", typeof(int), typeof(AnimationBehavior), new PropertyMetadata(0));
+
+        #endregion Private attached properties
+
+        private static void SourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
+        {
+            if (o is not Image image)
+                return;
+
+            InitAnimation(image);
+        }
+
+        private static void RepeatBehaviorChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
+        {
+            GetAnimator(o)?.OnRepeatBehaviorChanged();
+        }
+
+        private static void AnimateInDesignModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            if (d is not Image image)
+                return;
+
+            InitAnimation(image);
+        }
+
+        private static bool CheckDesignMode(Image image, Uri sourceUri, Stream sourceStream)
+        {
+            if (IsInDesignMode(image) && !GetAnimateInDesignMode(image))
+            {
+                try
+                {
+                    if (sourceStream != null)
+                    {
+                        SetStaticImage(image, sourceStream);
+                    }
+                    else if (sourceUri != null)
+                    {
+                        var bmp = new BitmapImage
+                        {
+                            UriSource = sourceUri
+                        };
+                        image.Source = bmp;
+                    }
+                }
+                catch
+                {
+                    image.Source = null;
+                }
+                return false;
+            }
+            return true;
+        }
+
+        private static void InitAnimation(Image image)
+        {
+            if (IsLoaded(image))
+            {
+                image.Unloaded += Image_Unloaded;
+            }
+            else
+            {
+                image.Loaded += Image_Loaded;
+                return;
+            }
+
+            int seqNum = GetSeqNum(image) + 1;
+            SetSeqNum(image, seqNum);
+            ClearAnimatorCore(image);
+
+            CancellationTokenSource ctx = new CancellationTokenSource();
+            image.Unloaded += (sender, args) => ctx.Cancel();
+            try
+            {
+                var stream = GetSourceStream(image);
+                if (stream != null)
+                {
+                    InitAnimationAsync(image, stream.AsBuffered(), GetRepeatBehavior(image), seqNum, GetCacheFramesInMemory(image), ctx.Token);
+                    return;
+                }
+
+                var uri = GetAbsoluteUri(image);
+                if (uri != null)
+                {
+                    InitAnimationAsync(image, uri, GetRepeatBehavior(image), seqNum, GetCacheFramesInMemory(image), ctx.Token);
+                }
+            }
+            catch (Exception ex)
+            {
+                OnError(image, ex, AnimationErrorKind.Loading);
+            }
+        }
+
+        private static void Image_Loaded(object sender, RoutedEventArgs e)
+        {
+            var image = (Image)sender;
+            image.Loaded -= Image_Loaded;
+            InitAnimation(image);
+        }
+
+        private static void Image_Unloaded(object sender, RoutedEventArgs e)
+        {
+            var image = (Image)sender;
+            image.Unloaded -= Image_Unloaded;
+            image.Loaded += Image_Loaded;
+
+            int seqNum = GetSeqNum(image) + 1;
+            SetSeqNum(image, seqNum);
+            ClearAnimatorCore(image);
+        }
+
+        private static bool IsLoaded(FrameworkElement element)
+        {
+            return element.IsLoaded;
+        }
+
+        private static Uri GetAbsoluteUri(Image image)
+        {
+            var uri = GetSourceUri(image);
+            if (uri == null)
+                return null;
+            if (!uri.IsAbsoluteUri)
+            {
+                var baseUri = ((IUriContext)image).BaseUri;
+                if (baseUri != null)
+                {
+                    uri = new Uri(baseUri, uri);
+                }
+                else
+                {
+                    throw new InvalidOperationException("Relative URI can't be resolved");
+                }
+            }
+            return uri;
+        }
+
+        private static async void InitAnimationAsync(Image image, Uri sourceUri, RepeatBehavior repeatBehavior, int seqNum, bool cacheFrameDataInMemory, CancellationToken cancellationToken)
+        {
+            if (!CheckDesignMode(image, sourceUri, null))
+                return;
+
+            try
+            {
+                var progress = new Progress<int>(percentage => OnDownloadProgress(image, percentage));
+                var animator = await ImageAnimator.CreateAsync(sourceUri, repeatBehavior, progress, image, cacheFrameDataInMemory, cancellationToken);
+                // Check that the source hasn't changed while we were loading the animation
+                if (GetSeqNum(image) != seqNum)
+                {
+                    animator.Dispose();
+                    return;
+                }
+
+                SetAnimatorCore(image, animator);
+                OnLoaded(image);
+                await StartAsync(image, animator);
+            }
+            catch (InvalidSignatureException)
+            {
+                await SetStaticImageAsync(image, sourceUri);
+                OnLoaded(image);
+            }
+            catch (Exception ex)
+            {
+                OnError(image, ex, AnimationErrorKind.Loading);
+            }
+        }
+
+        private static async void InitAnimationAsync(Image image, Stream stream, RepeatBehavior repeatBehavior, int seqNum, bool cacheFrameDataInMemory, CancellationToken cancellationToken)
+        {
+            if (!CheckDesignMode(image, null, stream))
+                return;
+
+            try
+            {
+                var animator = await ImageAnimator.CreateAsync(stream, repeatBehavior, image, cacheFrameDataInMemory, cancellationToken);
+                // Check that the source hasn't changed while we were loading the animation
+                if (GetSeqNum(image) != seqNum)
+                {
+                    animator.Dispose();
+                    return;
+                }
+
+                SetAnimatorCore(image, animator);
+                OnLoaded(image);
+                await StartAsync(image, animator);
+            }
+            catch (InvalidSignatureException)
+            {
+                SetStaticImage(image, stream);
+                OnLoaded(image);
+            }
+            catch (Exception ex)
+            {
+                OnError(image, ex, AnimationErrorKind.Loading);
+            }
+        }
+
+        private static void SetAnimatorCore(Image image, Animator animator)
+        {
+            SetAnimator(image, animator);
+            animator.Error += AnimatorError;
+            animator.AnimationStarted += AnimatorAnimationStarted;
+            animator.AnimationCompleted += AnimatorAnimationCompleted;
+            image.Source = animator.Bitmap;
+        }
+
+        private static async Task StartAsync(Image image, Animator animator)
+        {
+            if (GetAutoStart(image))
+                animator.Play();
+            else
+                await animator.ShowFirstFrameAsync();
+        }
+
+        private static void ClearAnimatorCore(Image image)
+        {
+            var animator = GetAnimator(image);
+            if (animator == null)
+                return;
+
+            animator.AnimationCompleted -= AnimatorAnimationCompleted;
+            animator.AnimationStarted -= AnimatorAnimationStarted;
+            animator.Error -= AnimatorError;
+            animator.Dispose();
+            SetAnimator(image, null);
+        }
+
+        // ReSharper disable once UnusedParameter.Local (used in WPF)
+        private static bool IsInDesignMode(DependencyObject obj)
+        {
+            return DesignerProperties.GetIsInDesignMode(obj);
+        }
+
+        private static async Task SetStaticImageAsync(Image image, Uri sourceUri)
+        {
+            try
+            {
+                var progress = new Progress<int>(percentage => OnDownloadProgress(image, percentage));
+                using var stream = await UriLoader.GetStreamFromUriAsync(sourceUri, progress);
+                SetStaticImageCore(image, stream);
+            }
+            catch (Exception ex)
+            {
+                OnError(image, ex, AnimationErrorKind.Loading);
+            }
+        }
+
+        private static void SetStaticImage(Image image, Stream stream)
+        {
+            try
+            {
+                SetStaticImageCore(image, stream);
+            }
+            catch (Exception ex)
+            {
+                OnError(image, ex, AnimationErrorKind.Loading);
+            }
+        }
+
+        private static void SetStaticImageCore(Image image, Stream stream)
+        {
+            stream.Seek(0, SeekOrigin.Begin);
+            var bmp = new BitmapImage();
+            bmp.BeginInit();
+            bmp.CacheOption = BitmapCacheOption.OnLoad;
+            bmp.StreamSource = stream;
+            bmp.EndInit();
+            image.Source = bmp;
+        }
+    }
+}

+ 14 - 0
src/XamlAnimatedGif/AnimationCompletedEventArgs.cs

@@ -0,0 +1,14 @@
+using System.Windows;
+
+namespace XamlAnimatedGif
+{
+    public delegate void AnimationCompletedEventHandler(DependencyObject d, AnimationCompletedEventArgs e);
+
+    public class AnimationCompletedEventArgs : RoutedEventArgs
+    {
+        public AnimationCompletedEventArgs(object source)
+            : base(AnimationBehavior.AnimationCompletedEvent, source)
+        {
+        }
+    }
+}

+ 27 - 0
src/XamlAnimatedGif/AnimationErrorEventArgs.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Windows;
+
+namespace XamlAnimatedGif
+{
+    public delegate void AnimationErrorEventHandler(DependencyObject d, AnimationErrorEventArgs e);
+
+    public class AnimationErrorEventArgs : RoutedEventArgs
+    {
+        public AnimationErrorEventArgs(object source, Exception exception, AnimationErrorKind kind)
+            : base(AnimationBehavior.ErrorEvent, source)
+        {
+            Exception = exception;
+            Kind = kind;
+        }
+
+        public Exception Exception { get; }
+
+        public AnimationErrorKind Kind { get; }
+    }
+
+    public enum AnimationErrorKind
+    {
+        Loading,
+        Rendering
+    }
+}

+ 14 - 0
src/XamlAnimatedGif/AnimationStartedEventArgs.cs

@@ -0,0 +1,14 @@
+using System.Windows;
+
+namespace XamlAnimatedGif
+{
+    public delegate void AnimationStartedEventHandler(DependencyObject d, AnimationStartedEventArgs e);
+
+    public class AnimationStartedEventArgs : RoutedEventArgs
+    {
+        public AnimationStartedEventArgs(object source)
+            : base(AnimationBehavior.AnimationStartedEvent, source)
+        {
+        }
+    }
+}

+ 711 - 0
src/XamlAnimatedGif/Animator.cs

@@ -0,0 +1,711 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+using System.Runtime.InteropServices;
+using XamlAnimatedGif.Decoding;
+using XamlAnimatedGif.Decompression;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif
+{
+    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)
+        {
+            _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));
+            }
+        }
+
+        private async Task LoadFrames(CancellationToken cancellationToken)
+        {
+            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;
+            }
+        }
+
+        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, progress);
+            try
+            {
+                // ReSharper disable once AccessToDisposedClosure
+                return await CreateAsyncCore(stream, metadata => create(stream, metadata));
+            }
+            catch
+            {
+                stream?.Dispose();
+                throw;
+            }
+        }
+
+        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
+
+        public int FrameCount => _metadata.Frames.Count;
+
+        private bool _isStarted;
+        private CancellationTokenSource _cancellationTokenSource;
+
+        public async void Play()
+        {
+            try
+            {
+                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();
+                }
+            }
+            catch (OperationCanceledException)
+            {
+            }
+            catch (Exception ex)
+            {
+                // ignore errors that might occur during Dispose
+                if (!_disposing)
+                    OnError(ex, AnimationErrorKind.Rendering);
+            }
+        }
+
+        private int _frameIndex;
+
+        private async Task RunAsync(CancellationToken cancellationToken)
+        {
+            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()
+        {
+            _timingManager.Pause();
+        }
+
+        public bool IsPaused => _timingManager.IsPaused;
+
+        public bool IsComplete
+        {
+            get
+            {
+                if (_isStarted)
+                    return _timingManager.IsComplete;
+                return false;
+            }
+        }
+
+        public event EventHandler CurrentFrameChanged;
+
+        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
+            {
+                _frameIndex = value;
+                OnCurrentFrameChanged();
+            }
+        }
+
+        private TimingManager CreateTimingManager(GifDataStream metadata, RepeatBehavior repeatBehavior)
+        {
+            var actualRepeatBehavior = GetActualRepeatBehavior(metadata, repeatBehavior);
+
+            var manager = new TimingManager(actualRepeatBehavior);
+            foreach (var frame in metadata.Frames)
+            {
+                manager.Add(GetFrameDelay(frame));
+            }
+
+            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
+
+        #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)
+            {
+                globalColorTable =
+                    metadata.GlobalColorTable
+                        .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
+                        .ToArray();
+            }
+
+            for (int i = 0; i < metadata.Frames.Count; i++)
+            {
+                var frame = metadata.Frames[i];
+                var colorTable = globalColorTable;
+                if (frame.Descriptor.HasLocalColorTable)
+                {
+                    colorTable =
+                        frame.LocalColorTable
+                            .Select(gc => Color.FromArgb(0xFF, gc.R, gc.G, gc.B))
+                            .ToArray();
+                }
+
+                int? transparencyIndex = null;
+                var gce = frame.GraphicControl;
+                if (gce is { HasTransparency: true })
+                {
+                    transparencyIndex = gce.TransparencyIndex;
+                }
+
+                palettes[i] = new GifPalette(transparencyIndex, colorTable);
+            }
+
+            return palettes;
+        }
+
+        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)
+
+            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);
+            }
+            // 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 async Task RenderFrameAsync(int frameIndex, CancellationToken cancellationToken)
+        {
+            await GetWaitLoadSingleFrameTask();
+            if (frameIndex < 0)
+                return;
+
+            var frame = _metadata.Frames[frameIndex];
+            var desc = frame.Descriptor;
+            var rect = GetFixedUpFrameRect(desc);
+
+            Stream indexStream = null;
+            if (!_cacheFrameDataInMemory)
+            {
+                indexStream = await GetIndexStreamAsync(frame, cancellationToken);
+            }
+            using (indexStream)
+            using (_bitmap.LockInScope())
+            {
+                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)
+                    {
+                        await indexStream.ReadAllAsync(indexBuffer, 0, desc.Width, cancellationToken);
+                    }
+                    else
+                    {
+                        indexBuffer = _cachedFrameBytes[frameIndex][y];
+                    }
+
+                    int offset = (desc.Top + y) * _stride + desc.Left * 4;
+
+                    if (transparencyIndex >= 0)
+                    {
+                        CopyFromBitmap(lineBuffer, _bitmap, offset, bufferLength);
+                    }
+
+                    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()
+        {
+            lock (_lockObject)
+            {
+                _frameLoadedEvent.SetResult(true);
+                _frameLoadedEvent = new TaskCompletionSource<bool>();
+            }
+        }
+
+        private Task GetWaitLoadSingleFrameTask()
+        {
+            if (_frameLoadedEvent == null)
+                return CompletedTask; // avoiding lock statement if loading frames completed
+            lock (_lockObject)
+            {
+                return _frameLoadedEvent.Task ?? CompletedTask;
+            }
+        }
+
+        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)
+            {
+                int y = pass.Start;
+                while (y < height)
+                {
+                    yield return y;
+                    y += pass.Step;
+                }
+            }
+        }
+
+        private static void CopyToBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
+        {
+            Marshal.Copy(buffer, 0, bitmap.BackBuffer + offset, length);
+        }
+
+        private static void CopyFromBitmap(byte[] buffer, WriteableBitmap bitmap, int offset, int length)
+        {
+            Marshal.Copy(bitmap.BackBuffer + offset, buffer, 0, length);
+        }
+
+        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 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;
+                        }
+                }
+            }
+
+            var gce = currentFrame.GraphicControl;
+            if (gce is { DisposalMethod: GifFrameDisposalMethod.RestorePrevious })
+            {
+                CopyFromBitmap(_previousBackBuffer, _bitmap, 0, _previousBackBuffer.Length);
+            }
+        }
+
+        private void ClearArea(IGifRect rect)
+        {
+            ClearArea(new Int32Rect(rect.Left, rect.Top, rect.Width, rect.Height));
+        }
+
+        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);
+            }
+
+            _bitmap.AddDirtyRect(new Int32Rect(rect.X, rect.Y, rect.Width, rect.Height));
+        }
+
+        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 async Task GetIndexBytesAsync(int frameIndex, byte[] buffer)
+        {
+            var startPosition = _metadata.Frames[frameIndex].ImageData.CompressedDataStartOffset;
+
+            _sourceStream.Seek(startPosition, SeekOrigin.Begin);
+            using var memoryStream = new MemoryStream(buffer);
+            await GifHelpers.CopyDataBlocksToStreamAsync(_sourceStream, memoryStream).ConfigureAwait(false);
+        }
+
+        internal BitmapSource Bitmap => _bitmap;
+
+        #endregion Rendering
+
+        #region Helper methods
+
+        private static TimeSpan GetFrameDelay(GifFrame frame)
+        {
+            var gce = frame.GraphicControl;
+            if (gce != null)
+            {
+                if (gce.Delay != 0)
+                    return TimeSpan.FromMilliseconds(gce.Delay);
+            }
+            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);
+        }
+
+        #endregion Helper methods
+
+        #region Finalizer and Dispose
+
+        ~Animator()
+        {
+            Dispose(false);
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private volatile bool _disposing;
+        private bool _disposed;
+
+        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 override string ToString()
+        {
+            string s = _sourceUri?.ToString() ?? _sourceStream.ToString();
+            return "GIF: " + s;
+        }
+
+        private class GifPalette
+        {
+            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];
+        }
+
+        internal async Task ShowFirstFrameAsync()
+        {
+            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; }
+
+        internal void OnRepeatBehaviorChanged()
+        {
+            if (_timingManager == null)
+                return;
+
+            var newValue = GetSpecifiedRepeatBehavior();
+            var newActualValue = GetActualRepeatBehavior(_metadata, newValue);
+            if (_timingManager.RepeatBehavior == newActualValue)
+                return;
+
+            _timingManager.RepeatBehavior = newActualValue;
+            Rewind();
+        }
+    }
+}

+ 62 - 0
src/XamlAnimatedGif/BrushAnimator.cs

@@ -0,0 +1,62 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using XamlAnimatedGif.Decoding;
+
+namespace XamlAnimatedGif
+{
+    public class BrushAnimator : Animator
+    {
+        private BrushAnimator(Stream sourceStream, Uri sourceUri, GifDataStream metadata, RepeatBehavior repeatBehavior, bool cacheFrameDataInMemory, CancellationToken cancellationToken) : base(sourceStream, sourceUri, metadata, repeatBehavior, cacheFrameDataInMemory, cancellationToken)
+        {
+            Brush = new ImageBrush { ImageSource = Bitmap };
+            RepeatBehavior = _repeatBehavior;
+        }
+
+        protected override RepeatBehavior GetSpecifiedRepeatBehavior() => RepeatBehavior;
+
+        protected override object AnimationSource => Brush;
+
+        public ImageBrush Brush { get; }
+
+        private RepeatBehavior _repeatBehavior;
+
+        public RepeatBehavior RepeatBehavior
+        {
+            get { return _repeatBehavior; }
+            set
+            {
+                _repeatBehavior = value;
+                OnRepeatBehaviorChanged();
+            }
+        }
+
+        public static Task<BrushAnimator> CreateAsync(Uri sourceUri, RepeatBehavior repeatBehavior, IProgress<int> progress = null)
+        {
+            return CreateAsync(sourceUri, repeatBehavior, false, CancellationToken.None, progress);
+        }
+
+        public static Task<BrushAnimator> CreateAsync(Uri sourceUri, RepeatBehavior repeatBehavior, bool cacheFrameDataInMemory, CancellationToken cancellationToken, IProgress<int> progress = null)
+        {
+            return CreateAsyncCore(
+                sourceUri,
+                progress,
+                (stream, metadata) => new BrushAnimator(stream, sourceUri, metadata, repeatBehavior, cacheFrameDataInMemory, cancellationToken));
+        }
+
+        public static Task<BrushAnimator> CreateAsync(Stream sourceStream, RepeatBehavior repeatBehavior)
+        {
+            return CreateAsync(sourceStream, repeatBehavior, false, CancellationToken.None);
+        }
+
+        public static Task<BrushAnimator> CreateAsync(Stream sourceStream, RepeatBehavior repeatBehavior, bool cacheFrameDataInMemory, CancellationToken cancellationToken)
+        {
+            return CreateAsyncCore(
+                sourceStream,
+                metadata => new BrushAnimator(sourceStream, null, metadata, repeatBehavior, cacheFrameDataInMemory, cancellationToken));
+        }
+    }
+}

+ 39 - 0
src/XamlAnimatedGif/CancellationExtensions.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif
+{
+    internal static class CancellationExtensions
+    {
+        public static async Task WithCancellationToken(this Task task, CancellationToken cancellationToken)
+        {
+            await await Task.WhenAny(task, cancellationToken.WhenCanceled());
+        }
+
+        public static async Task<T> WithCancellationToken<T>(this Task<T> task, CancellationToken cancellationToken)
+        {
+            var firstTaskToFinish = await Task.WhenAny(task, cancellationToken.WhenCanceled());
+            if (firstTaskToFinish == task)
+                return await task;
+
+            await firstTaskToFinish;
+
+            // Will never be reached because the previous statement will throw, but necessary to satisfy the compiler
+            throw new OperationCanceledException(cancellationToken);
+        }
+
+        public static Task WhenCanceled(this CancellationToken cancellationToken)
+        {
+            var tcs = new TaskCompletionSource<int>();
+            var registration = default(CancellationTokenRegistration);
+            registration = cancellationToken.Register(o =>
+            {
+                ((TaskCompletionSource<int>)o).TrySetCanceled();
+                // ReSharper disable once AccessToModifiedClosure
+                registration.Dispose();
+            }, tcs);
+            return tcs.Task;
+        }
+    }
+}

+ 51 - 0
src/XamlAnimatedGif/Decoding/GifApplicationExtension.cs

@@ -0,0 +1,51 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    // label 0xFF
+    internal class GifApplicationExtension : GifExtension
+    {
+        internal const int ExtensionLabel = 0xFF;
+
+        public int BlockSize { get; private set; }
+        public string ApplicationIdentifier { get; private set; }
+        public byte[] AuthenticationCode { get; private set; }
+        public byte[] Data { get; private set; }
+
+        private GifApplicationExtension()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.SpecialPurpose; }
+        }
+
+        internal static async Task<GifApplicationExtension> ReadAsync(Stream stream)
+        {
+            var ext = new GifApplicationExtension();
+            await ext.ReadInternalAsync(stream).ConfigureAwait(false);
+            return ext;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            // Note: at this point, the label (0xFF) has already been read
+
+            byte[] bytes = new byte[12];
+            await stream.ReadAllAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+            BlockSize = bytes[0]; // should always be 11
+            if (BlockSize != 11)
+                throw GifHelpers.InvalidBlockSizeException("Application Extension", 11, BlockSize);
+
+            ApplicationIdentifier = GifHelpers.GetString(bytes, 1, 8);
+            byte[] authCode = new byte[3];
+            Array.Copy(bytes, 9, authCode, 0, 3);
+            AuthenticationCode = authCode;
+            Data = await GifHelpers.ReadDataBlocksAsync(stream).ConfigureAwait(false);
+        }
+    }
+}

+ 26 - 0
src/XamlAnimatedGif/Decoding/GifBlock.cs

@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal abstract class GifBlock
+    {
+        internal static async Task<GifBlock> ReadAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            int blockId = await stream.ReadByteAsync().ConfigureAwait(false);
+            if (blockId < 0)
+                throw new EndOfStreamException();
+            return blockId switch
+            {
+                GifExtension.ExtensionIntroducer => await GifExtension.ReadAsync(stream, controlExtensions).ConfigureAwait(false),
+                GifFrame.ImageSeparator => await GifFrame.ReadAsync(stream, controlExtensions).ConfigureAwait(false),
+                GifTrailer.TrailerByte => await GifTrailer.ReadAsync().ConfigureAwait(false),
+                _ => throw GifHelpers.UnknownBlockTypeException(blockId),
+            };
+        }
+
+        internal abstract GifBlockKind Kind { get; }
+    }
+}

+ 10 - 0
src/XamlAnimatedGif/Decoding/GifBlockKind.cs

@@ -0,0 +1,10 @@
+namespace XamlAnimatedGif.Decoding
+{
+    internal enum GifBlockKind
+    {
+        Control,
+        GraphicRendering,
+        SpecialPurpose,
+        Other
+    }
+}

+ 21 - 0
src/XamlAnimatedGif/Decoding/GifColor.cs

@@ -0,0 +1,21 @@
+namespace XamlAnimatedGif.Decoding
+{
+    internal struct GifColor
+    {
+        internal GifColor(byte r, byte g, byte b)
+        {
+            R = r;
+            G = g;
+            B = b;
+        }
+
+        public byte R { get; }
+        public byte G { get; }
+        public byte B { get; }
+
+        public override string ToString()
+        {
+            return $"#{R:x2}{G:x2}{B:x2}";
+        }
+    }
+}

+ 37 - 0
src/XamlAnimatedGif/Decoding/GifCommentExtension.cs

@@ -0,0 +1,37 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifCommentExtension : GifExtension
+    {
+        internal const int ExtensionLabel = 0xFE;
+
+        public string Text { get; private set; }
+
+        private GifCommentExtension()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.SpecialPurpose; }
+        }
+
+        internal static async Task<GifCommentExtension> ReadAsync(Stream stream)
+        {
+            var comment = new GifCommentExtension();
+            await comment.ReadInternalAsync(stream).ConfigureAwait(false);
+            return comment;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            // Note: at this point, the label (0xFE) has already been read
+
+            var bytes = await GifHelpers.ReadDataBlocksAsync(stream).ConfigureAwait(false);
+            if (bytes != null)
+                Text = GifHelpers.GetString(bytes);
+        }
+    }
+}

+ 98 - 0
src/XamlAnimatedGif/Decoding/GifDataStream.cs

@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifDataStream
+    {
+        public GifHeader Header { get; private set; }
+        public GifColor[] GlobalColorTable { get; set; }
+        public IList<GifFrame> Frames { get; set; }
+        public IList<GifExtension> Extensions { get; set; }
+        public ushort RepeatCount { get; set; }
+
+        private GifDataStream()
+        {
+        }
+
+        internal static async Task<GifDataStream> ReadAsync(Stream stream)
+        {
+            var file = new GifDataStream();
+            await file.ReadInternalAsync(stream).ConfigureAwait(false);
+            return file;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            Header = await GifHeader.ReadAsync(stream).ConfigureAwait(false);
+
+            if (Header.LogicalScreenDescriptor.HasGlobalColorTable)
+            {
+                GlobalColorTable = await GifHelpers.ReadColorTableAsync(stream, Header.LogicalScreenDescriptor.GlobalColorTableSize).ConfigureAwait(false);
+            }
+            await ReadFramesAsync(stream).ConfigureAwait(false);
+
+            var netscapeExtension =
+                            Extensions
+                                .OfType<GifApplicationExtension>()
+                                .FirstOrDefault(GifHelpers.IsNetscapeExtension);
+
+            RepeatCount = netscapeExtension != null
+                ? GifHelpers.GetRepeatCount(netscapeExtension)
+                : (ushort)1;
+        }
+
+        private async Task ReadFramesAsync(Stream stream)
+        {
+            List<GifFrame> frames = new List<GifFrame>();
+            List<GifExtension> controlExtensions = new List<GifExtension>();
+            List<GifExtension> specialExtensions = new List<GifExtension>();
+            while (true)
+            {
+                try
+                {
+                    var block = await GifBlock.ReadAsync(stream, controlExtensions).ConfigureAwait(false);
+
+                    if (block.Kind == GifBlockKind.GraphicRendering)
+                        controlExtensions = new List<GifExtension>();
+
+                    if (block is GifFrame frame)
+                    {
+                        frames.Add(frame);
+                    }
+                    else if (block is GifExtension extension)
+                    {
+                        switch (extension.Kind)
+                        {
+                            case GifBlockKind.Control:
+                                controlExtensions.Add(extension);
+                                break;
+                            case GifBlockKind.SpecialPurpose:
+                                specialExtensions.Add(extension);
+                                break;
+
+                                // Just discard plain text extensions for now, since we have no use for it
+                        }
+                    }
+                    else if (block is GifTrailer)
+                    {
+                        break;
+                    }
+                }
+                // Follow the same approach as Firefox:
+                // If we find extraneous data between blocks, just assume the stream
+                // was successfully terminated if we have some successfully decoded frames
+                // https://dxr.mozilla.org/firefox/source/modules/libpr0n/decoders/gif/nsGIFDecoder2.cpp#894-909
+                catch (UnknownBlockTypeException) when (frames.Count > 0)
+                {
+                    break;
+                }
+            }
+
+            this.Frames = frames.AsReadOnly();
+            this.Extensions = specialExtensions.AsReadOnly();
+        }
+    }
+}

+ 17 - 0
src/XamlAnimatedGif/Decoding/GifDecoderException.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public abstract class GifDecoderException : Exception
+    {
+        protected GifDecoderException(string message) : base(message) { }
+        protected GifDecoderException(string message, Exception inner) : base(message, inner) { }
+
+        protected GifDecoderException(
+          SerializationInfo info,
+          StreamingContext context)
+            : base(info, context) { }
+    }
+}

+ 28 - 0
src/XamlAnimatedGif/Decoding/GifExtension.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal abstract class GifExtension : GifBlock
+    {
+        internal const int ExtensionIntroducer = 0x21;
+
+        internal new static async Task<GifExtension> ReadAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            // Note: at this point, the Extension Introducer (0x21) has already been read
+
+            int label = stream.ReadByte();
+            if (label < 0)
+                throw new EndOfStreamException();
+            return label switch
+            {
+                GifGraphicControlExtension.ExtensionLabel => await GifGraphicControlExtension.ReadAsync(stream).ConfigureAwait(false),
+                GifCommentExtension.ExtensionLabel => await GifCommentExtension.ReadAsync(stream).ConfigureAwait(false),
+                GifPlainTextExtension.ExtensionLabel => await GifPlainTextExtension.ReadAsync(stream, controlExtensions).ConfigureAwait(false),
+                GifApplicationExtension.ExtensionLabel => await GifApplicationExtension.ReadAsync(stream).ConfigureAwait(false),
+                _ => throw GifHelpers.UnknownExtensionTypeException(label),
+            };
+        }
+    }
+}

+ 50 - 0
src/XamlAnimatedGif/Decoding/GifFrame.cs

@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifFrame : GifBlock
+    {
+        internal const int ImageSeparator = 0x2C;
+
+        public GifImageDescriptor Descriptor { get; private set; }
+        public GifColor[] LocalColorTable { get; private set; }
+        public IList<GifExtension> Extensions { get; private set; }
+        public GifImageData ImageData { get; private set; }
+        public GifGraphicControlExtension GraphicControl { get; set; }
+
+        private GifFrame()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.GraphicRendering; }
+        }
+
+        internal new static async Task<GifFrame> ReadAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            var frame = new GifFrame();
+
+            await frame.ReadInternalAsync(stream, controlExtensions).ConfigureAwait(false);
+
+            return frame;
+        }
+
+        private async Task ReadInternalAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            // Note: at this point, the Image Separator (0x2C) has already been read
+
+            Descriptor = await GifImageDescriptor.ReadAsync(stream).ConfigureAwait(false);
+            if (Descriptor.HasLocalColorTable)
+            {
+                LocalColorTable = await GifHelpers.ReadColorTableAsync(stream, Descriptor.LocalColorTableSize).ConfigureAwait(false);
+            }
+            ImageData = await GifImageData.ReadAsync(stream).ConfigureAwait(false);
+            Extensions = controlExtensions.ToList().AsReadOnly();
+            GraphicControl = Extensions.OfType<GifGraphicControlExtension>().LastOrDefault();
+        }
+    }
+}

+ 10 - 0
src/XamlAnimatedGif/Decoding/GifFrameDisposalMethod.cs

@@ -0,0 +1,10 @@
+namespace XamlAnimatedGif.Decoding
+{
+    public enum GifFrameDisposalMethod
+    {
+        None = 0,
+        DoNotDispose = 1,
+        RestoreBackground = 2,
+        RestorePrevious = 3
+    }
+}

+ 54 - 0
src/XamlAnimatedGif/Decoding/GifGraphicControlExtension.cs

@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    // label 0xF9
+    internal class GifGraphicControlExtension : GifExtension
+    {
+        internal const int ExtensionLabel = 0xF9;
+
+        public int BlockSize { get; private set; }
+        public GifFrameDisposalMethod DisposalMethod { get; private set; }
+        public bool UserInput { get; private set; }
+        public bool HasTransparency { get; private set; }
+        public int Delay { get; private set; }
+        public int TransparencyIndex { get; private set; }
+
+        private GifGraphicControlExtension()
+        {
+
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.Control; }
+        }
+
+        internal static async Task<GifGraphicControlExtension> ReadAsync(Stream stream)
+        {
+            var ext = new GifGraphicControlExtension();
+            await ext.ReadInternalAsync(stream).ConfigureAwait(false);
+            return ext;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            // Note: at this point, the label (0xF9) has already been read
+
+            byte[] bytes = new byte[6];
+            await stream.ReadAllAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+            BlockSize = bytes[0]; // should always be 4
+            if (BlockSize != 4)
+                throw GifHelpers.InvalidBlockSizeException("Graphic Control Extension", 4, BlockSize);
+            byte packedFields = bytes[1];
+            DisposalMethod = (GifFrameDisposalMethod) ((packedFields & 0x1C) >> 2);
+            UserInput = (packedFields & 0x02) != 0;
+            HasTransparency = (packedFields & 0x01) != 0;
+            Delay = BitConverter.ToUInt16(bytes, 2) * 10; // milliseconds
+            TransparencyIndex = bytes[4];
+        }
+    }
+}

+ 39 - 0
src/XamlAnimatedGif/Decoding/GifHeader.cs

@@ -0,0 +1,39 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifHeader : GifBlock
+    {
+        public string Signature { get; private set; }
+        public string Version { get; private set; }
+        public GifLogicalScreenDescriptor LogicalScreenDescriptor { get; private set; }
+
+        private GifHeader()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.Other; }
+        }
+
+        internal static async Task<GifHeader> ReadAsync(Stream stream)
+        {
+            var header = new GifHeader();
+            await header.ReadInternalAsync(stream).ConfigureAwait(false);
+            return header;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            Signature = await GifHelpers.ReadStringAsync(stream, 3).ConfigureAwait(false);
+            if (Signature != "GIF")
+                throw GifHelpers.InvalidSignatureException(Signature);
+            Version = await GifHelpers.ReadStringAsync(stream, 3).ConfigureAwait(false);
+            if (Version != "87a" && Version != "89a")
+                throw GifHelpers.UnsupportedVersionException(Version);
+            LogicalScreenDescriptor = await GifLogicalScreenDescriptor.ReadAsync(stream).ConfigureAwait(false);
+        }
+    }
+}

+ 114 - 0
src/XamlAnimatedGif/Decoding/GifHelpers.cs

@@ -0,0 +1,114 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal static class GifHelpers
+    {
+        public static async Task<string> ReadStringAsync(Stream stream, int length)
+        {
+            byte[] bytes = new byte[length];
+            await stream.ReadAllAsync(bytes, 0, length).ConfigureAwait(false);
+            return GetString(bytes);
+        }
+
+        public static async Task ConsumeDataBlocksAsync(Stream sourceStream, CancellationToken cancellationToken = default)
+        {
+            await CopyDataBlocksToStreamAsync(sourceStream, Stream.Null, cancellationToken);
+        }
+
+        public static async Task<byte[]> ReadDataBlocksAsync(Stream stream, CancellationToken cancellationToken = default)
+        {
+            using var ms = new MemoryStream();
+            await CopyDataBlocksToStreamAsync(stream, ms, cancellationToken);
+            return ms.ToArray();
+        }
+
+        public static async Task CopyDataBlocksToStreamAsync(Stream sourceStream, Stream targetStream, CancellationToken cancellationToken = default)
+        {
+            int len;
+            // the length is on 1 byte, so each data sub-block can't be more than 255 bytes long
+            byte[] buffer = new byte[255];
+            while ((len = await sourceStream.ReadByteAsync(cancellationToken)) > 0)
+            {
+                await sourceStream.ReadAllAsync(buffer, 0, len, cancellationToken).ConfigureAwait(false);
+#if LACKS_STREAM_MEMORY_OVERLOADS
+                await targetStream.WriteAsync(buffer, 0, len, cancellationToken);
+#else
+                await targetStream.WriteAsync(buffer.AsMemory(0, len), cancellationToken);
+#endif
+            }
+        }
+
+        public static async Task<GifColor[]> ReadColorTableAsync(Stream stream, int size)
+        {
+            int length = 3 * size;
+            byte[] bytes = new byte[length];
+            await stream.ReadAllAsync(bytes, 0, length).ConfigureAwait(false);
+            GifColor[] colorTable = new GifColor[size];
+            for (int i = 0; i < size; i++)
+            {
+                byte r = bytes[3 * i];
+                byte g = bytes[3 * i + 1];
+                byte b = bytes[3 * i + 2];
+                colorTable[i] = new GifColor(r, g, b);
+            }
+            return colorTable;
+        }
+
+        public static bool IsNetscapeExtension(GifApplicationExtension ext)
+        {
+            return ext.ApplicationIdentifier == "NETSCAPE"
+                && GetString(ext.AuthenticationCode) == "2.0";
+        }
+
+        public static ushort GetRepeatCount(GifApplicationExtension ext)
+        {
+            if (ext.Data.Length >= 3)
+            {
+                return BitConverter.ToUInt16(ext.Data, 1);
+            }
+            return 1;
+        }
+
+        public static Exception UnknownBlockTypeException(int blockId)
+        {
+            return new UnknownBlockTypeException("Unknown block type: 0x" + blockId.ToString("x2"));
+        }
+
+        public static Exception UnknownExtensionTypeException(int extensionLabel)
+        {
+            return new UnknownExtensionTypeException("Unknown extension type: 0x" + extensionLabel.ToString("x2"));
+        }
+
+        public static Exception InvalidBlockSizeException(string blockName, int expectedBlockSize, int actualBlockSize)
+        {
+            return new InvalidBlockSizeException(
+                $"Invalid block size for {blockName}. Expected {expectedBlockSize}, but was {actualBlockSize}");
+        }
+
+        public static Exception InvalidSignatureException(string signature)
+        {
+            return new InvalidSignatureException("Invalid file signature: " + signature);
+        }
+
+        public static Exception UnsupportedVersionException(string version)
+        {
+            return new UnsupportedGifVersionException("Unsupported version: " + version);
+        }
+
+        public static string GetString(byte[] bytes)
+        {
+            return GetString(bytes, 0, bytes.Length);
+        }
+
+        public static string GetString(byte[] bytes, int index, int count)
+        {
+            return Encoding.UTF8.GetString(bytes, index, count);
+        }
+    }
+}

+ 29 - 0
src/XamlAnimatedGif/Decoding/GifImageData.cs

@@ -0,0 +1,29 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifImageData
+    {
+        public byte LzwMinimumCodeSize { get; set; }
+        public long CompressedDataStartOffset { get; set; }
+
+        private GifImageData()
+        {
+        }
+
+        internal static async Task<GifImageData> ReadAsync(Stream stream)
+        {
+            var imgData = new GifImageData();
+            await imgData.ReadInternalAsync(stream).ConfigureAwait(false);
+            return imgData;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            LzwMinimumCodeSize = (byte)stream.ReadByte();
+            CompressedDataStartOffset = stream.Position;
+            await GifHelpers.ConsumeDataBlocksAsync(stream).ConfigureAwait(false);
+        }
+    }
+}

+ 45 - 0
src/XamlAnimatedGif/Decoding/GifImageDescriptor.cs

@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifImageDescriptor : IGifRect
+    {
+        public int Left { get; private set; }
+        public int Top { get; private set; }
+        public int Width { get; private set; }
+        public int Height { get; private set; }
+        public bool HasLocalColorTable { get; private set; }
+        public bool Interlace { get; private set; }
+        public bool IsLocalColorTableSorted { get; private set; }
+        public int LocalColorTableSize { get; private set; }
+
+        private GifImageDescriptor()
+        {
+        }
+
+        internal static async Task<GifImageDescriptor> ReadAsync(Stream stream)
+        {
+            var descriptor = new GifImageDescriptor();
+            await descriptor.ReadInternalAsync(stream).ConfigureAwait(false);
+            return descriptor;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            byte[] bytes = new byte[9];
+            await stream.ReadAllAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+            Left = BitConverter.ToUInt16(bytes, 0);
+            Top = BitConverter.ToUInt16(bytes, 2);
+            Width = BitConverter.ToUInt16(bytes, 4);
+            Height = BitConverter.ToUInt16(bytes, 6);
+            byte packedFields = bytes[8];
+            HasLocalColorTable = (packedFields & 0x80) != 0;
+            Interlace = (packedFields & 0x40) != 0;
+            IsLocalColorTableSorted = (packedFields & 0x20) != 0;
+            LocalColorTableSize = 1 << ((packedFields & 0x07) + 1);
+        }
+    }
+}

+ 55 - 0
src/XamlAnimatedGif/Decoding/GifLogicalScreenDescriptor.cs

@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifLogicalScreenDescriptor : IGifRect
+    {
+        public int Width { get; private set; }
+        public int Height { get; private set; }
+        public bool HasGlobalColorTable { get; private set; }
+        public int ColorResolution { get; private set; }
+        public bool IsGlobalColorTableSorted { get; private set; }
+        public int GlobalColorTableSize { get; private set; }
+        public int BackgroundColorIndex { get; private set; }
+        public double PixelAspectRatio { get; private set; }
+
+        internal static async Task<GifLogicalScreenDescriptor> ReadAsync(Stream stream)
+        {
+            var descriptor = new GifLogicalScreenDescriptor();
+            await descriptor.ReadInternalAsync(stream).ConfigureAwait(false);
+            return descriptor;
+        }
+
+        private async Task ReadInternalAsync(Stream stream)
+        {
+            byte[] bytes = new byte[7];
+            await stream.ReadAllAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+
+            Width = BitConverter.ToUInt16(bytes, 0);
+            Height = BitConverter.ToUInt16(bytes, 2);
+            byte packedFields = bytes[4];
+            HasGlobalColorTable = (packedFields & 0x80) != 0;
+            ColorResolution = ((packedFields & 0x70) >> 4) + 1;
+            IsGlobalColorTableSorted = (packedFields & 0x08) != 0;
+            GlobalColorTableSize = 1 << ((packedFields & 0x07) + 1);
+            BackgroundColorIndex = bytes[5];
+            PixelAspectRatio =
+                bytes[6] == 0
+                    ? 0.0
+                    : (15 + bytes[6]) / 64.0;
+        }
+
+        int IGifRect.Left
+        {
+            get { return 0; }
+        }
+
+        int IGifRect.Top
+        {
+            get { return 0; }
+        }
+    }
+}

+ 69 - 0
src/XamlAnimatedGif/Decoding/GifPlainTextExtension.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif.Decoding
+{
+    // label 0x01
+    internal class GifPlainTextExtension : GifExtension
+    {
+        internal const int ExtensionLabel = 0x01;
+
+        public int BlockSize { get; private set; }
+        public int Left { get; private set; }
+        public int Top { get; private set; }
+        public int Width { get; private set; }
+        public int Height { get; private set; }
+        public int CellWidth { get; private set; }
+        public int CellHeight { get; private set; }
+        public int ForegroundColorIndex { get; private set; }
+        public int BackgroundColorIndex { get; private set; }
+        public string Text { get; private set; }
+
+        public IList<GifExtension> Extensions { get; private set; }
+
+        private GifPlainTextExtension()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.GraphicRendering; }
+        }
+
+        internal new static async Task<GifPlainTextExtension> ReadAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            var plainText = new GifPlainTextExtension();
+            await plainText.ReadInternalAsync(stream, controlExtensions).ConfigureAwait(false);
+            return plainText;
+        }
+
+        private async Task ReadInternalAsync(Stream stream, IEnumerable<GifExtension> controlExtensions)
+        {
+            // Note: at this point, the label (0x01) has already been read
+
+            byte[] bytes = new byte[13];
+            await stream.ReadAllAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+
+            BlockSize = bytes[0];
+            if (BlockSize != 12)
+                throw GifHelpers.InvalidBlockSizeException("Plain Text Extension", 12, BlockSize);
+
+            Left = BitConverter.ToUInt16(bytes, 1);
+            Top = BitConverter.ToUInt16(bytes, 3);
+            Width = BitConverter.ToUInt16(bytes, 5);
+            Height = BitConverter.ToUInt16(bytes, 7);
+            CellWidth = bytes[9];
+            CellHeight = bytes[10];
+            ForegroundColorIndex = bytes[11];
+            BackgroundColorIndex = bytes[12];
+
+            var dataBytes = await GifHelpers.ReadDataBlocksAsync(stream).ConfigureAwait(false);
+            Text = GifHelpers.GetString(dataBytes);
+            Extensions = controlExtensions.ToList().AsReadOnly();
+        }
+    }
+}

+ 23 - 0
src/XamlAnimatedGif/Decoding/GifTrailer.cs

@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Decoding
+{
+    internal class GifTrailer : GifBlock
+    {
+        internal const int TrailerByte = 0x3B;
+
+        private GifTrailer()
+        {
+        }
+
+        internal override GifBlockKind Kind
+        {
+            get { return GifBlockKind.Other; }
+        }
+
+        internal static Task<GifTrailer> ReadAsync()
+        {
+            return Task.FromResult(new GifTrailer());
+        }
+    }
+}

+ 10 - 0
src/XamlAnimatedGif/Decoding/IGifRect.cs

@@ -0,0 +1,10 @@
+namespace XamlAnimatedGif.Decoding
+{
+    internal interface IGifRect
+    {
+        int Left { get; }
+        int Top { get; }
+        int Width { get; }
+        int Height { get; }
+    }
+}

+ 17 - 0
src/XamlAnimatedGif/Decoding/InvalidBlockSizeException.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public class InvalidBlockSizeException : GifDecoderException
+    {
+        internal InvalidBlockSizeException(string message) : base(message) { }
+        internal InvalidBlockSizeException(string message, Exception inner) : base(message, inner) { }
+
+        protected InvalidBlockSizeException(
+            SerializationInfo info,
+            StreamingContext context)
+            : base(info, context) { }
+    }
+}

+ 18 - 0
src/XamlAnimatedGif/Decoding/InvalidSignatureException.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public class InvalidSignatureException : GifDecoderException
+    {
+        internal InvalidSignatureException(string message) : base(message) { }
+        internal InvalidSignatureException(string message, Exception inner) : base(message, inner) { }
+
+        protected InvalidSignatureException(
+            SerializationInfo info,
+            StreamingContext context)
+            : base(info, context)
+        { }
+    }
+}

+ 18 - 0
src/XamlAnimatedGif/Decoding/UnknownBlockTypeException.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public class UnknownBlockTypeException : GifDecoderException
+    {
+        internal UnknownBlockTypeException(string message) : base(message) { }
+        internal UnknownBlockTypeException(string message, Exception inner) : base(message, inner) { }
+
+        protected UnknownBlockTypeException(
+            SerializationInfo info,
+            StreamingContext context)
+            : base(info, context)
+        { }
+    }
+}

+ 18 - 0
src/XamlAnimatedGif/Decoding/UnknownExtensionTypeException.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public class UnknownExtensionTypeException : GifDecoderException
+    {
+        internal UnknownExtensionTypeException(string message) : base(message) { }
+        internal UnknownExtensionTypeException(string message, Exception inner) : base(message, inner) { }
+
+        protected UnknownExtensionTypeException(
+            SerializationInfo info,
+            StreamingContext context)
+            : base(info, context)
+        { }
+    }
+}

+ 18 - 0
src/XamlAnimatedGif/Decoding/UnsupportedGifVersionException.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace XamlAnimatedGif.Decoding
+{
+    [Serializable]
+    public class UnsupportedGifVersionException : GifDecoderException
+    {
+        internal UnsupportedGifVersionException(string message) : base(message) { }
+        internal UnsupportedGifVersionException(string message, Exception inner) : base(message, inner) { }
+
+        protected UnsupportedGifVersionException(
+            SerializationInfo info,
+            StreamingContext context)
+            : base(info, context)
+        { }
+    }
+}

+ 56 - 0
src/XamlAnimatedGif/Decompression/BitReader.cs

@@ -0,0 +1,56 @@
+namespace XamlAnimatedGif.Decompression
+{
+    class BitReader
+    {
+        private readonly byte[] _buffer;
+
+        public BitReader(byte[] buffer)
+        {
+            _buffer = buffer;
+        }
+
+        private int _bytePosition = -1;
+        private int _bitPosition;
+        private int _currentValue = -1;
+        public int ReadBits(int bitCount)
+        {
+            // The following code assumes it's running on a little-endian architecture.
+            // It's probably safe to assume it will always be the case, because:
+            // - Windows only supports little-endian architectures: x86/x64 and ARM (which supports
+            //   both endiannesses, but Windows on ARM is always in little-endian mode)
+            // - No platforms other than Windows support XAML applications
+            // If the situation changes, this code will have to be updated.
+
+            if (_bytePosition == -1)
+            {
+                _bytePosition = 0;
+                _bitPosition = 0;
+                _currentValue = ReadInt32(); //BitConverter.ToInt32(_buffer, _bytePosition);
+            }
+            else if (bitCount > 32 - _bitPosition)
+            {
+                int n = _bitPosition >> 3;
+                _bytePosition += n;
+                _bitPosition &= 0x07;
+                _currentValue = ReadInt32() >> _bitPosition;
+            }
+            int mask = (1 << bitCount) - 1;
+            int value = _currentValue & mask;
+            _currentValue >>= bitCount;
+            _bitPosition += bitCount;
+            return value;
+        }
+
+        private int ReadInt32()
+        {
+            int value = 0;
+            for (int i = 0; i < 4; i++)
+            {
+                if (_bytePosition + i >= _buffer.Length)
+                    break;
+                value |= _buffer[_bytePosition + i] << (i << 3);
+            }
+            return value;
+        }
+    }
+}

+ 251 - 0
src/XamlAnimatedGif/Decompression/LzwDecompressStream.cs

@@ -0,0 +1,251 @@
+using System;
+using System.IO;
+using Buffer = System.Buffer;
+using System.Runtime.CompilerServices;
+using System.Diagnostics;
+
+namespace XamlAnimatedGif.Decompression
+{
+    class LzwDecompressStream : Stream
+    {
+        private const int MaxCodeLength = 12;
+        private readonly BitReader _reader;
+        private readonly CodeTable _codeTable;
+        private int _prevCode;
+        private byte[] _remainingBytes;
+        private bool _endOfStream;
+
+        public LzwDecompressStream(byte[] compressedBuffer, int minimumCodeLength)
+        {
+            _reader = new BitReader(compressedBuffer);
+            _codeTable = new CodeTable(minimumCodeLength);
+        }
+        public override void Flush()
+        {
+        }
+
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override void SetLength(long value)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            ValidateReadArgs(buffer, offset, count);
+
+            if (_endOfStream)
+                return 0;
+
+            int read = 0;
+
+            FlushRemainingBytes(buffer, offset, count, ref read);
+
+            while (read < count)
+            {
+                int code = _reader.ReadBits(_codeTable.CodeLength);
+                
+                if (!ProcessCode(code, buffer, offset, count, ref read))
+                {
+                    _endOfStream = true;
+                    break;
+                }
+            }
+            return read;
+        }
+
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override bool CanRead => true;
+
+        public override bool CanSeek => false;
+
+        public override bool CanWrite => true;
+
+        public override long Length
+        {
+            get { throw new NotSupportedException(); }
+        }
+
+        public override long Position
+        {
+            get { throw new NotSupportedException(); }
+            set { throw new NotSupportedException(); }
+        }
+
+        private void InitCodeTable()
+        {
+            _codeTable.Reset();
+            _prevCode = -1;
+        }
+
+        private static byte[] CopySequenceToBuffer(byte[] sequence, byte[] buffer, int offset, int count, ref int read)
+        {
+            int bytesToRead = Math.Min(sequence.Length, count - read);
+            Buffer.BlockCopy(sequence, 0, buffer, offset + read, bytesToRead);
+            read += bytesToRead;
+            byte[] remainingBytes = null;
+            if (bytesToRead < sequence.Length)
+            {
+                int remainingBytesCount = sequence.Length - bytesToRead;
+                remainingBytes = new byte[remainingBytesCount];
+                Buffer.BlockCopy(sequence, bytesToRead, remainingBytes, 0, remainingBytesCount);
+            }
+            return remainingBytes;
+        }
+
+        private void FlushRemainingBytes(byte[] buffer, int offset, int count, ref int read)
+        {
+            // If we read too many bytes last time, copy them first;
+            if (_remainingBytes != null)
+                _remainingBytes = CopySequenceToBuffer(_remainingBytes, buffer, offset, count, ref read);
+        }
+
+        [Conditional("DISABLED")]
+        private static void ValidateReadArgs(byte[] buffer, int offset, int count)
+        {
+            if (buffer == null) throw new ArgumentNullException(nameof(buffer));
+            if (offset < 0)
+                throw new ArgumentOutOfRangeException(nameof(offset), "Offset can't be negative");
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count), "Count can't be negative");
+            if (offset + count > buffer.Length)
+                throw new ArgumentException("Buffer is to small to receive the requested data");
+        }
+
+        private bool ProcessCode(int code, byte[] buffer, int offset, int count, ref int read)
+        {
+            if (code < _codeTable.Count)
+            {
+                var sequence = _codeTable[code];
+                if (sequence.IsStopCode)
+                {
+                    return false;
+                }
+                if (sequence.IsClearCode)
+                {
+                    InitCodeTable();
+                    return true;
+                }
+                _remainingBytes = CopySequenceToBuffer(sequence.Bytes, buffer, offset, count, ref read);
+                if (_prevCode >= 0)
+                {
+                    var prev = _codeTable[_prevCode];
+                    var newSequence = prev.Append(sequence.Bytes[0]);
+                    _codeTable.Add(newSequence);
+                }
+            }
+            else
+            {
+                var prev = _codeTable[_prevCode];
+                var newSequence = prev.Append(prev.Bytes[0]);
+                _codeTable.Add(newSequence);
+                _remainingBytes = CopySequenceToBuffer(newSequence.Bytes, buffer, offset, count, ref read);
+            }
+            _prevCode = code;
+            return true;
+        }
+
+        struct Sequence
+        {
+            public Sequence(byte[] bytes)
+                : this()
+            {
+                Bytes = bytes;
+            }
+
+            private Sequence(bool isClearCode, bool isStopCode)
+                : this()
+            {
+                IsClearCode = isClearCode;
+                IsStopCode = isStopCode;
+            }
+
+            public byte[] Bytes { get; }
+
+            public bool IsClearCode { get; }
+
+            public bool IsStopCode { get; }
+
+            public static Sequence ClearCode { get; } = new Sequence(true, false);
+
+            public static Sequence StopCode { get; } = new Sequence(false, true);
+
+            public Sequence Append(byte b)
+            {
+                var bytes = new byte[Bytes.Length + 1];
+                Bytes.CopyTo(bytes, 0);
+                bytes[Bytes.Length] = b;
+                return new Sequence(bytes);
+            }
+        }
+
+        class CodeTable
+        {
+            private readonly int _minimumCodeLength;
+            private readonly Sequence[] _table;
+            private int _count;
+            private int _codeLength;
+
+            public CodeTable(int minimumCodeLength)
+            {
+                _minimumCodeLength = minimumCodeLength;
+                _codeLength = _minimumCodeLength + 1;
+                int initialEntries = 1 << minimumCodeLength;
+                _table = new Sequence[1 << MaxCodeLength];
+                for (int i = 0; i < initialEntries; i++)
+                {
+                    _table[_count++] = new Sequence(new[] {(byte) i});
+                }
+                Add(Sequence.ClearCode);
+                Add(Sequence.StopCode);
+            }
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            public void Reset()
+            {
+                _count = (1 << _minimumCodeLength) + 2;
+                _codeLength = _minimumCodeLength + 1;
+            }
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            public void Add(Sequence sequence)
+            {
+                // Code table is full, stop adding new codes
+                if (_count >= _table.Length)
+                    return;
+
+                _table[_count++] = sequence;
+                if ((_count & (_count - 1)) == 0 && _codeLength < MaxCodeLength)
+                    _codeLength++;
+            }
+
+            public Sequence this[int index]
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get
+                {
+                    return _table[index];
+                }
+            }
+
+            public int Count
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get { return _count; }
+            }
+
+            public int CodeLength
+            {
+                [MethodImpl(MethodImplOptions.AggressiveInlining)]
+                get { return _codeLength; }
+            }
+        }
+    }
+}

+ 16 - 0
src/XamlAnimatedGif/DownloadProgressEventArgs.cs

@@ -0,0 +1,16 @@
+using System.Windows;
+
+namespace XamlAnimatedGif
+{
+    public delegate void DownloadProgressEventHandler(DependencyObject d, DownloadProgressEventArgs e);
+
+    public class DownloadProgressEventArgs : RoutedEventArgs
+    {
+        public int Progress { get; set; }
+
+        public DownloadProgressEventArgs(object source, int progress) : base(AnimationBehavior.DownloadProgressEvent, source)
+        {
+            Progress = progress;
+        }
+    }
+}

+ 17 - 0
src/XamlAnimatedGif/Extensions/BitArrayExtensions.cs

@@ -0,0 +1,17 @@
+using System.Collections;
+
+namespace XamlAnimatedGif.Extensions
+{
+    static class BitArrayExtensions
+    {
+        public static short ToInt16(this BitArray bitArray)
+        {
+            short n = 0;
+            for (int i = bitArray.Length - 1; i >= 0; i--)
+            {
+                n = (short) ((n << 1) + (bitArray[i] ? 1 : 0));
+            }
+            return n;
+        }
+    }
+}

+ 79 - 0
src/XamlAnimatedGif/Extensions/StreamExtensions.cs

@@ -0,0 +1,79 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace XamlAnimatedGif.Extensions
+{
+    static class StreamExtensions
+    {
+        public static async Task ReadAllAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
+        {
+            int totalRead = 0;
+            while (totalRead < count)
+            {
+#if LACKS_STREAM_MEMORY_OVERLOADS
+                int n = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken);
+#else
+                int n = await stream.ReadAsync(buffer.AsMemory(offset + totalRead, count - totalRead), cancellationToken);
+#endif
+                if (n == 0)
+                    throw new EndOfStreamException();
+                totalRead += n;
+            }
+        }
+
+        public static void ReadAll(this Stream stream, byte[] buffer, int offset, int count)
+        {
+            int totalRead = 0;
+            while (totalRead < count)
+            {
+                int n = stream.Read(buffer, offset + totalRead, count - totalRead);
+                if (n == 0)
+                    throw new EndOfStreamException();
+                totalRead += n;
+            }
+        }
+
+        public static async Task<int> ReadByteAsync(this Stream stream, CancellationToken cancellationToken = default)
+        {
+            var buffer = new byte[1];
+#if LACKS_STREAM_MEMORY_OVERLOADS
+            int n = await stream.ReadAsync(buffer, 0, 1, cancellationToken);
+#else
+            int n = await stream.ReadAsync(buffer.AsMemory(0, 1), cancellationToken);
+#endif
+            if (n == 0)
+                return -1;
+            return buffer[0];
+        }
+
+        public static Stream AsBuffered(this Stream stream)
+        {
+            if (stream is BufferedStream bs)
+                return bs;
+            return new BufferedStream(stream);
+        }
+
+        public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progress, int bufferSize = 81920, CancellationToken cancellationToken = default)
+        {
+            byte[] buffer = new byte[bufferSize];
+            int bytesRead;
+            long bytesCopied = 0;
+#if LACKS_STREAM_MEMORY_OVERLOADS
+            while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
+#else
+            while ((bytesRead = await source.ReadAsync(buffer.AsMemory(), cancellationToken).ConfigureAwait(false)) != 0)
+#endif
+            {
+#if LACKS_STREAM_MEMORY_OVERLOADS
+                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+#else
+                await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
+#endif
+                bytesCopied += bytesRead;
+                progress?.Report(bytesCopied);
+            }
+        }
+    }
+}

+ 29 - 0
src/XamlAnimatedGif/Extensions/WritableBitmapExtensions.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Windows.Media.Imaging;
+
+namespace XamlAnimatedGif.Extensions
+{
+    static class WritableBitmapExtensions
+    {
+        public static IDisposable LockInScope(this WriteableBitmap bitmap)
+        {
+            return new WriteableBitmapLock(bitmap);
+        }
+
+        class WriteableBitmapLock : IDisposable
+        {
+            private readonly WriteableBitmap _bitmap;
+
+            public WriteableBitmapLock(WriteableBitmap bitmap)
+            {
+                _bitmap = bitmap;
+                _bitmap.Lock();
+            }
+
+            public void Dispose()
+            {
+                _bitmap.Unlock();
+            }
+        }
+    }
+}

+ 55 - 0
src/XamlAnimatedGif/ImageAnimator.cs

@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Media.Animation;
+using XamlAnimatedGif.Decoding;
+
+namespace XamlAnimatedGif
+{
+    internal class ImageAnimator : Animator
+    {
+        private readonly Image _image;
+
+        public ImageAnimator(Stream sourceStream, Uri sourceUri, GifDataStream metadata, RepeatBehavior repeatBehavior,
+            Image image) : this(sourceStream, sourceUri, metadata, repeatBehavior, image, false, CancellationToken.None)
+        {
+        }
+
+        public ImageAnimator(Stream sourceStream, Uri sourceUri, GifDataStream metadata, RepeatBehavior repeatBehavior, Image image, bool cacheFrameDataInMemory, CancellationToken cancellationToken) : base(sourceStream, sourceUri, metadata, repeatBehavior, cacheFrameDataInMemory, cancellationToken)
+        {
+            _image = image;
+            OnRepeatBehaviorChanged(); // in case the value has changed during creation
+        }
+
+        protected override RepeatBehavior GetSpecifiedRepeatBehavior() => AnimationBehavior.GetRepeatBehavior(_image);
+
+        protected override object AnimationSource => _image;
+
+        public static Task<ImageAnimator> CreateAsync(Uri sourceUri, RepeatBehavior repeatBehavior, IProgress<int> progress, Image image)
+        {
+            return CreateAsync(sourceUri, repeatBehavior, progress, image, false, CancellationToken.None);
+        }
+
+        public static Task<ImageAnimator> CreateAsync(Uri sourceUri, RepeatBehavior repeatBehavior, IProgress<int> progress, Image image, bool cacheFrameDataInMemory, CancellationToken cancellationToken)
+        {
+            return CreateAsyncCore(
+                sourceUri,
+                progress,
+                (stream, metadata) => new ImageAnimator(stream, sourceUri, metadata, repeatBehavior, image, cacheFrameDataInMemory, cancellationToken));
+        }
+
+        public static Task<ImageAnimator> CreateAsync(Stream sourceStream, RepeatBehavior repeatBehavior, Image image)
+        {
+            return CreateAsync(sourceStream, repeatBehavior, image);
+        }
+
+        public static Task<ImageAnimator> CreateAsync(Stream sourceStream, RepeatBehavior repeatBehavior, Image image, bool cacheFrameDataInMemory, CancellationToken cancellationToken)
+        {
+            return CreateAsyncCore(
+                sourceStream,
+                metadata => new ImageAnimator(sourceStream, null, metadata, repeatBehavior, image, cacheFrameDataInMemory, cancellationToken));
+        }
+    }
+}

+ 13 - 0
src/XamlAnimatedGif/Properties/Xmlns.cs

@@ -0,0 +1,13 @@
+using System.Windows.Markup;
+using XamlAnimatedGif.Properties;
+
+[assembly: XmlnsDefinition(XmlnsInfo.XmlNamespace, "XamlAnimatedGif")]
+[assembly: XmlnsPrefix(XmlnsInfo.XmlNamespace, "gif")]
+
+namespace XamlAnimatedGif.Properties
+{
+    static class XmlnsInfo
+    {
+        public const string XmlNamespace = "https://github.com/XamlAnimatedGif/XamlAnimatedGif";
+    }
+}

+ 130 - 0
src/XamlAnimatedGif/TimingManager.cs

@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media.Animation;
+
+namespace XamlAnimatedGif
+{
+    class TimingManager
+    {
+        private readonly List<TimeSpan> _timeSpans = new List<TimeSpan>();
+        private int _current;
+        private int _count;
+        private bool _isComplete;
+        private TimeSpan _elapsed;
+
+        public TimingManager(RepeatBehavior repeatBehavior)
+        {
+            RepeatBehavior = repeatBehavior;
+        }
+
+        public RepeatBehavior RepeatBehavior { get; set; }
+
+        public void Add(TimeSpan timeSpan)
+        {
+            _timeSpans.Add(timeSpan);
+        }
+
+        public async Task<bool> NextAsync(CancellationToken cancellationToken)
+        {
+            if (IsComplete)
+                return false;
+
+            await IsPausedAsync(cancellationToken);
+
+            var repeatBehavior = RepeatBehavior;
+
+            var ts = _timeSpans[_current];
+            await Task.Delay(ts, cancellationToken);
+            _current++;
+            _elapsed += ts;
+
+            if (repeatBehavior.HasDuration)
+            {
+                if (_elapsed >= repeatBehavior.Duration)
+                {
+                    IsComplete = true;
+                    return false;
+                }
+            }
+
+            if (_current >= _timeSpans.Count)
+            {
+                _count++;
+                if (repeatBehavior.HasCount)
+                {
+                    if (_count < repeatBehavior.Count)
+                    {
+                        _current = 0;
+                        return true;
+                    }
+                    IsComplete = true;
+                    return false;
+                }
+                else
+                {
+                    _current = 0;
+                    return true;
+                }
+            }
+            return true;
+        }
+
+        public void Reset()
+        {
+            _current = 0;
+            _count = 0;
+            _elapsed = TimeSpan.Zero;
+            IsComplete = false;
+        }
+
+        public event EventHandler Completed;
+
+        protected virtual void OnCompleted()
+        {
+            Completed?.Invoke(this, EventArgs.Empty);
+        }
+
+        public bool IsComplete
+        {
+            get { return _isComplete; }
+            private set
+            {
+                _isComplete = value;
+                if (value)
+                    OnCompleted();
+            }
+        }
+
+        private readonly Task _completedTask = Task.FromResult(0);
+        private TaskCompletionSource<int> _pauseCompletionSource;
+        public void Pause()
+        {
+            if (IsPaused) return; // Make this a no-op.
+            IsPaused = true;
+            _pauseCompletionSource = new TaskCompletionSource<int>();
+        }
+
+        public void Resume()
+        {
+            if (!IsPaused) return; // Make this a no-op.
+            var tcs = _pauseCompletionSource;
+            tcs?.TrySetResult(0);
+            _pauseCompletionSource = null;
+            IsPaused = false;
+        }
+
+        public bool IsPaused { get; private set; }
+
+        private Task IsPausedAsync(CancellationToken cancellationToken)
+        {
+            var tcs = _pauseCompletionSource;
+            if (tcs != null)
+            {
+                return tcs.Task.WithCancellationToken(cancellationToken);
+            }
+            return _completedTask;
+        }
+    }
+}

+ 134 - 0
src/XamlAnimatedGif/UriLoader.cs

@@ -0,0 +1,134 @@
+using System;
+using System.IO;
+using System.IO.Packaging;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using XamlAnimatedGif.Extensions;
+
+namespace XamlAnimatedGif
+{
+    internal class UriLoader
+    {
+        public static Task<Stream> GetStreamFromUriAsync(Uri uri, IProgress<int> progress)
+        {
+            if (uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"))
+                return GetNetworkStreamAsync(uri, progress);
+            return GetStreamFromUriCoreAsync(uri);
+        }
+
+        private static async Task<Stream> GetNetworkStreamAsync(Uri uri, IProgress<int> progress)
+        {
+            string cacheFileName = GetCacheFileName(uri);
+            var cacheStream = await OpenTempFileStreamAsync(cacheFileName);
+            if (cacheStream == null)
+            {
+                await DownloadToCacheFileAsync(uri, cacheFileName, progress);
+                cacheStream = await OpenTempFileStreamAsync(cacheFileName);
+            }
+            progress.Report(100);
+            return cacheStream;
+        }
+        private static async Task DownloadToCacheFileAsync(Uri uri, string fileName, IProgress<int> progress)
+        {
+            try
+            {
+                using var client = new HttpClient();
+                var request = new HttpRequestMessage(HttpMethod.Get, uri);
+                var response = await client.SendAsync(request);
+                response.EnsureSuccessStatusCode();
+                long length = response.Content.Headers.ContentLength ?? 0;
+                using var responseStream = await response.Content.ReadAsStreamAsync();
+                using var fileStream = await CreateTempFileStreamAsync(fileName);
+                IProgress<long> absoluteProgress = null;
+                if (progress != null)
+                {
+                    absoluteProgress =
+                        new Progress<long>(bytesCopied =>
+                        {
+                            if (length > 0)
+                                progress.Report((int)(100 * bytesCopied / length));
+                            else
+                                progress.Report(-1);
+                        });
+                }
+                await responseStream.CopyToAsync(fileStream, absoluteProgress);
+            }
+            catch
+            {
+                DeleteTempFile(fileName);
+                throw;
+            }
+        }
+
+        private static Task<Stream> GetStreamFromUriCoreAsync(Uri uri)
+        {
+            if (uri.Scheme == PackUriHelper.UriSchemePack)
+            {
+                var sri = uri.Authority == "siteoforigin:,,,"
+                    ? Application.GetRemoteStream(uri)
+                    : Application.GetResourceStream(uri);
+
+                if (sri != null)
+                    return Task.FromResult(sri.Stream);
+
+                throw new FileNotFoundException("Cannot find file with the specified URI");
+            }
+
+            if (uri.Scheme == Uri.UriSchemeFile)
+            {
+                return Task.FromResult<Stream>(File.OpenRead(uri.LocalPath));
+            }
+
+            throw new NotSupportedException("Only pack:, file:, http: and https: URIs are supported");
+        }
+
+        private static Task<Stream> OpenTempFileStreamAsync(string fileName)
+        {
+            string path = Path.Combine(Path.GetTempPath(), fileName);
+            Stream stream = null;
+            try
+            {
+                stream = File.OpenRead(path);
+            }
+            catch (FileNotFoundException)
+            {
+            }
+            return Task.FromResult(stream);
+        }
+
+        private static Task<Stream> CreateTempFileStreamAsync(string fileName)
+        {
+            string path = Path.Combine(Path.GetTempPath(), fileName);
+            Stream stream = File.OpenWrite(path);
+            stream.SetLength(0);
+            return Task.FromResult(stream);
+        }
+
+        private static void DeleteTempFile(string fileName)
+        {
+            string path = Path.Combine(Path.GetTempPath(), fileName);
+            if (File.Exists(path))
+                File.Delete(path);
+        }
+
+        private static string GetCacheFileName(Uri uri)
+        {
+            using var sha1 = SHA1.Create();
+            var bytes = Encoding.UTF8.GetBytes(uri.AbsoluteUri);
+            var hash = sha1.ComputeHash(bytes);
+            return ToHex(hash);
+        }
+
+        private static string ToHex(byte[] bytes)
+        {
+            return bytes.Aggregate(
+                new StringBuilder(),
+                (sb, b) => sb.Append(b.ToString("X2")),
+                sb => sb.ToString());
+        }
+    }
+}

+ 40 - 0
src/XamlAnimatedGif/XamlAnimatedGif.csproj

@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <UseWPF>true</UseWPF>
+    <SignAssembly>true</SignAssembly>
+    <AssemblyOriginatorKeyFile>XamlAnimatedGif.snk</AssemblyOriginatorKeyFile>
+    <TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
+  </PropertyGroup>
+  <PropertyGroup Label="Package properties">
+    <Title>XAML Animated GIF</Title>
+    <Description>A simple library to display animated GIF images in WPF applications</Description>
+    <Authors>Thomas Levesque</Authors>
+    <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
+    <PackageProjectUrl>https://github.com/XamlAnimatedGif/XamlAnimatedGif</PackageProjectUrl>
+    <PackageTags>wpf;xaml;animated;gif</PackageTags>
+    <PackageReleaseNotes>https://github.com/XamlAnimatedGif/XamlAnimatedGif/releases</PackageReleaseNotes>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
+    <PackageIcon>assets/xamlanimatedgif.png</PackageIcon>
+    <SupportedOSPlatformVersion>10.0.22621.0</SupportedOSPlatformVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(TargetFramework)' == 'net45'">
+    <DefineConstants>$(DefineConstants);LACKS_STREAM_MEMORY_OVERLOADS</DefineConstants>
+  </PropertyGroup>
+  <ItemGroup Condition="'$(TargetFramework)' == 'net45'">
+    <Reference Include="PresentationCore" />
+    <Reference Include="PresentationFramework" />
+    <Reference Include="WindowsBase" />
+    <Reference Include="System.Xaml" />
+    <Reference Include="System.Net.Http" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="InternalsVisibleTo.MSBuild" Version="1.0.4" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+    <PackageReference Include="MinVer" Version="2.3.1" PrivateAssets="All" />
+  </ItemGroup>
+  <ItemGroup>
+    <InternalsVisibleTo Include="XamlAnimatedGif.Demo, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f5fb0928fd3a0e9d56701d22ef2d7f38ed87ba03ef594ddd5eccc269b64029d7b85d775f112a88394a7d0155b54987a5f2614a08ad0ec34c61431b05ab239afe2c6f2be909ac635dad6240af73934792b62bfe88a57b4a03275818dc304678dd6d22654b0b425165ce000eacdd7a0f2b9ac10e6e6db6e40db4e888ae1fbeebc0" />
+  </ItemGroup>
+</Project>

BIN
src/XamlAnimatedGif/XamlAnimatedGif.snk