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.
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:
- 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. - 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. - 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.
- Pixel Data: This is the actual image data. This is implemented with the
Bitmap
class.
2.3 It is not a popular image format these days
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:
- Simplicity: The BMP format is straightforward and easy to understand, which makes it relatively simple to write programs that can read or write BMPs.
- Lossless: BMP is a lossless format, meaning it doesn't lose any image quality.
- Universal Support: Almost all image editing software and many operating systems support BMP.
Cons:
- 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.
- Lacks Advanced Features: BMP doesn't support features like transparency (like PNG) or different compression levels (like JPEG).
- 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 touint8_t
. - Assignment: For example,
Byte
anduint8_t
can be assigned to anotherByte
. - Stream Read/Write: For example,
Byte
can be written to an output stream, andByte
can be read from an input stream. This will be more complicated forWord
andDoubleWord
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:
-
explicit
andconstexpr
Usage:explicit
: Theexplicit
keyword prevents implicit type conversions. In a class sensitive to data types likeSizedWord
, 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 fromWordSize
, 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.
-
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 initializeSizedWord
with a value of a different type. - Double Cast: The first
static_cast<T>(value)
seems redundant sincevalue
is already of typeT
. This could be an overly cautious practice and might not be necessary in this specific case.
- Usage Scenario: This constructor
-
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.
- Some contructors and operators are set to default because the only member,
-
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.
- This helps avoid conflict with other methods that take in the same template parameter but are looking for a different size of
-
destinationStream.exceptions()
and.put
Method:.exceptions()
sets up a condition "register" for the stream, where if certain bits are set (std::ios::failbit
orstd::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.
-
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. IfWordSize
is 32 bits, you'll loop 4 times, and the bit-shifting places each 8-bit byte into its correct position in the 32-bitWordSize
.
- The expression
-
friend
Shorthand Example:- Normally, we declare a
friend
function like this, then define it later:
- Normally, we declare a
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.
-
#include <type_traits>
:- This header is included for the use of
std::enable_if_t
andstd::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.
- This header is included for the use of
-
explicit operator WordSize()
:- The
explicit
keyword is used to prevent implicit conversions. This ensures that you can't accidentally use aSizedWord
object where aWordSize
is expected without making your intention clear. Even though there's no argument, it prevents cases like:
- The
WordSize ws = mySizedWord; // This will cause a compile-time error because of `explicit`.
WordSize ws = static_cast<WordSize>(mySizedWord); // This is fine.
static_assert(...)
Usage:- This line checks at compile-time that the type
WordSize
you're using to instantiate theSizedWord
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 theSizedWord
template. If the conditions are not met, a compile-time error will be generated, stopping the build.
- This line checks at compile-time that the type
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;
}
}