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



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


Comments

  • answer to junk "00" byte(s) info. in bitmap

    Posted by kid01 on 05/02/2005 02:27pm

    each scan line(width)-bytes size should be multiple of 4, so 7x7 bitmap is stored as 8x7 ( 8 = 4*2) --> there is "00" at each end of line(eol) 9x7 bitmap as 12x7 ( 8=4*2 <9 < 12=4*3) --> there are "00 00 00" at each eol base on this fact, in the original program, sizing & reading method can be compacted. I hope it will be help for you

    Reply
  • load bitmap image

    Posted by hdnhien on 06/26/2004 04:03am

    please help me step by step to read, load and draw a bitmap image on a MDI MFC document, explain each step. thank you very much!

    Reply
  • reading from the scanner to my application

    Posted by Legacy on 02/03/2004 12:00am

    Originally posted by: yamen

    reading from the scanner to my application??
    i'm working on an image pro... appliction but i need to load the image directly to my application,now i'm loading the image from a file on my hard disk

    Reply
  • Why RGB are stored BGR in the bitmap file

    Posted by Legacy on 11/07/2003 12:00am

    Originally posted by: Arie

    About the RGB stored in the file : its in intel format (read about Big & small indian format)
    
    the number are stored in memory as LO-HI value

    Sample RGB colrs are quad bytes 00 RR GG BB

    the 00 RR will swap so they are RR 00 (named the 2 bytes it WORD1)
    the GG BB will swap so they are BB GG (named the 2 bytes it WORD2)
    and because it a int value then the word RR 00 & BB GG are awapes as HI LO
    waht we got is WORD1 WORD2 now we need to perform a swap between them so we wll get WORD2 WORD1 and now if we display the context of WORD1 and WORD 2 we get

    BB GG RR 00


    and it will explain why it saved like that U can read the four bytes in each pixel and and the value with 0x00FFFFFF and U will get the correct color value (rememeber that the step will be 3 bytes step because the pixel color is saved in 3 bytes for each pixel)

    I hope it will make sense

    Reply
  • memory scanner

    Posted by Legacy on 05/25/2003 12:00am

    Originally posted by: kosc

    I can't fing utilite to scan memory for bitmaps (of activ process) so I'll try to make my own.
    Q: Are textures & other stuff stored in memory in bitmap format ?

    Reply
  • I want to convert bmp to greyscaled

    Posted by Legacy on 03/13/2003 12:00am

    Originally posted by: Abu

    Any one please help me.
    I want to read a bmp and convert it to grey scale.
    And then want to access the pixels grey value for processing.

    Any codesegment or project like this will be great help for me.

    Abu

    Reply
  • What is that manipulation!!

    Posted by Legacy on 01/18/2003 12:00am

    Originally posted by: Deepak M, Natarajan

    What is the mainpulation to convert r,g,b values to WORD type

    Reply
  • EXCELLENT website!!

    Posted by Legacy on 11/10/2002 12:00am

    Originally posted by: Justin

    Wow this is really the best website in describing details of bitmap files that I have encountered so far!! and the comments added by you guys did save me a hell lot of trouble in the debugging process..

    Cheers!!

    Reply
  • The right thing at the right time

    Posted by Legacy on 11/05/2002 12:00am

    Originally posted by: Marc

    I am just developing a computer game resource file format. Your article just played in my hands.

    thank you
    Marc

    Reply
  • MJaybe wrong sizeof

    Posted by Legacy on 11/05/2002 12:00am

    Originally posted by: Marc

    Hi,
    i just tried out to read the bitmap file header and got some strange values out when I read the BitmapInfo afterwards. Then i found out that the sizeof operator got 16 bytes for my BitmapFile Header although its just 14 big. I think this has to do how the compiler alignes the code and structures in memory. Wouldn't it be safer to hardcode the size 14 into the fread operation ?

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 20, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds