[WPF] Display an animated GIF image

WPF is a great technology, but sometimes it seems to be missing some really basic features… A frequently mentioned example is the lack of support for animated GIF images. Actually, the GIF format itself is supported by the imaging API, but the Image control only shows the first frame of the animation.

Many solutions to this problem have been proposed on technical forums and blogs, usually variations of the following approaches:

  • Use the MediaElement control: unfortunately this control only supports URI like file:// or http://, not the pack:// URI schema used for WPF resources; this means the image can’t be included in the resources, it has to be in a separate file. Furthermore, transparency for GIF images isn’t supported in MediaElement, which makes the final result quite ugly
  • Use the PictureBox control from Windows Forms, via a WindowsFormsHost: I personnally dislike using WinForms controls in WPF, it really looks like a hack…
  • Create a custom control that inherits Image and handles the animation. Some solutions take advantage of the ImageAnimator class from System.Drawing (GDI), others use a WPF animation to change the current frame. It’s a rather “clean” approach, but it forces you to use a specific control for GIF images. Also, the solution using ImageAnimator turns out not to be very smooth, the animation is quite jerky.

As you might have guessed, I don’t find any of these solutions really satisfying… Furthermore, none of the implementations I’ve seen of the third approach handles the duration of each frame properly, they only assume that all frames last 100ms (which is almost always true, but almost isn’t good enough IMHO…). So I kept the best ideas from each approach I’ve seen, and I came up with my own solution. Here are the goals I set to attain:

  • No dependency on Windows Forms or GDI
  • Display the animated image in a standard Image control
  • Use the same XAML code for normal and animated images
  • Support for transparency
  • Correct handling of frame duration

To achieve this result, I started from a very simple, even obvious idea: to animate the image, all you have to do is apply an animation to the Source property of the Image control. WPF provides all the necessary tools to do that; in this case, the ObjectAnimationUsingKeyFrames class fits the bill perfectly: it allows to specify at what exact time a given value should be assigned to the property, which makes it easy to take the frame duration into account.

The next problem is to extract the frames from the image: fortunately WPF supports this natively, and the BitmapDecoder class provides a Frames property to do exactly that. So, no big difficulty so far…

Finally, last obstacle: extract the duration of each frame. It’s the part that took me the longest, because I needed to do some research… I first thought I would need to read the file manually and decode the binary data myself. But eventually the solution is quite simple, and takes advantage of the BitmapMetadata class. The only difficulty has been to find the “path” of the metadata that contains the delay, but after a few minutes of trial and error, here it is: /grctlext/Delay.

The final solution is implemented as an attached property named AnimatedSource, that applies to the Image control, and can be used instead of Source:

<Image Stretch="None" my:ImageBehavior.AnimatedSource="/Images/animation.gif" />

This property can also be assigned a normal (not animated) image, it will be displayed normally; therefore this property can be used without worrying about whether the image to display will be animated or not.

So in the end, all the goals have been achieved, and we even get some icing on the cake: this solution also works in the designer (at least in Visual Studio 2010), so the animation is immediately visible when you set the AnimatedSource property :)

Without further ado, here’s the complete code:

    public static class ImageBehavior
    {
        #region AnimatedSource

        [AttachedPropertyBrowsableForType(typeof(Image))]
        public static ImageSource GetAnimatedSource(Image obj)
        {
            return (ImageSource)obj.GetValue(AnimatedSourceProperty);
        }

        public static void SetAnimatedSource(Image obj, ImageSource value)
        {
            obj.SetValue(AnimatedSourceProperty, value);
        }

        public static readonly DependencyProperty AnimatedSourceProperty =
            DependencyProperty.RegisterAttached(
              "AnimatedSource",
              typeof(ImageSource),
              typeof(ImageBehavior),
              new UIPropertyMetadata(
                null,
                AnimatedSourceChanged));

        private static void AnimatedSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            Image imageControl = o as Image;
            if (imageControl == null)
                return;

            var oldValue = e.OldValue as ImageSource;
            var newValue = e.NewValue as ImageSource;
            if (oldValue != null)
            {
                imageControl.BeginAnimation(Image.SourceProperty, null);
            }
            if (newValue != null)
            {
                imageControl.DoWhenLoaded(InitAnimationOrImage);
            }
        }

        private static void InitAnimationOrImage(Image imageControl)
        {
            BitmapSource source = GetAnimatedSource(imageControl) as BitmapSource;
            if (source != null)
            {
                var decoder = GetDecoder(source) as GifBitmapDecoder;
                if (decoder != null && decoder.Frames.Count > 1)
                {
                    var animation = new ObjectAnimationUsingKeyFrames();
                    var totalDuration = TimeSpan.Zero;
                    BitmapSource prevFrame = null;
                    FrameInfo prevInfo = null;
                    foreach (var rawFrame in decoder.Frames)
                    {
                        var info = GetFrameInfo(rawFrame);
                        var frame = MakeFrame(
                            source,
                            rawFrame, info,
                            prevFrame, prevInfo);

                        var keyFrame = new DiscreteObjectKeyFrame(frame, totalDuration);
                        animation.KeyFrames.Add(keyFrame);
                        
                        totalDuration += info.Delay;
                        prevFrame = frame;
                        prevInfo = info;
                    }
                    animation.Duration = totalDuration;
                    animation.RepeatBehavior = RepeatBehavior.Forever;
                    if (animation.KeyFrames.Count > 0)
                        imageControl.Source = (ImageSource)animation.KeyFrames[0].Value;
                    else
                        imageControl.Source = decoder.Frames[0];
                    imageControl.BeginAnimation(Image.SourceProperty, animation);
                    return;
                }
            }
            imageControl.Source = source;
            return;
        }

        private static BitmapDecoder GetDecoder(BitmapSource image)
        {
            BitmapDecoder decoder = null;
            var frame = image as BitmapFrame;
            if (frame != null)
                decoder = frame.Decoder;

            if (decoder == null)
            {
                var bmp = image as BitmapImage;
                if (bmp != null)
                {
                    if (bmp.StreamSource != null)
                    {
                        decoder = BitmapDecoder.Create(bmp.StreamSource, bmp.CreateOptions, bmp.CacheOption);
                    }
                    else if (bmp.UriSource != null)
                    {
                        Uri uri = bmp.UriSource;
                        if (bmp.BaseUri != null && !uri.IsAbsoluteUri)
                            uri = new Uri(bmp.BaseUri, uri);
                        decoder = BitmapDecoder.Create(uri, bmp.CreateOptions, bmp.CacheOption);
                    }
                }
            }

            return decoder;
        }

        private static BitmapSource MakeFrame(
            BitmapSource fullImage,
            BitmapSource rawFrame, FrameInfo frameInfo,
            BitmapSource previousFrame, FrameInfo previousFrameInfo)
        {
            DrawingVisual visual = new DrawingVisual();
            using (var context = visual.RenderOpen())
            {
                if (previousFrameInfo != null && previousFrame != null &&
                    previousFrameInfo.DisposalMethod == FrameDisposalMethod.Combine)
                {
                    var fullRect = new Rect(0, 0, fullImage.PixelWidth, fullImage.PixelHeight);
                    context.DrawImage(previousFrame, fullRect);
                }

                context.DrawImage(rawFrame, frameInfo.Rect);
            }
            var bitmap = new RenderTargetBitmap(
                fullImage.PixelWidth, fullImage.PixelHeight,
                fullImage.DpiX, fullImage.DpiY,
                PixelFormats.Pbgra32);
            bitmap.Render(visual);
            return bitmap;
        }

        private class FrameInfo
        {
            public TimeSpan Delay { get; set; }
            public FrameDisposalMethod DisposalMethod { get; set; }
            public double Width { get; set; }
            public double Height { get; set; }
            public double Left { get; set; }
            public double Top { get; set; }

            public Rect Rect
            {
                get { return new Rect(Left, Top, Width, Height); }
            }
        }

        private enum FrameDisposalMethod
        {
            Replace = 0,
            Combine = 1,
            RestoreBackground = 2,
            RestorePrevious = 3
        }

        private static FrameInfo GetFrameInfo(BitmapFrame frame)
        {
            var frameInfo = new FrameInfo
            {
                Delay = TimeSpan.FromMilliseconds(100),
                DisposalMethod = FrameDisposalMethod.Replace,
                Width = frame.PixelWidth,
                Height = frame.PixelHeight,
                Left = 0,
                Top = 0
            };

            BitmapMetadata metadata;
            try
            {
                metadata = frame.Metadata as BitmapMetadata;
                if (metadata != null)
                {
                    const string delayQuery = "/grctlext/Delay";
                    const string disposalQuery = "/grctlext/Disposal";
                    const string widthQuery = "/imgdesc/Width";
                    const string heightQuery = "/imgdesc/Height";
                    const string leftQuery = "/imgdesc/Left";
                    const string topQuery = "/imgdesc/Top";

                    var delay = metadata.GetQueryOrNull<ushort>(delayQuery);
                    if (delay.HasValue)
                        frameInfo.Delay = TimeSpan.FromMilliseconds(10 * delay.Value);

                    var disposal = metadata.GetQueryOrNull<byte>(disposalQuery);
                    if (disposal.HasValue)
                        frameInfo.DisposalMethod = (FrameDisposalMethod) disposal.Value;

                    var width = metadata.GetQueryOrNull<ushort>(widthQuery);
                    if (width.HasValue)
                        frameInfo.Width = width.Value;

                    var height = metadata.GetQueryOrNull<ushort>(heightQuery);
                    if (height.HasValue)
                        frameInfo.Height = height.Value;

                    var left = metadata.GetQueryOrNull<ushort>(leftQuery);
                    if (left.HasValue)
                        frameInfo.Left = left.Value;

                    var top = metadata.GetQueryOrNull<ushort>(topQuery);
                    if (top.HasValue)
                        frameInfo.Top = top.Value;
                }
            }
            catch (NotSupportedException)
            {
            }

            return frameInfo;
        }

        private static T? GetQueryOrNull<T>(this BitmapMetadata metadata, string query)
            where T : struct
        {
            if (metadata.ContainsQuery(query))
            {
                object value = metadata.GetQuery(query);
                if (value != null)
                    return (T) value;
            }
            return null;
        }

        #endregion
    }

And here’s the DoWhenLoaded extension method used in the code above:

public static void DoWhenLoaded<T>(this T element, Action<T> action)
    where T : FrameworkElement
{
    if (element.IsLoaded)
    {
        action(element);
    }
    else
    {
        RoutedEventHandler handler = null;
        handler = (sender, e) =>
        {
            element.Loaded -= handler;
            action(element);
        };
        element.Loaded += handler;
    }
}

Enjoy :)

Update: the code that retrieves the frame duration only works on Windows Seven, and on Windows Vista if the Platform Update is installed (untested). The default duration (100ms) will be used instead on other versions of Windows. I will update the article if I find a solution that works on all operating systems (I know I could use System.Drawing.Bitmap, but I’d rather not depend on this…)

Update 2: as pointed out by Klaus in the comments, the ImageBehavior class didn’t handle some important attributes of the frames: the diposal method (whether a frame should entirely replace the previous one, or be combined with it), and the frame position (Left/Top/Width/Height). I updated the code to handle these attributes properly. Thank you Klaus!

Update 3: a commenter on the French version of my blog pointed out a problem when the AnimatedSource is an image in a resource dictionary; the UriSource wasn’t correctly interpreted when it was a relative URI. This problem is now fixed. Thank you, “anonymous”!

Update 4: uploaded an example project to demonstrate the code.

kick it on DotNetKicks.com

Advertisement
Posted in WPF. Tags: , , . 43 Comments »

43 Responses to “[WPF] Display an animated GIF image”

  1. Marc Says:

    This is really nice, except I am getting “jumpy” behavior from the animated gif..

    • Thomas Levesque Says:

      Hi Marc,
      For me the animation works smoothly… Perhaps there is something unusual in the gif you’re trying to display. Could you post it somewhere so I can look into the problem?

  2. Vitthal Says:

    Hi Thomas,
    It looks good but in wpf app it gives me an error on foll. line

    that : the attachable property “AnimatedSource” was not found in type “ImageBehavior”.
    can u please help me resolve this problem?
    Thannks in advance.

    • Thomas Levesque Says:

      Hi Vitthal,
      This error can appear in the designer, but it should disappear once you compile the project. Make sure your namespace declarations are correct.
      Regards,
      Thomas

  3. Vitthal Says:

    Hi thomas thanks for reply.
    But i dont know which namespace i need to add.
    can u please explain me?
    Thanks.

    • Thomas Levesque Says:

      Vitthal,

      You need an attribute like that on the root element:

      xmlns:my="clr-namespace:MyNamespace"

      where MyNamespace is the C# namespace where you put the ImageBehavior class.

      If the class is in another assembly, use this syntax:

      xmlns:my="clr-namespace:MyNamespace;assembly=MyAssembly"

      See this page for details on XAML namespace mapping

  4. Vitthal Says:

    Hi Thomas.
    I don’t know where i’m wrong.
    On the xaml.vb file i have added foll. code
    Namespace ImageBehave
    ….
    End Namespace

    And in this namspace i added the class provided by you.
    Then on xaml page i reffered it like..
    xmlns:my=”clr-namespace:ImageBehave”

    But it shows me the same error.
    I done it using vb.net.

    One more error for extension method
    that it can write in module.

    I really stuck here.
    Please help me to get out of this.

    Thank you.

  5. jimmy Says:

    Thanks… thought this was great!
    Question on something I was going to look into. Do you think it would be possible to pause the animation – say – on a trigger/datatrigger? I guess switch the animated source to something that is not animated would be the simple/quick way?

    • Thomas Levesque Says:

      Hi Jimmy,

      Sure, you can do that. AnimatedSource is an attached property, so you can set it in a trigger. I think the easiest way to stop the animation would be to set AnimatedSource to null, and assign the GIF image to Source instead. Note that it wouldn’t exactly pause the animation on its current frame, it would display the first frame… You could probably modify the code so that it really suspends the animation, but I’m not sure how to do it.

  6. daves561 Says:

    Very well done. Thank you. For what it’s worth, because my project messes around with the StreamSource, I needed to do a Seek back to the start before creating the decoder in ImageBehavior.GetDecoder().

    Also, thanks for using the “as” operator correctly. I so tire of examples that use “as” and then don’t check for null.

  7. Klaus Says:

    This is very clever, thank you!

    Many animated GIFs do not use ‘replace’ mode, but ‘combine’ mode, i.e. the next frame shall be painted on top of what is already there. This can be recognized by checking the “/grctlext/Disposal” metadata (1 for combine mode).
    In this case, the DiscreteObjectKeyFrames cannot just contain the original frames from the GifBitmapDecoder, but one has to combine the previous frame with the new one, e.g. by using RenderTargetBitmap with two stacked Image controls. It is also important to take “/imgdesc/Left” and “/imgdesc/Top” into account.

    Klaus

    • Thomas Levesque Says:

      Hi Klaus,

      Thanks for your comment, I wasn’t aware of these subtleties of the GIF format! I would gladly improve the code to handle them, but I don’t have any images with the characteristics you mention ; do you know where I could find some examples?

      Regards,
      Thomas

  8. Zamolxesz Says:

    Hi Thomas,

    very nice piece of code, excellent work! No wonder your post shows up as the first result for “XAML Animated GIF” :) One quick question though: How can I make a regular Image Control to be animated from code? Is that possible? Would help a lot, thank you in advance!

    P.S. : I’ve also tried to change the visibility of the Image whose animation I attached using your code, to no avail. Just a swift observation from my part. Keep up the good work, cheers!

    • Thomas Levesque Says:

      Hi,

      You can set the AnimatedSource property from code :

      ImageBehavior.SetAnimatedSource(imageControl, imageSource);

      As for the Visibility, I’m not sure what the problem is… my code isn’t doing anything with this property.

      Regards,
      Thomas

  9. guyguy Says:

    Hi Thomas,

    Thanks for the code. However, I’m having memory issues with it – since I started using it, my application consumes hundreds of MB (while before it needed only 50 MB)….

    Thanks!

    • guyguy Says:

      Some more info – I’m having 124 frames in the image. Here is a typical frame info -

      delay = 0.03 sec
      DisposalMethod = Combine
      Height = 760
      Width = 650

      • Thomas Levesque Says:

        Hi Guyguy,

        Actually it’s not really surprising… You’re not loading 1 image, but rather 124 images, so it’s bound to use up a lot of memory. If you display your GIF in another app (a web browser for instance) you will probably notice a similar increase in memory usage. Large animated GIFs with so many frames are quite uncommon, mainly for this reason. Perhaps you should consider using a video instead.

        Regards,
        Thomas

  10. guyguy Says:

    Thanks for the answer. I actually thought about using a video instead, only that I need the transparency in the image…
    Do you think it could be possible to load the frames in chunks and not all together, so that there is no need to hold all frames in memory at the same time? Or maybe a better solution?

    • Thomas Levesque Says:

      Mmm… that’s a hard one. I guess it would be possible to load the frames on demand and avoid keeping them in memory, but since they’re only 0.03 second apart from each other, that would hurt performance a lot…

  11. meton Says:

    Thanks for sharing, it works perfectly !!!!

  12. Carlos Says:

    Hello.
    I have tonz of errors tring make this work

    Plese can upload compete sourcecode example, working so I cnan donload test and see why what im doing is not working?
    Please!!! : )

    • Thomas Levesque Says:

      Hi Carlos,
      I’m on holidays so I can’t post an example now. The code should contain everything you need, I only omitted is the using directives because Visual Studio can add them automatically (using the smart tags).
      Regards,
      Thomas

    • Thomas Levesque Says:

      Carlos,

      I uploaded an example project to demonstrate how to use the code. (see link at the end of the post)

      Regards,
      Thomas

  13. Benjamin Says:

    Hello,

    I wonder if it is possible to delete an GIF this way?
    I mean that at first i just allowed jpg’s, but now i want to allow GIF’s, this worked alright. But my application allows to delete the file from disk, and you can see where i am going at, when I delete, it of course said “File being used by another process”.

    When using images this was easy:
    BitmapImage image = new BitmapImage();
    image.BeginInit();
    image.CacheOption = BitmapCacheOption.OnLoad;
    image.UriSource = new Uri(path);
    image.EndInit();

    But here, i can’t find the right solution for this, Can you think of a way to achieve this?

    • Benjamin Says:

      Nevermind,

      That code i posted was wrapped in a converter, and when I apply this converter on your dependency property, it works. I forgot you mentioned your dependency property reacts just like the original Source property of

      So everything just works perfectly :) Thx!

  14. Carlos Says:

    Hey Thomas , im just asking not pushing, when you back form Holidays?

  15. Carlos Says:

    Thank you so much is now work in correct way!!
    I can fix my mistakes.

    I load gif from different places and all work, but i found some gif just not play
    http://forum.arcadecontrols.com/index.php?topic=76883.0
    I test gif called ghost.gif and ghost-box.gif and this 2 not play.
    Some idea?
    Another questions.
    Is possible make gif start play when i click over

    Thank you gain!!
    Great job ! help me a lot !!!

    • Thomas Levesque Says:

      Are you using Windows XP? Unfortunately on XP the metadata for the frames isn’t available, so I don’t have the necessary information about where and when to display the frames… You could try to reencode the images differently so that each frame contains the whole image (rather than just the diff between consecutive images)

  16. Carlos Says:

    Hello
    No, i use windows 8.

    • Thomas Levesque Says:

      Hi,
      The problem seems to be that in your images the delay has a value of 0… In this case you should probably use the default value. Just change the code a bit in the GetFrameInfo method: replace if (delay.HasValue) with if (delay.HasValue && delay.Value > 0)

  17. Greg Says:

    I’ve been using this code a bit and really, really want it to work for me. I got it to work using a new property for some cases where I have to bind directly to a Bitmap instance:

    controls:ImageBehavior.BitmapContent=”{Binding AdImage}”

    I do this by converting the System.Drawing.Bitmap to a System.Windows.Media.Imaging.BitmapImage and then using that through the rest of your code.

    Now, my only problem is that when I do so, the images become blurry. Text that was crips now seems blurry. I think that this is due to the use of the RenderTargetBitmap, which only supports PixelFormats.Pbgra32, but I’m not 100% sure. Some blurring or interpolation seems to happen in there.

    Have you or anyone else noticed this? You can try this image for an example:
    http://i529.photobucket.com/albums/dd338/VGOTheMeekGeekVGV/Internet%20Marketing%20Ads/VGO-banner_160x600-Animated.gif

    • Thomas Levesque Says:

      Hi Greg,
      Why do you have to use a System.Drawing.Bitmap? Can’t you use a WPF image directly? Anyway, I don’t think your problem is related to my code, but rather to the code that converts the image…
      Regards,
      Thomas

      • Greg Says:

        I had to use a System.Drawing.Bitmap because someone else’s code was providing that type. Originally, it was using a custom control to bind to an instance of a Bitmap. Basically, there’s a web service that’s providing an image and the client code loads it as a Bitmap. Maybe I could have changed that, but probably would have still had to make some changes, since I have an object, nor a URI or a resource. I didn’t have time to change the code that was providing the Bitmap.

    • Greg Says:

      Ah, never mind! It had more to do with either UseLayoutRounding or SnapsToDevicePixels on some parent elements, I think. Looks good now … and thanks for the great code!


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.