Exploring the Internal Structure of a 24-Bit Uncompressed Bitmap File

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.



Click here for a larger image.

Environment: VC6 + Visual Studio SP5, Win98/NT/2000/XP

The Reason for This Article

I’ve recently had some experience dealing with image editing. Since 1997, I’ve wondered what a bitmap file format looks like inside. Because there are API functions that load and display bitmaps, I never actually got into the detail of the bitmap file’s internal structure. Recently, I was able to get some new information on the internal structure of bitmap files. So, I am sharing my experience with everyone.

I am sure most people know this already. This article deals the simplest form of bitmap—a 24-bit, uncompressed bitmap file. There is no RGBQUAD structure or compression. It should effectively demonstrate the internal structure of a bitmap. For other related topics, such as color table and compressions, they can be easily understood based on the stuff in this article. First, let’s begin with the data structures that describe the bitmap.

Bitmap File Internal Structure Overview

A bitmap file consists of four different parts. The first structure is the BITMAPFILEHEADER structure. You can check the Visual Studio’s Help file for this structure:

typedef
struct
tagBITMAPFILEHEADER
{
 
WORD    
bfType;
 
DWORD 
bfSize;
 
WORD    
bfReserved1;
 
WORD    
bfReserved2;
 
DWORD 
bfOffBits; 
}
BITMAPFILEHEADER, *PBITMAPFILEHEADER;

Here are explanations for these member variables:

  • bfType“: The type of this file usually should be two letters, “B” and “M,” as two bytes (combined, it is one WORD with the value “B” as the upper 8 bits and “M” as the lower 8 bits).
  • bfSize“: The size of the file, in bytes. If you right-click a bitmap file, then select Property and check the size of the file (not the actual size on the disk), it should be the same as what this variable contains. This variable is extremely useful. I’ll show you later.
  • bfReserved1” and “bfReserved2“: Useless; should be 0 at all times.
  • bfOffBits“: This variable indicates how many bytes are from the beginning of the file to the actual pixels. On my computer, it always returns at 54 (14 bytes for BITMAPFILEHEADER and 40 bytes for BITMAPINFO). Other rumors say this variable should be only 40. I guess it should be right too for some cases. But, on my computer, this is always 54.

After this structure, you will encounter another structure, called BITMAPINFO, at least on my computer. Every 24-bit, non-compressed bitmap is written to the disk like this: The first part of the bitmap is the BITMAPFILEHEADER and the next part is the BITMAPINFO. Visual Studio describes BITMAPINFO like this:

typedef struct
tagBITMAPINFO
{
  BITMAPINFOHEADER 
bmiHeader;
  RGBQUAD                       
bmiColors[1];
} BITMAPINFO, *PBITMAPINFO;

This is actually two structures put together with the name of BITMAPINFO. When you process this structure, you really don’t want to process them all together using a single fread(). Instead, process them one at a time (I guess any programmer would know that).

First, let me explain simply what BITMAPINFOHEADER is. It is described like this in Visual Studio:

typedef struct
tagBITMAPINFOHEADER
{
   
DWORD
biSize;
   
LONG
biWidth;
   
LONG
biHeight;
   
WORD
biPlanes;
   
WORD
biBitCount;
   
DWORD
biCompression;
   
DWORD
biSizeImage;
   
LONG
biXPelsPerMeter;
   
LONG
biYPelsPerMeter;
   
DWORD
biClrUsed;
   
DWORD
biClrImportant;
}
BITMAPINFOHEADER, *PBITMAPINFOHEADER;

Explanations for these member variables are in the following table:

Variable Description Value
biSize The size of the structure. Basically equal to size of(BITMAPINFOHEADER). It should be 40.
biWidth The width of the image  
biHeight The height of the image  
biPlanes The number of planes of the bitmap. In our case it is 1; I guess in most cases it should be 1.
biBitCount The number of bites per pixel: 1bit (two colors black and white), 4 bits (16 colors using a color lookup table stored in RGBQUAD), 8 bits (256 colors using a color lookup table stored in RGBQUAD), and finally 24 bits (2^24 colors using 3 bytes, each for red, green, and blue). In our case it is 24.
biCompression 0 denotes no compression, 1 to 3 denote three different RLE compression methods: 1 for RLE 8 bits compression, 2 for RLE 4 bits compression, and 3 for bit fields. I don’t know much about compressions (except they save spaces). In our case it is 0.
biSizeImage The total size of the image (the number of bytes from the first pixel of the file to the last pixel of the file). Note: This variable may not be equal to biWidth times biHeight.  
biXPelsPerMeter The number of pixels in one meter in x-axis. This is 0 in our case.
biYPelsPerMeter The number of pixels in one meter in y-axis. This is 0 too in our case.
biClrUsed The number of colors used in this bitmap. In our case, it is 0.
biClrImportant The number of colors that is important for this bitmap. In our case, it is 0.

After the BITMAPINFOHEADER structure comes a RGBQUAD variable; this is a color table. It is commonly either 16 colors or 256 colors, depending on how the file is specified in BITMAPINFOHEADER’s biBitCount. In our case, it does not exist in the file because we are using a 24-bit bitmap. This article gives you a simple idea what the bitmap looks like inside, without using any Win32 API functions or MFC class methods. Once the detail of this simple idea is known, other situation will be explained by extending this simple idea with some mathematical imaginations and some creativity.

After BITMAPINFO, the remainder of the file is used to store pixel points. It is a linear array of bytes occupying the rest of the file. For 24-bit uncompressed, this array of bytes seems to be nothing more than just RGB color values. That is not true at all; for certain situations, each scan line of the bitmap is separated by 1, 2, or even 3 zeroes. These are junk bytes. (I don’t know what else to call them.) The bytes shouldn’t be read and used as pixels. If you do use them as pixels, it will mess up the orders of pixels and generate a distorted image. Man, did I learn this lesson the hard way.

Just One Last Thing about Bitmaps

I don’t know who set the standard or why, but the image is usually stored in inverted order. That is, the image is stored so that the top left corner is stored at the end of the file, and the bottom right corner is stored as the first pixel after BITMAPINFO. Thus, the starting pixel is the lower-right corner of the bitmap, and the order of all pixel RGB bytes are B-G-R. The following table illustrates this idea:

Image 1

The actual image:

four colors:
1. Red (255, 0, 0)
2. Green (0, 255, 0)
3. Blue (0, 0, 255)
4. Purple Blue (128, 0, 255)

Image 2

The inverted image stored:

four colors
1. Red (255, 0, 0)
2. Green (0, 255, 0)
3. Blue (0, 0, 255)
4. Purple Blue (128, 0, 255)

Image 3

The actual inverted image:

Inside the file, all colors were written inverted:
1. Red (255, 0, 0) -> Blue (0, 0, 255)
2. Green (0, 255, 0) -> Still green
3. Blue (0, 0, 255) -> Red (255, 0, 0)
4. Purple Blue (128, 0, 255) -> Pink (255, 0, 128)

Image 1 is the actual image when you open a bitmap file with the Windows Paint program or any other image editor. Image 2 is a false image of Image 1, stored as a file. The reason for the word “false” is that the RGB bytes are not inverted. Image 3 is the actual image in the file if you read the file from the starting pixel to the end of file. It is like writing “I am an idiot!” backwards—”!toidi na ma I”. So if a bitmap’s pixels bits are like this:

(255, 0, 0) (0, 255, 0) (0, 0, 255)

in the real file, the pixels are stored like this:

(255, 0, 0) (0, 255, 0), (0, 0, 255) exactly backwards.

How the Pixels Should be Read from the File to Represent the Correct Image

This is where most people would fall. We now know the image is inversely stored as a file. But not many people know to make the total pixels equal an even number; junk bytes (useless bytes) are used to fill the number of pixels to be an even number. I don’t know if this is the correct analogy. As far as I have observed, this is true. For example:

This is a simple 31 X 31 pixels^2 24-bit uncompressed bitmap. Its total size on disk is 3,030 bytes. Let’s work out some mathematics: 31 * 31 * 3 = 2883 bytes (Note: times 3 in the end because we are counting bytes, and each pixel consists 3 bytes; each byte denotes R, G, and B values), plus two structures, BITMAPFILEHEADER and BITMAPINFOHEADER (14 + 40 bytes = 54 bytes). The total size of file ideally should be: 2883 + 54 = 2937 bytes. Compare to 3,030 bytes, there are 93 extra bytes. These are the junk bytes. They could be found at the end of each pixel line (I would refer these pixel lines as scan lines).

Let’s check another example:

This is a 7 by 7 pixels, 24-bit, uncompressed bitmap. Its total size on disk is 222 bytes. This actual size is larger than the ideal size of the bitmap. A little simple math can demonstrate this: 7 X 7 X 3 + 14 + 40 = 201. 7 and 7 is the width and height in pixels of the image. 3 means each pixel has 3 bytes to represent the values of red, green, and blue; 14 is the size of BITMAPFILEHEADER, and 40 is the size of BITMAPINFOHEADER. Compared to the actual size, there are 21 extra bytes. These are the junk bytes, which were written as 0’s. By looking at the image in hexadecimal format, you can see these junk bytes, as the underlined 3 “00”s:

……
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 00 00 00
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 00 00 00
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 00 00
00
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 00
00 00
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80
00 00 00
80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80
00 00 00 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF 80 80 FF
80 00 00 00

As you use various sizes of 24-bit, uncompressed bitmaps, you will see these junk bytes sometimes can be only one “00” in each scan line, or sometimes it is two “00”s in each scan line, and sometimes it is three “00”s in each scan line. I haven’t seen four or a larger number of “00”s as junk bytes in each scan line. I guess it is not necessary to add such a large number of “00”s as junk bytes in each scan line. Anyway, no matter how many bytes are used as junk bytes, they must be ignored. And it is very easy to determine how many such bytes need to be ignored in each line. Again, simple math is used to determine the number of bytes that must be ignored:

(Total size of bitmap on disk – (width * height * 3 + 14 + 40)) / height = number of bytes needed to be ignored during each line scan.

How to Parse a 24-bit, Uncompressed Bitmap File—The Source Code

We’ve explored every trick used to create and possibly encrypt an image into a bitmap. It is very easy to read a 24-bit uncompressed bitmap without the help of any WIN32, MFC, or other APIs to parse such a bitmap file. The following code is how you can do it:

BOOL CBitmaploadDoc::LoadBitmap24(const char * fn)
{
 FILE * fp;
 BYTE r, g, b;
 BITMAPFILEHEADER bmFileHdr;
 BITMAPINFO bmInfo;
 int EndByte = 1;
 BOOL bDone = FALSE;
 WORD * temp;
 WORD * t2;

 // open file to read
 fp = fopen(fn, "rb");

 // read BITMAPFILEHEADER
 fread((char *)&bmFileHdr, sizeof(BITMAPFILEHEADER), 1, fp);

 // you read BITMAPINFOHEADER and RGBQUAD separately or together
 // by reading them into the BITMAPINFO structure. When
 // RGBQUAD is not empty (bitmap is 16 colors or 256 colors),
 // you must read these two structures separately.
 fread((char *)&bmInfo, sizeof(BITMAPINFO), 1, fp);

 // check the bitmap info. If it is not 24-bit, and uncompressed,
 //and the colors used and are important is not 0, return FALSE.
 if (bmInfo.bmiHeader.biBitCount != 24 ||
     bmInfo.bmiHeader.biCompression != 0 ||
     (bmInfo.bmiHeader.biClrUsed != 0 &&
     bmInfo.bmiHeader.biClrImportant != 0))
 {
   fclose(fp);
   AfxMessageBox("This does not appear to be a 24-bit
                  uncompressedbitmap.");
   return FALSE;
 }

 // get the width and height of the bitmap.
 bm_width = (WORD)bmInfo.bmiHeader.biWidth;   // this is a class
                                              // member
 bm_height = (WORD)bmInfo.bmiHeader.biHeight; // this is a class
                                              // member
 if (bm_width > 800 || bm_height > 600)
 {
   fclose(fp);
   AfxMessageBox("This bitmap is way too big. Bitmap
                  shouldn't be larger than 800X600.");
   return FALSE;
 }

 // set new file pointer position at 54 from the beginning of
 // the file. 0 to 53 are used to store BITMAPFILEINFO and
 // BITMAPINFOHEADER.
 fseek(fp, bmFileHdr.bfOffBits, SEEK_SET);

 // start the mathematical calculation for junk bytes.
 if ((bmInfo.bmiHeader.biSizeImage - bm_width *
                                     bm_height * 3) == 0)
 {
   EndByte = 0; // only when there are no junk bytes
 }
 else
 {
   // find junk bytes.
   EndByte = (bmInfo.bmiHeader.biSizeImage - bm_width
                                           * bm_height * 3)
                                           / bm_height;
 }

 // create 2 empty arrays. One stores pixels from the file,
 // the other is used to invert the array.
 temp = new WORD[bm_width * bm_height];
 pBitmapPixels = new WORD[bm_width * bm_height]; // this is a
                                                 // class member
 int count = bm_width * bm_height - 1;

 // read the pixels.
 while (!feof(fp))
 {
   // read scan line
   for (int i = 0; i < bm_width; i++)
   {
   // because the bitmap is stored inverted, the color
   // byte Blue is always the first
       if (!feof(fp))
       {
         fread((BYTE *)&b, sizeof(BYTE), 1, fp);
       }
       else
       {
       // this is true only when the file is totally
       // corrupted.
         bDone = TRUE;
         break;
       }
       // Then we read the Green byte.
       if (!feof(fp))
       {
         fread((BYTE *)&g, sizeof(BYTE), 1, fp);
       }
       else
       {
       // this is true only when the file is totally
       // corrupted.
         bDone = TRUE;
         break;
       }
       // Finally, the Red byte.
       if (!feof(fp))
       {
         fread((BYTE *)&r, sizeof(BYTE), 1, fp);
       }
       else
       {
       // this is  true only when the file is totally
       // corrupted.
         bDone = TRUE;
         break;
       }

       // convert 24-bit color (3 bytes for R, G, B) into
       // 16-bit, 5-6-5 format WORD pixel.
       temp[count] = (WORD)((((WORD)r&0xf8)<<8)|
                           (((WORD)g&0xfc)<<3)|
                           (((WORD)b&0xf8)>>3));
       count--;     // counter operation for pixel index.
   }

   // When the for loop is done, one scan line is completed.
   // This is where you skip the junk bytes. If you don't do
   // that, the end result will be a distorted image.
   // Not good.
   if (!bDone)
   {
     fseek(fp, EndByte, SEEK_CUR);
   }
   else
   {
     break;
   }
 }

 // We do another inversion of the array.
 count = 0;
 for (int y = 0; y < bm_height; y++)
 {
   for (int x = bm_width - 1; x > -1; x--)
   {
      t2 = temp + y * bm_width + x;
      pBitmapPixels[count] = *t2;
      count++;
   }
 }
 delete [] temp;      // when done, the old array is discarded.

 fclose(fp);          // close file

 return TRUE;         // return success.
}

Final Note

When I finished writing this, I checked the Internet. There are articles that actually talk about how to load 24-bit, uncompressed bitmaps. Even though I couldn’t find one that described the details as much as I have done, some of them actually give enough information for a programmer to explore and to achieve the details I have done here.

Anyway, I hope this article gives everyone some direction about how a bitmap is stored. In real life, the situation is a bit more complex because there are 4-bit, 8-bit, and 16-bit bitmaps, using compression. I hope this article can at least help people start to extend the idea to solve these complicated situations. For me, this is enough to use for designing some cool RPG games.

Good luck to everyone.

Downloads

Download demo project “BitmapLoadApp.zip”- 17 Kb

Download source “BitmapLoad.zip”- 42 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read