I'm trying to write some code to export animated .gifs from a WPF application using GifBitmapEncoder. What I have so far works fine but when I view the resulting .gif it only runs once and then stops - I'd like to have it looping indefinitely.
I've found this previous similar question:
How do I make a GIF repeat in loop when generating with BitmapEncoder
However, he is using the BitmapEncoder from Windows.Graphics.Imaging rather than the Windows.Media.Imaging version, which seems to be a bit different. Nonetheless, that gave me a direction and after a bit more googling I came up with this:
Dim encoder As New GifBitmapEncoder
Dim metaData As New BitmapMetadata("gif")
metaData.SetQuery("/appext/Application", System.Text.Encoding.ASCII.GetBytes("NETSCAPE2.0"))
metaData.SetQuery("/appext/Data", New Byte() {3, 1, 0, 0, 0})
'The following line throws the exception "The designated BitmapEncoder does not support global metadata.":
'encoder.Metadata = metaData
If DrawingManager.Instance.SelectedFacing IsNot Nothing Then
For Each Frame As Frame In DrawingManager.Instance.SelectedFacing.Frames
Dim bmpFrame As BitmapFrame = BitmapFrame.Create(Frame.CombinedImage, Nothing, metaData, Nothing)
encoder.Frames.Add(bmpFrame)
Next
End If
Dim fs As New FileStream(newFileName, FileMode.Create)
encoder.Save(fs)
fs.Close()
Initially I tried adding the metadata directly to the encoder (as in the commented-out line in the code above), but at runtime that throws the exception "The designated BitmapEncoder does not support global metadata". I can instead attach my metadata to each frame, but although that doesn't crash it the resultant .gif doesn't loop either (and I would expect that the looping metadata would need to be global anyway).
Can anyone offer any advice?
I finally got this to work after studying this article and referencing the raw bytes of GIF files. If you want to do so yourself, you can get the bytes in hex format using PowerShell like so...
$bytes = [System.IO.File]::ReadAllBytes("C:\Users\Me\Desktop\SomeGif.gif")
[System.BitConverter]::ToString($bytes)
The GifBitmapEncoder appears to write the Header, Logical Screen Descriptor, then the Graphics Control Extension. The "NETSCAPE2.0" extension is missing. In GIFs from other sources that do loop, the missing extension always appears right before the Graphics Control Extension.
So I just plugged in the bytes after the 13th byte, since the first two sections are always this long.
// After adding all frames to gifEncoder (the GifBitmapEncoder)...
using (var ms = new MemoryStream())
{
gifEncoder.Save(ms);
var fileBytes = ms.ToArray();
// This is the NETSCAPE2.0 Application Extension.
var applicationExtension = new byte[] { 33, 255, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0 };
var newBytes = new List<byte>();
newBytes.AddRange(fileBytes.Take(13));
newBytes.AddRange(applicationExtension);
newBytes.AddRange(fileBytes.Skip(13));
File.WriteAllBytes(saveFile, newBytes.ToArray());
}
Did you know that you can just download this functionality? Take a look at the WPF Animated GIF page on CodePlex. Alternatively, there is WPF Animated GIF 1.4.4 on Nuget Gallery. If you prefer a tutorial, then take a look at the GIF Animation in WPF page on the Code Project website.
#PaulJeffries, I do apologise... I misunderstood your question. I have used some code from a post here before to animate a .gif file. It is quite straight forward and you might be able to 'reverse engineer' it for your purposes. Please take a look at the How do I get an animated gif to work in WPF? post to see if that helps. (I am aware that the code's actual purpose is also to animate a .gif).
Related
First off, I've read through the existing StackOverflow answers on this specific issue. The consensus from those answers seems to be about permissions or existing files, etc... I've eliminated all of those issues.
Basically, the flow here is as follows:
The app takes a 24-bit PNG file and reading it into a Bitmap object, bmpOriginal.
The app saves bmpOriginal to a memory stream using the JPG encoder (obtained from looping through ImageCodecInfo.GetImageEncoders() until I found the one with the "image/jpeg" MimeType.
The app creates a new Bitmap, bmpOptimized, from the memory stream in step 2 and displays it in a PictureBox. So far, so good - everything works exactly as intended and I can even see JPG compression artifacts in the new Bitmap, so I know the encoder is working.
Later on in the code flow, the user clicks a button and it should save bmpOptimized to a new file, using the Bitmap.Save() method.
When I run this, it throws an error about "A generic error occurred in GDI+".
I double-checked to make sure the folder was writeable and that the file didn't already exist. In fact, the app actually does create a file in the right location, but it's empty (0 bytes).
The only other thing that seemed odd was that the bmpOriginal and bmpOptimized both have the same RawFormat value:
{[ImageFormat: b96b3caf-0728-11d3-9d7b-0000f81ef32e]}
...even though bmpOriginal comes from the PNG and bmpOptimized is from the encoded JPEG.
EDIT:
The code looks like this:
public Bitmap bmpOriginal;
public Bitmap bmpOptimized;
...
// Step 1
bmpOriginal = new Bitmap("foo.png");
// Step 2
using(MemoryStream ms = new MemoryStream())
{
ImageCodecInfo _jpgEncoder = _getEncoder("image/jpeg");
EncoderParameters _encoderParams = new EncoderParameters(1);
_encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 70L);
bmpOriginal.Save(ms, _jpgEncoder, _encoderParams);
// Step 3
bmpOptimized = new Bitmap(ms);
}
And then later on when the user clicks the button for step 4:
bmpOptimized.Save("bar.jpg");
...that's where the error happens.
Note that this edit is simply adding in the simplified version of the flow, since people are apparently downvoting this question for lack of source code. The one person who commented actually provided the correct answer - I needed to preserve the MemoryStream until after I saved the optimized bitmap.
Try this below
Please check whether memory stream didn't disposed until bmpOptimized image getting save in the desired location. If problem persist, then Check if path exists.
I have used the sample code as found in the answer at how to create an animated gif in .net
ie
System.Windows.Media.Imaging.GifBitmapEncoder gEnc = new GifBitmapEncoder();
foreach (System.Drawing.Bitmap bmpImage in images)
{
var src = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
bmpImage.GetHbitmap(),
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
gEnc.Frames.Add(BitmapFrame.Create(src));
}
using(FileStream fs = new FileStream(path, FileMode.Create))
{
gEnc.Save(fs);
}
I would now like to set the speed of the animate gif. The help in this area is terrible. Anyone got any idea of how to do it? This must surely be possible. It seems to be such a basic function of an animated gif but they seem to have gone out of their way to make it difficult.
Thanks in advance.
I don't think you can do it with GifBitmapEncoder. Theoretically, you could use BitmapMetadata.SetQuery to set the value of /grctlext/Delay on each frame (the delay is specified in the Graphic Control Extension of each frame, as explained in the Wikipedia article). But the doc says:
Graphics Interchange Format (GIF) images do not support global
preview, global thumbnails, global metadata, frame level thumbnails,
or frame level metadata.
Which is technically incorrect, because the GIF format itself does support global and frame-level metadata; it's just the GifBitmapEncoder class that doesn't support it.
So I think your only options are to use another, more complete image manipulation library, or to do it manually. The GIF format is rather simple; it's described in details on this site, including the parts about animation and LZW compression.
Alternatively, instead of doing the whole encoding yourself, you could use GifBitmapEncoder to do the heavy lifting, and just patch the metadata in the resuting stream afterwards. The code in my XamlAnimatedGif library might help; you won't be able to use it directly, but you could reuse some parts of it, as it implements a full GIF decoder.
At some point in my program the user selects a bitmap to use as the background image of a Panel object. When the user does this, the program immediately draws the panel with the background image and everything works fine. When the user clicks "Save", the following code saves the bitmap to a DataTable object.
MyDataSet.MyDataTableRow myDataRow = MyDataSet.MyDataTableRow.NewMyDataTableRow(); //has a byte[] column named BackgroundImageByteArray
using (MemoryStream stream = new MemoryStream())
{
this.Panel.BackgroundImage.Save(stream, ImageFormat.Bmp);
myDataRow.BackgroundImageByteArray = stream.ToArray();
}
Everything works fine, there is no out of memory exception with this stream, even though it contains all the image bytes. However, when the application launches and loads saved data, the following code throws an Out of Memory Exception:
using (MemoryStream stream = new MemoryStream(myDataRow.BackGroundImageByteArray))
{
this.Panel.BackgroundImage = Image.FromStream(stream);
}
The streams are the same length. I don't understand how one throws an out of memory exception and the other doesn't. How can I load this bitmap?
P.S. I've also tried
using (MemoryStream stream = new MemoryStream(myDataRow.BackgroundImageByteArray.Length))
{
stream.Write(myDataRow.BackgroundImageByteArray, 0, myDataRow.BackgroundImageByteArray.Length); //throw OoM exception here.
}
The issue I think is here:
myDataRow.BackgroundImageByteArray = stream.ToArray();
Stream.ToArray() . Be advised, this will convert the stream to an array of bytes with length = stream.Length. Stream.Legnth is size of the buffer of the stream, which is going to be larger than the actual data that is loaded into it. You can solve this by using Stream.ReadByte() in a while loop until it returns a -1, indicating the end of the data within the stream.
You might give this library a look.
http://arraysegments.codeplex.com/
Project Description
Lightweight extension methods for ArraySegment, particularly useful for byte arrays.
Supports .NET 4.0 (client and full), .NET 4.5, Metro/WinRT, Silverlight 4 and 5, Windows Phone 7 and 7.5, all portable library profiles, and XBox.
How can I convert a System.Drawing.Bitmap to GDK# Image so that I can set to the image widget.
I have tried this...
System.Drawing.Bitmap b = new Bitmap (1, 1);
Gdk.Image bmp = new Gdk.Image (b);
UPDATE:
Bitmap bmp=new Bitmap(50,50);
Graphics g=Graphics.FromImage(bmp);
System.Drawing.Font ff= new System.Drawing.Font (System.Drawing.FontFamily.GenericMonospace, 12.0F, FontStyle.Italic, GraphicsUnit.Pixel);
g.DrawString("hello world",ff,Brushes.Red,new PointF(0,0));
MemoryStream ms = new MemoryStream ();
bmp.Save (ms, ImageFormat.Png);
Gdk.Pixbuf pb= new Gdk.Pixbuf (ms);
image1.Pixbuf=pb;
Exception:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> GLib.GException: Unrecognized image file format
at Gdk.PixbufLoader.Close()
at Gdk.PixbufLoader.InitFromStream(Stream stream)
at Gdk.PixbufLoader..ctor(Stream stream)
at Gdk.Pixbuf..ctor(Stream stream)
One ugly, but working, way is to store the bitmap as a PNG in a MemoryStream.
To save the Bitmap, you can use the Save method:
b.Save(myMemoryStream, ImageFormat.Png);
That was easy enough. Loading the PNG data into the Gdk# Pixbuf is also rather easy; you can use the appropriate constructor:
Pixbuf pb = new Gdk.Pixbuf(myMemoryStream);
You may need to reset the memory stream so the reading position is at the start of the stream before creating the Pixbuf.
A word of caution: I do not consider this the best, or even a "good" solution. Transferring data between two object-oriented data structures by serializing and deserializing the data has a certain code smell to it. I genuinely hope someone else can come up with a better solution.
EDIT: As for the used libraries: This answer uses only plain GDI+ (System.Drawing.Bitmap) and Gdk# (Gdk.Pixbuf). Note that a Gtk.Image is a widget that displays a Gdk.Pixbuf. As such, Gtk.Image is the equivalent of Windows Forms' PictureBox, whereas Gdk.Pixbuf is roughly equivalent to Windows Forms' System.Drawing.Bitmap.
EDIT2: After testing your code, I have found that there are three additional preconditions to ensure before you can run your minimum example:
As suspected above, you must reset the stream position to the beginning of the after saving your Bitmap and before loading your Pixbuf: ms.Position = 0;
You must compile the application for x86 CPUs.
You must invoke Gtk.Application.Init(); before you do anything with Pixbuf.
You may draw in Gtk# like in Winforms. For this you must obtain System.Drawing.Graphics object and then you may draw lines, images and text on it. You may do it like this: 1. Create new Widget. 2. Subscribe on ExposeEvent. 3. On event handler write some code:
protected void OnExposeEvent(object o, ExposeEventArgs e)
{
Gdk.Window window = e.Event.Window;
using (System.Drawing.Graphics graphics =
Gtk.DotNet.Graphics.FromDrawable(window))
{
// draw your stuff here...
graphics.DrawLine(new System.Drawing.Pen(System.Drawing.Brushes.Black), 0, 0, 30, 40);
}
}
Also you need to add reference on gtk-dotnet.dll.
try this ....
Gdk.Pixbuf pixbufImage = mew Gdk.Pixbuf(#"images/test.png");
Gtk.Image gtkImage = new Gtk.Image(pixbufImage);
Gdk.Image gdkImage = gtkImage.ImageProp;
My application receives a sequence of images (BitmapImage) from external device with rate 30 fps.
I'm using Aforge.net library for save the received stream in .avi file.
I used the following code for inizializing the AVIWriter:
AVIWriter writer;
writer = new AVIWriter("wmv3");
writer.FrameRate = 30;
writer.Open("test.avi", 320, 240);
And for each frame received I add it in the video stream, with the following code line:
writer.AddFrame(ResizeBitmap(BitmapImage2Bitmap(e.ColorFrame.BitmapImage),320,240));
But the generated file is too heavy. (10 secondos corresponds to about 3Mb).
I tryied also setting a low level of writer.Quality , but the result seems the same (just 5-7% less).
So, I need a more efficient compression.
What are the compressions supported in Aforge.net ? What compression should I use in order to reducing the weight of saved file?
I suspect that interframe compression is not used in AVIWriter (but I may be wrong).
You may try to use VideoFileWriter from Aforge.Video.FFMPEG instead:
var writer = new VideoFileWriter();
writer.Open("test.mpg", 320, 240, 30, VideoCodec.Default, 1000);
// add your frame
writer.WriteVideoFrame(frame);
Remember to put dlls from Externals/ffmpeg/bin from AForge zip into your output directory.