Luna McNultyWords

Writing BMP Images from Scratch

March 28, 2020

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: a single red pixel

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

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 with cat and see how it’s made:

$ cat 1x1.bmp
BM:6(#.#.5A�

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:

$ xxd -g 1 1x1.bmp 
00000000: 42 4d 3a 00 00 00 00 00 00 00 36 00 00 00 28 00  BM:.......6...(.
00000010: 00 00 01 00 00 00 01 00 00 00 01 00 18 00 00 00  ................
00000020: 00 00 04 00 00 00 23 2e 00 00 23 2e 00 00 00 00  ......#...#.....
00000030: 00 00 00 00 00 00 35 41 ef 00                    ......5A..

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:

$ printf "%d\n" 0x42
66
$ printf "%d\n" 0x4d
77

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)

$ printf "%d %d %d\n" 0x35 0x41 0xef
53 65 239

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:

#include <stdio.h>
#include <stdlib.h>
int main() {

    // Make an array containing the B and M characters that
    // identify the file as a BMP.
    char tag[] = { 'B', 'M' };

    FILE *fp = fopen("test.bmp", "w+"); // Open a file for writing
    fwrite(&tag, sizeof(tag), 1, fp);   // Write the tag to the file
    fclose(fp);                         // Close the file
}

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.

#include <stdio.h>
#include <stdlib.h>
int main() {
    char tag[] = { 'B', 'M' };

    // Create an integer array for the header   
    int header[] = {
        0x3a, // File Size
        0x00, // Unused
        0x36  // Byte offset of pixel data
    };

    FILE *fp = fopen("test.bmp", "w+");
    fwrite(&tag, sizeof(tag), 1, fp);
    fwrite(&header, sizeof(header), 1, fp); // Write the header to disk
    fclose(fp);
}

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:

#include <stdio.h>
#include <stdlib.h>
int main() {

    int width = 1;           // Keep these in variables, since
    int height = 1;          // they'll change later.

    char tag[] = { 'B', 'M' };
    int header[] = {
        0x3a, 0x00, 0x36,
        0x28,                // Header Size
        width, height,       // Image dimensions in pixels
        0x180001,            // 24 bits/pixel, 1 color plane
        0,                   // BI_RGB no compression
        0,                   // Pixel data size in bytes
        0x002e23, 0x002e23,  // Print resolution
        0, 0,                // No color palette
    };

    FILE *fp = fopen("test.bmp", "w+");
    fwrite(&tag, sizeof(tag), 1, fp);
    fwrite(&header, sizeof(header), 1, fp); 
    fclose(fp);
}

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.

#include <stdio.h>
#include <stdlib.h>
int main() {
    int width = 1; 
    int height = 1;
    char tag[] = { 'B', 'M' };
    int header[] = {
        0x3a, 0x00, 0x36, 0x28, width, height, 0x180001, 
        0, 0, 0x002e23, 0x002e23, 0, 0 
    };

    // Store as char array, because we want each value to take up
    // one byte.
    char bitmap[] = {
        0x35, // Blue
        0x41, // Green
        0xef, // Red
        0x00  // Padding
    };

    FILE *fp = fopen("test.bmp", "w+");
    fwrite(&tag, sizeof(tag), 1, fp);
    fwrite(&header, sizeof(header), 1, fp); 
    fwrite(&bitmap, sizeof(bitmap), 1, fp);
    fclose(fp);
}

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

A single red pixel

Yay! Let’s try and change the color:

char bitmap[] = {
    0xa4, // Blue
    0x55, // Green
    0x00, // Red
    0x00  // Padding
};

A single navy blue pixel

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 saves us from 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.

void write_bmp(char *filename, char rgb[], int length, int width) {

    // Calculate the image height from its width and the array length
    int height = (length / 3) / width;

    // The size of the pixel data. For now, use width + 1 to handle 
    // row padding.
    int bitmap_size =  3 * height * (width + 1);

    // The pixel data is now variable-length, so we need to use
    // malloc.
    char *bitmap = (char *) malloc(bitmap_size * sizeof(char));

    // Zeroing out the data will set all the pixels to black.
    for (int i = 0; i < bitmap_size; i++) bitmap[i] = 0;

    char tag[] = { 'B', 'M' };
    int header[] = {
        0,                   // File size... update at the end.
        0, 0x36, 0x28,
        width, height,       // Image dimensions in pixels

        0x180001, 0, 0, 0x002e23, 0x002e23, 0, 0,
    };
    // Update file size: just the sum of the sizes of the arrays
    // we write to disk.
    header[0] = sizeof(tag) + sizeof(header) + bitmap_size;

    FILE *fp = fopen(filename, "w+");
    fwrite(&tag, sizeof(tag), 1, fp);
    fwrite(&header, sizeof(header), 1, fp);

    // Malloc returns a pointer, so we no longer need to get the
    // adress of bitmap
    fwrite(bitmap, bitmap_size * sizeof(char), 1, fp);
    fclose(fp);
    
    free(bitmap);
}

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 3×2 pixel image:

The french flag

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);
}

Run that program and we get:

An image with the aspect ratio of the above French flag, but entirely black

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.

// Function to round an int to a multiple of 4
int round4(int x) {
    return x % 4 == 0 ? x : x - x % 4 + 4;
}

void write_bmp(char *filename, char rgb[], int length, int width) {
    int height = (length / 3) / width;

    // Pad the width of the destination to a multiple of 4
    int padded_width = round4(width * 3);
    
    int bitmap_size = height * padded_width * 3;
    char *bitmap = (char *) malloc(bitmap_size * sizeof(char));
    for (int i = 0; i < bitmap_size; i++) bitmap[i] = 0;

    // For each pixel in the RGB image...
    for (int row = 0; row < height; row++) {
        for (int col = 0; col < width; col++) {
            
            // For R, G, and B...
            for (int color = 0; color < 3; color++) {

                // Get the index of the destination image
                int index = row * padded_width + col * 3 + color;

                // Set the destination to the value of the src at row, col.
                bitmap[index] = rgb[3*(row * width + col) + (2 - color)];
            }
        }
    }

    char tag[] = { 'B', 'M' };
    int header[] = {
        0, 0, 0x36, 0x28, width, height, 0x180001, 
        0, 0, 0x002e23, 0x002e23, 0, 0
    };
    header[0] = sizeof(tag) + sizeof(header) + bitmap_size;
    FILE *fp = fopen(filename, "w+");
    fwrite(&tag, sizeof(tag), 1, fp);
    fwrite(&header, sizeof(header), 1, fp);
    fwrite(bitmap, bitmap_size * sizeof(char), 1, fp);
    fclose(fp);
    free(bitmap);
}

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);

    // 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);
    
    // 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);

}

And there we have it:

The French flag The Belgian flag The trangender pride flag The flag of the United Arab Emirates

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.↩︎