5

I want to create a System.Drawing.Bitmap instance "manually", which contains an animation.

The Bitmap instance to create should meet the following criteria:

  • It is an animation (image.FrameDimensionsLists has a Time dimension)
  • It has multiple frames (image.GetFrameCount(dimension) > 1)
  • I can obtain the delay between the frames (image.GetPropertyItem(0x5100).Value)

I'm pretty sure it is possible to create such an image via some WinApi. This is what the GIF Decoder actually does as well.

I know that I can play the animation if I have the frames from any source by doing it manually, but I want to do it in a compatible way: If I could produce such a Bitmap, I could simply use it on a Button, Label, PictureBox or any other existing control, and the built-in ImageAnimator could also handle it automatically.

Most of the similar topics suggest to convert the frames into an animated GIF; however, this is not a good solution, because it does not handle true color and semi-transparency (eg. APNG animation).

Update: After some exploring I learned that I could implement a decoder using WIC; however, I do not want to register a new decoder in Windows, and it uses COM, which I want to avoid if possible. Not mentioning that at the end I will have an IWICBitmapSource, which I still need to convert to a Bitmap.

Update 2: I have set a bounty. You are the winner if you can implement the following method:

public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)
{
    // Any WinApi is allowed. WIC is also allowed, but not preferred.
    // Creating an animated GIF is not an acceptable answer. What if frames are from an APNG?
}
György Kőszeg
  • 17,093
  • 6
  • 37
  • 65

3 Answers3

5
    public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)

Setting strict limits on the expected implementation like that is not very wise. It is technically possible by taking advantage of the TIFF image format, it is capable of storing multiple frames. They are however not time-based, only the GIF codec support that. One extra argument is required so the control can be updated when the next image needs to be rendered. Like this:

    public static Image CreateAnimation(Control ctl, Image[] frames, int[] delays) {
        var ms = new System.IO.MemoryStream();
        var codec = ImageCodecInfo.GetImageEncoders().First(i => i.MimeType == "image/tiff");

        EncoderParameters encoderParameters = new EncoderParameters(2);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.MultiFrame);
        encoderParameters.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)EncoderValue.CompressionLZW);
        frames[0].Save(ms, codec, encoderParameters);

        encoderParameters = new EncoderParameters(1);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);
        for (int i = 1; i < frames.Length; i++) {
            frames[0].SaveAdd(frames[i], encoderParameters);
        }
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.SaveFlag, (long)EncoderValue.Flush);
        frames[0].SaveAdd(encoderParameters);

        ms.Position = 0;
        var img = Image.FromStream(ms);
        Animate(ctl, img, delays);
        return img;
    }

The Animate() method needs a Timer to select the next frame and get the control updated:

    private static void Animate(Control ctl, Image img, int[] delays) {
        int frame = 0;
        var tmr = new Timer() { Interval = delays[0], Enabled = true };
        tmr.Tick += delegate {
            frame++;
            if (frame >= delays.Length) frame = 0;
            img.SelectActiveFrame(FrameDimension.Page, frame);
            tmr.Interval = delays[frame];
            ctl.Invalidate();
        };
        ctl.Disposed += delegate { tmr.Dispose(); };
    }

Sample usage:

    public Form1() {
        InitializeComponent();
        pictureBox1.Image = CreateAnimation(pictureBox1,
            new Image[] { Properties.Resources.Frame1, Properties.Resources.Frame2, Properties.Resources.Frame3 },
            new int[] { 1000, 2000, 300 });
    }

A smarter way to go about it is to drop the return value requirement completely so you don't have to generate the TIFF. And just use the Animate() method with an Action<Image> argument to get the control's property updated. But not what you asked for.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thank you for your answer. As I said I can simulate any animation programmatically and this is what I do for example when I extract frames from an APNG animation. However, the GIF decoder is able to create a single Bitmap, which contains every needed information to animate the image and I also want to achieve that somehow. Here you make a multipage TIFF image by using the TIFF encoder (and the decoder when you load it back from the stream), which results a Bitmap instance with Page dimension instead of Time. Thus the built-in ImageAnimator will not play the animation on a Button, for example. – György Kőszeg Aug 18 '15 at 07:53
  • Well, yes, this is all quite obvious. None of the Windows image encoders support the time dimension, that's where the buck stops. You need that Animate() method. Works well, I recommend you use it. – Hans Passant Aug 18 '15 at 08:03
  • Thanks, I have already implemented a custom animator like this. But the key is the compatibility here (and the curiosity, of course). I know that among the built-in decoders the GIF decoder is the only one, which utilizes this Bitmap feature and creates a Bitmap with the criteria listed in the question. I could reformulate the question as follows: how does the GIF decoder do this and how can I achieve the same result regardless of the source format? – György Kőszeg Aug 18 '15 at 08:24
  • I can accept solution that uses WIC as well because I can convert the IWICBitmapSource to Bitmap by using CopyPixels: http://www.nuonsoft.com/blog/2011/10/17/introduction-to-wic-how-to-use-wic-to-load-an-image-and-draw-it-with-gdi/ – György Kőszeg Aug 18 '15 at 08:26
  • The GIF decoder doesn't do it, it relies on a 3rd party *encoder* that created the image file. You are tone-deaf to the "None of the Window image encoders support the time dimension" comment. Including WIC. Why this is so lame is covered in [this Q+A](http://stackoverflow.com/a/18033514/17034). – Hans Passant Aug 18 '15 at 08:43
  • Not really. There are 3 *decoders*, which support multipage Bitmaps: TIFF, GIF and Icon (with dimensions Page, Time and Resolution, respectively). Load an animgif and check the result Bitmap if you are skeptic. The *encoders* are however dumber: Only TIFF encoder supports multipage image, GIF doesn't, and there is no built-in Icon encoder at all. I still can create both animgif and multi-res icons without using any encoder. But the question is not about the encoders. The question is how can I create an animated Bitmap "manually" from a non-GIF source. – György Kőszeg Aug 18 '15 at 09:19
  • No, this question is actually about *encoders* and not decoders because that is what you need to create the multi-frame bitmap. Not getting anywhere with this so I'm signing off, good luck with it. – Hans Passant Aug 18 '15 at 09:27
1

Unfortunately we can extend neither System.Drawing.Image nor System.Drawing.Bitmap so overriding image.FrameDimensionsLists and similar members is out of question, and as Hans Passant mentioned, None of the Windows image encoders support the time dimension by itself. However I believe in this specific case based on

I know that I can play the animation if I have the frames from any source by doing it manually, but I want to do it in a compatible way: If I could produce such a Bitmap, I could simply use it on a Button, Label, PictureBox or any other existing control, and the built-in ImageAnimator could also handle it automatically.

We can get away by implementing a new class which can handle Animations and then implicitly cast it to Bitmap. We can use extension methods to automatically activate animations for controls. I know this method is hacky, but I thought maybe it would worth mentioning it.

Here is a rough implementation and sample usage.

AnimatedBitmap: handles frame and time based animation base on provided sequences:

 public class Sequence
    {
        public Image Image { get; set; }
        public int Delay { get; set; }
    }

    public class AnimatedBitmap:IDisposable
    {
        private readonly Bitmap _buffer;
        private readonly Graphics _g;
        private readonly Sequence[] _sequences;
        private readonly CancellationTokenSource _cancelToken;

        public event EventHandler FrameUpdated;

        protected void OnFrameUpdated()
        {
            if (FrameUpdated != null)
                FrameUpdated(this, EventArgs.Empty);
        }

        public AnimatedBitmap(int width, int height, params Sequence[] sequences)
        {
            _buffer = new Bitmap(width, height, PixelFormat.Format32bppArgb) {Tag = this};

            _sequences = sequences;
            _g=Graphics.FromImage(_buffer);
            _g.CompositingMode=CompositingMode.SourceCopy;

            _cancelToken = new CancellationTokenSource();
            Task.Factory.StartNew(Animate
                , TaskCreationOptions.LongRunning
                , _cancelToken.Token);
        }

        private void Animate(object obj)
        {
            while (!_cancelToken.IsCancellationRequested)
                foreach (var sequence in _sequences)
                {
                    if (_cancelToken.IsCancellationRequested)
                        break;

                    _g.Clear(Color.Transparent);
                    _g.DrawImageUnscaled(sequence.Image,0,0);
                    _g.Flush(FlushIntention.Flush);
                    OnFrameUpdated();
                    Thread.Sleep(sequence.Delay);
                }

            _g.Dispose();
            _buffer.Dispose();
        }

        public AnimatedBitmap(params Sequence[] sequences)
            : this(sequences.Max(s => s.Image.Width), sequences.Max(s => s.Image.Height), sequences)
        {
        }

        public void Dispose()
        {
            _cancelToken.Cancel();
        }

        public static implicit operator Bitmap(AnimatedBitmap animatedBitmap)
        {
            return animatedBitmap._buffer;
        }

        public static explicit operator AnimatedBitmap(Bitmap bitmap)
        {
            var tag = bitmap.Tag as AnimatedBitmap;
            if (tag != null)
                return tag;

            throw new InvalidCastException();
        }

        public static AnimatedBitmap CreateAnimation(Image[] frames, int[] delays)
        {
            var sequences = frames.Select((t, i) => new Sequence {Image = t, Delay = delays[i]}).ToArray();
            var animated=new AnimatedBitmap(sequences);
            return animated;
        }
    }

AnimationController: handles control animation updating

public static class AnimationController
{
    private static readonly List<Control> Controls =new List<Control>();
    private static CancellationTokenSource _cancelToken;

    static AnimationController()
    {
        _cancelToken = new CancellationTokenSource();
        _cancelToken.Cancel();
    }

    private static void Animate(object arg)
    {
        while (!_cancelToken.IsCancellationRequested)
        {
            Controls.RemoveAll(c => !(c.BackgroundImage.Tag is AnimatedBitmap));

            foreach (var c in Controls)
            {
                var control = c;
                if (!control.Disposing)
                    control.Invoke(new Action(() => control.Refresh()));
            }

            Thread.Sleep(40);
        }
    }

    public static void StartAnimation(this Control control)
    {
        if (_cancelToken.IsCancellationRequested)
        {
            _cancelToken = new CancellationTokenSource();
            Task.Factory.StartNew(Animate
                , TaskCreationOptions.LongRunning
                , _cancelToken.Token);
        }

        Controls.Add(control);
        control.Disposed += Disposed;
    }

    private static void Disposed(object sender, EventArgs e)
    {
        (sender as Control).StopAnimation();
    }

    public static void StopAnimation(this Control control)
    {
        Controls.Remove(control);
        if(Controls.Count==0)
            _cancelToken.Cancel();
    }

    public static void SetAnimatedBackground(this Control control, AnimatedBitmap bitmap)
    {
        control.BackgroundImage = bitmap;
        control.StartAnimation();
    }
}

and here is the sample usage:

    public Form1()
    {
        InitializeComponent();

        var frame1 = Image.FromFile(@"1.png");
        var frame2 = Image.FromFile(@"2.png");

        var animatedBitmap= new AnimatedBitmap(
            new Sequence {Image = frame1, Delay = 33},
            new Sequence {Image = frame2, Delay = 33}
            );

        // or we can do
        //animatedBitmap = AnimatedBitmap.CreateAnimation(new[] {frame1, frame2}, new[] {1000, 2000});

        pictureBox1.SetAnimatedBackground(animatedBitmap);
        button1.SetAnimatedBackground(animatedBitmap);
        label1.SetAnimatedBackground(animatedBitmap);
        checkBox1.SetAnimatedBackground(animatedBitmap);

        //or we can do
        //pictureBox1.BackgroundImage = animatedBitmap;
        //pictureBox1.StartAnimation();
    }
ali
  • 846
  • 2
  • 18
  • 34
user3473830
  • 7,165
  • 5
  • 36
  • 52
0

There you go (short answer:-):

public void Bitmap CreateAnimation(Bitmap[] frames, int[] delays)
{
    throw new NotSupportedException();
}

Seriously, you can safely remove the bounty right now because there is no solution with the constraints that you have set. Theoretically you can implement a custom WIC codec, but it requires COM registration in order to be used (I'm not even sure it will be used by GDI+, for instance, although based on WIC, WPF is stuck to built in codecs), will introduce deployment problems and just isn't worth. It's weird that System.Drawing.Image has no virtual methods, cannot be inherited and ImageAnimator is hardcode bound to it, but this how it is. You are supposed to live with "out of the box" animated Gif support, or use your own solution :-).
What about your curiosity, I think you just started with wrong assumption

I'm pretty sure it is possible to create such an image via some WinApi. This is what the GIF Decoder actually does as well.

and then in comments

how does the GIF decoder do this and how can I achieve the same result regardless of the source format

It doesn't. The key word here is decoder. APIs (or managed API wrappers:-)) would call it when providing "image" services for you. For instance, IWICBitmapDecoder::GetFrameCount method most probably is used by GdipImageGetFrameCount (or Image.GetFrameCount if you prefer). In general, you can add frames and options only to encoders, and decoders are the only ones that can return such information to the caller.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343