← Blog

Writing BMP Images from Scratch

March 28, 2020

I work with image files all the time. Many of my programming projects revolve around making pretty pictures or turning them into websites. Recently, however, it occurred to me that I’ve never worked with a raster graphics file directly. At the lowest-level, I’ve always just manipulated an RGB array in memory, then called out to some library to handle saving my work.

This thought made me a little uncomfortable. The way I used computers was completely overturned once I came to understand the underlying text representation that makes up web pages, config files, source code, etc. What if understanding binary data representations is just as important?

Thus, I decided it was time to write a program that could produce images from scratch. This meant using a language without a runtime, that’s good for manipulating bytes directly, and which I already needed to brush up on anyway: C. I went with BMP as an output format. There are a few even simpler formats, like TGA, but since I’m writing a blog post, I thought I might as well go with something viewable in a browser.

For the impatient, here’s the end product. For everyone else, let’s walk through the problem together.

Dissecting a BMP

Before we begin coding, let’s start by looking inside an existing BMP file. Here’s the one we’ll be starting with:

Ok, maybe that’s too hard to see. Let’s scale it by 50x:

Yes, this is just a 1x1 pixel image with RGB color (239, 65, 53). I made it in GIMP, making sure to check “Do not write color space information” in the export dialogue.1 Now, let’s look inside the file and see how it’s made. The tool I usually use for this sort of thing is cat. Let’s got ahead and try it:

Oh yeah. This is a binary file. So to see inside it, we’ll need a different tool. For this, let’s use the command xxd. This will show us the byte data of a file in hexadecimal format. We’ll use the -g 1 flag to group the data in 1-byte groups:

This is better, but how do we read it?

The most import part is in the center, starting with 42 4d… and ending with …ef 00. This is the byte-data of the file in hexadecimal format. As a reminder, hexadecimal is a base-sixteen representation of a number using a-f for the digits after 9. Some people can easily convert between hexadecimal and decimal in their heads, but for the rest of us, there’s printf. Let’s convert the 42 and 4d to decimal:

These numbers are the ascii codes for ‘B’ and ‘M’, which appear at the start of every BMP file to indicate its format. You’ll notice that these letters also appear on the right-hand side of the xxd output: it’s just a representation of the same data in ascii. We won’t need to worry about it too much, since most of our data is not in ascii format. Finally, on the left-hand side, we have 000000xx:. These are just the indices in hexadecimal. There are 16 columns in each row of bytes, so the numbering increases 0x10 in each row.

Now that we know how to read the xxd output, let’s try and interpret the byte data. In principle we should be looking at the specification, but that will be boring… so lets just go to the Wikipedia article, which tells us that every BMP file starts with a 14-byte header. Let’s mark that in blue:

42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00
00 00 04 00 00 00 23 2e 00 00 23 2e 00 00 00 00
00 00 00 00 00 00 35 41 ef 00                  

The middle of the file is a little complicated, so for now let’s skip to the last part: the pixel array. Each pixel is three bytes, one for each color. Since BMP is a Microsoft format and Microsoft likes doing things backwards, the order of the bytes is normally Blue, Green, Red, starting at the bottom-left pixel of the image. Each row of pixels gets zero-padded to a multiple of four bytes. We have one row of one pixel, so it will be three bytes padded to four. Thus, our pixel array will by the last four bytes of the file. Let’s just confirm that 35 41 ef matches the colors we chose at the beginning: which were (239, 65, 53)

Great, it matches. Not coincidentally, this is the color we get if we add color: #ef4135 in CSS. Let’s use it to mark the pixel array in our xxd output:

42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00
00 00 04 00 00 00 23 2e 00 00 23 2e 00 00 00 00
00 00 00 00 00 00 35 41 ef 00

In-between the header and the pixel data, we have 40 bytes. This matches the size of a DIB header in BITMAPINFOHEADER format, so it’s probably safe to assume that this is what we have. Let’s mark it in green:

42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00
00 00 04 00 00 00 23 2e 00 00 23 2e 00 00 00 00
00 00 00 00 00 00 35 41 ef 00

Great. Now that we’ve got a basic understanding of the format, let’s lay the groundwork for our C code.

Starting with C — The BMP Header

Let’s try and write a program that will output an identical 1×1 BMP file. Here’s what we’ll start with:

If we run this now, it will output the first two bytes of the BMP header. Let’s add the rest. According to Wikipedia, the rest of the header consists of:

We’ll figure out how to calculate the size and offset later. But for now, let’s just copy what we got from the xxd output.

00000000: 42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00  BM:.......6...(.

These are all integers, so we’ll add a new int array to our C code and write it to the file. To represent a hexadecimal number in C, you put 0x before it. Note that we write 0x3a rather than 0x0000003a because the file data is stored in little-endian format, where the least-significant bit comes first, whereas in C, we write the hex numbers in big-endian format. If this is confusing, think about how dates are written differently in various parts of the world. Europeans write dates in Day-Month-Year format, where the smallest unit comes first. The Chinese, by contrast, write dates in Year-Month-Day format, where the biggest unit comes first.2 The dates are the same, whether it’s 03-28-2020 or 2020-28-03; the former is just in big-endian and the latter in little-endian. The same is true with hex numbers: 0x3a000000 in little endian equals 0x0000003a in big endian.

The DIB Header

Great. Now that we’ve made our way through the BMP header, we need to write the DIB header. This will specify more information needed to display the image. It can be in various formats, but we saw above that we’re using the 40-byte BITMAPINFOHEADER. So again, let’s check each field against our xxd output:

42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00
00 00 04 00 00 00 23 2e 00 00 23 2e 00 00 00 00
00 00 00 00 00 00 35 41 ef 00

Adding all of these to our C code, we get:

The Pixel Data

Finally, we need to output an array of pixels. Recall that each pixel is represented by its B, G, and R values, and each row is padded to a multiple of 4 bytes. Since we have one pixel, that means we’ll be outputting an array of 4 bytes.

Alright, now let’s run the program and see what we get:

Yay! Let’s try and change the color:

Hurrah!

Generalizing

Ok, so now we know how to output a valid BMP file. But sometime we might want to output a different image, maybe even one with more than one pixel. So let’s refactor our main into a function that outputs a given RGB array to a file.

The pixel data is stored in a 1D array —this might seem counter-intuitive for a 2D image, but it’s the normal way of handling pixel data in computer graphics, because it’s more efficient and avoids having to deal with pointers to arrays. Since the pixel data is 1D, in addition to passing its length as an argument (as C doesn’t automatically track array lengths), we also need the width of the image so we know where each new row starts.

Now that we’ve got that, let’s try calling it on an RGB array. For no particular reason, let’s try and output this 6×6 pixel image:

Run that program and we get:

Ok, so we’ve got the dimensions right. Now all we need to do is copy the RGB data into the bitmap array in the correct order (BGR) and with the correct padding.

Alright, that should do it. Now lets write out some flags (chosen for simplicity):

int main() {

    char fr[] = {
        0, 85, 164,    // Bleu
        255, 255, 255, // Blanc
        239, 65, 53,   // Rouge
        0, 85, 164,
        255, 255, 255,
        239, 65, 53,
    };
    write_bmp("french_flag.bmp", fr, sizeof(fr) / sizeof(char), 3);

    char bgm[] = {
        0, 0, 0,       // Black
        253, 218, 36,  // Yellow
        239, 51, 64,   // Red
        0, 0, 0,
        253, 218, 36,
        239, 51, 64,
    };
    write_bmp("belgian_flag.bmp", bgm, sizeof(bgm) / sizeof(char), 3);
    
    // Flag of the United Arab Emirates
    char *uae = (char*) malloc(12 * 6* sizeof(char) * 3);
    for (int row = 0; row < 6; row++) {
        for (int col = 0; col < 12; col++) {
            if (col < 3) {
                uae[3 * (row * 12 + col)] = 255;
                uae[3 * (row * 12 + col) + 1] = 0;
                uae[3 * (row * 12 + col) + 2] = 0;
            } else if (row < 2) {
                uae[3 * (row * 12 + col)] = 0;
                uae[3 * (row * 12 + col) + 1] = 116;
                uae[3 * (row * 12 + col) + 2] = 33;
            } else if (row < 4) {
                uae[3 * (row * 12 + col)] = 255;
                uae[3 * (row * 12 + col) + 1] = 255;
                uae[3 * (row * 12 + col) + 2] = 255;
            } else {
                uae[3 * (row * 12 + col)] = 0;
                uae[3 * (row * 12 + col) + 1] = 0;
                uae[3 * (row * 12 + col) + 2] = 0;
            }
        }
    }
    write_bmp("uae_flag.bmp", uae, 3*6*12, 12);

    // Transgender Pride Flag
    char *trans = (char*) malloc(8 * 5* sizeof(char) * 3);
    for (int row = 0; row < 5; row++) {
        for (int col = 0; col < 8; col++) {
            if (row == 0 || row == 4) {
                trans[3 * (row * 8 + col)] = 91;
                trans[3 * (row * 8 + col) + 1] = 207;
                trans[3 * (row * 8 + col) + 2] = 250;
            } else if (row == 1 || row == 3) {
                trans[3 * (row * 8 + col)] = 245;
                trans[3 * (row * 8 + col) + 1] = 171;
                trans[3 * (row * 8 + col) + 2] = 185;
            } else {
                trans[3 * (row * 8 + col)] = 255;
                trans[3 * (row * 8 + col) + 1] = 255;
                trans[3 * (row * 8 + col) + 2] = 255;
            }
        }
    }
    write_bmp("trans_flag.bmp", trans, 3*5*8, 8);

}

And there we have it:

Limitations

This only produces uncompressed files. This is fine for the tiny images we’re making: in fact, it beats most other formats in terms of disk usage at this size because of how little metadata we use. However, as the image size increases, we start to get very large file sizes: at 512×512, the output of this program is more than 100x the size of the same image converted to a lossless PNG. Thus, you wouldn’t want to use this program as much more than a toy. Nevertheless, walking through a toy program can be a great way to learn.


  1. Actually, I didn’t realize to do this until after I had gone through these steps. But it makes everything simpler, so lets pretend I did it right the first time.

  2. Incidentally, the Chinese date format is almost always the one you want to use in computing, because its the only one where the alphabetical sort gives the dates in-order.

    The US date format doesn’t make much numeric sense, and thus doesn’t help understanding endian-ness, because it puts the second-smallest unit first.