Image Formatting in C# (GDI+)

Working with the GDI+ framework is not (in some cases) as trivial as you would think. It may be easy to create a new Bitmap object and draw some primitive shapes and text to it, but when you start trying to use the GDI+ library for changing the format of an Image it gets a little more tricky. Here I am going to discuss some of the ins and outs of working with GDI+ for image formatting, such as creating/saving images and changing image.s formats.

My initial goal was to create a method that could accept an Image object as input and output a TIFF formatted byte array. I.m going to start out by describing how you go about creating a multi-framed tiff image in some basic steps:

  1. Create the Image object.
  2. Save the Image object using the Image.Save(Stream, ImageCodecInfo, EncoderParameters) method.
    1. The ImageCodecInfo should be the ImageCodecInfo instance returned from ImageCodecInfo.GetImageEncoders() whose MimeType property matches .image/tiff..
    2. The EncoderParamaters should be a new instance of a the EncoderParameters object with one parameter. The parameter should be create with Encoder.SaveFlag and EncoderValue.MultiFrame casted to long.
  3. After the first .Save() has been called, SaveAdd() should be called for each frame that you want to add to the image. SaveAdd() should be called with the Image object along with an EncoderParameters instance that is similar to the .Save() call except the EncoderValue should be .FrameDimensionPage. instead.
  4. Once all frames have been added, the Image needs to be told that there are no more frames by calling .SaveAdd() with just the EncoderParameters field with an EncoderValue of .Flush..

One of the key concepts to note here (that isn.t completely obvious) is that as you add new frames to the image with SaveAdd(), it is adding on to the stream that the image was originally saved to. In other words, you tell the Image object where to save the image data to, and as you add more frames to the image, the Image object appends the saved data. If you were to use the Image.Save(string, ImageCodecInfo, EncoderParameters) method instead of the Stream version, you could watch the size of the file change in between each call to SaveAdd().

Note: If you rely on Visual Studio.s intellisense like I do, and you type .Encoder.. and expect to see a list of the enumeration values but don.t, it is probably because you have a .using System.Text;. in your class file. The System.Text namespace has a class named .Encoder. as well that conflicts with the one in System.Drawing.Imaging; so when your intellisense pops up for the Encoder class it is displaying the members of the System.Text.Encoder class, not the System.Drawing.Imaging.Encoder class.

private void createMultiFrameTiff(String fileName) 
{ 
	ImageCodecInfo codec = null; 
	foreach (ImageCodecInfo cCodec in ImageCodecInfo.GetImageEncoders()) 
	{ 
		if (cCodec.MimeType == "image/tiff") 
			codec = cCodec; 
	}

	EncoderParameters imageParams = new EncoderParameters(1); 
	imageParams.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.MultiFrame);

	Image newImage = (Image)new Bitmap(150, 150); 
	drawImageText(newImage, "Frame 1");

	Image newImage2 = (Image)new Bitmap(150, 150); 
	drawImageText(newImage2, "Frame 2");

	Image newImage3 = (Image)new Bitmap(150, 150); 
	drawImageText(newImage3, "Frame 3");

	// Save the initial image as MultiFrame 
	newImage.Save(fileName, codec, imageParams);

	// Change the parameters of the image to add frames. 
	imageParams.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);

	// Add frame 2 
	newImage.SaveAdd(newImage2, imageParams); 
	newImage2.Dispose();

	// Add frame 3 
	newImage.SaveAdd(newImage3, imageParams); 
	newImage3.Dispose();

	// Set the image params to "Flush" to indicate we are done adding frames. 
	imageParams.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.Flush); 
	newImage.SaveAdd(imageParams);

	imageParams.Dispose(); 
}

You might have noticed that in the example above I used the .Save(string, .) method instead of .Save(Stream, .). Originally I had intended for the createMultiFrameTiff() method to return an Image object that could be used right away. After adding all the frames and Flushing the image the image still only contains a single frame (at least that.s what the GetFrameCount() method returns). It is only after you save the Image to a file system and load the image back into a completely new Image object by using Image.FromFile() that multiple frames are returned by GetFrameCount().

GDI+ is very odd in this respect because no matter what, if you don.t save the image to an actual file and load the file into the Image object, the multiple frames are not registered and thus not returned in the calls to GetFrameCount() or SelectActiveFrame(). Even saving the image to a stream, reading the bytes from the stream, loading the bytes into a new stream and calling Image.FromStream with the new stream does not really work. With that approach, I was able to the correct count of frames from GetFrameCount(), but as soon as you call SelectActiveFrame() a .Generic. GDI+ exception is thrown. In fact, any time I have loaded a multi-frame TIFF using Image.FromStream and call SelectActiveFrame() I receive exceptions from GDI+.

However, even though GetFrameCount() and SelectActiveFrame() don.t work when building a multi-frame TIFF image in memory doesn.t work properly, the data in the stream is still a valid multi-frame TIFF. If you save the bytes from the stream that is used to build the multi-frame TIFF to a file and then call Image.FromFile() you will find that the image has all the frames that you added to it and can select each individual frame successfully.

Note: The Windows Picture Viewer (preview) application supports multiple framed TIFF images. This is one of the only popular image viewing programs that I have found that supports multi-frame TIFF images. Photoshop doesn.t, Paint.Net doesn.t, Microsoft Paint DEFINATELY doesn.t.

Compression

Specifying the compression of the image is very easy. All you have to do is add an additional parameter to the EncoderParameters object. Instead of creating the EncoderParameters object with one (1) parameter, create it with two (2) and set the appropriate EncoderValue. For example:

imageParams.Param[1] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.CompressionCCITT3);

Download Example

Here is a link to download the (working) example code for creating (and displaying) a multi-frame TIFF image.

Leave a Reply

Your email address will not be published.

Humanity Verification *Captcha loading...