Writing Images: Windows Bitmap
Last time we looked at the very simple Portable Anymap format to easily write images. But there is a catch with portable anymaps: they are a little but uncommon. While macOS and most Linux desktops support them even with thumbnails, there is no default support on Windows. Also, most webbrowsers don’t support them either. They are also a little bit unknown in non-development environments. This time we’ll look at a much more common file format: Windows Bitmap.
Windows bitmaps with their iconic file extension .bmp are undestood by almost every software that processes images. As you will see they are just a little bit more complicated than Portable Anymaps and thus easy to write and read. On the other side, BMPs have a lot of restrictions which limit their use. One limit is, that BMP cannot save any transparency information. Another problem is the size of BMPs because they normaly save uncompressed image data.
The Bitmap File Format
We won’t look at every detail of the bitmap file format but focus on the important features and properties so that we can write our own uncompressed bitmaps easily and fast. If you want are more indepth look at this file format I recommend the Wikipedia article.
The bitmap file format is a completely binary file format. This isn’t bad at all, in fact, while it is a little more inconvienient for humans, it makes the data size smaller and its format is exactly descibed. You will see that there is no black magic required to write such binary bitmap files. There is just one thing you need to be aware: the byte order. I’ve already written an article about that topic. Windows bitmaps always save their multibyte data as little endian.
Header
A Bitmap starts with two headers, one that describes the file itself (called the BITMAPFILEHEADER in the documentation) and one that describes the saved image (the BITMAPINFOHEADER). You always need to write both, but technicaly it doesn’t make a difference if there were one big header or two smaller ones.
In the next two subsections I present you the members of these headers. It may look a bit overwhelming at first, but stay with me. You will see that many of them are always constant or are easily filled. If you don’t understand everything at first you can also jump directly to the code, it will help you understand everything.
One last word about the data type informations in these tables. These types are from the Microsoft documentation and are commonly used in Windows code. They are in fact just different names for common datatypes. To write bitmaps, you only need to understand what they represent. Here is a short table:
Type | Description |
---|---|
WORD | 2 byte / 16 bit unsigned integer |
DWORD | 4 byte / 32 bit unsigned integer, DWORD means DOUBLE WORD |
LONG | 4 byte / 32 bit signed integer |
BITMAPFILEHEADER
This header is the first thing found in a bitmap file.
Name | Data type | Size | Short description |
---|---|---|---|
bfType | WORD | 2 bytes | This is always "BM" |
bfSize | DWORD | 4 bytes | The filesize in bytes |
bfReserved1 | WORD | 2 bytes | Reserved and unused, must be 0, but some software uses it regardless |
bfReserved2 | WORD | 2 bytes | Reserved and unused, must be 0, but some software uses it regardless |
bfOffBits | DWORD | 4 bytes | Offset to the image data from the beginning of the file |
These names are directly from the Microsoft bitmap documentation. It is linked at the Format specifications and Links section.
BITMAPINFOHEADER
Directly after the BITMAPFILEHEADER follows this header.
Name | Data type | Size | Description |
---|---|---|---|
biSize | DWORD | 4 bytes | size of this header, here we can use the constant value 40 |
biWidth | LONG | 4 bytes | width of the image |
biHeight | LONG | 4 bytes | height of the image, but this value can be negative, I come to this later |
biPlanes | WORD | 2 bytes | not used anymore, must be set to 1 |
biBitCount | WORD | 2 bytes | bits per pixel, can be 1, 4, 8, 16, 24 or 32 |
biCompression | DWORD | 4 bytes | which compression is used, can be 0, 1, 2 or 3 |
biSizeImage | DWORD | 4 bytes | the size of the image data in byte, can be 0 if no compression is used |
biXPelsPerMeter | LONG | 4 bytes | horizontal resolution in pixels per meter, often just set to 0 |
biYPelsPerMeter | LONG | 4 bytes | vertical resolution in pixels per meter, often just set to 0 |
biClrUsed | DWORD | 4 bytes | if no color table is used, this can be set to 0 |
biClrImportant | DWORD | 4 bytes | if no color table is used, this can be set to 0 |
In this header are some things that need to be addressed in greater detail. Let’s start with the most wierd thing: The height can be negative. If the height is positive, the image data is expected to be written from bottom to top. I don’t know why this was decided, but it seems that this is the most common form bitmaps are saved. Some of the compression options are also only available, if this direction is choosen. If the height is negative the image is expected to be written in the IMHO more natural top to bottom direction.
Now we look at the biBitCount
value. It describes how many bits are expected for a single pixel. The values 1, 4 and 8 are
for indexed colors. This means we have a color table where we define all colors that can appear in an image. We then only save
an index of an color in this table as the actual image data. We won’t look into the benefits and problems with this approch
today, as we only look at 24 bits per pixel. This is the most simple way to write images because every channel (red, green and blue) get a full byte per pixel (3 * 8 bit).
Data
And now the best thing: as we save bitmaps uncompressed we can easily write out our pixel data. A pixel with only 0 bytes represents black and only 255 bytes represents white. But there are two catches: bitmaps save BGR (blue-green-red) data, not RGB (red-green-blue). Today you probably save your images in memory in RGB so you need to inverse the byte order before putting out a pixel.
The second catch is the already described order of rows, from bottom to top. If your image data is already ordered this way, you can easily dump them out to a file. But if you save them from top to bottom, you need to reverse them while writing or flip them before. Flipping beforehand may have some performance benefits by utilising the processor cache lines better, at least if implemented correctly. But in this article we just flip the order while writing it, even if it may not be the best performing solution. My focus in this post is more on simplicity than performance.
Implementation
As I did with the portable anymap I show you now a simple implementation of a windows bitmap writer in C.
#include <stdio.h>
#include <stdint.h>
#define WIDTH 300
#define HEIGHT 200
// This changes the padding behaviour of the
// compiler. I come to this later.
#pragma pack(push, 1)
typedef struct {
uint16_t bfType;
uint32_t bfSize;
uint16_t bfReserved1;
uint16_t bfReserved2;
uint32_t bfOffBits;
} BitmapFileHeader;
typedef struct {
uint32_t biSize;
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
int32_t biXPelsPerMeter;
int32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
} BitmapInfoHeader;
// This restores the padding behaviour.
#pragma pack(pop)
int main()
{
FILE* outfile = fopen("out.bmp", "wb");
// Both headers have a combined size of 54 bytes.
// We need WIDTH * HEIGHT pixels and each of them needs
// three bytes (one for each channel).
uint32_t filesize = 54 + WIDTH * HEIGHT * 3;
// First we fill the file header.
BitmapFileHeader fileHeader;
fileHeader.bfType = 'B' | 'M' << 8; // reversed because of Little Endian
fileHeader.bfSize = filesize;
fileHeader.bfReserved1 = 0;
fileHeader.bfReserved2 = 0;
fileHeader.bfOffBits = 54; // both headers have a size of 54 byte
// Next we fill the info header.
BitmapInfoHeader infoHeader;
infoHeader.biSize = 40; // this header is 40 bytes long
infoHeader.biWidth = WIDTH;
infoHeader.biHeight = HEIGHT;
infoHeader.biPlanes = 1; // must be 1
infoHeader.biBitCount = 24; // 24 bit per pixel = 1 byte per channel
infoHeader.biCompression = 0; // no compression
infoHeader.biSizeImage = 0; // 0 because we don't use compression
infoHeader.biXPelsPerMeter = 0;
infoHeader.biYPelsPerMeter = 0;
infoHeader.biClrUsed = 0;
infoHeader.biClrImportant = 0;
// Then we write them both to the file.
// This only works because of the padding changes
// and ignores endianess. Normaly you should NOT
// do this, but this simplifices this example.
fwrite(&fileHeader, sizeof(BitmapFileHeader), 1, outfile);
fwrite(&infoHeader, sizeof(BitmapInfoHeader), 1, outfile);
// We write the image from bottom to top.
// In this example the image data is created
// procedurally, so we really don't need to do it
// this way. But if you would read the image data
// from an array, you could use the following loops:
for (int y = HEIGHT - 1; y >= 0; y--)
{
for (int x = 0; x < WIDTH; x++)
{
// Image data creation:
int r = (x / (float)WIDTH) * 255;
int g = (y / (float)WIDTH) * 255;
int b = (y / (float)HEIGHT) * 255;
// We write a single pixel in BGR order
unsigned char colors[3] = {b, g, r};
fwrite(colors, 3, 1, outfile);
}
}
fclose(outfile);
return 0;
}
This example outputs the following image: I actually converted the original BMP to PNG for a smaller size and because my site is not configured serve BMP files.
You might have noticed I used types like uint16_t from stdint.h
instead of WORD, DWORD, LONG.
This makes this code more portable as the other types are Windows specific. If you already include
windows.h
, you can easily use those types instead. Actually, you might use BITMAPFILEHEADER and
BITMAPINFOHEADER directly.
Because this is only an example how to write a bitmap file, I used code to create an image procedurally. And in this case the orientation of the image doen’t really matter. Normaly it would, so for demonstration purposes my Y-loop writes the data from bottom to top, as you would with an actual image, maybe read from an array oder rendered via raytracing on the fly. I hope this clarifies the writing procedure.
Last but not least I need to talk about padding, packed structs and why this example is not very portable.
Maybe you’ve already noticed the #pragma pack(...)
directives in this example. The reason they exists is,
that C structs may have something called a padding. Simply put, a padding is introduced in a struct to align
the members of a struct to adresses that are dividable by some factor. For example a 2 byte uint16t is aligned
to an even address while a 4 byte uint32t is aligned to an address divisible by 4. If a 2 byte member in a struct
is followed by a 4 byte member, a 2 byte padding is introduced, so that the second member is correctly aligned.
Excactly this happens with the BITMAPFILEHEADER in the example code between bfType
and bfSize
. This causes
the struct to have a size of 16 instead of 14 bytes. This will corrupt our header so we can’t read our image.
Padding is normaly useful, because it allows optimised memory access. But if we wan’t to write a struct directly
to a file, it causes havok.
The solution to this is using a packed struct. A packed struct does not contain any padding, so we can write
it out without problems. Here we come to the first problem: There is no standarised way to declare a packed
struct. I choosed the #pragma pack(...)
directive, because it seems to have been adopted by many compilers.
The #pragma pack(push, 1)
directive saves the current packing status on a stack and also sets it to 1, which
means that we disable padding completly. The #pragma pack(pop)
restores the old status.
There are a lot of problems with this solution:
- I’ve already written that it is not standarised. If your compiler doesn’t support it, your code wouldn’t work.
- Accessing struct membery that are not aligned may be slow. This isn’t that important if you just use this struct to save one image file, but if you use the same struct for some calculations it may hurt your performance.
- It is also heavily coupled to the x86 processor architecture.
The x86 architecture is really common in personal computers, but it is by far not the only architecture out there. Padding and alignment are specifics of the processor architecture. Some architectures even require some sort of alignment, or you can’t access the membery. Also we ignored the endianness completly, because x86 uses little endian, as the BMP file format do. But on big endian systems all multibyte members need to be swapped.
Using the solution presented in the example really helped to keep the code short and understandable. It is also okay for small experiments or demos. But for anything just a little bit more important you shouldn’t do it that way. Most of the time it is better to output each member of the struct seperatly (preferably with some buffering). This way you have a lot of repetitve code but on the other hand you don’t need to deal with padding or alignment and also you can deal with endianess on each struct member as needed.
Format specifications and Links
- Wikipedia
- Microsoft Docs: BITMAPFILEHEADER
- Microsoft Docs: BITMAPINFOHEADER
- Sourcecode on GitHub: GitHub Gist