Homework 3 Notes

1. Instructions

This assignment is Part 1 of a two-part assignment. We are going to allow our vector graphics library to read and write Windows bitmap images. It is independent from the previous two assignments. We will not touch any of the preexisting code (i.e., VG and Xml namespaces). Instead, we will create two new namespaces and implement the following classes (and unit tests as needed):

  • Namespace Binary: Wrapper classes for unsigned integers.

    • Binary::Byte: A 8-bit unsigned integer.
    • Binary::Word: A 16-bit unsigned integer.
    • Binary::DoubleWord: A 32-bit unsigned integer.
  • Namespace BitmapGraphics: Classes for reading and writing Windows bitmap images.

    • BitmapGraphics::WindowsBitmapHeader: The header of a Windows bitmap image.
    • BitmapGraphics::BitmapIterator: An iterator for reading and writing Windows bitmap images.
    • BitmapGraphics::Bitmap: A 24-bit bitmap image.
    • BitmapGraphics::Color: A 24-bit color.

hw3_uml

The details of these classes are described in the following sections.


2. Introduction to Windows Bitmap File Format

2.1 What is Windows Bitmap (BMP)?

Windows Bitmap File, commonly referred to as BMP, is a raster graphics image file format used to store bitmap digital images. It was originally developed by Microsoft for use on the Windows platform but is now recognized by many programs on both Macs and PCs.

2.2 Structure of a BMP file

A BMP file consists of:

  1. File Header: Information about the file itself, the size of the file, and the offset to where the image data starts. This will be implemented with the WindowsBitmapHeader class.
  2. Info Header: It contains information about the image, like its height, width, the number of color planes, bits per pixel, compression method used, the size of the image data, etc. This is also implemented with the WindowsBitmapHeader class.
  3. Color Palette: It's used when the image has 256 colors or fewer. Each color in the palette is typically represented with three bytes (for RGB). We will not implement this.
  4. Pixel Data: This is the actual image data. This is implemented with the Bitmap class.

BMP used to be more popular in earlier days, especially on Windows systems, due to its simplicity and the fact that it was natively supported by Windows. Over time, with the advent of more advanced and efficient image formats, its usage has decreased, especially on the web and in applications where file size and compression matter. However, due to its simplicity, we will use it as a learning exercise.

2.4 Pros and Cons

Pros:

  1. Simplicity: The BMP format is straightforward and easy to understand, which makes it relatively simple to write programs that can read or write BMPs.
  2. Lossless: BMP is a lossless format, meaning it doesn't lose any image quality.
  3. Universal Support: Almost all image editing software and many operating systems support BMP.

Cons:

  1. File Size: BMP files can be much larger than their JPEG or PNG counterparts because they're often uncompressed. This makes them unsuitable for web use, where download speed and bandwidth are concerns.
  2. Lacks Advanced Features: BMP doesn't support features like transparency (like PNG) or different compression levels (like JPEG).
  3. No Metadata Support: Unlike formats like JPEG or PNG, BMP doesn't have a standard way of embedding metadata (like camera details, GPS coordinates, etc.).

3. Implementing the Binary Namespace

We will implement three binary classes. They need to support the following operations:

  • Constructor: By default, the value is 0.
  • Type Casting: For example, Byte can be cast to uint8_t.
  • Assignment: For example, Byte and uint8_t can be assigned to another Byte.
  • Stream Read/Write: For example, Byte can be written to an output stream, and Byte can be read from an input stream. This will be more complicated for Word and DoubleWord because of endianness.

3.1 Byte

Take a look at Byte.h:

#pragma once

#include <cstdint>
#include <iostream>

namespace Binary
{

    class Byte
    {
    public:
        Byte(uint8_t val = 0);

        operator uint8_t() const;

        Byte &operator=(const Byte &other);
        Byte &operator=(uint8_t val);

        static Byte read(std::istream &sourceStream);
        void write(std::ostream &destinationStream) const;

    private:
        uint8_t value;
    };

}

Note a few things:

Type Casting

It simply returns the value member variable:

Byte::operator uint8_t() const
{
    return value;
}

Example usage:

Binary::Byte b{ 0x12 };
uint8_t u1 = b;  // implicit type casting
uint8_t u2 = static_cast<uint8_t>(b);  // explicit type casting

In both implicit and explicit type casting, the compiler sees that we're trying to assign a Byte object to a uint8_t variable, and it looks for a way to make this work. Since we've defined a conversion to uint8_t, the compiler uses that conversion.

Assignment

The two assignment operators are defined as follows:

Byte &Byte::operator=(const Byte &other)
{
    value = other.value;
    return *this;
}

Byte &Byte::operator=(uint8_t val)
{
    value = val;
    return *this;
}

Those two operators return the reference to the object itself, so that we can chain the assignment operators:

Binary::Byte byte1{0x12};
Binary::Byte byte2 = byte1 = 0x34;  // now both equals to '4'

Copy assignment is also supported:

Binary::Byte byte1{0x12};
Binary::Byte byte2 = byte1;
byte2 = '4';  // now only byte2 equals to '4'

Stream Read/Write

The read() and write() methods are implemented as follows:

Byte Byte::read(std::istream &sourceStream)
{
    uint8_t byteValue;
    sourceStream.read(reinterpret_cast<char *>(&byteValue), sizeof(byteValue));
    return Byte(byteValue);
}

void Byte::write(std::ostream &destinationStream) const
{
    destinationStream.write(reinterpret_cast<const char *>(&value), sizeof(value));
}

Note that we need to use reinterpret_cast to convert between uint8_t and char pointers. This is because std::istream::read() and std::ostream::write() take char pointers as arguments. This is a bit of a hack, but it's the only way to do it.

They are designed to read/write one byte from/to a stream. You can use them like this:

#include <sstream>

// read from a stream
std::stringstream ss{"abc"};
auto b = Binary::Byte::read(ss);  // now b equals to 'a'
b = Binary::Byte::read(ss);       // now b equals to 'b'
b = Binary::Byte::read(ss);       // now b equals to 'c'

// write to a stream
b = 'x';
std::stringstream ss2;
b.write(ss2);  // now ss2.str() equals to "x"

Here, note that std::stringstream inherits from std::istream and std::ostream. So it is a subclass of both std::istream and std::ostream. Therefore, it can be used as both an input stream and an output stream.

3.2 Word

Take a look at Word.h:

#pragma once

#include <cstdint>
#include <iostream>

namespace Binary
{

    class Word
    {
    public:
        Word(uint16_t val = 0);

        operator uint16_t() const;

        Word &operator=(const Word &other);
        Word &operator=(uint16_t val);

        static Word readLittleEndian(std::istream &sourceStream);
        void writeLittleEndian(std::ostream &destinationStream) const;

        static Word readBigEndian(std::istream &sourceStream);
        void writeBigEndian(std::ostream &destinationStream) const;

    private:
        uint16_t value;

        void writeNativeOrder(std::ostream &destinationStream) const;
        void writeSwappedOrder(std::ostream &destinationStream) const;
    };

}

The only difference between Word and Byte is that Word supports both little-endian and big-endian input formats.

The read methods are implemented as follows:

    Word Word::readLittleEndian(std::istream &sourceStream)
    {
        uint8_t byte1, byte2;
        sourceStream.read(reinterpret_cast<char *>(&byte1), sizeof(byte1));
        sourceStream.read(reinterpret_cast<char *>(&byte2), sizeof(byte2));

#ifdef Little_Endian_
        return Word(static_cast<uint16_t>(byte1) | (static_cast<uint16_t>(byte2) << 8));
#else
        return Word(static_cast<uint16_t>(byte2) | (static_cast<uint16_t>(byte1) << 8));
#endif
    }

    Word Word::readBigEndian(std::istream &sourceStream)
    {
        uint8_t byte1, byte2;
        sourceStream.read(reinterpret_cast<char *>(&byte1), sizeof(byte1));
        sourceStream.read(reinterpret_cast<char *>(&byte2), sizeof(byte2));

#ifdef Little_Endian_
        return Word(static_cast<uint16_t>(byte2) | (static_cast<uint16_t>(byte1) << 8));
#else
        return Word(static_cast<uint16_t>(byte1) | (static_cast<uint16_t>(byte2) << 8));
#endif
    }

Quesion: Notice that even inside the two different read methods, we have a conditional compilation based on the Little_Endian_ macro. Why do we need this macro when the two methods are already responsible for reading the correct endianness?

Answer: The BMP file we're reading can exhibit different endianness, determined by its header information. Consequently, the Word class (and DoubleWord by extension) must accommodate both formats. That is why there are readLittleEndian and readBigEndian methods. Just as importantly, when translating these bytes into coherent words and double words on the user's system, they must always match the user's native endianness. To handle this consistency, we employ the Little_Endian_ macro. So, to answer the question, the methods are responsible for reading in the correct endianness, but the macro is responsible for creating the Word and DoubleWord objects in the correct endianness.

The write methods are implemented as follows:

    void Word::writeLittleEndian(std::ostream &destinationStream) const
    {
#ifdef Little_Endian_
        writeNativeOrder(destinationStream);
#else
        writeSwappedOrder(destinationStream);
#endif
    }

    void Word::writeBigEndian(std::ostream &destinationStream) const
    {
#ifdef Little_Endian_
        writeSwappedOrder(destinationStream);
#else
        writeNativeOrder(destinationStream);
#endif
    }

Basically, writeNativeOrder means writing the bytes in the order they are stored in memory, and writeSwappedOrder means writing the bytes in the opposite order. Those two methods are implemented as follows:

    // Private helper methods
    void Word::writeNativeOrder(std::ostream &destinationStream) const
    {
        uint8_t byte1 = value & 0xFF;
        uint8_t byte2 = (value >> 8) & 0xFF;

        destinationStream.write(reinterpret_cast<const char *>(&byte1), sizeof(byte1));
        destinationStream.write(reinterpret_cast<const char *>(&byte2), sizeof(byte2));
    }

    void Word::writeSwappedOrder(std::ostream &destinationStream) const
    {
        uint8_t byte1 = (value >> 8) & 0xFF;
        uint8_t byte2 = value & 0xFF;

        destinationStream.write(reinterpret_cast<const char *>(&byte1), sizeof(byte1));
        destinationStream.write(reinterpret_cast<const char *>(&byte2), sizeof(byte2));
    }

Simple bit shifting and masking is used to extract the bytes from the value member variable.

3.3 DoubleWord

Basically the same as Word, but with more bit shifting.

3.4 Templatizing Binary Classes

The three binary classes are very similar. They only differ in the number of bits they store. We can use templates to reduce code duplication. Take a look at SizedWord.h:

#pragma once

#include <iostream>
#include <type_traits>

namespace Binary
{
    template<typename WordSize> class SizedWord
    {
    public:
        using data_type = WordSize;

        explicit constexpr SizedWord(WordSize value) : myData{ value } {}

        template<typename U = WordSize, typename T> explicit constexpr SizedWord(T value)
            : myData(static_cast<U>(static_cast<T>(value)))
        {
        }

        SizedWord() = default;
        SizedWord(const SizedWord& other) = default;
        SizedWord(SizedWord&& other) = default;
        ~SizedWord() = default;
        SizedWord& operator=(const SizedWord& other) = default;
        SizedWord& operator=(SizedWord&& other) = default;

        // Will only compile if WordSize is 1 byte, otherwise gives an error at the caller site.  
        template<typename U = WordSize, typename T = std::enable_if_t<sizeof(U) == 1>>
        void write(std::ostream& destinationStream) const
        {
            destinationStream.exceptions(std::ios::failbit | std::ios::badbit);
            destinationStream.put(static_cast<char>(myData));
        }

        template<typename U = WordSize, typename T = std::enable_if_t<sizeof(U) != 1>>
        void writeLittleEndian(std::ostream& destinationStream) const
        {
            //my stream will now throw an error of std::ios::failure if it can't read / write for some reason.
            destinationStream.exceptions(std::ios::failbit | std::ios::badbit);

            for (size_t byte = 0; byte != sizeof(WordSize); ++byte)
            {
                const auto c = static_cast<unsigned char>((myData >> (8 * byte)));
                destinationStream.put(c);
            }
        }

        template<typename U = WordSize, typename T = std::enable_if_t<sizeof(U) != 1>>
        void writeBigEndian(std::ostream& destinationStream) const
        {
            destinationStream.exceptions(std::ios::failbit | std::ios::badbit);

            for (auto byte = sizeof(WordSize); byte != 0; --byte)
            {
                const auto c = static_cast<unsigned char>((myData >> (8 * (byte - 1))));
                destinationStream.put(c);
            }
        }

        template<typename U = WordSize, typename = std::enable_if_t<sizeof(U) == 1>>
        static SizedWord read(std::istream& sourceStream)
        {
            sourceStream.exceptions(std::ios::failbit | std::ios::badbit);
            return SizedWord{ static_cast<WordSize>(sourceStream.get()) };
        }

        template<typename U = WordSize, typename = std::enable_if_t<sizeof(U) != 1>>
        static SizedWord readLittleEndian(std::istream& sourceStream)
        {
            sourceStream.exceptions(std::ios::failbit | std::ios::badbit);
            SizedWord word{ 0 };

            for (size_t byte = 0; byte != sizeof(U); ++byte)
            {
                char c = 0;
                sourceStream.get(c);
                word.myData |= static_cast<unsigned char>(c) << (8 * byte);
            }

            return word;
        }

        template<typename U = WordSize, typename = std::enable_if_t<sizeof(U) != 1>>
        static SizedWord readBigEndian(std::istream& sourceStream)
        {
            sourceStream.exceptions(std::ios::failbit | std::ios::badbit);
            SizedWord word{ 0 };

            for (auto byte = sizeof(U); byte != 0; --byte)
            {
                char c = 0;
                sourceStream.get(c);
                word.myData |= static_cast<unsigned char>(c) << (8 * (byte - 1));
            }

            return word;
        }

        bool operator==(const SizedWord& rhs) const noexcept { return myData == rhs.myData; }
        bool operator!=(const SizedWord& rhs) const noexcept { return !(operator==(rhs)); }

        explicit operator WordSize() const { return myData; }

        friend std::ostream& operator<<(std::ostream& os, const SizedWord& word)
        {
            os << word.myData;
            return os;
        }

    private:
        static_assert(std::is_integral_v<WordSize>&& std::is_unsigned_v<WordSize>, "Unsigned Integral Type required for template creation of base type");

        WordSize myData{};
    };
}

Then we can define replace the contents of Byte.h, Word.h, and DoubleWord.h with the following

// In Byte.h
namespace Binary
{
    using Byte = SizedWord<uint8_t>;
}

// In Word.h
namespace Binary
{
    using Word = SizedWord<uint16_t>;
}

// In DoubleWord.h
namespace Binary
{
    using DoubleWord = SizedWord<uint32_t>;
}

Several things to note:

  1. explicit and constexpr Usage:

    • explicit: The explicit keyword prevents implicit type conversions. In a class sensitive to data types like SizedWord, allowing implicit type conversion might cause unintentional bugs or misleading results. It ensures that if the user tries to initialize the class with a type different from WordSize, a compile-time error will be raised.
    • constexpr: It allows the constructor and other methods to be run at compile-time, providing an optimization opportunity. Especially for big images or frequent object creation, this can offer performance benefits.
  2. Complex Type Casting:

    • Usage Scenario: This constructor template<typename U = WordSize, typename T> explicit constexpr SizedWord(T value) would be used when you explicitly want to initialize SizedWord with a value of a different type.
    • Double Cast: The first static_cast<T>(value) seems redundant since value is already of type T. This could be an overly cautious practice and might not be necessary in this specific case.
  3. Default Constructors and Operators:

    • Some contructors and operators are set to default because the only member, myData, is trivially copiable. In this context, the default (compiler-generated) implementations of these constructors and assignment operators are sufficient and optimized.
  4. std::enable_if_t<sizeof(U) == 1>:

    • This helps avoid conflict with other methods that take in the same template parameter but are looking for a different size of U. It enables certain methods only when the condition is met, otherwise excludes them from overload resolution.
  5. destinationStream.exceptions() and .put Method:

    • .exceptions() sets up a condition "register" for the stream, where if certain bits are set (std::ios::failbit or std::ios::badbit), an exception will be thrown.
    • .put() is used to put a single character into the stream. You could use <<, but .put() is more explicit about writing a single character and avoids any format flags that << may obey.
  6. Bit-Shifting in word.myData |= ...:

    • The expression static_cast<unsigned char>(c) << (8 * byte) bit-shifts the character to the correct position in the 16 or 32-bit word. If WordSize is 32 bits, you'll loop 4 times, and the bit-shifting places each 8-bit byte into its correct position in the 32-bit WordSize.
  7. friend Shorthand Example:

    • Normally, we declare a friend function like this, then define it later:
class MyClass {
friend void someFunc(MyClass& obj);
// ...
};
void someFunc(MyClass& obj) { /*...*/ }
  • But in our code, it's done all inline:
friend std::ostream& operator<<(std::ostream& os, const SizedWord& word) {
    os << word.myData;
    return os;
}

This is just a convenient way of declaring the friend function and providing its implementation inline within the class definition. It's shorthand and does both jobs at once.

  1. #include <type_traits>:

    • This header is included for the use of std::enable_if_t and std::is_integral_v, std::is_unsigned_v. These are type traits that allow for compile-time type checking and method enabling or disabling based on those types.
  2. explicit operator WordSize():

    • The explicit keyword is used to prevent implicit conversions. This ensures that you can't accidentally use a SizedWord object where a WordSize is expected without making your intention clear. Even though there's no argument, it prevents cases like:
WordSize ws = mySizedWord;  // This will cause a compile-time error because of `explicit`.

WordSize ws = static_cast<WordSize>(mySizedWord);  // This is fine.
  1. static_assert(...) Usage:
    • This line checks at compile-time that the type WordSize you're using to instantiate the SizedWord template is both integral and unsigned. This is to ensure that the class is only used in a manner that the original author intended.
    • The static_assert is not within any function, so this check will happen at compile-time whenever you attempt to instantiate the SizedWord template. If the conditions are not met, a compile-time error will be generated, stopping the build.

4. Implementing the WindowsBitmapHeader Class

4.1 Introduction to Windows Bitmap Header

The header of a Windows bitmap image contains information about the file itself, and it is used to determine if the file is a Windows bitmap, etc. The following table shows the structure of the header:

Name Size and Values Description
File Header (14 bytes)
firstIdentifier Byte; Must be equal to B To detemrine if the file is a Windows bitmap
secondIdentifier Byte; Must be equal to M To detemrine if the file is a Windows bitmap
fileSize DoubleWord The actual size of the file in its entirety, in bytes
reserved DoubleWord; Must be equal to 0 Reserved for future use
rawImageByteOffset DoubleWord The offset, in bytes, from the beginning of the file to the beginning of the bitmap data. This will always equal to the size of header + any color table
Info Header (40 bytes)
infoHeaderSize DoubleWord; Must be equal to 40 For verification
bitmapWidth DoubleWord The width of the bitmap, in pixels
bitmapHeight DoubleWord The height of the bitmap, in pixels
numberOfPlanes Word; Must be equal to 1 Unused
bitsPerPixel Word; Can be one of [1, 4, 8, 16, 24] The number of bits per pixel
compressionType DoubleWord; The compression type used
compressedImageSize DoubleWord; 0 if N/A The size of the compressed image, in bytes
horizontalPixelsPerMeter DoubleWord The horizontal resolution of the image, in pixels per meter. Used for printing. Often ignored
verticalPixelsPerMeter DoubleWord The vertical resolution of the image, in pixels per meter. Used for printing. Often ignored
numberOfColors DoubleWord. 0 if N/A The number of colors in the color palette. If 0, then the number of colors is determined by the bits per pixel
numberOfImportantColors DoubleWord. 0 if All The number of important colors used. If 0, then all colors are important

The header will usually be followed by a color table, but since we are only supporting 24-bit bitmaps, we can ignore it.

4.2 WindowsBitmapHeader Class

Let's make a class to represent the header. Take a look at WindowsBitmapHeader.h:

#pragma once

#include "Byte.h"
#include "DoubleWord.h"
#include <istream>

namespace BitmapGraphics
{
    class WindowsBitmapHeader
    {
    public:
        explicit WindowsBitmapHeader(int width = 0, int height = 0);
        explicit WindowsBitmapHeader(std::istream& sourceStream);

        void readFileHeader(std::istream& sourceStream);
        void writeFileHeader(std::ostream& destinationStream) const;

        void readInfoHeader(std::istream& sourceStream);
        void writeInfoHeader(std::ostream& destinationStream) const;

        void read(std::istream& sourceStream);
        void write(std::ostream& destinationStream) const;

        Binary::DoubleWord getFileSize() const { return fileSize; }
        Binary::DoubleWord getBitmapWidth() const { return bitmapWidth; }
        Binary::DoubleWord getBitmapHeight() const { return bitmapHeight; }

    private:
        // file header
        const static Binary::Byte firstIdentifier;
        const static Binary::Byte secondIdentifier;
        Binary::DoubleWord fileSize;
        const static Binary::DoubleWord reserved;
        const static Binary::DoubleWord rawImageByteOffset;

        // info header
        const static Binary::DoubleWord infoHeaderBytes;
        Binary::DoubleWord bitmapWidth;
        Binary::DoubleWord bitmapHeight;
        const static Binary::Word numberOfPlanes;
        const static Binary::Word bitsPerPixel;
        const static Binary::DoubleWord compressionType;
        const static Binary::DoubleWord compressedImageSize;
        const static Binary::DoubleWord horizontalPixelsPerMeter;
        const static Binary::DoubleWord verticalPixelsPerMeter;
        const static Binary::DoubleWord numberOfColors;
        const static Binary::DoubleWord numberOfImportantColors;
    };
}

4.3 Attributes

The private member variables are file and info headers found in the table above. Most of them are marked const static because they are the same of all instances of our header class. We can define their values in the WindowsBitmapHeader.cpp file:

#include "WindowsBitmapHeader.h"
#include "VerifyEquality.h"

namespace BitmapGraphics
{
    // File header fields
    const Binary::Byte WindowsBitmapHeader::firstIdentifier{ 'B' };
    const Binary::Byte WindowsBitmapHeader::secondIdentifier{ 'M' };
    const Binary::DoubleWord WindowsBitmapHeader::reserved{ 0 };
    const Binary::DoubleWord WindowsBitmapHeader::rawImageByteOffset{ 54 };

    // Info header fields
    const Binary::DoubleWord WindowsBitmapHeader::infoHeaderBytes{ 40 };
    const Binary::Word WindowsBitmapHeader::numberOfPlanes{ 1 };
    const Binary::Word WindowsBitmapHeader::bitsPerPixel{ 24 };
    const Binary::DoubleWord WindowsBitmapHeader::compressionType{ 0 };
    // ... these we don't care about the values so just set to 0
    const Binary::DoubleWord WindowsBitmapHeader::compressedImageSize{ 0 };
    const Binary::DoubleWord WindowsBitmapHeader::horizontalPixelsPerMeter{ 0 };
    const Binary::DoubleWord WindowsBitmapHeader::verticalPixelsPerMeter{ 0 };
    const Binary::DoubleWord WindowsBitmapHeader::numberOfColors{ 0 };
    const Binary::DoubleWord WindowsBitmapHeader::numberOfImportantColors{ 0 };

    // Public methods implementation ...
}

4.4 Constructors

The only attributes left to initialize are fileSize, bitmapWidth, and bitmapHeight. We can do that in the constructors:

    // Public methods implementation ...
    WindowsBitmapHeader::WindowsBitmapHeader(const int width, const int height) :
        bitmapWidth(width),
        bitmapHeight(height)
    {
        const auto rowStride = ((width * static_cast<uint16_t>(bitsPerPixel) + 31) / 32) * 4;

        fileSize = Binary::DoubleWord(rowStride * height + static_cast<uint32_t>(rawImageByteOffset));
    }

    WindowsBitmapHeader::WindowsBitmapHeader(std::istream& sourceStream)
    {
        read(sourceStream);
    }

    // Other methods implementation ...

Calculation of rowStride

BMP format often pads each row of pixel data so that it aligns to a 4-byte (32-bit) boundary. Also, in a 24-bit BMP, each pixel takes up 3 bytes: one for Red, one for Green, and one for Blue. This is what the rowStride calculation is for.

  • ((width * static_cast<uint16_t>(bitsPerPixel) + 31) / 32) * 4

    • width * static_cast<uint16_t>(bitsPerPixel): This calculates the number of bits needed for one row.

    • + 31: Adds 31 to align the number of bits closer to a multiple of 32.

    • / 32: Divides by 32, essentially rounding up to the nearest 32-bit boundary.

    • * 4: Finally, it multiplies by 4 to get the number of bytes (since 32 bits = 4 bytes).

The row stride ensures that each row starts on a 32-bit boundary, which is a requirement in many bitmap formats including the Windows BMP format.

Calculation of fileSize

BMP files include both a file header and an info header, which also consume space. That's why rawImageByteOffset (which indicates where the actual image data starts) is added.

  • fileSize = Binary::DoubleWord(rowStride * height + static_cast<uint32_t>(rawImageByteOffset));

    • rowStride * height: This calculates the total bytes needed for the pixel data.

    • + static_cast<uint32_t>(rawImageByteOffset): Adds the offset to the beginning of the image data. This offset accounts for the size of the bitmap file header and bitmap info header.

The fileSize then becomes the sum of the size needed for the image data and the size of the headers. This way, it provides an accurate file size that includes both the image and the necessary metadata.

4.5 Reading

Notice that the second constructor above calls the read() method. For generality, this methods takes a reference to a std::istream as a parameter and does two things:

  • For each const static header, it confirms that the value read from the stream matches the expected value.
  • For other headers, it assigns them to the respective private attributes. (And of course, these headers have public getter methods).
    void WindowsBitmapHeader::readInfoHeader(std::istream& sourceStream)
    {
        verifyEquality(infoHeaderBytes, Binary::DoubleWord::readLittleEndian(sourceStream), "infoHeaderBytes");
        bitmapWidth = Binary::DoubleWord::readLittleEndian(sourceStream);
        bitmapHeight = Binary::DoubleWord::readLittleEndian(sourceStream);
        verifyEquality(numberOfPlanes, Binary::Word::readLittleEndian(sourceStream), "numberOfPlanes");
        verifyEquality(bitsPerPixel, Binary::Word::readLittleEndian(sourceStream), "bitsPerPixel");
        verifyEquality(compressionType, Binary::DoubleWord::readLittleEndian(sourceStream), "compressionType");
        Binary::DoubleWord::readLittleEndian(sourceStream); // compressedImageSize
        Binary::DoubleWord::readLittleEndian(sourceStream); // horizontalPixelsPerMeter
        Binary::DoubleWord::readLittleEndian(sourceStream); // verticalPixelsPerMeter
        Binary::DoubleWord::readLittleEndian(sourceStream); // numberOfColors
        Binary::DoubleWord::readLittleEndian(sourceStream); // numberOfImportantColors
    }

    void WindowsBitmapHeader::readFileHeader(std::istream& sourceStream)
    {
        verifyEquality(firstIdentifier, Binary::Byte::read(sourceStream), "firstIdentifier");
        verifyEquality(secondIdentifier, Binary::Byte::read(sourceStream), "secondIdentifier");
        fileSize = Binary::DoubleWord::readLittleEndian(sourceStream);
        verifyEquality(reserved, Binary::DoubleWord::readLittleEndian(sourceStream), "reserved");
        verifyEquality(rawImageByteOffset, Binary::DoubleWord::readLittleEndian(sourceStream), "rawImageByteOffset");
    }

    void WindowsBitmapHeader::read(std::istream& sourceStream)
    {
        readFileHeader(sourceStream);
        readInfoHeader(sourceStream);
    }

VerifyEquality.h

How is the verifyEquality() method implemented? It is actually just a simple template function that throws an exception if the two arguments are not equal.

Take a look at VerifyEquality.h:

#pragma once

#include <sstream>

template<typename T> void verifyEquality(const T& expected, const T& actual, const std::string& name = "")
{
    if (expected != actual)
    {
        std::stringstream sstr;
        sstr << name << " - expected: " << expected << ", actual: " << actual;
        throw std::runtime_error{ sstr.str().c_str() };
    }
}

4.6 Writing

The writing method is just writing the headers in the correct order:

    void WindowsBitmapHeader::write(std::ostream& destinationStream) const
    {
        writeFileHeader(destinationStream);
        writeInfoHeader(destinationStream);
    }

    void WindowsBitmapHeader::writeFileHeader(std::ostream& destinationStream) const
    {
        firstIdentifier.write(destinationStream);
        secondIdentifier.write(destinationStream);
        fileSize.writeLittleEndian(destinationStream);
        reserved.writeLittleEndian(destinationStream);
        rawImageByteOffset.writeLittleEndian(destinationStream);
    }

    void WindowsBitmapHeader::writeInfoHeader(std::ostream& destinationStream) const
    {
        infoHeaderBytes.writeLittleEndian(destinationStream);
        bitmapWidth.writeLittleEndian(destinationStream);
        bitmapHeight.writeLittleEndian(destinationStream);
        numberOfPlanes.writeLittleEndian(destinationStream);
        bitsPerPixel.writeLittleEndian(destinationStream);
        compressionType.writeLittleEndian(destinationStream);
        compressedImageSize.writeLittleEndian(destinationStream);
        horizontalPixelsPerMeter.writeLittleEndian(destinationStream);
        verticalPixelsPerMeter.writeLittleEndian(destinationStream);
        numberOfColors.writeLittleEndian(destinationStream);
        numberOfImportantColors.writeLittleEndian(destinationStream);
    }

5. BitmapIterator Class

5.1 Introduction to Bitmap Iterator

We need a way to iterate through the pixels of a bitmap. This is what the BitmapIterator class is for. We want to have a class that we can use like this:

Bitmap bitmap{ 3, 2 };
BitmapIterator it{ bitmap };

it.nextPixel(); // move to the next pixel
it.nextScanLine(); // move to the next scan line
Color color = it.getColor(); // get the color of the current pixel

5.2 IBitmapIterator Interface

We will start by defining an interface for the BitmapIterator class. Take a look at IBitmapIterator.h:

#pragma once

#include <memory>

namespace BitmapGraphics
{
    class Color;

    class IBitmapIterator
    {
    public:
        IBitmapIterator() = default;
        virtual ~IBitmapIterator() = default;

        IBitmapIterator(const IBitmapIterator&) = delete;
        IBitmapIterator& operator =(IBitmapIterator const&) = delete;
        IBitmapIterator(IBitmapIterator&&) = delete;
        IBitmapIterator& operator=(IBitmapIterator&&) = delete;

        virtual void nextScanLine() = 0;
        virtual bool isEndOfImage() const = 0;
        virtual void nextPixel() = 0;
        virtual bool isEndOfScanLine() const = 0;
        virtual Color getColor() const = 0;

        virtual int getBitmapWidth() const = 0;
        virtual int getBitmapHeight() const = 0;
    };

    using HBitmapIterator = std::unique_ptr<IBitmapIterator>;
}

The interface comes with a handle type HBitmapIterator, which is a std::unique_ptr to an IBitmapIterator. This is because we want to use the handle type to manage the lifetime of the IBitmapIterator object. The handle type is defined in the Bitmap.h file, introduced in the next section.

5.3 BitmapIterator Class

#pragma once

#include "IBitmapIterator.h"
#include "Bitmap.h"

namespace BitmapGraphics
{
    class BitmapIterator final : public IBitmapIterator
    {
    public:
        explicit BitmapIterator(Bitmap& bitmap);

        void nextScanLine() override;
        bool isEndOfImage() const override;
        void nextPixel() override;
        bool isEndOfScanLine() const override;
        Color getColor() const override;

        int getBitmapWidth() const override;
        int getBitmapHeight() const override;

    private:
        Bitmap& myBitmap;
        Bitmap::ScanLineIterator myScanLine;
        Bitmap::PixelIterator myPixel;
    };
}

The BitmapIterator class inherits from IBitmapIterator and implements all the pure virtual methods. It also has three private member variables:

#include "BitmapIterator.h"

namespace BitmapGraphics
{
    BitmapIterator::BitmapIterator(Bitmap& bitmap) :
        myBitmap{ bitmap },
        myScanLine{ bitmap.begin() }
    {
        if (!BitmapIterator::isEndOfImage())
        {
            myPixel = myScanLine->begin();
        }
    }

    void BitmapIterator::nextScanLine()
    {
        ++myScanLine;
        if (!isEndOfImage())
        {
            myPixel = myScanLine->begin();
        }
    }

    bool BitmapIterator::isEndOfImage() const
    {
        return myScanLine == myBitmap.end();
    }

    void BitmapIterator::nextPixel()
    {
        ++myPixel;
    }

    bool BitmapIterator::isEndOfScanLine() const
    {
        return myPixel == myScanLine->end();
    }

    Color BitmapIterator::getColor() const
    {
        return *myPixel;
    }

    int BitmapIterator::getBitmapWidth() const
    {
        return myBitmap.getWidth();
    }

    int BitmapIterator::getBitmapHeight() const
    {
        return myBitmap.getHeight();
    }

}

6. Bitmap Class

The Bitmap will so far just be a simple container for the pixels (of type Color). Take a look at Bitmap.h:

#pragma once

#include "IBitmapIterator.h"
#include "Color.h"
#include <vector>

namespace BitmapGraphics
{
    class Bitmap
    {
    public:
        using ScanLine = std::vector<Color>;
        using PixelIterator = ScanLine::iterator;

    private:
        using ScanLineCollection = std::vector<ScanLine>;

    public:
        using ScanLineIterator = ScanLineCollection::iterator;

        // Construct empty bitmap -- will be filled by decoder
        Bitmap() = default;
        Bitmap(const int width, const int height) : myWidth{ width }, myHeight{ height } { }

        Bitmap(const Bitmap&) = default;
        Bitmap(Bitmap&&) = default;
        ~Bitmap() = default;

        Bitmap& operator=(const Bitmap&) = default;
        Bitmap& operator=(Bitmap&&) = default;

        template<class SL> void addScanLine(SL&& scanLine)
        {
            myScanLines.push_back(std::forward<SL>(scanLine));
        }

        ScanLineIterator begin();
        ScanLineIterator end();

        int getWidth() const { return myWidth; }
        int getHeight() const { return myHeight; }

        HBitmapIterator createIterator();

    private:
        int myWidth{ 0 };
        int myHeight{ 0 };

        ScanLineCollection myScanLines;
    };

    using HBitmap = std::unique_ptr<Bitmap>;
}

We here provide two ways to iterate through the bitmap: one is to use the begin() and end() methods, and the other is to use the createIterator() method. The latter is more convenient, but the former is more flexible:

#include "Bitmap.h"
#include "BitmapIterator.h"
#include <memory>

namespace BitmapGraphics
{
    Bitmap::ScanLineIterator Bitmap::begin()
    {
        return myScanLines.begin();
    }

    Bitmap::ScanLineIterator Bitmap::end()
    {
        return myScanLines.end();
    }

    HBitmapIterator Bitmap::createIterator()
    {
        return std::make_unique<BitmapIterator>(*this);
    }
}

7. Color Class

The Color class is a simple container for the RGB values. Take a look at Color.h:

#pragma once

#include "Byte.h"

namespace BitmapGraphics
{
    class Color
    {
    public:
        explicit Color(
            const Binary::Byte& red = 0_byte,
            const Binary::Byte& green = 0_byte,
            const Binary::Byte& blue = 0_byte) noexcept;

        Color(const Color&) noexcept = default;
        Color(Color&&) noexcept = default;

        Color& operator=(const Color&) = default;
        Color& operator=(Color&&) = default;

        ~Color() noexcept = default;

        static Color read(std::istream& sourceStream);
        void write(std::ostream& destinationStream) const;

        Binary::Byte getRed() const { return myRed; }
        Binary::Byte getGreen() const { return myGreen; }
        Binary::Byte getBlue() const { return myBlue; }

        bool operator==(const Color& rhs) const;

    private:
        Binary::Byte myRed;
        Binary::Byte myGreen;
        Binary::Byte myBlue;
    };

    std::ostream& operator<<(std::ostream& os, const Color& color);
}

This is a user-defined literal for creating Byte objects more naturally using a syntax like 5_byte.

// user defined literal constant. E.g., 5_byte.
constexpr Binary::Byte operator ""_byte(const unsigned long long ch)
{
    return Binary::Byte(narrow_cast<std::uint8_t>(ch));
}

Here is the implementation of the Color class:

#include "Color.h"

namespace BitmapGraphics
{

    Color::Color(
        const Binary::Byte& red,
        const Binary::Byte& green,
        const Binary::Byte& blue) noexcept :
        myRed{ red },
        myGreen{ green },
        myBlue{ blue }
    {
    }

    Color Color::read(std::istream& sourceStream)
    {
        Color color;

        color.myBlue = Binary::Byte::read(sourceStream);
        color.myGreen = Binary::Byte::read(sourceStream);
        color.myRed = Binary::Byte::read(sourceStream);

        return color;
    }

    void Color::write(std::ostream& destinationStream) const
    {
        myBlue.write(destinationStream);
        myGreen.write(destinationStream);
        myRed.write(destinationStream);
    }

    bool Color::operator==(const Color& rhs) const
    {
        return getRed() == rhs.getRed() && (getGreen() == rhs.getGreen()) && (getBlue() == rhs.getBlue());
    }

    std::ostream& operator<<(std::ostream& os, const Color& color)
    {
        color.write(os);
        return os;
    }

}