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.

Final 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.