Cutting Edge

Working with Images in the .NET Framework

Dino Esposito

Code download available at:CuttingEdge0307.exe(343 KB)

Contents

The System.Drawing.Imaging Namespace
Working with Images
Supported Formats and Compression
Images and Data Binding
Summing It Up

Image processing has never been easy within Win32®; only the bravest developers ever attempted to play with the Graphical Device Interface (GDI) API. With the Windows® 2000 shell common controls, developers using Win32 gained the ability to display GIF and JPEG images using the ListView control, which supports background images and accepts common-use formats such as BMP, GIF, and JPEG. You can put an empty ListView control in your Win32 dialog, set its background image, and you're finished. The code in Figure 1 demonstrates this.

Figure 1 Using ListView to Display an Image

BOOL DisplayFile(HWND hwndImage, LPCTSTR szFile) { // The window must be a listview TCHAR szClass[MAX_PATH]; GetClassName(hwndImage, szClass, MAX_PATH); if (lstrcmpi(szClass, _T("SysListView32"))) return FALSE; LVBKIMAGE lvbi; ZeroMemory(&lvbi, sizeof(LVBKIMAGE)); lvbi.ulFlags = LVBKIF_STYLE_NORMAL|LVBKIF_SOURCE_URL; lvbi.pszImage = (LPTSTR)szFile; lvbi.cchImageMax = lstrlen(szFile); SendMessage(hwndImage, LVM_SETBKIMAGE, 0, (LPARAM) (LVBKIMAGE*)&lvbi); return TRUE; }

Overall, developers had to either struggle with images in Win32-based applications or buy third-party libraries. The situation is radically different with the Microsoft® .NET Framework thanks to a number of managed classes that encapsulate the new GDI+ functionality. The GDI+ subsystem is a native component of the Windows XP and Windows Server™ 2003 operating systems that is available to 32-bit and 64-bit Windows-based apps. For earlier versions of Windows, download a backward-compatible version of the GDI+ libraries (gdiplus.dll) from https://www.microsoft.com/msdownload/platformsdk/sdkupdate (look under Core SDK downloads).

As I mentioned, the .NET Framework encapsulates the whole spectrum of GDI+ functionality in a number of managed classes that wrap GDI+ low-level functions. In this column, I'll focus on the imaging API to show how to accomplish a few common tasks. In doing so, I'll discuss some GDI+ infrastructure, and review the internal architecture of the library. Before going on, let me clarify a point about GDI+: the core services of GDI+ are still in unmanaged code. The classes in the .NET Framework are merely wrappers around them. This is something that is expected to change in the upcoming version of Windows, code-named Longhorn.

The System.Drawing.Imaging Namespace

In the .NET Framework, GDI+ functions are defined in the System.Drawing namespace. The namespace includes three child namespaces, which encompass 2D drawing, imaging, and text manipulation. In System.Drawing you find the basic drawing primitives to handle fonts, brushes, pens, and bitmaps. Advanced 2D and vector graphics effects are provided by the classes in System.Drawing.Drawing2D. In particular, you'll find gradients, blending, paths, and regions. Image rendering, color manipulation, and metafile support is available in System.Drawing.Imaging. Finally, the classes in System.Drawing.Text manage an application's privately and publicly installed font collections.

The base class for representing and handling images is Image. It is an abstract class that provides core functionality for three more specific types of images: bitmaps, metafiles, and icons. In the .NET Framework, a bitmap is not necessarily a .bmp file. The Bitmap class encapsulates a GDI+ map of bits, namely the pixel data for a graphic image and all of its attributes. A Bitmap object is an object used to work with images defined by pixel data. Properties of a bitmap include width and height, the color palette, the raw format (GIF, BMP, JPEG, and so forth), the physical size, the pixel format (alpha or the number of bits per pixel). As you can see from even this quick description, the Bitmap object is a general representation of a pixel-based image as opposed to a record-based image like a metafile or icon.

The Bitmap class provides useful methods to rotate or flip the bits, save to a number of formats, and load from a number of data stores. The Bitmap class also features a GetThumbnailImage method. If the Bitmap object contains thumbnail data, then a thumbnail image is created from that data; otherwise, a thumbnail is dynamically generated by scaling the main image.

The MetaFile class defines a graphic metafile—a file made of records that describe a sequence of graphics operations. A metafile is created and displayed by recording and playing back these records. Metafiles are useful for rendering images that can be easily expressed with a set of vector operations such as drawing lines and curves. Obviously, this is not possible in all cases. Photographic images and small icons can't be drawn well with vector graphics. Let's focus more on pixel-based images and the Bitmap class.

A bitmap can be constructed dynamically by allocating a graphic memory context or it can be loaded from existing data. The Bitmap class features a long list of constructors and a few static methods to let you import pixel information from just about anywhere. The Bitmap constructor accepts data from disk files, streams, and other Image-based objects. It also features a couple of static methods to import a Bitmap from Win32 resources and HICON handles. This latter feature guarantees a good deal of interoperability between managed code and Win32 DLLs.

In GDI+, just as in Win32 GDI programming, disposing of graphics objects when you're finished with it is a fundamental rule. It is not as critical as in Win32, where you're responsible for cleaning up the resources you use, but it's still important and highly recommended. In the .NET Framework, the system garbage collector frees any objects that have gone out of scope, but since graphical resources can be expensive in terms of memory, freeing them as soon as possible is always the best practice. With the garbage collector, you will not know the exact moment the cleanup takes place. Thus, to optimize the application's performance consider releasing the resources right away by using the object's Dispose method. All GDI+ graphics objects provide an effective implementation of the Dispose method.

Working with Images

Assuming that you have a PictureBox control in your Windows Forms application, displaying an image from file is as easy as running the following code:

picture.Image = new Bitmap(fileName);

The PictureBox control has two features that make it a really compelling control to use in custom applications. Through the SizeMode property, the PictureBox control allows you to center the image in the client area of the picture box. Furthermore, by assigning a different value to the property you can also stretch the image to fit into the specified area. Valid values for the property are taken from the PictureBoxSizeMode enumeration, whose elements are listed in Figure 2.

Figure 2 PictureBoxSizeMode Properties

Property Description
AutoSize The PictureBox control takes the size of the image that it contains. If the form is not large enough and not resizable, the image is clipped.
CenterImage The image is displayed in the center if the PictureBox is larger than the image. If the image is larger than the PictureBox, the picture is placed in the center of the PictureBox and the outside edges are clipped.
Normal The image is placed in the upper-left corner of the PictureBox. The image is clipped if it is larger than the PictureBox that contains it.
StretchImage The image within the PictureBox is stretched or shrunk to fit the size of the PictureBox. The aspect ratio of the image is not maintained.

By default, the Image is placed in the upper-left corner of the PictureBox control, and any part of the image too large for it is clipped. The AutoSize option automatically resizes the control so that it exactly fits the image. If the user resizes the form, the PictureBox control is resized accordingly. You should note that in this case the new size of the picture box can easily overwrite existing controls, causing discrepancies. If you opt for the StretchImage mode, the image will be stretched or shrunk regardless of its aspect ratio. (See Figure 3 for a good and a bad example.)

Figure 3 StretchImage Mode Example

Figure 3** StretchImage Mode Example **

If an image is too large for the area you have reserved for it on the form, the simplest way to stretch it while maintaining its aspect ratio is to create a thumbnail:

Image img = picture.Image; // Reduce each dimension by half Image newImg = img.GetThumbnailImage(img.Width/2, img.Height/2, null, IntPtr.Zero); // Show the image pictureBox1.Image = newImg;

Note that using the GetThumbnailImage method to scale an image is not necessarily a good idea, especially for reducing by a small amount. If the image has embedded thumbnails (say from a digital camera) GetThumbnailImage takes it and scales it up to the desired size. As you can imagine, this can introduce a lot of artifacts. In this case, it would be much better to create a bitmap of the desired size and call DrawImage to draw it.

When creating a thumbnail, you can specify the new width and height of the image. It suffices that you indicate values obtained by applying a common scale factor. Ideally, you would specify the factor that ensures that the largest dimension (width or height) will fit in the space. The previous code snippet simply sizes the image 75 percent smaller, shrinking it in half in each dimension.

While GetThumbnailImage is a good way to obtain an image of a given size, you can use the DrawImage method of the Graphics class to render cropped and scaled images. One of the method overloads receives a Bitmap object and a Rectangle object. The rectangle indicates the area in which the image will be rendered. If the size of the destination rectangle is different from the size of the original image, the image is scaled to fit the rectangle.

Another interesting point is that even when an image is bound to a PictureBox control, you can obtain its reference and manipulate it in memory. To apply changes back to the picture box, simply call the Refresh method. Earlier, I mentioned two compelling features of the PictureBox control. One is the SizeMode property; the other is the control's ability to refresh its client area automatically. If you've ever developed graphic interfaces in Win32 using C/C++ and the SDK, this will be familiar to you. Due to the characteristics of the Windows OS, the output of a window needs to be refreshed when two windows overlap or one is partially moved off the screen. A control—and the PictureBox is no exception—incorporates that code and makes it transparently available to callers.

Rotating and flipping images is easy, too. All you have to do is call the RotateFlip method:

picture.Image.RotateFlip(RotateType.Rotate90FlipNone); picture.Refresh();

The RotateFlip method works on the source image; it doesn't create or return a new one. For this reason, if you're working on an image bound to a PictureBox control, you have to refresh the control to display changes. The RotateFlip enumeration includes elements to control both the angle of the rotation and the direction of the flipping. The previous code rotates the image 90 degrees clockwise and applies no flipping.

GDI+ realizes a complete abstraction over the physical format of the image, making it possible for you to easily code nice features such as writing a copyright note on a displayed image. To modify the bits of the image you must first obtain a Graphics object. The Graphics object is the GDI+ counterpart of the GDI device context. You should think of it as the central control from which to call into a lot of primitives. Everything you draw or fill through a Graphics object acts on a particular canvas. Typical drawing surfaces include the window background (including the control's background), in-memory bitmaps, and printers. You can obtain a Graphics object directly from the canvas.

If you want to make in-memory manipulations, get the Graphics object from the bitmap, like so:

Graphics g = Graphics.FromImage(picture.Image); g.DrawString(text, new Font("verdana", 8), new SolidBrush(Color.Black), 0,0); g.Dispose(); picture.Refresh();

You must dispose of the Graphics object to make sure you can use the bitmap to refresh. If the Graphics object hasn't been freed, the PictureBox's Image object will still be locked and not refresh properly.

This code snippet opens the bitmap that's bound to the PictureBox control and gets a Graphics object for it. Next, it writes a string of text using the specified font and brush starting at the upper-left corner. Since the modification has been made to the Bitmap object associated with the control, there's no need to rebind. The Refresh method is enough because now the text is part of the image and remains there even if you rotate or stretch the image (see Figure 4).

Figure 4 In-memory Manipulation

Figure 4** In-memory Manipulation **

Using a similar approach, you can also create new images from scratch and use them both as in-memory objects or save them to disk. The following code creates a color gradient and replaces the background of the picture box:

int width = pictureBox1.Width; int height = pictureBox1.Height; Graphics g; g = pictureBox1.CreateGraphics(); Rectangle area; area = new Rectangle(0, 0, width, height); LinearGradientBrush brInterior; brInterior = new LinearGradientBrush(area, Color.SkyBlue, Color.AntiqueWhite, LinearGradientMode.Horizontal); g.FillRectangle(brInterior, 0, 0, width, height);

Figure 5 shows the final result. If you move the picture box off the screen, though, the clipped area is not automatically repainted. A better approach that also solves this issue is to bind the dynamically created image to the picture box instead of redrawing:

int width = pictureBox1.Width; int height = pictureBox1.Height; Bitmap bmp = new Bitmap(width, height); Graphics g = Graphics.FromImage(bmp); Rectangle area = new Rectangle(0, 0, width, height); LinearGradientBrush brInterior; brInterior = new LinearGradientBrush(area, Color.SkyBlue, Color.AntiqueWhite, LinearGradientMode.Horizontal); g.FillRectangle(brInterior, 0, 0, width, height); pictureBox1.Image = bmp;

Figure 5 Replacing the Background

Figure 5** Replacing the Background **

You create a new Bitmap object as an empty block of memory whose size equals the size of the PictureBox. The Graphics object is obtained from the bitmap using the FromImage static method. In this way, you declare the newly created Bitmap object as the backing for the image being drawn. When you're done with the graphic work, bind the instance of the Bitmap class to the picture box.

Supported Formats and Compression

Figure 6 lists all the image formats that the .NET Framework supports. BMP is the standard image format used by Windows to store images. Additionally, you can set the number of colors a BMP image can support. The number of bits used to store each pixel determines the total number of possible colors that could be assigned to that pixel and subsequently the color map of the image itself. The number of bits per pixel for BMP files is specified in the file header and can vary from 1 to 64. Commonly used values are 1, 4, 8, and 24. BMP files are usually not compressed and therefore are not well-suited for transfer across the Internet. They might also contain a color table or color palette. A color table represents a small array of colors (usually 16 or 256) being used by the pixels in the image. Pixels are not represented with color information; they simply point to an entry in the table. Each pixel is represented by a 4-bit number or 8-bit number so there are 16 or 256 colors in the palette. Each color in the table is represented by a 24-bit number that is 8 bits for each base color component (red, green, blue).

Figure 6 Image Formats in the .NET Framework

Format Description
BMP Bitmap
EMF Enhanced Windows Metafile
EXIF Exchangeable Image File
GIF Graphics Interchange Format
Icon Windows Icon
JPEG Joint Photographic Experts Group
MemoryBmp Memory Bitmap
PNG W3C Portable Network Graphics
TIFF Tag Image File Format
WMF Windows Metafile

The GIF format is a standard on the Web. GIF images are optimized for images that contain lines and curves, blocks of solid color, and sharp boundaries between colors. As you can see, this description is tailored to drawn images commonly used on Web sites. GIF images are compressed to save space, but the compression algorithm is not lossy, meaning that a decompressed image is exactly the same as the original. The GIF format supports a maximum of 256 colors (8 bits per pixel).

GIF images can also have transparent colors—only one color in a file can be designated as such. This means that pixels of that color will be ignored at rendering time. As a result, in those positions the image will have the same background color of any Web page or form that lies directly beneath it. In addition, a sequence of GIF images can be stored in a single file to form an animated GIF.

JPEG is a compression scheme that works well for photographic images. Unlike the GIF format, the JPEG compression algorithm doesn't preserve all the original information. To save more space, a JPEG compressed image discards some raw information. A decompressed image, therefore, is different from the original. However, because JPEG compression is mostly used to store natural scenes, the human eye often doesn't notice any difference.

JPEGs store 24 bits per pixel and are capable of displaying 16 million colors, but do not support transparency or animation. The level of compression in JPEG images is configurable. You should note that higher compression levels result in smaller files because more data is discarded. Subsequently, the smaller the file, the poorer the reproduction. The optimal compression ratio really depends on the image and its use. In general, a 15:1 ratio generates an imperceptible loss of data. This doesn't mean that you can't compress at 30:1 with good results. The contents of the image—the scene and the disposition of the pixels—is an influential factor. Blocks of relatively solid colors normally suffer under high compression ratios, but they are uncommon in real-world images.

Comparing GIF and JPEG images, you should note that they have a rather specular (or mirror-image) set of characteristics; JPEGs perform poorly where GIFs are ideal, and vice versa. GIFs are recommended for text, geometric figures, and images that have large regions of different colors. GIFs maintain sharp differences between close colors while JPEGs attempt to smooth and blur differences. More important than this, though, is the overall resolution of the image. The higher the original resolution is, the more data is available to compress.

The EXIF format is frequently used by digital cameras to store captured images. EXIF is an extension of the JPEG format that adds extra information such as the date of the shot, model of camera, and other user information.

The PNG format has been introduced by the W3C as a better replacement for the GIF format. It's an unlossy compressed format that supports true-color (24 bits per pixel) and grayscale images. In addition, it provides an alpha color component which lets you indicate the level of blending you want between original pixels and the background color. It also improves the GIF's ability to store images progressively so they can be rendered as they arrive over a network connection.

The .NET Framework also supports TIFF images, an extremely flexible format supported by a variety of image-processing applications. TIFF files can store images with an arbitrary number of bits per pixel and using a variety of compression algorithms. Information related to the image (type of compression, orientation, colors, and so on) can be stored in the file and arranged through the use of tags. The TIFF format can be extended as needed by the approval and addition of new tags. At the highest level of abstraction, TIFF images look like XML files in that both support tags and use them to identify pieces of information by name instead of by position. As a result, two TIFF images can have different byte layouts yet can both be correctly processed by the same application.

This code shows how to save a dynamically created image:

Bitmap bmp = new Bitmap(width, height); Graphics g = Graphics.FromImage(bmp); // Work on the Graphics object and create the image CreateTheImage(g); g.Dispose(); // Save to JPEG bmp.Save("file.jpg", ImageFormat.Jpeg); bmp.Dispose();

To convert an existing image to another format, use this code:

Bitmap bmp = new Bitmap(fileGif); bmp.Save(fileJpeg, ImageFormat.Jpeg);

The Save method is more powerful than the previous code samples show. It can write to a variety of sources, not just disk files. For example, you can create dynamic images (such as charts and banners) over the Web and save them to the ASP.NET Response.OutputStream property, like so:

Response.ContentType = "image/jpeg"; Bitmap bmp = new Bitmap(width, height); Graphics g = Graphics.FromImage(bmp); CreateTheImage(g); g.Dispose(); bmp.Save(Response.OutputStream, ImageFormat.Jpeg); bmp.Dispose();

A similar approach can be taken using ASP nearly identically.

When saving to JPEG images, you can also control the compression ratio of the algorithm. You must use a different overload of the Save method, as you can see here:

public void Save( string filename, ImageCodecInfo encoder, EncoderParameters encoderParams );

The first step is to get the ImageCodecInfo structure for the JPEG image. The GDI+ interface provides no direct method to get this object. You must resort to a little trick—enumerate all the image encoders and check their MIME type properties against the JPEG MIME type string (image/jpeg). The ImageCodecInfo structure contains information inherent in the encoding and decoding of the image (see Figure 7).

Figure 7 ImageCodecInfo Structure

mimeType = "image/jpeg"; ImageCodecInfo GetEncoderInfo(string mimeType) { int j; ImageCodecInfo[] encoders; encoders = ImageCodecInfo.GetImageEncoders(); for(j = 0; j < encoders.Length; ++j) { if(encoders[j].MimeType == mimeType) return encoders[j]; } return null; }

The EncoderParameters argument represents an array of encoding parameters. Each element of the array is an EncoderParameter type. Possible parameters are listed as members of the static class Encoder. For example, the parameter Compression lets you choose a compression engine for TIFF images. The Quality parameter lets you choose the desired quality of the JPEG compression. This code compresses a JPEG with a 40:1 ratio:

// Set the quality to 40 (must be a long) Encoder qualityEncoder = Encoder.Quality; EncoderParameter ratio = new EncoderParameter(qualityEncoder, 40L); // Add the quality parameter to the list codecParams = new EncoderParameters(1); codecParams.Param[0] = ratio; // Save to JPG bmp.Save(fileName, jpegCodecInfo, codecParams);

Images and Data Binding

The .NET Framework provides great support for data binding for both ASP.NET and Windows Forms applications. Binding images to controls in ASP.NET is a bit harder that it is in Windows Forms because of the limitations of the <img> tag in HTML. The tag only accepts a file name or a URL. While using URLs allows you to return images over a stream, it still requires a second round-trip to the browser. Data binding is more direct for Windows Forms applications. To bind a database BLOB field to the Image property of a PictureBox control, start by creating a Binding object, like so:

Binding b = new Binding("Image", dataTable, "photo"); pictureBox1.DataBindings.Add(b);

You must have the BLOB field stored in a DataTable field. In the previous code snippet, that field is named photo. Notice that creating a Binding object isn't sufficient to bind the property to the control. You must first add the Binding object to the DataBindings collection of a particular control. Of course, you must ensure that the control you bind to exposes a public property as in the Binding object. So far, the description of the data binding mechanism is quite general and applies to all controls and data types.

At this point, the .NET Framework will try to bind Image with the contents of the BLOB field. If you run that code, you'll get an exception because the system tries to cast the data type representing the BLOB field—an array of bytes—to the type of the Image property, the Image class. The cast is logically possible but can't happen automatically. You need to inject code in the middle of the process to transform the array of bytes into a valid Image object. Thus, an actual conversion needs to take place inside a handler for the Format event. The Binding class supplies the Format event just to let you manually bind properties and raw data (see Figure 8).

Figure 8 Binding Properties and Data

void Format_Blob(object sender, ConvertEventArgs e) { // Cast the raw data Byte[] img = (Byte[]) e.Value; // Copy raw data to a memory stream MemoryStream ms = new MemoryStream(); int offset = 0; ms.Write(img, offset, img.Length - offset); // Create a bitmap from the memory stream Bitmap bmp = new Bitmap(ms); ms.Close(); // Bind the bitmap e.Value = bmp; }

The ConvertEventArgs class contains a Value property which is a read/write buffer. It contains the raw data and can be modified to contain the data to bind. The Image property of the PictureBox control can only be bound to an Image object, so you simply need to create an image from the array of bytes. Unfortunately, this can't be done directly because the Bitmap class doesn't have a constructor that accepts arrays of bytes. For this reason, you first create a MemoryStream object, fill it with the bytes of the image, and then create the bitmap from the memory stream. The newly created Bitmap object is then placed in the Value buffer and successfully bound to the Image property of the PictureBox control. The reverse must be done to save an image to a BLOB field in a disconnected DataTable. In this case, use the Parse event instead of Format. The logic and the signature are identical.

Summing It Up

The support for images in the .NET Framework via GDI+ is even greater than I've described here. I haven't even discussed image processing capabilities, color and text manipulation, paths, regions and gradients, metafiles, effects, or alpha blending. When you explore those areas yourself, you'll see that the .NET Framework greatly improves the experience of graphics manipulation.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET, Applied XML Programming for .NET, and Programming ASP.NET all from Microsoft Press, he spends his time teaching classes on ASP.NET and speaking at conferences. Reach Dino at dinoe@wintellect.com.