In this article, I will show you how to implement a BMP image loader from scratch in C++. BMP is one of the oldest image formats on the Windows platform and it is supported on most other operating systems. BMP can store two-dimensional raster images with optional compression and transparency. In this article, we will implement a simplified version of the BMP format specification that will support only 24 and 32 bits depth images in the BGR and BGRA color spaces. To make our life simpler we can also ignore the, optional, compression component.
Even if you don’t plan to use BMP images, it is still a useful programming exercise to write a BMP reader/writer in C++.
From a programming point of view, a BMP file is a binary file in the little-endian format. For our purposes, we can divide a BMP image in four regions:
file header - all BMP images starts with a five elements file header. This has information about the file type, file size and location of the pixel data.
bitmap header - also named the info header. This has information about the width/height of the image, bits depth and so on.
color header - contains informations about the color space and bit masks
pixel data.
It is probably easier to show you directly the code for the file header:
The last field of the above structure represents the position, in bytes, from the start of the file to where the pixel data is stored.
If you want to read the header using the above structure, you’ll need to keep in mind that a compiler is free to add padding to a struct to align the data for a particular machine. With most modern C++ compilers (e.g. GCC, Clang, MSVC, Intel) you can use the next pragma pack syntax to ask for a specific alignment:
Without pragma pack the above struct takes 16 bytes on my machine, with the pragma pack instructions, same struct takes 14 bytes. You can obviously chose to read every field of the struct separately and avoid the padding problem, but it is more cumbersome and error prone.
The second region of a BMP file can be described by the next structure:
From the above we need to consider only the width, height, bit_count and compression. The compression is set to 0 for images with 24 bits per pixel and 3 for images with 32 bits per pixel.
The third region of can be described by:
The color masks are initialized to BGRA format and are only used for images with transparency (32 bits depth in our case).
A peculiarity of the BMP image format is that, if the height is negative, you have the origin of the image in the top left corner. If the height is a positive number, the origin of the image is at the bottom left corner. For simplicity, we will consider only the case when the image height is a positive number and the origin is always in the bottom left corner.
The BMP image format expects every row of data to be aligned to a four bytes boundary or padded with zero if this is not the case. For a 32 bits per pixel image the alignment condition is always satisfied. In the case of a 24 bits per pixel images, the alignment is satisfied only if the image width is divisible by 4, otherwise we’ll need to pad the rows with zeros.
Using the above two structs we can define a new BMP struct that can read/write a BMP image from disk, create a BMP object in memory, modify the pixel data and so on …
A possible implementation could look like this:
Suppose that we want to be able to read, write and directly modify the pixel data of a BMP image, something like in the next code:
Please note that bmp3 from the above code has a width of 209 pixel, which means it will need padding with zeros to align the rows to a 4 bytes boundary.
For testing purposes we’ll use a few images Shapes.bmp, Shapes-24.bmp, t1-24.bmp, t2-24.bmp please use the above links if you want to download the images in BMP format. The first image looks like this:
We’ll start by writing the code that loads the image from disk. Please note, that this is not intended to read allBMP image variations. It will work only with 32 or 24 bits per pixel, uncompressed, images in the format BGRA or BGR. I’ve tested the code with BMP images generated with GIMP, Paint.NET and Microsoft Paint.
In order to read the image, we need to open the image as a binary file, read the headers, use the image size information to resize the data vector and finally read the pixel data. In case the data was padded with zeros we need to read these too and discard the padding data. Some editors will add extra information in the file that we can safely ignore, we just need to adjust header and file size for this. This is necessary in case the user decides to save the processed image.
For saving the image to disk we consider only the 24 and 32 bits per pixel case.
In the 24 bits per pixel case, if the width is divisible by 4, we write the data just like for the 32 bits per pixel case. If the width is not divisible by 4, we increase the row stride, by adding 1 repeatedly, until it is divisible by 4. We fill a padding vector with zeros that will be used at the end of each line. We modify the bitmap headers to take into account the new file size and write the headers like in the previous cases. The data vector is written one row at a time: we write a row, we write the padding data, we write the next row and so on …
Here is the code for writing the image to disk:
At this point, you can read and write a BMP file.
Next, we can write the code for creating a BMP image in memory. For this, like before, we consider only images with 24 and 32 bits per pixel. By default, the image will have 32 bits per pixel, unless the user passes false to the has_alpha parameter. The constructor needs to set the width and height for the image, the header sizes, the file size, the offset data (the position at which the pixel data is written in the file), the bits per pixel count, the compression type and resize the data vector to accommodate the image size:
The only part that remains to be implemented is the modify pixel data part. A quick and dirty approach, without error checking, is to fill a rectangular region from the image with a particular color. For example, we could write:
The above will fill with Red a rectangular region from bmp2.
A better idea is to refactor the above code into a member function of the BMP struct:
Now, we can rewrite the main function:
You can find the complete source code on the GitHub repository for this article.
If you are interested to learn more about modern C++ I would recommend reading A tour of C++ by Bjarne Stroustrup.