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:
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 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
sizeof(tag), 1, fp); // Write the tag to the file
fwrite(&tag, // Close the file
fclose(fp); }
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:
- 4 bytes The size of the BMP file in bytes
- 4 bytes Reserved; actual value depends on the application that creates the image
- 4 bytes The offset, i.e. starting address, of the byte where the bitmap image data (pixel array) can be found.
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+");
sizeof(tag), 1, fp);
fwrite(&tag, sizeof(header), 1, fp); // Write the header to disk
fwrite(&header,
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
-
4 bytes - The size of this header, in bytes:
28 00 00 00
.- This is 40 bytes in hexadecimal. Our header will remain the same length.
- 4 bytes - The bitmap width in pixels:
01 00 00 00
-
4 bytes - The bitmap height in pixels:
01 00 00 00
.- Instead of copying directly, we should output
width
andheight
variables.
- Instead of copying directly, we should output
- 2 bytes - The number of color planes (must be 1):
01 00
-
2 bytes - the number of bits per pixel:
18 00
- We will combine these into a single integer so that we can store it in the same array.
-
4 bytes - The compression method being used:
00 00 00 00
- 0 means no compression
-
4 bytes - The image size (in bytes):
04 00 00 00
- This is actually ignored for images without compression, so we can just use 0.
- 4 bytes - The horizontal resolution of the image:
23 2e 00 00
-
4 bytes - The vertical resolution of the image:
23 2e 00 00
- As far as I can tell, these only matter for printing. We’ll just use what Gimp gave us.
- 4 bytes - the number of colors in the color palette, or 0 to default
to 2n:
00 00 00 00
- 4 bytes - the number of important colors used, or 0 when every color
is important; generally ignored:
00 00 00 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
// Image dimensions in pixels
width, height, 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+");
sizeof(tag), 1, fp);
fwrite(&tag, sizeof(header), 1, fp);
fwrite(&header,
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+");
sizeof(tag), 1, fp);
fwrite(&tag, sizeof(header), 1, fp);
fwrite(&header, sizeof(bitmap), 1, fp);
fwrite(&bitmap,
fclose(fp); }
Alright, now let’s run the program and see what we get:
Yay! Let’s try and change the color:
char bitmap[] = {
0xa4, // Blue
0x55, // Green
0x00, // Red
0x00 // Padding
};
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,
// Image dimensions in pixels
width, height,
0x180001, 0, 0, 0x002e23, 0x002e23, 0, 0,
};// Update file size: just the sum of the sizes of the arrays
// we write to disk.
0] = sizeof(tag) + sizeof(header) + bitmap_size;
header[
FILE *fp = fopen(filename, "w+");
sizeof(tag), 1, fp);
fwrite(&tag, sizeof(header), 1, fp);
fwrite(&header,
// Malloc returns a pointer, so we no longer need to get the
// adress of bitmap
sizeof(char), 1, fp);
fwrite(bitmap, bitmap_size *
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:
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,
};"french_flag.bmp", fr, sizeof(fr) / sizeof(char), 3);
write_bmp( }
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.
// 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.
3*(row * width + col) + (2 - color)];
bitmap[index] = rgb[
}
}
}
char tag[] = { 'B', 'M' };
int header[] = {
0, 0, 0x36, 0x28, width, height, 0x180001,
0, 0, 0x002e23, 0x002e23, 0, 0
};0] = sizeof(tag) + sizeof(header) + bitmap_size;
header[FILE *fp = fopen(filename, "w+");
sizeof(tag), 1, fp);
fwrite(&tag, sizeof(header), 1, fp);
fwrite(&header, sizeof(char), 1, fp);
fwrite(bitmap, bitmap_size *
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,
};"french_flag.bmp", fr, sizeof(fr) / sizeof(char), 3);
write_bmp(
char bgm[] = {
0, 0, 0, // Black
253, 218, 36, // Yellow
239, 51, 64, // Red
0, 0, 0,
253, 218, 36,
239, 51, 64,
};"belgian_flag.bmp", bgm, sizeof(bgm) / sizeof(char), 3);
write_bmp(
// 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) {
3 * (row * 8 + col)] = 91;
trans[3 * (row * 8 + col) + 1] = 207;
trans[3 * (row * 8 + col) + 2] = 250;
trans[else if (row == 1 || row == 3) {
} 3 * (row * 8 + col)] = 245;
trans[3 * (row * 8 + col) + 1] = 171;
trans[3 * (row * 8 + col) + 2] = 185;
trans[else {
} 3 * (row * 8 + col)] = 255;
trans[3 * (row * 8 + col) + 1] = 255;
trans[3 * (row * 8 + col) + 2] = 255;
trans[
}
}
}"trans_flag.bmp", trans, 3*5*8, 8);
write_bmp(
// 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) {
3 * (row * 12 + col)] = 255;
uae[3 * (row * 12 + col) + 1] = 0;
uae[3 * (row * 12 + col) + 2] = 0;
uae[else if (row < 2) {
} 3 * (row * 12 + col)] = 0;
uae[3 * (row * 12 + col) + 1] = 116;
uae[3 * (row * 12 + col) + 2] = 33;
uae[else if (row < 4) {
} 3 * (row * 12 + col)] = 255;
uae[3 * (row * 12 + col) + 1] = 255;
uae[3 * (row * 12 + col) + 2] = 255;
uae[else {
} 3 * (row * 12 + col)] = 0;
uae[3 * (row * 12 + col) + 1] = 0;
uae[3 * (row * 12 + col) + 2] = 0;
uae[
}
}
}"uae_flag.bmp", uae, 3*6*12, 12);
write_bmp(
}
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.
-
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.↩︎
-
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.↩︎