Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Bitmap Manipulation Class With Support For Format Conversion, Bitmap Retrieval from a URL, Overlays, etc.

0.00/5 (No votes)
31 Aug 2003 1  
Provides an overview and discussion of the author's BitmapManipulator C# class, including examples of each feature, as well as numerous .NET/GDI+ caveats.

Sample Image - bitmapmanip.jpg

Abstract

With the release of the GDI+ API, Microsoft significantly increased the power and flexibility of its graphics API, while at the same time increasing the complexity and surface-area of an already obfuscated GDI API. Fortunately, the .NET Framework provides the System.Drawing namespace hierarchy, wrapping much of the GDI+ API in a (somewhat) manageable facade. Unfortunately, much complexity remains, and consequently many simple image manipulation tasks, such as resizing a bitmap, cropping a region, or converting from one format to another, are either complicated or not immediately obvious, and sometimes both.

In this article, I present my BitmapManipulator class, written in C#, which implements some common image manipulation functions, as well as advanced capabilities like alpha-blending an overlay image atop a bitmap, while hiding messy GDI+ details. I originally developed this class for use in a web-based photo album application, and as a result the emphasis is on simplicity and ease of use, as well as performance. In spite of its origins, this class and the code therein should be suitable to a variety of applications, as evidenced by the Windows Forms test program included in the source archive.

Introduction

This article presents the BitmapManipulator class, reviews its functionality, and walks the reader through key elements of the source code, pointing out interesting techniques or caveats. For a reader interested only in using the class in an application, the code walk-through sections may be skipped, though all readers are encouraged to review the entire article.

The attached source archive contains Visual Studio.NET 2002 project files, however the source code should work with either VS.NET 2K2 or VS.NET 2K3.

Background

BitmapManipulator is built atop the .NET Framework GDI+ wrapper classes, which reside in the System.Drawing namespace. While a comprehensive survey of System.Drawing is well beyond the scope of this article, a cursory review will likely be useful.

All images are represented by the System.Drawing.Image class, or one of its derivatives. Image provides the base properties and methods applicable to all types of images. Its subclasses System.Drawing.Bitmap and System.Drawing.Imaging.Metafile provide additional functionality for bitmapped images (that is, raster images; images described by pixels), and metafile images, respectively. For the remainder of this article, we will focus our attention on the Bitmap class, which is used to represent not only BMP files, but JPEG, GIF, PNG, TIFF, and all other raster file formats supported by GDI+.

In addition to the Bitmap class, System.Drawing.Graphics features prominently in the code for BitmapManipulator. Graphics is the abstraction used to represent a "display device", though in this context "display device" means more than a monitor. Conceptually, in GDI+ almost all manipulations of a bitmap (such as rotating, cropping, etc) are not performed on a Bitmap at all, but rather, on a Graphics object. In turn, that Graphics object may represent the screen, a printer, or (in our case) the image content of a Bitmap object. Those of you familiar with the Win32 GDI API will recognize that Graphics is an object-oriented form of the GDI Device Context (DC).

In addition to Bitmap and Graphics, a few enumerations such as PixelFormat, and classes such as ImageFormat, will be encountered below. Fortunately, most of these instances are fairly self-explanatory, therefore the overview provided above should be sufficient to at least begin to review the implementation of BitmapManipulator.

BitmapManipulator Gestalt

BitmapManipulator is a static class, implemented in a single C# source file. From a design standpoint, it follows the pattern my colleague Richard Cooper calls "Function Bucket"; that is, it simply exposes static methods which are completely self-contained, and at best tangentially related to one another. While this pattern can be undesirable in some cases, in this case it makes for a coherent, concise, easy to use class that can be dropped in anywhere.

In general, each function performs some sort of manipulation on a bitmap. To that end, each function takes at least a Bitmap object, as well as any parameters necessary for the operation to be performed. Further, each function returns a Bitmap, which is always a separate Bitmap instance from that passed in. At the time I wrote BitmapManipulator, I required this functionality for obscure reasons, however it can be advantageous in other situations as well. One must remember, however, to Dispose() the input Bitmap when it is no longer needed, to make most efficient use of resources.

An example of a BitmapManipulator function is below:

public static Bitmap ScaleBitmap(Bitmap inputBmp, double scaleFactor)

In this case, in addition to the input Bitmap parameter, a scale factor controls the scale operation to be performed. With few exceptions, most methods of BitmapManipulator take this form.

The Bitmap manipulations

GetBitmapFromUri

public static Bitmap GetBitmapFromUri(String uri);
public static Bitmap GetBitmapFromUri(Uri uri);
 
public static Bitmap GetBitmapFromUri(String uri, int timeoutMs);
public static Bitmap GetBitmapFromUri(Uri uri, int timeoutMs);

Though various overloaded forms of this method are provided, they all perform the same task: retrieve an image file from a URI, load that file into a Bitmap, and return the resulting object.

The only interesting point to note about these methods is the wrapper around the WebRequest/WebResponse classes, and their associated exceptions: when I was writing BitmapManipulator, I needed to present a meaningful error message to users when image downloading failed, therefore in these methods I trap the exceptions associated with the usual HTTP errors, and package them into a catch-all BitmapManipException, along with a (somewhat) user-friendly error message. The original exception is always available in the InnerException property, of course, but this feature makes it easy to integrate image download functionality into an application without exposing users to any sharp edges.

ConvertBitmap

public static Bitmap ConvertBitmap(Bitmap inputBmp, String destMimeType);
public static Bitmap ConvertBitmap(Bitmap inputBmp, 
       System.Drawing.Imaging.ImageFormat destFormat);

As the name implies, each of the overloads of this method convert from one bitmap format to another (e.g., JPEG to GIF or TIFF to PNG). The code required to do this is an example of the simple yet non-obvious contortions one often goes through with GDI+:

//Create an in-memory stream which will be used to save

//the converted image

System.IO.Stream imgStream = new System.IO.MemoryStream();
//Save the bitmap out to the memory stream, 

//using the format indicated by the caller

inputBmp.Save(imgStream, destFormat);

//At this point, imgStream contains the binary form of the

//bitmap in the target format.  All that remains is to load it

//into a new bitmap object

Bitmap destBitmap = new Bitmap(imgStream);

That's right; the image is saved to a memory stream in the destination file format, then loaded from the stream into a new Bitmap object. Absurd, but functional.

ConvertBitmapToJpeg

public static Bitmap ConvertBitmapToJpeg(Bitmap inputBmp, int quality);

A special case of ConvertBitmap, this function converts an bitmap to JPEG, with the additional option to specify the quality parameter of the JPEG encoder. This parameter varies between 0 (horrific distortion, excellent compression) to 100 (lossless, minimal compression). I originally required this functionality because, in my photo album application, I wanted to force user photos to a quality level of 50 or less, to minimize disk space usage. Passing -1 for the quality is equivalent to using ConvertBitmap with a target format of JPEG.

Unfortunately, specifying the quality parameter to the JPEG encoder is non-trivial, and non-obvious. As this seems to be a frequently asked question, it is likely useful to present the code required:

//Create an in-memory stream which will be used to save

//the converted image

System.IO.Stream imgStream = new System.IO.MemoryStream();

//Get the ImageCodecInfo for the desired target format

ImageCodecInfo destCodec = FindCodecForType
        (MimeTypeFromImageFormat(ImageFormat.Jpeg));
if (destCodec == null) {
    //No codec available for that format

    throw new ArgumentException("The requested format " +
        MimeTypeFromImageFormat(ImageFormat.Jpeg) +
        " does not have an available codec installed",
        "destFormat");
}

//Create an EncoderParameters collection to contain the

//parameters that control the dest format's encoder

EncoderParameters destEncParams = new EncoderParameters(1);

//Use quality parameter

EncoderParameter qualityParam = new 
        EncoderParameter(Encoder.Quality, quality);
destEncParams.Param[0] = qualityParam;

//Save w/ the selected codec and encoder parameters

inputBmp.Save(imgStream, destCodec, destEncParams);

//At this point, imgStream contains the binary form of the

//bitmap in the target format.  All that remains is to load it

//into a new bitmap object

Bitmap destBitmap = new Bitmap(imgStream);

To summarize, one must find the ImageCodecInfo object for the target format (JPEG in this case), know that the undocumented JPEG encoder accepts a parameter Encoder.Quality with a value between 0 and 100, create an EncoderParameter object to represent this parameter, and an EncoderParameters collection to contain the EncoderParameterObject. Finally, one passes the ImageCodecInfo and EncoderParameters objects to the Save method of the Bitmap class, and voila.

ConvertBitmapToTiff

public static Bitmap ConvertBitmapToTiff(Bitmap inputBmp, 
          TiffCompressionEnum compression);

Conceptually, this method is identical to ConvertBitmapToJpeg, except this method converts a bitmap to the TIFF format. While TIFF does not support the quality parameter exposed by the JPEG encoder, it does support a number of compression algorithms based on the color depth of the image. Therefore, instead of the quality parameter, ConvertBitmapToTiff takes a parameter of type BitmapManipulator.TiffCompressionEnum, which can have any one of the following values:

  • CCITT3
  • CCITT4
  • LZW
  • RLE
  • None
  • Unspecified

Be warned that CCITT3, CCITT4, and RLE do not appear to work correctly with 24 or 32-bit TIFF files; an exception is raised from deep within GDI+. Given that this area of GDI+ is virtually undocumented, a few idiosyncrasies are to be expected. This problem can be explored somewhat readily with the sample application included in the source archive.

Passing TiffCompressionEnum.Unspecified is equivalent to calling ConvertBitmap and specifying TIFF as the target format.

The same code used in ConvertBitmapToJpeg applies to ConvertBitmapToTiff, except the magic encoder parameter is compression instead of quality.

ScaleBitmap

public static Bitmap ScaleBitmap(Bitmap inputBmp, double scaleFactor);
public static Bitmap ScaleBitmap(Bitmap inputBmp, 
           double xScaleFactor, double yScaleFactor);

Obviously, this function scales the dimensions of a bitmap by a scale factor. Passing 1.0 returns an exact copy of the input bitmap, 2.0 yields a bitmap twice the size of the original, 0.5 a bitmap at half the size, etc.

The implementation of ScaleBitmap is notable for its counter-intuitiveness:

//Create a new bitmap object based on the input

Bitmap newBmp = new Bitmap(
    (int)(inputBmp.Size.Width*xScaleFactor),
    (int)(inputBmp.Size.Height*yScaleFactor),
    PixelFormat.Format24bppRgb);
    //Graphics.FromImage doesn't like Indexed pixel format

    
    
//Create a graphics object attached to the new bitmap

Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Set the interpolation mode to high quality bicubic

//interpolation, to maximize the quality of the scaled image

newBmpGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
newBmpGraphics.ScaleTransform((float)xScaleFactor, (float)yScaleFactor);

//Draw the bitmap in the graphics object, which will apply

//the scale transform

//Note that pixel units must be specified to 

//ensure the framework doesn't attempt

//to compensate for varying horizontal resolutions 

//in images by resizing; in this case,

//that's the opposite of what we want.

Rectangle drawRect = new Rectangle(0, 0, 
       inputBmp.Size.Width, inputBmp.Size.Height);
newBmpGraphics.DrawImage(inputBmp, drawRect, 
       drawRect, GraphicsUnit.Pixel);

//Return the bitmap, as the operations on the graphics object

//are applied to the bitmap

newBmpGraphics.Dispose();

//newBmp will have a RawFormat of MemoryBmp because it was created

//from scratch instead of being based on inputBmp. 

//Since it it inconvenient

//for the returned version of a bitmap to be of a 

//different format, now convert

//the scaled bitmap to the format of the source bitmap

return ConvertBitmap(newBmp, inputBmp.RawFormat);

Here, a Graphics object is created, bound to a new blank bitmap with the same dimensions as the input bitmap scaled by the scaling factors. The interpolation mode is set appropriately, in this case the slowest but highest quality mode. Next, ScaleTransform is called on the Graphics object, with the x and y scale factors passed as parameters. Note that, at this point, the input image has not been copied to the Graphics object; instead, ScaleTransform is adding a transformation to the "pipeline" for the Graphics object, so that any future operations on the Graphics object will undergo a scaling. This is a powerful concept, but counter-intuitive initially.

Finally, the input image is drawn onto the Graphics object, and since the scale transform is installed, the image will be scaled before being rendered into the previously created blank bitmap.

This concept of a transformation pipeline is central to many of the transformations supported by GDI+.

ResizeBitmap

public static Bitmap ResizeBitmap(Bitmap inputBmp, 
                       int imgWidth, int imgHeight);

The implementation of ResizeBitmap is deliciously simple:

//Simply compute scale factors that result in the desired size, 

//then call ScaleBitmap

return ScaleBitmap(inputBmp,
    (float)imgWidth/(float)inputBmp.Size.Width,
    (float)imgHeight/(float)inputBmp.Size.Height);

Since GDI+ does not provide the notion of a resize transform, resizing must be implemented in terms of scaling factors. Due to the precision of floating point math, it is possible the resulting image could be off by a pixel after round-off error, but for most applications this is not critical.

ThumbnailBitmap

Until now, all of the methods we have examined have been somewhat pedestrian. This method, on the other hand, presents a very useful, and somewhat obscure ability: given a bitmap, and a bounding rectangle, returns a copy of the input bitmap, scaled to the largest size that will fit within the bounding rectangle, without changing the image aspect ratio.

Once again, I created this feature for my photo album application, so I could present a table of thumbnails, which each table cell a uniform size.

The implementation of this method is unremarkable; as one might imagine, it relies upon ScaleBitmap to do the heavy lifting.

RotateBitmapRight

public static Bitmap RotateBitmapRight90(Bitmap inputBmp);
public static Bitmap RotateBitmapRight180(Bitmap inputBmp);
public static Bitmap RotateBitmapRight270(Bitmap inputBmp);

Rotates a bitmap right by 90, 180, and 270 degrees, respectively. Surprisingly, the implementation is simple and straightforward, an exception when dealing with GDI+. Consider RotateBitmapRight90:

//Copy bitmap

Bitmap newBmp = (Bitmap)inputBmp.Clone();
newBmp.RotateFlip(RotateFlipType.Rotate90FlipNone);

//The RotateFlip transformation converts bitmaps to memoryBmp,

//which is uncool.  Convert back now

return ConvertBitmap(newBmp, inputBmp.RawFormat);

Amazingly, the Bitmap class provides a RotateFlip method, which performs the rotation. No Graphics object or burdensome additional lines of code needed. Fortunately, some confusion is retained in that this intuitiveness is itself anomalous.

ReverseBitmap and FlipBitmap

public static Bitmap ReverseBitmap(Bitmap inputBmp);
public static Bitmap FlipBitmap(Bitmap inputBmp);

Reverse (mirror-image) and flip (upside-down) an image, respectively. These also use the Bitmap.RotateFlip method, and little more.

CropBitmap

public static Bitmap CropBitmap(Bitmap inputBmp, Rectangle cropRectangle);

Crops the input image, returning a bitmap containing the portion of the input bitmap enclosed by the crop rectangle. The implementation of this method is another simple yet non-obvious solution:

//Create a new bitmap object based on the input

Bitmap newBmp = new Bitmap(cropRectangle.Width,
    cropRectangle.Height,
    PixelFormat.Format24bppRgb);
    //Graphics.FromImage doesn't like Indexed pixel format


//Create a graphics object and attach it to the bitmap

Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Draw the portion of the input image in the crop rectangle

//in the graphics object

newBmpGraphics.DrawImage(inputBmp,
    new Rectangle(0, 0, cropRectangle.Width, cropRectangle.Height),
    cropRectangle,
    GraphicsUnit.Pixel);

//Return the bitmap

newBmpGraphics.Dispose();

//newBmp will have a RawFormat of MemoryBmp because it was created

//from scratch instead of being based on inputBmp. 

//Since it it inconvenient

//for the returned version of a bitmap to be 

//of a different format, now convert

//the scaled bitmap to the format of the source bitmap

return ConvertBitmap(newBmp, inputBmp.RawFormat);

Notice that GDI+ is not providing any explicit crop support. Instead, a new bitmap is created, equal in dimensions to the crop rectangle. Then, a Graphics object is bound to this new Bitmap, and Graphics.DrawImage is called to draw the input image data within the crop rectangle onto the Graphics object and thus the new bitmap. Simple, and non-obvious.

OverlayBitmap

public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, Point overlayPoint);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, ImageCornerEnum corner);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, int overlayAlpha, Point overlayPoint);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, int overlayAlpha, ImageCornerEnum corner);

This method is the main element that sets this class apart from myriad other C# imaging classes. With this method, one (presumably smaller) bitmap can be overlaid atop another (presumably larger) bitmap, at a corner, the center, or an arbitrary point, and with an arbitrary alpha (transparency). This was developed so that my photo album application could place a small, translucent watermark on each photo.

Use of this method is straightforward: pass the image upon which the overlay is placed, the image to overlay, some specification of where the overlay image goes, and an optional alpha value (0 to 100; 0 is transparent, 100 is opaque).

Implementation of this method is anything but straightforward. While overlaying one image atop another is relatively straightforward (just bind a Graphics object to the input bitmap, and call Graphics.DrawImage to copy the overlay image), including transparency in the overlay is rather complex:

//Convert alpha to a 0..1 scale

float overlayAlphaFloat = (float)overlayAlpha / 100.0f;

//Copy the destination bitmap

//NOTE: Can't clone here, because if destBmp is indexed instead of just RGB,

//Graphics.FromImage will fail

Bitmap newBmp = new Bitmap(destBmp.Size.Width,
    destBmp.Size.Height);

//Create a graphics object attached to the bitmap

Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Draw the input bitmap into this new graphics object

newBmpGraphics.DrawImage(destBmp,
    new Rectangle(0, 0,
                destBmp.Size.Width,
                destBmp.Size.Height),
    0, 0, destBmp.Size.Width, destBmp.Size.Height,
    GraphicsUnit.Pixel);

//Create a new bitmap object the same size as the overlay bitmap

Bitmap overlayBmp = new Bitmap(bmpToOverlay.Size.Width, 
                                bmpToOverlay.Size.Height);

//Make overlayBmp transparent

overlayBmp.MakeTransparent(overlayBmp.GetPixel(0,0));

//Create a graphics object attached to the bitmap

Graphics overlayBmpGraphics = Graphics.FromImage(overlayBmp);

First, a new Bitmap object is created with the same dimensions as the input bitmap. A Graphics object is bound to the Bitmap object, and the input Bitmap is copied to the new Bitmap object with DrawImage(Unscaled). Similarly, a new Bitmap object is created for the overlay bitmap, the first pixel in the overlay bitmap is set to be the transparent pixel (this is optional; remove it if the overlay bitmaps shouldn't have a transparent background), and a Graphics object bound to the copy of the overlay Bitmap.

//Create a color matrix which will be applied to the overlay bitmap

//to modify the alpha of the entire image

float[][] colorMatrixItems = {
    new float[] {1, 0, 0, 0, 0},
    new float[] {0, 1, 0, 0, 0},
    new float[] {0, 0, 1, 0, 0},
    new float[] {0, 0, 0, overlayAlphaFloat, 0},
    new float[] {0, 0, 0, 0, 1}
};
ColorMatrix colorMatrix = new ColorMatrix(colorMatrixItems);

//Create an ImageAttributes class to contain a color matrix attribute

ImageAttributes imageAttrs = new ImageAttributes();
imageAttrs.SetColorMatrix(colorMatrix, 
       ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

//Draw the overlay bitmap into the graphics object, 

//applying the image attributes

//which includes the reduced alpha

Rectangle drawRect = new Rectangle(0, 0, 
       bmpToOverlay.Size.Width, bmpToOverlay.Size.Height);
overlayBmpGraphics.DrawImage(bmpToOverlay,
    drawRect,
    0, 0, bmpToOverlay.Size.Width, bmpToOverlay.Size.Height,
    GraphicsUnit.Pixel,
    imageAttrs);
overlayBmpGraphics.Dispose();

Next, a 5x5 matrix is created, which consists of the identity matrix with cell (4,4) set to the alpha value, scaled from 0 to 1. Those readers with any linear algebra experience will recognize that a linear transformation is being built here, though to what end is perhaps not yet clear.

This matrix is used to create an ImageAttributes object, passed to ImageAttributes.SetColorMatrix. The matrix effectively encodes a transformation on the color of each pixel in the image, where color is a quintuplet with the fourth element consisting of alpha. Again, a bit of linear algebra experience will lead the reader to conclude that all of this matrix work is a verbose way of saying "scale the alpha channel by the scaling factor (0-1)".

Finally, the familiar Graphics.DrawImage is used to draw the overlay Bitmap into the overlay copy Bitmap, passing the overlay Bitmap through the color transformation matrix, so the result is the original overlay bitmap, but with transparency information set according to the alpha value.

//overlayBmp now contains bmpToOverlay w/ the alpha applied.

//Draw it onto the target graphics object

//Note that pixel units must be specified 

//to ensure the framework doesn't attempt

//to compensate for varying horizontal resolutions 

//in images by resizing; in this case,

//that's the opposite of what we want.

newBmpGraphics.DrawImage(overlayBmp,
    new Rectangle(overlayPoint.X, overlayPoint.Y, 
    bmpToOverlay.Width, bmpToOverlay.Height),
    drawRect,
    GraphicsUnit.Pixel);
newBmpGraphics.Dispose();

//Recall that newBmp was created as a memory bitmap; 

//convert it to the format

//of the input bitmap

return ConvertBitmap(newBmp, destBmp.RawFormat);

Now, given a version of the overlay Bitmap with transparency, that version is placed atop the input bitmap with another Graphics.DrawImage call. Apart from cleanup and formalities, that is the extent of the translucent overlay odyssey.

Clearly, this is the most painful example of GDI+ obfuscation and contortions. However, the end result is a professional, compelling blending of the input and overlay bitmaps, well worth the effort.

Misc

A few other public methods exist for MIME type conversion, etc, however they do not perform any substantive bitmap manipulation roles.

The sample application

Included in the source archive for this article is a simple Windows Forms application in C#, which exercises all of the functionality in BitmapManipulator. Note that this application is written to demonstrate use of the BitmapManipulator class and to grant the reader instant gratification, not to demonstrate the author's mastery of Windows Forms development. Error handling and input validation have been elided, and UI cleanup left as an exercise to the reader.

Conclusion

This article introduced the BitmapManipulator class, and delved somewhat into its inner workings with GDI+. Hopefully the reader has gained some understanding of basic GDI+ programming concepts in the process, or at the very least a healthy aversion to any future encounters with GDI+. The ambitious reader is encouraged to extend BitmapManipulator to perform other useful functions. In particular, a forthcoming article will explore the issue of dithering between color depths, a capability which is conspicuously absent from the .NET wrapper atop GDI+.

History

  • 9-1-03 Initial publication

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here