Skip to content

Commit

Permalink
Cache keyframes instead of whole animation
Browse files Browse the repository at this point in the history
So that animation with different repeat behaviors don't need to be
cached separately.
  • Loading branch information
thomaslevesque committed Jan 4, 2020
1 parent ead8b3c commit e6af027
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 94 deletions.
58 changes: 33 additions & 25 deletions WpfAnimatedGif/AnimationCache.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
Expand All @@ -8,21 +9,18 @@ namespace WpfAnimatedGif
{
static class AnimationCache
{
private class CacheKey
private struct CacheKey
{
private readonly ImageSource _source;
private readonly RepeatBehavior _repeatBehavior;

public CacheKey(ImageSource source, RepeatBehavior repeatBehavior)
public CacheKey(ImageSource source)
{
_source = source;
_repeatBehavior = repeatBehavior;
}

private bool Equals(CacheKey other)
{
return ImageEquals(_source, other._source)
&& Equals(_repeatBehavior, other._repeatBehavior);
return ImageEquals(_source, other._source);
}

public override bool Equals(object obj)
Expand All @@ -35,10 +33,7 @@ public override bool Equals(object obj)

public override int GetHashCode()
{
unchecked
{
return (ImageGetHashCode(_source) * 397) ^ _repeatBehavior.GetHashCode();
}
return ImageGetHashCode(_source);
}

private static int ImageGetHashCode(ImageSource image)
Expand Down Expand Up @@ -99,21 +94,21 @@ private static Uri GetUri(ImageSource image)
}
}

private static readonly Dictionary<CacheKey, ObjectAnimationUsingKeyFrames> _animationCache = new Dictionary<CacheKey, ObjectAnimationUsingKeyFrames>();
private static readonly Dictionary<CacheKey, AnimationCacheEntry> _animationCache = new Dictionary<CacheKey, AnimationCacheEntry>();
private static readonly Dictionary<CacheKey, int> _referenceCount = new Dictionary<CacheKey, int>();

public static void IncrementReferenceCount(ImageSource source, RepeatBehavior repeatBehavior)
public static void IncrementReferenceCount(ImageSource source)
{
var cacheKey = new CacheKey(source, repeatBehavior);
var cacheKey = new CacheKey(source);
int count;
_referenceCount.TryGetValue(cacheKey, out count);
count++;
_referenceCount[cacheKey] = count;
}

public static void DecrementReferenceCount(ImageSource source, RepeatBehavior repeatBehavior)
public static void DecrementReferenceCount(ImageSource source)
{
var cacheKey = new CacheKey(source, repeatBehavior);
var cacheKey = new CacheKey(source);
int count;
_referenceCount.TryGetValue(cacheKey, out count);
if (count > 0)
Expand All @@ -128,24 +123,37 @@ public static void DecrementReferenceCount(ImageSource source, RepeatBehavior re
}
}

public static void AddAnimation(ImageSource source, RepeatBehavior repeatBehavior, ObjectAnimationUsingKeyFrames animation)
public static void Add(ImageSource source, AnimationCacheEntry entry)
{
var key = new CacheKey(source, repeatBehavior);
_animationCache[key] = animation;
var key = new CacheKey(source);
_animationCache[key] = entry;
}

public static void RemoveAnimation(ImageSource source, RepeatBehavior repeatBehavior, ObjectAnimationUsingKeyFrames animation)
public static void Remove(ImageSource source)
{
var key = new CacheKey(source, repeatBehavior);
var key = new CacheKey(source);
_animationCache.Remove(key);
}

public static ObjectAnimationUsingKeyFrames GetAnimation(ImageSource source, RepeatBehavior repeatBehavior)
public static AnimationCacheEntry Get(ImageSource source)
{
var key = new CacheKey(source);
_animationCache.TryGetValue(key, out var entry);
return entry;
}
}

internal class AnimationCacheEntry
{
public AnimationCacheEntry(ObjectKeyFrameCollection keyFrames, Duration duration, int repeatCountFromMetadata)
{
var key = new CacheKey(source, repeatBehavior);
ObjectAnimationUsingKeyFrames animation;
_animationCache.TryGetValue(key, out animation);
return animation;
KeyFrames = keyFrames;
Duration = duration;
RepeatCountFromMetadata = repeatCountFromMetadata;
}

public ObjectKeyFrameCollection KeyFrames { get; }
public Duration Duration { get; }
public int RepeatCountFromMetadata { get; }
}
}
144 changes: 75 additions & 69 deletions WpfAnimatedGif/ImageBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ private static void AnimatedSourceChanged(DependencyObject o, DependencyProperty
imageControl.Unloaded -= ImageControlUnloaded;
imageControl.IsVisibleChanged -= VisibilityChanged;

AnimationCache.DecrementReferenceCount(oldValue, GetRepeatBehavior(imageControl));
AnimationCache.DecrementReferenceCount(oldValue);
var controller = GetAnimationController(imageControl);
if (controller != null)
controller.Dispose();
Expand Down Expand Up @@ -342,7 +342,7 @@ static void ImageControlUnloaded(object sender, RoutedEventArgs e)
return;
var source = GetAnimatedSource(imageControl);
if (source != null)
AnimationCache.DecrementReferenceCount(source, GetRepeatBehavior(imageControl));
AnimationCache.DecrementReferenceCount(source);
var controller = GetAnimationController(imageControl);
if (controller != null)
controller.Dispose();
Expand All @@ -357,8 +357,6 @@ private static void RepeatBehaviorChanged(DependencyObject o, DependencyProperty
ImageSource source = GetAnimatedSource(imageControl);
if (source != null)
{
if (!Equals(e.OldValue, e.NewValue))
AnimationCache.DecrementReferenceCount(source, (RepeatBehavior)e.OldValue);
if (imageControl.IsLoaded)
InitAnimationOrImage(imageControl);
}
Expand Down Expand Up @@ -445,59 +443,69 @@ private static void InitAnimationOrImage(Image imageControl)

private static ObjectAnimationUsingKeyFrames GetAnimation(Image imageControl, BitmapSource source)
{
var animation = AnimationCache.GetAnimation(source, GetRepeatBehavior(imageControl));
if (animation != null)
return animation;
GifFile gifMetadata;
var decoder = GetDecoder(source, imageControl, out gifMetadata) as GifBitmapDecoder;
if (decoder != null && decoder.Frames.Count > 1)
{
var fullSize = GetFullSize(decoder, gifMetadata);
int index = 0;
animation = new ObjectAnimationUsingKeyFrames();
var totalDuration = TimeSpan.Zero;
BitmapSource baseFrame = null;
foreach (var rawFrame in decoder.Frames)
var cacheEntry = AnimationCache.Get(source);
if (cacheEntry == null)
{
var decoder = GetDecoder(source, imageControl, out GifFile gifMetadata) as GifBitmapDecoder;
if (decoder != null && decoder.Frames.Count > 1)
{
var metadata = GetFrameMetadata(decoder, gifMetadata, index);

var frame = MakeFrame(fullSize, rawFrame, metadata, baseFrame);
var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
animation.KeyFrames.Add(keyFrame);

totalDuration += metadata.Delay;

switch (metadata.DisposalMethod)
var fullSize = GetFullSize(decoder, gifMetadata);
int index = 0;
var keyFrames = new ObjectKeyFrameCollection();
var totalDuration = TimeSpan.Zero;
BitmapSource baseFrame = null;
foreach (var rawFrame in decoder.Frames)
{
case FrameDisposalMethod.None:
case FrameDisposalMethod.DoNotDispose:
baseFrame = frame;
break;
case FrameDisposalMethod.RestoreBackground:
if (IsFullFrame(metadata, fullSize))
{
baseFrame = null;
}
else
{
baseFrame = ClearArea(frame, metadata);
}
break;
case FrameDisposalMethod.RestorePrevious:
// Reuse same base frame
break;
var metadata = GetFrameMetadata(decoder, gifMetadata, index);

var frame = MakeFrame(fullSize, rawFrame, metadata, baseFrame);
var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
keyFrames.Add(keyFrame);

totalDuration += metadata.Delay;

switch (metadata.DisposalMethod)
{
case FrameDisposalMethod.None:
case FrameDisposalMethod.DoNotDispose:
baseFrame = frame;
break;
case FrameDisposalMethod.RestoreBackground:
if (IsFullFrame(metadata, fullSize))
{
baseFrame = null;
}
else
{
baseFrame = ClearArea(frame, metadata);
}
break;
case FrameDisposalMethod.RestorePrevious:
// Reuse same base frame
break;
}

index++;
}

index++;

int repeatCount = GetRepeatCountFromMetadata(decoder, gifMetadata);
cacheEntry = new AnimationCacheEntry(keyFrames, totalDuration, repeatCount);
AnimationCache.Add(source, cacheEntry);
}
animation.Duration = totalDuration;

animation.RepeatBehavior = GetActualRepeatBehavior(imageControl, decoder, gifMetadata);
}

AnimationCache.AddAnimation(source, GetRepeatBehavior(imageControl), animation);
AnimationCache.IncrementReferenceCount(source, GetRepeatBehavior(imageControl));
if (cacheEntry != null)
{
var animation = new ObjectAnimationUsingKeyFrames
{
KeyFrames = cacheEntry.KeyFrames,
Duration = cacheEntry.Duration,
RepeatBehavior = GetActualRepeatBehavior(imageControl, cacheEntry.RepeatCountFromMetadata)
};
AnimationCache.IncrementReferenceCount(source);
return animation;
}

return null;
}

Expand Down Expand Up @@ -712,37 +720,35 @@ private static BitmapSource MakeFrame(
return result;
}

private static RepeatBehavior GetActualRepeatBehavior(Image imageControl, BitmapDecoder decoder, GifFile gifMetadata)
private static RepeatBehavior GetActualRepeatBehavior(Image imageControl, int repeatCountFromMetadata)
{
// If specified explicitly, use this value
var repeatBehavior = GetRepeatBehavior(imageControl);
if (repeatBehavior != default(RepeatBehavior))
return repeatBehavior;

int repeatCount;
if (gifMetadata != null)
{
repeatCount = gifMetadata.RepeatCount;
}
else
{
repeatCount = GetRepeatCount(decoder);
}
if (repeatCount == 0)
if (repeatCountFromMetadata == 0)
return RepeatBehavior.Forever;
return new RepeatBehavior(repeatCount);
return new RepeatBehavior(repeatCountFromMetadata);
}

private static int GetRepeatCount(BitmapDecoder decoder)
private static int GetRepeatCountFromMetadata(BitmapDecoder decoder, GifFile gifMetadata)
{
var ext = GetApplicationExtension(decoder, "NETSCAPE2.0");
if (ext != null)
if (gifMetadata != null)
{
return gifMetadata.RepeatCount;
}
else
{
byte[] bytes = ext.GetQueryOrNull<byte[]>("/Data");
if (bytes != null && bytes.Length >= 4)
return BitConverter.ToUInt16(bytes, 2);
var ext = GetApplicationExtension(decoder, "NETSCAPE2.0");
if (ext != null)
{
byte[] bytes = ext.GetQueryOrNull<byte[]>("/Data");
if (bytes != null && bytes.Length >= 4)
return BitConverter.ToUInt16(bytes, 2);
}
return 1;
}
return 1;
}

private static BitmapMetadata GetApplicationExtension(BitmapDecoder decoder, string application)
Expand Down

0 comments on commit e6af027

Please sign in to comment.