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
About Adam Nelson
My name is Adam Nelson. I've been a professional programmer since 1996, working on everything from database development, early first-generation web applications, modern n-tier distributed apps, high-performance wireless security tools, to my current position as Principal Engineer at Cryptos Mobile Systems building security and connectivity solutions for embedded systems. I have a wide range of skills and interests, including cryptography, image processing, computational linguistics, military history, 3D graphics, database optimization, and mathematics, to name a few. I generally do little besides work (either for my employer or on my own self-edification projects), read, and train. Click here to view Adam Nelson's online profile. |