Create an animated bitmap programmatically

I want to create an instance System.Drawing.Bitmap

"manually" that contains animation.

The created instance Bitmap

must meet the following criteria:

  • This is an animation ( image.FrameDimensionsLists

    has a time dimension)
  • It has multiple frames ( image.GetFrameCount(dimension) > 1

    )
  • I can get delay between frames ( image.GetPropertyItem(0x5100).Value

    )

I'm sure it is possible to create such an image through WinApi. This is what a GIF decoder actually does.

I know I can reproduce the animation if I have frames from any source by doing it manually, but I want to do it in a consistent way. If I could create a raster image, I could just use it in the Button

, Label

, PictureBox

or any other existing control, and a built-in ImageAnimator

can also handle it automatically.

Most of these themes offer to convert frames to animated GIF; however, this is not a good solution as it does not support true color and translucency (like APNG animation).

Update: After some research I found out that I can implement a decoder using WIC ; however I don't want to register a new decoder on Windows and it uses COM which I want to avoid if possible. Not to mention that at the end I will have an IWICBitmapSource that I still need to convert to Bitmap

.

Update 2 : I installed bounty. You are a 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?
}

      

+3


source to share


3 answers


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

      

Putting strong constraints on an expected implementation like this isn't very smart. It is technically possible using the TIFF image format, it is capable of storing multiple frames. However, they are not based on time, but only supports GIF codec. One additional argument is required, so the control can be updated when it needs to display the next image. 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 requires a timer to select the next frame and update the control:



    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 do this is to drop the return value requirement entirely, so you don't need to generate a TIFF. And just use the Animate () method with an argument Action<Image>

to get the control property updated. But not what you asked for.

+3


source


Unfortunately, we cannot redistribute neither System.Drawing.Image

, nor System.Drawing.Bitmap

, so overriding image.FrameDimensionsLists

and similar members is out of the question, and Hans Passant's Windows image encoders maintain time measurement on their own. However, I do believe in this specific case based on

I know I can play the animation if I have frames from any source by doing it manually, but I want to do it in a compatible way: If I could create a bitmap like this, I could just use it on a Button, Label, PictureBox or any other existing control and built-in ImageAnimator can handle it automatically as well.

We can get away by injecting a new class that can handle animations and then implicitly cast it into Bitmap. We can use extension methods to automatically activate animations for controls. I know this method is a hack, but I thought it might be worth mentioning this.

Here is a rough implementation and example usage.

AnimatedBitmap: Handles the framework and time-based animation base on the 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 updating animation controls

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 a usage example:

    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();
    }

      

+1


source


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 limits you set. In theory, you could implement a custom WIC codec, but COM registration is required to use it (I'm not even sure if GDI + will use it, for example, although WIC-based WPF is tied to native codecs) would present deployment issues and just isn't worth it. It is strange that System.Drawing.Image

it has no virtual methods, cannot be inherited, but ImageAnimator

is hardcoded with it, but it is. You should live with animated Gif support out of the box, or use your own solution :-).
What about your curiosity, I think you just started with a wrong assumption

I'm sure it is possible to create such an image through WinApi. This is exactly what the GIF decoder does.

and then in the comments

how does a GIF decoder do it and how can I achieve the same result regardless of the original format

This is not true. The key word here is decoder . The APIs (or managed APIs :-)) would call this when providing "image" services for you. For example, the method IWICBitmapDecoder::GetFrameCount

is most likely used GdipImageGetFrameCount

(or Image.GetFrameCount

if you prefer). In general, you can only add frames and options for encoders, and decoders are the only ones that can return such information to the caller.

0


source







All Articles