NHollmann

English | German

Hi, I'm Nicolas Hollmann and this is my personal site and blog.
Happy reading! 📖

Writing Images: Windows Bitmap

March 14, 202111 min read
Tags:

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.

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: Bitmap sample 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:

  1. I’ve already written that it is not standarised. If your compiler doesn’t support it, your code wouldn’t work.
  2. 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.
  3. 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.



© 2022, Nicolas Hollmann