Introduction
One of the most basic and most important components of games is graphics. There are a number of ways to get graphics into games, but one of the most common is to load them in from a file. Thisarticle will discuss how to load an image that has been saved as a device independent bitmap (DIB); more commonly referred to as a Windows bitmap. All code presented will be in C++.
File types
One of the most important aspects of reading information from a file is knowing what type it is. There are two basic types: text and binary. The difference between the two is text saves allinformation as strings of characters and binary saves the computer representation of any values.
For example, if you have an unsigned integer with the value 847385 and saved this number in text format to a file, you could open that file up in any text editor and you would see the number847385 staring back at you. Assuming that an unsigned integer is 32bits long, this number would be stored in memory with the hexadecimal values 19 EE 0C 00. From looking at this you can see that itlooks backwards, but that is how Intel based machines store values. The least significant byte is stored first, with the most significant being last. This is also how information is written into abinary file also. After the value 847385 is written to a binary file, if you were to examine the file with a text editor, you would just see a bunch of meaningless characters. However, if you were toexamine the file with a hexadecimal editor, you would see the values 19 EE 0C 00.
Image files stored as a DIB are also saved in binary format. We will look at how to load these properly soon. First I will go over how a DIB file is formatted.
Bitmap file format
There are four sections that make up a bitmap file. They are the bitmap header, bitmap info, colour palette and the bitmap data. These sections will always appear in this order, but the colourpalette will not always be present. For anyone programming on the Windows platform, there are already structures available to load this information. All you have to do is #include to get access to thee structures. For those not programming in Windows, you will have to define these structures yourself. Below you can see these structures as they are defined in windows.h.
//File information header
//provides general information about the file
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
//Bitmap information header
//provides information specific to the image data
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;
//Colour palette
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
Note that there is no structure for the data. That is because the data is simple a run of bytes.
//provides general information about the file
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
//Bitmap information header
//provides information specific to the image data
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;
//Colour palette
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
Opening a file
The first thing we need to do to read in a bitmap file is to open the file. The function we will use to open the file is:
FILE *fopen(const char *filename, const char mode).
The first parameter is the name of the file and the second parameter is the mode to open with. Here is the code snippet you need to read from a file.
//include the header for file access#include
//function to load the bitmap
loadBMP(char *file) {
//file handle used in all file operations
FILE *in;
//open the file for reading in binary mode
in=fopen(file,"rb");
The "rb" portion is very important in the fopen function. This is what allows us to read in binary format. If this is not set properly you will not be able to read the file correctly.
That's all there is. The file is now open and ready for reading.
//function to load the bitmap
loadBMP(char *file) {
//file handle used in all file operations
FILE *in;
//open the file for reading in binary mode
in=fopen(file,"rb");
BITMAPFILEHEADER
The first thing you need to read from the file is the file header. The function we will use for reading is:
size_t fread(void *ptr, size_t size, size_t nelem, FILE *stream);
The first parameter is a pointer to where you want the data stored. The second parameter is the size of each element that you want to read. The third is the number of elements of that size toread. The final parameter a pointer to the stream obtained from fopen.
Here's how to read in the file header:
BITMAPFILEHEADER bmfh;
fread(&bmfh,sizeof(BITMAPFILEHEADER),1,in);
As you can see, it only takes one read to fill all the information in the structure. Using sizeof() on BITMAPFILEHEADER will return a size of 16 bytes. This is also the exact size of the fileheader. Since there is only one header, the third element is set to one. And of course, the stream we are using is the last element.
Now that we have loaded the file header, let's examine each element for their meaning.
WORD bfType;
This will tell you if the file is a bitmap type or not. This number is always 19778. If it is not, then the file is not a bitmap file.
DWORD bfSize;
This is the total size of the file, including all the headers.
WORD bfReserved1; WORD bfReserved2;
These two are reserved and should contain all zeros.
DWORD bfOffBits;
This is the offset to the image data from the start of the file. This will vary depending on whether or not there is a colour palette.
BITMAPINFOHEADER
Here you use the same method that was used to load BITMAPFILEHEADER:
BITMAPINFOHEADER bmih;
fread(&bmih,sizeof(BITMAPINFOHEADER),1,in);
The sizeof() should return 40 bytes on this read. Since there is only one BITMAPFILEHEADER, the number to read in is 1.
Now let's look at the elements of BITMAPFILEHEADER:
DWORD biSize;
This is the size of BITMAPFILEHEADER. It should be 40.
LONG biWidth;
This is the width of the image in pixels.
LONG biHeight;
This is the height of the image in pixels.
WORD biPlanes;
This is the number of bit planes in the image. It should always be 1.
WORD biBitCount;
This is the number of bits per pixel for colours. It will be 1,4,8 or 24.
DWORD biCompression;
This is the compression, if any, used. It will be set to 0 for no compression.
DWORD biSizeImage;
This should be set to the size of the image data. Sometimes it may be set to 0.
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
These set the horizontal and vertical resolution in pixels per meter.
DWORD biClrUsed;
This specifies the number of colours used from the colour palette. If the image is 24 bits per pixel, then this is usually 0.
DWORD biClrImportant;
This is the number of colour indexes that are important. If it is set to 0, all indexes are important.
fread(&bmfh,sizeof(BITMAPFILEHEADER),1,in);
fread(&bmih,sizeof(BITMAPINFOHEADER),1,in);
LONG biYPelsPerMeter;
RGBQUAD
This information is only read in from the file if there is a colour table. There is only a colour table if the image is less than 24 bits per pixel. The code below will load the colour table:
//set the number of colours
numColours=1 << bmih.biBitCount;
//load the palette for 8 bits per pixel
if(bmih.biBitCount == 8) {
colours=new RGBQUAD[numColours];
fread(colours,sizeof(RGBQUAD),numColours,in);
}
In this article, I will only be dealing with 8bit or 24bit bitmaps, so there will only be a colour table for the 8bit variety. With the code above, it can be easily expanded to accommodate 4bit and 1bit colour tables as well.
The sizeof() here will return 4. The variable numColours will be 256 for an 8bit image. So this will load in a 256 colour palette. On a Windows PC, most colour palettes are stored in RGB format. In a bitmap file, this is reversed. There is also a reserved byte at the end of each colour. The palette loaded will look like BGRr. Where the 'r' is the reserved byte.
numColours=1 << bmih.biBitCount;
//load the palette for 8 bits per pixel
if(bmih.biBitCount == 8) {
colours=new RGBQUAD[numColours];
fread(colours,sizeof(RGBQUAD),numColours,in);
}
Image data
Now we come to the most important part of the file. This is where all the pixels that make up the image will be stored. To find out the size of this section, you could look at bmih.biSizeImage. However, this value will sometimes be 0. To make sure you always have a valid value, you can use the following code:
DWORD size;
size=bmfh.bfSize-bmfh.bfOffBits;
This takes the size of the file and subtracts the size of all the header information. This will always give you an accurate result. Next create a temporary variable to store image data in so you can work with it and read the image data from the file:
BYTE *tempPixelData;
tempPixelData=new BYTE[size];
if(tempPixelData==NULL) {
fclose(in);
return false;
}
fread(tempPixelData,sizeof(BYTE),size,in);
And that's it. One read reads in the entire bitmap, no matter how large it is. The only consideration is if you have enough memory to hold the image.
Once you have the data loaded, you might think you are ready to use it. However, there are a couple of things to consider about how the data is formatted.
First, each line of data must end on a DWORD(4 byte) boundary. If the width of your image does not fall on a DWORD boundary, then it will be padded with zeros to fill it out. For example, if you have an image that is 254x254, the DWORD boundary for this image is 256. The image data loaded from this file will be 256x254. Before the data can be used, the padding must be removed. Also note that some image editors will also make sure the file ends on a DWORD boundary. This needs to be taken into account in the code. There are comments to show where this needs to be considered.
Second, the data can be stored either in forward order or reverse order. The way to tell is to check the bmih.biHeight value. If this value is negative, the data is stored in forward order. If it is positive, reverse order is used.
Now we'll set up a couple of variables to hold the widths:
//byteWidth is the width of the actual image in bytes
//padWidth is the width of the image plus the extra padding
LONG byteWidth,padWidth;
//initially set both to the width of the image
byteWidth=padWidth=(LONG)((float)width*(float)bpp/8.0);
//add any extra space to bring each line to a DWORD boundary
while(padWidth%4!=0) {
padWidth++;
}
I will just throw all the code out and then go through it:
DWORD diff;
int offset;
LONG height;
height=bmih.biHeight;
//set diff to the actual image size(no padding)
diff=height*byteWidth;
//allocate memory for the image
pixelData=new BYTE[diff];
if(pixelData==NULL) {
fclose(in);
return false;
}
//bitmap is inverted, so the padding needs to be removed
//and the image reversed
//Here you can start from the back of the file or the front,
//after the header. The only problem is that some programs
//will pad not only the data, but also the file size to
//be divisible by 4 bytes.
if(height>0) {
int j==size-3;
offset=padWidth-byteWidth;
for(int i=0;i
if((i+1)%padWidth==0) {
i+=offset;
}
*(pixelData+j+2)=*(tempPixelData+i);
*(pixelData+j+1)=*(tempPixelData+i+1);
*(pixelData+j)=*(tempPixelData+i+2);
j++;
}
}
//the image is not reversed. Only the padding needs to be removed.
else {
height=height*-1;
offset=0;
do {
memcpy((pixelData+(offset*byteWidth)),
(tempPixelData+(offset*padWidth)),
byteWidth);
offset++;
} while(offset
}
First the actual size of the image is determined and memory is allocated for it. Then the height is checked to see if the image is reversed or not. If the image is reversed, the data needs to be copied byte by byte into the final storage area. If the image is not reversed, then we can make use of memcpy() to quickly move the data. This is not a big deal as image loading should not occur in time critical code anyway.
size=bmfh.bfSize-bmfh.bfOffBits;
tempPixelData=new BYTE[size];
if(tempPixelData==NULL) {
fclose(in);
return false;
}
fread(tempPixelData,sizeof(BYTE),size,in);
//padWidth is the width of the image plus the extra padding
LONG byteWidth,padWidth;
//initially set both to the width of the image
byteWidth=padWidth=(LONG)((float)width*(float)bpp/8.0);
//add any extra space to bring each line to a DWORD boundary
while(padWidth%4!=0) {
padWidth++;
}
int offset;
LONG height;
height=bmih.biHeight;
//set diff to the actual image size(no padding)
diff=height*byteWidth;
//allocate memory for the image
pixelData=new BYTE[diff];
if(pixelData==NULL) {
fclose(in);
return false;
}
//bitmap is inverted, so the padding needs to be removed
//and the image reversed
//Here you can start from the back of the file or the front,
//after the header. The only problem is that some programs
//will pad not only the data, but also the file size to
//be divisible by 4 bytes.
if(height>0) {
int j==size-3;
offset=padWidth-byteWidth;
for(int i=0;i if((i+1)%padWidth==0) {
i+=offset;
}
*(pixelData+j+2)=*(tempPixelData+i);
*(pixelData+j+1)=*(tempPixelData+i+1);
*(pixelData+j)=*(tempPixelData+i+2);
j++;
}
}
//the image is not reversed. Only the padding needs to be removed.
else {
height=height*-1;
offset=0;
do {
memcpy((pixelData+(offset*byteWidth)),
(tempPixelData+(offset*padWidth)),
byteWidth);
offset++;
} while(offset }
An example
Now that we have our bitmap loaded, I will show you how you can use this in an OpenGL program to load texture data from a bitmap image. First I will post the header file for my class to make things easier to follow:
#ifndef _BITMAP_H
#define _BITMAP_H
#include
#include
class Bitmap {
public:
//variables
RGBQUAD *colours;
BYTE *pixelData;
bool loaded;
LONG width,height;
WORD bpp;
//methods
Bitmap(void);
Bitmap(char *);
~Bitmap();
bool loadBMP(char *);
private:
//variables
BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;
//methods
void reset(void);
};
#endif //_BITMAP_H
As you can see, I left the data that needs to be accessed public to make it easier to use.
Now for an example of loading a texture into an OpenGL program:
bool loadTexture() {
Bitmap *image;
image=new Bitmap();
if(image==NULL) {
return false;
}
if (image->loadBMP("mytexture.bmp")) {
glGenTextures(1, &texture[0]);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, 3, image->width, image->height, 0,
GL_RGB, GL_UNSIGNED_BYTE, image->pixelData);
}
else {
return false;
}
if (image) {
delete image;
}
return true;
}
As you can see, all you have to do is allocate space for the Bitmap object, tell it to load the bitmap and send it to OpenGL.
#define _BITMAP_H
#include
#include
class Bitmap {
public:
//variables
RGBQUAD *colours;
BYTE *pixelData;
bool loaded;
LONG width,height;
WORD bpp;
//methods
Bitmap(void);
Bitmap(char *);
~Bitmap();
bool loadBMP(char *);
private:
//variables
BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;
//methods
void reset(void);
};
#endif //_BITMAP_H
Bitmap *image;
image=new Bitmap();
if(image==NULL) {
return false;
}
if (image->loadBMP("mytexture.bmp")) {
glGenTextures(1, &texture[0]);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, 3, image->width, image->height, 0,
GL_RGB, GL_UNSIGNED_BYTE, image->pixelData);
}
else {
return false;
}
if (image) {
delete image;
}
return true;
}