Chances are you’ve created or seen a bitmap file before, it’s a binary file so it won’t be easily deciphered by a plain text editor but at its core it’s basically a large array of pixel data. We’ll examine the structure of bitmap files and write a small function to create one. We are using the bitmap format because of how simple it is, there are simpler more archaic formats such as ppm which can actually be created with a text editor but requires a 3rd party program to view.
Bitmaps aren’t very common especially when we have made such advancements in image compression technology over the last few decades, such things as pngs, mozilla jpeg and bulky formats such as psd.
What we will be writing is a function to save a 24 bit bitmap without compression. This tutorial is also more about implementing a file format from specifications as opposed to creating images as there are faster formats such as PPM.
Bitmap format
Basically a bitmap file has a header (or two depending on how you class them) and then a body, the rest are optional and not really needed if you just want to get something out. See BMP_file_format#/File_structure.
Notice 3 structures that aren’t optional, those are what we need. They are Bitmap file header, DIB/image header and pixel array. Also note the size restriction(s), the file header has to be 14 bytes while our pixel array can be as large as we want. A larger image will lead to a larger file size as we will see soon.
File Header
Again, the file header has exactly 14 bytes split into 5 fields
Field name | Size in bytes | Description |
---|---|---|
bfType | 2 | The characters 'BM' |
bfSize | 4 | The size of the file in bytes |
bfReserved1 | 2 | Unused - must be 0 |
bfReserved2 | 2 | Unused - must be 0 |
bfOffBits | 4 | Offset to start of pixel data |
Image header
Field name | Size in bytes | Description |
---|---|---|
bfType | 2 | The characters 'BM' |
bfSize | 4 | The size of the file in bytes |
bfReserved1 | 2 | Unused - must be 0 |
bfReserved2 | 2 | Unused - must be 0 |
bfOffBits | 4 | Offset to start of pixel data |
Implementation
Let’s start by creating a struct to hold some simple RGB data.
struct rgb_data {
float r, g, b;
};
Next we create the function definition
void save_bitmap(const char *file_name, int width, int height, int dpi, rgb_type *pixel_data);
As input we take a filename, a width, height, dpi (dots per inch) and our array of pixels that we want to use to construct the image.
// create a file object that we will use to write our image
FILE *image;
// we want to know how many pixels to reserve
int image_size = width * height;
// a byte is 4 bits but we are creating a 24 bit image so we can represent a pixel with 3
// our final file size of our image is the width * height * 4 + size of bitmap header
int file_size = 54 + 4 * image_size;
// pixels per meter https://www.wikiwand.com/en/Dots_per_inch
int ppm = dpi * 39.375;
// bitmap file header (14 bytes)
// we could be savages and just create 2 array but since this is for learning lets
// use structs so it can be parsed by someone without having to refer to the spec
// since we have a non-natural set of bytes, we must explicitly tell the
// compiler to not pad anything, on gcc the attribute alone doesn't work so
// a nifty trick is if we declare the smallest data type last the compiler
// *might* ignore padding, in some cases we can use a pragma or gcc's
// __attribute__((__packed__)) when declaring the struct
// we do this so we can have an accurate sizeof() which should be 14, however
// this won't work here since we need to order the bytes as they are written
struct bitmap_file_header {
unsigned char bitmap_type[2]; // 2 bytes
int file_size; // 4 bytes
short reserved1; // 2 bytes
short reserved2; // 2 bytes
unsigned int offset_bits; // 4 bytes
} bfh;
// bitmap image header (40 bytes)
struct bitmap_image_header {
unsigned int size_header; // 4 bytes
unsigned int width; // 4 bytes
unsigned int height; // 4 bytes
short int planes; // 2 bytes
short int bit_count; // 2 bytes
unsigned int compression; // 4 bytes
unsigned int image_size; // 4 bytes
unsigned int ppm_x; // 4 bytes
unsigned int ppm_y; // 4 bytes
unsigned int clr_used; // 4 bytes
unsigned int clr_important; // 4 bytes
} bih;
// if you are on Windows you can include <windows.h>
// and make use of the BITMAPFILEHEADER and BITMAPINFOHEADER structs
memcpy(&bfh.bitmap_type, "BM", 2);
bfh.file_size = file_size;
bfh.reserved1 = 0;
bfh.reserved2 = 0;
bfh.offset_bits = 0;
bih.size_header = sizeof(bih);
bih.width = width;
bih.height = height;
bih.planes = 1;
bih.bit_count = 24;
bih.compression = 0;
bih.image_size = file_size;
bih.ppm_x = ppm;
bih.ppm_y = ppm;
bih.clr_used = 0;
bih.clr_important = 0;
image = fopen(file_name, "wb");
// compiler woes so we will just use the constant 14 we know we have
fwrite(&bfh, 1, 14, image);
fwrite(&bih, 1, sizeof(bih), image);
// write out pixel data, one last important this to know is the ordering is backwards
// we have to go BGR as opposed to RGB
for (int i = 0; i < image_size; i++) {
rgb_data BGR = data[i];
float red = (BGR.r);
float green = (BGR.g);
float blue = (BGR.b);
// if you don't follow BGR image will be flipped!
unsigned char color[3] = {
blue, green, red
};
fwrite(color, 1, sizeof(color), image);
}
fclose(image);
Now let’s render out a sample image
int width = 400,
height = 400,
dpi = 96;
rgb_data *pixels = new rgb_data[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int a = y * width + x;
if ((x > 50 && x < 350) && (y > 50 && y < 350)) {
pixels[a].r = 255;
pixels[a].g = 255;
pixels[a].b = 5;
} else {
pixels[a].r = 55;
pixels[a].g = 55;
pixels[a].b = 55;
}
}
}
save_bitmap("black_border.bmp", width, height, dpi, pixels);
Voila, you’ve created a bitmap.
You can take it the next step and probably look at the compression field and see if you can make any changes to the filesize or reverse the save_bitmap(...)
function to read one instead and use some SetPixel()
function in a library such as SDL to draw a bitmap on screen.