DragonWins Home Page

Working with BMP Files

(Last Mod: 27 November 2010 21:37:52 )



Quick Links



This page is quite a bit more than just information about the BMP file format; if that is all you are looking for then you probably want to go directly to the Simplified Windows BMP Bitmap File Format Specification.

Instead, this page develops a set of basic, but very useful, C functions for working with BMP files and the images they contain. The emphasis is as much on writing good code as it is on what that code does.

The first step in the classic problem solving process is to define the problem to be solved. While this sounds obvious, it is actually here where most people make the biggest mistake - they fail to spend enough time clearly understanding what it is they are trying to accomplish and what constitutes success.

Initially, we could probably describe our goals very simply as follows:

  1. Read and Write BMP files.
  2. Edit the images in a BMP file.

But if we settled for this and proceeded to start banging out code, we would almost certainly end up with a confused mish-mash of functions that needed major surgery every time we realized that we needed or wanted yet another specific ability. So let's give this a bit more thought before proceeding.

What does it mean to "read and write BMP files"? Until we know more about the BMP file format we aren't in much of a position to define that a whole lot better, which tells us that before we proceed much further we need to start learning about the file format. You should probably at least skim the Simplified Windows BMP Bitmap File Format Specification at this point. Don't worry about understanding everything that is in it on the first read through; just try to get a general feel for what it contains. For now we can make quite a bit of progress by simply recognizing that reading and writing BMP files is fundamentally distinct from editing images. The first is simply how to store data in a file while the second deals with changing what that data is. Let's leverage that distinction and choose to work with the images themselves completely separate from reading and writing them to a file. To do this, we will devise a generic way of representing an image in memory and develop functions for working with it. We will then develop functions that can read the data from a BMP file and translate it into our generic representation and, conversely, functions that can translate our generic representation into a format suitable for storing as a BMP file. Approaching the problem this way not only separates the overall problem into two smaller problems, but also makes it clear how we would approach the problem of supporting other file formats, such as PNG or JPG, should we choose to do so later.

For the moment, let's assume that we have successfully developed this generic image representation and turn our attention to the second goal. What does it mean to "edit" an image? Let's start by listing some of the possible things that we might want to do:

  1. Create a new image.
  2. Copy portions of one image into another.
  3. Resize an image.
  4. Crop an image.
  5. Mirror/flip the contents of an image.
  6. Get the color information for a specific pixel.
  7. Set the color information for a specific pixel.
  8. Fill an entire image with a given color.
  9. Draw basic shapes such as lines and circles.
  10. Display an image on the screen.
  11. Use the mouse or arrow keys to draw on an image.
  12. Convert color images to gray scale or monotone images.
  13. Perform image processing tasks such as edge enhancement or other filtering tasks.

This list is far from exhaustive and no matter how extensive we were to make it we will invariably discover things that we would like to have that aren't on it. So let's keep things very simple for now and choose only the most basic capabilities. However, as we implement them, let's try to do so in a way that lends itself to adding new functionality later.

If we want to write our C code so that it is ANSI-compliant, then we need to forego things such as displaying the images on the screen and interacting with them using the mouse. Instead, we will focus on functions that work on the images in memory. The user can use programs such as Paint to see the results. This is not as limiting as it sounds. Later, if someone wants to add interactive display and editing, they can do so by writing functions that do the user interaction to determine what is to be done with the image and then call our functions to actually do it. This approach is known as "abstraction" or "layering" and is very common in developing large applications. So let's focus on the very basics:

  1. Create a new image of a user-specified size.
  2. Destroy an image (and free up the memory) when we are done with it.
  3. Get the color information for a specific pixel.
  4. Set the color information for a specific pixel.

When all is said and done, everything else that we might want to do can be accomplished by using these functions. In fact, we will take this one step further and decide right now that everything else that we might choose to do will be done by using these functions -- in other words, these functions will be the only ones allowed to directly act on the image data.

Now that we have our goals more narrowly defined it is time to start making some other decisions. The first one is how to represent the image in memory. The most flexible and generic way to represent color images is as raw RGB data where each pixel is described by a "color triplet" consisting of three numbers that represent the intensity of the red, green, and blue components of the color at that point. If we really wanted to be generic, we might choose to use a floating point representation for each number and constrain each value to be between 0.0 and 1.0. Not long ago, we would have shied away from doing this because floating point operations were very slow compared to integer operations. However, not only has the overall speed of processors increased to a point where this is not a major factor for us, but extensive work has been done in improving the speed of floating point operations because they are so vital to graphic processing. Be all of this as it may, we will still shy away from using floating point representations because it will make our lives considerably easier. Instead, we will represent each color component as an intensity between 0 and 2N-1 where N is the "bit depth" of the intensity (not of the image, as will be described momentarily). What should we make N? It turns out that N=8 is sufficient for all of the BMP formats that are of interest to us and, in fact, it is sufficient for nearly any graphic representation of normal images. The bit depth of the image (not the intensity) refers to how many bits are needed to describe the color content of each pixel. If each pixel has three colors and each color requires 8 bits to represent it, then the pixel as a whole requires 24 bits and is therefore described as a 24-bit image. This is sufficient depth to permit more than 16 million colors.

So now let's start laying out our functions. We will be creating a structure called IMAGE and developing functions to manipulate the data stored in that structure, so let's adopt the convention that all of our functions will be named IMAGE_function_name(). Furthermore, since virtually all of these functions will require a pointer to an IMAGE structure as an argument, let's stipulate up front that it will always be the first argument. With this in mind, the following are the functions that we need to write:

We could eliminate the IMAGE pointer from the IMAGE_new() parameter list, but having it there makes all of the function calls consistent. It also gives us the ability to use this function to recycle images by passing a pointer to an existing IMAGE structure. If we truly want a new image, then we simply pass it a NULL pointer instead.

So what things do we need to store in an IMAGE structure? The main thing, of course, are the data for all of the pixels. But we also need to keep track of how many rows and how many columns are in the image. It was mentioned earlier that the most natural way to store the pixel data is in a three dimensional array where the dimensions are {row, col, color component}. Assuming that such an array is part of the IMAGE structure, then the contents of our last two structures starts becoming pretty apparent:

int IMAGE_get_color(IMAGE *image, int row, int col, int color)

{

return image->data[row][col][color];

}

 

int IMAGE_set_color(IMAGE *image, int row, int col, int color, int value)

{

image->data[row][col][color] = value;

return image->data[row][col][color];

}

 

int    IMAGE_set_color(IMAGE *image, int row, int col, int color, int value);

But while something like this would work, we are going to be quite a bit more disciplined in how we craft our functions. Following the guidelines in A Structured Approach to Working with Structures, we