DEV Community

Cover image for C++: How to Write a Bitmap Image from Scratch.
M Muiz Hassan
M Muiz Hassan

Posted on • Edited on

C++: How to Write a Bitmap Image from Scratch.

Image processing is a vast subject covering simple filtering to complex object detection. For those of you who want to go in depth and learn how an image is stored in the computer, this is the right place to start.

In this tutorial we'll be seeing how to write a simple cyan colored bitmap image.

  • Preview:

Preview

The approach used in this tutorial is only compatible with systems that process data in little-endian byte format (used in most Intel and AMD machines).

Prerequisites:

  • Basic knowledge of C++

I. Introduction to Binary File Formats

A binary file is a collection of bits i.e. 1s and 0s, stored in a file. In most binary file formats information is stored byte by byte, byte is a collection of 8 bits. The bytes are arranged in an orderly fashion to store information. Most binary files have two major portions:

  • Header

Header in a binary file contains meta data (additional information about the data, for example width and height of an image).

  • Body

The body of a binary file contains actual data. In case of a bitmap file this is the information about each pixel in the image.

II. What is a Bitmap File?

A bitmap file basically consists of a header of variable size, however most commonly used header is of 54 bytes. In this tutorial we'll be writing a 24 bit Bitmap image. The body of a 24 bit bitmap image contains value for each pixel. Each pixel consists of 3 color channels in BGR (blue, green and red) sequence, where each channel is of 1 byte. Therefore each pixel in a 24 bit bitmap file is of 3 bytes (or 24 bits).
(For detailed specifications of the bitmap file format check: Bitmap-Specifications)

III. Code

(For Complete Example Code: Click Here)

  • Including Standard Libraries:


#include <cstdint> // for specific size integers
#include <fstream> // for file handling
using namespace std;


Enter fullscreen mode Exit fullscreen mode
  • Creating the Header:

The Bitmap file has two headers for meta data. First header (bitmap file header) tells that this binary file is a bitmap file. The next header (bitmap info header) contains additional meta-data about the image.
For bitmap file header (14 bytes) we need to create the following data structure:



struct BmpHeader {
    char bitmapSignatureBytes[2] = {'B', 'M'};
    uint32_t sizeOfBitmapFile = 54 + 786432;
    uint32_t reservedBytes = 0;
    uint32_t pixelDataOffset = 54;
} bmpHeader;


Enter fullscreen mode Exit fullscreen mode

The first two bytes 'B' and 'M' are unique to the bitmap file format (there still are different versions of it check bitmap format specification). The next four bytes contain total size of the bitmap file in bytes. As we are creating a 512 by 512 bitmap image, total size is the sum of size of meta data (14 + 40 = 54 bytes) and size of pixels (512 * 512 * 3 = 7863432, each pixel is of 3 bytes in 24 bit bitmap file). The last field of bitmap file header contains the offset to the pixel data of the image from the start of the file. In our case the offset is 54 bytes, as our pixel data is right after the bitmap file header and info header (14 + 40 = 54).

The bitmap file has various types of info header, but the most common one is the 40 byte Bitmap Info Header, so we need to write it down in the following data structure:



struct BmpInfoHeader {
    uint32_t sizeOfThisHeader = 40;
    int32_t width = 512; // in pixels
    int32_t height = 512; // in pixels
    uint16_t numberOfColorPlanes = 1; // must be 1
    uint16_t colorDepth = 24;
    uint32_t compressionMethod = 0;
    uint32_t rawBitmapDataSize = 0; // generally ignored
    int32_t horizontalResolution = 3780; // in pixel per meter
    int32_t verticalResolution = 3780; // in pixel per meter
    uint32_t colorTableEntries = 0;
    uint32_t importantColors = 0;
} bmpInfoHeader;


Enter fullscreen mode Exit fullscreen mode

The height of bitmap file can also be negative (you may have noticed the signed integers). When height is negative the first pixel in file is drawn at top left of the image. However the standard for bitmap files is to use positive height and the first pixel in file is drawn at the bottom left of the image followed by other pixels.

The bitmap file may also contain a color table but it is not mandatory in a 24 bit bitmap file, so we will not be creating one in this example.

  • Creating pixel:

As I'll be creating a mono color bitmap image (all pixels have same value), I'm creating an additional struct for the pixel:



struct Pixel {
    uint8_t blue = 255;
    uint8_t green = 255;
    uint8_t red = 0;
} pixel;


Enter fullscreen mode Exit fullscreen mode

Each channel in a pixel must be an unsigned integer of 1 byte for a 24 bit bitmap file. Where 255 specifies maximum color intensity and 0 specifies absence of the color.

  • Main function:


int main(int argc, char **argv) {
    ofstream fout("output.bmp", ios::binary);

    fout.write((char *) &bmpHeader, 14);
    fout.write((char *) &bmpInfoHeader, 40);

    // writing pixel data
    size_t numberOfPixels = bmpInfoHeader.width * bmpInfoHeader.height;
    for (int i = 0; i < numberOfPixels; i++) {
        fout.write((char *) &pixel, 3);
    }
    fout.close();

    return 0;
}


Enter fullscreen mode Exit fullscreen mode

A struct may have padding (additional bytes) added by the compiler for optimizing performance. Hence sizeof() is not going to return accurate value. So I have manually specified the size of each struct in ofstream::write() method.

IV. Conclusion

So if you've succeeded in writing your first bitmap file give yourself a pat on the shoulder. Next you may be interested in writing different types of bitmap files, for that check bitmap specifications. Kudos and keep learning!

Top comments (1)

Collapse
 
gscacco profile image
Gianluca Scacco

First of all, thank you for this article.
I would like to report an error on how you write the structures in the file. You are right: the structs my have paddings (often !). But specifying the size manually doesn't solve the problem if you write the whole struct with one write.

On my laptop with a 64 bit linux operating system, the real size of bmpHeader is 16 bytes. If you write only 14 bytes on disk two significant bytes of data were lost and the saved struct is corrupted. In general you can't rely on the size and the position of the fields of a struct in memory.

This sort of problem is solved, usually, serializing data on disk. The following code works for me:

void BmpHeader::save_on_file(std::ofstream& fout) {
    fout.write(this->bitmapSignatureBytes, 2);
    fout.write((char*)&this->sizeOfBitmapFile, sizeof(uint32_t));
    fout.write((char*)&this->reservedBytes, sizeof(uint32_t));
    fout.write((char*)&this->pixelDataOffset, sizeof(uint32_t));
  }
Enter fullscreen mode Exit fullscreen mode
void BmpInfoHeader::save_on_file(std::ofstream& fout) {
    fout.write((char*)&this->sizeOfThisHeader, sizeof(uint32_t));
    fout.write((char*)&this->width, sizeof(int32_t));
    fout.write((char*)&this->height, sizeof(int32_t));
    fout.write((char*)&this->numberOfColorPlanes, sizeof(uint16_t));
    fout.write((char*)&this->colorDepth, sizeof(uint16_t));
    fout.write((char*)&this->compressionMethod, sizeof(uint32_t));
    fout.write((char*)&this->rawBitmapDataSize, sizeof(uint32_t));
    fout.write((char*)&this->horizontalResolution, sizeof(int32_t));
    fout.write((char*)&this->verticalResolution, sizeof(int32_t));
    fout.write((char*)&this->colorTableEntries, sizeof(uint32_t));
    fout.write((char*)&this->importantColors, sizeof(uint32_t));
  }
Enter fullscreen mode Exit fullscreen mode
void Pixel::save_on_file(std::ofstream& fout) {
    fout.write((char*)&this->blue, sizeof(uint8_t));
    fout.write((char*)&this->green, sizeof(uint8_t));
    fout.write((char*)&this->red, sizeof(uint8_t));
  }
Enter fullscreen mode Exit fullscreen mode