Homework 4 Notes

1. Intructions

In Homework 3, we added support for reading and writing Windows Bitmap (BMP) files with classes like WindowsBitmapHeader, BitmapIterator, Bitmap, and Color. While these provided basic functionalities, we can't reasonably expect clients to know how to use them properly. Furthermore, we don't really want to clients to call a different set of classes for each file format.

This is when CodecLibrary comes in. It's a dynamic registry for bitmap encoders and decoders. It's a user-friendly interface that abstracts away the complexities of file formats and decoding mechanisms.

In this assignment, we'll implement the following classes (not necessarily in the following order) under the BitmapGraphics namespace:

  • CodecLibrary: A registry for bitmap encoders and decoders. It will use the prototype pattern to create new instances of the appropriate registered encoder or decoder.
  • Encoders:
    • IBitmapEncoder: An interface for bitmap encoders. It will provide a method for encoding a bitmap to a stream.
    • WindowsBitmapEncoder: An implementation of IBitmapEncoder for encoding bitmaps to Windows Bitmap (BMP) files.
  • Decoders:
    • IBitmapDecoder: An interface for bitmap decoders. It will provide a method for decoding a stream to a bitmap.
    • WindowsBitmapDecoder: An implementation of IBitmapDecoder for decoding Windows Bitmap (BMP) files to bitmaps.

In Homework 3, we have also implemented the IBitmapIterator interface, whose primary job is to iterate over a bitmap, and return the pixel's color at the current position. At the time, it probably was not clear why we need to separate the implementation of the iterator from the bitmap itself. However, this design becomes useful if one want to do extra manipulation on the pixels, such as inverting the colors, or adding brightness to the image. We here refer to these modified iterators as "decorators". In this assignment, we will implement the following decorators to test our CodecLibrary:

  • BitmapIteratorDecorator: A derived class of IBitmapIterator that acts as a base class for all decorators. The derived decorators include:
    • ColorInversionDecorator: A decorator that inverts the colors of a bitmap.
    • BrightnessDecorator: A decorator that adds brightness to a bitmap.

At the end of the assignment, we will have the following class architecture:

hw4_uml


2. Implementing CodecLibrary

2.1. CodecLibrary Overview

CODEC is short for "code/decode". It's a generic term for a mechanism that facilitates the conversion of information to and from different formats. Modern design of CODECs allows encoders and decoders to be added dynamically, sometimes even without restarting the application. This is done by having a library that maintains a registry of CODECs, and when given a file, it looks up the appropriate CODEC to use based on the file's header information.

2.2 CodecLibrary Registration Methods

In this assignment, we will implement a CodecLibrary class that acts as a registry for bitmap encoders and decoders. It will use the prototype pattern to create new instances of the appropriate registered encoder or decoder. The prototype pattern is a creational design pattern that allows us to create new objects by cloning existing ones. Take a look at CodecLibrary.h:

#pragma once

#include "IBitmapDecoder.h"
#include "IBitmapEncoder.h"

#include <string>
#include <vector>

namespace BitmapGraphics
{
    class CodecLibrary
    {
    public:
        void registerEncoder(HBitmapEncoder encoder);
        void registerDecoder(HBitmapDecoder decoder);

        // auto determination version of createDecoder
        HBitmapDecoder createDecoder(std::istream& sourceStream);

        // mime type version of createDecoder
        HBitmapDecoder createDecoder(
            const std::string& mimeType,
            std::istream& sourceStream);

        HBitmapEncoder createEncoder(
            const std::string& mimeType,
            HBitmapIterator bitmapIterator);

    private:
        std::vector<HBitmapEncoder> myEncoders;
        std::vector<HBitmapDecoder> myDecoders;

        using EncoderIterator = std::vector<HBitmapEncoder>::iterator;
        using DecoderIterator = std::vector<HBitmapDecoder>::iterator;
    };

}

Here, CodecLibrary gets the prototypes of the encoders and decoders from the client via the corresponding register methods, which are implemented in CodecLibrary.cpp:

namespace BitmapGraphics
{
    void CodecLibrary::registerEncoder(HBitmapEncoder encoder)
    {
        myEncoders.push_back(std::move(encoder));
    }

    void CodecLibrary::registerDecoder(HBitmapDecoder decoder)
    {
        myDecoders.push_back(std::move(decoder));
    }
}

2.3. CodecLibrary Encoder/Decoder Creation Methods

Then, when the client wants to create a new encoder or decoder, it calls the corresponding create method, which will return a new instance of the appropriate encoder or decoder. Note that these methods are named "create" rather than "get" to emphasize the fact that the returned encoder or decoder is a new instance, not the prototype.

The CodecLibrary supports three creation methods:

  • HBitmapDecoder createDecoder(std::istream& sourceStream): This method will read the first chunk of the stream to determine the file type, and then create a new decoder based on the file type.
  • HBitmapDecoder createDecoder(const std::string& mimeType, std::istream& sourceStream): This method will create a new decoder based on the given MIME type.
  • HBitmapEncoder createEncoder(const std::string& mimeType, HBitmapIterator bitmapIterator): This method will create a new encoder based on the given MIME type.

The "First Chunk" here is the first few bytes of the file stream of arbitrary size (in this case 100). The first chunk is used to determine the file type. For example, the first two bytes of a BMP file are "BM", and the first two bytes of a PNG file are "89 50". The first chunk is read from the stream, and then the stream is reset to the beginning. The first chunk is then passed to each decoder to determine if it supports the file type. If a decoder supports the file type (MIME type, or "Multipurpose Internet Mail Extensions), it will return true, and the decoder will be used to decode the file. Otherwise, the next decoder will be tried. If no decoder supports the file type, an exception will be thrown.

The encoder creater is simpler. It simply iterates through the list of encoders, and returns the first encoder that supports the given MIME type. If no encoder supports the MIME type, an exception will be thrown.

The create methods are implemented as follows. Although we haven't seen how to implement the encoder and decoder classes, hopefully the code below will give you a sense of how they will be implemented, too:

namespace
{
    const int ChunkSize{ 100 };
}

namespace BitmapGraphics
{
    // auto determination version of createDecoder
    HBitmapDecoder CodecLibrary::createDecoder(std::istream& sourceStream)
    {
        // get first chunk to use for type determination
        char firstChunk[ChunkSize]{};
        sourceStream.get(firstChunk, ChunkSize);

        // reposition stream back to beginning
        sourceStream.clear();
        sourceStream.seekg(std::istream::beg);

        for (const auto& decoder : myDecoders)
        {
            if (decoder->isSupported(firstChunk))
            {
                return decoder->clone(sourceStream);
            }
        }

        throw std::runtime_error{ "No decoder for source stream" };
    }

    // mime type version of createDecoder
    HBitmapDecoder CodecLibrary::createDecoder(const std::string& mimeType,
        std::istream& sourceStream)
    {
        for (const auto& decoder : myDecoders)
        {
            if (decoder->getMimeType() == mimeType)
            {
                return decoder->clone(sourceStream);
            }
        }

        throw std::runtime_error{ "No decoder for " + mimeType };
    }

    HBitmapEncoder CodecLibrary::createEncoder(
        const std::string& mimeType,
        HBitmapIterator bitmapIterator)
    {
        for (const auto& encoder : myEncoders)
        {
            if (encoder->getMimeType() == mimeType)
            {
                return encoder->clone(std::move(bitmapIterator));
            }
        }

        throw std::runtime_error{ "No encoder for " + mimeType };
    }
}

3. Implementing Encoders

3.1. IBitmapEncoder Interface

We will create the interface class IBitmapEncoder for two purposes: * To provide a common interface for all encoders used in the CodecLibrary. * To be derived by all encoder classes.

Take a look at IBitmapEncoder.h:

#pragma once

#include "IBitmapIterator.h"
#include <memory>
#include <string>

namespace BitmapGraphics
{
    class IBitmapEncoder;

    using HBitmapEncoder = std::unique_ptr<IBitmapEncoder>;

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

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

        virtual HBitmapEncoder clone(HBitmapIterator bitmapIterator) = 0;

        virtual void encodeToStream(std::ostream& os) = 0;

        virtual const std::string& getMimeType() const = 0;
    };
}

As you can see, we have already seen getMimeType() and clone() in CodecLibrary::createEncoder(). The encodeToStream() method will be implemented by the derived classes. Note that we also define a handle type HBitmapEncoder for std::unique_ptr<IBitmapEncoder>.

3.2. WindowsBitmapEncoder Class

Since this class is going to be used in the prototype pattern, we must make sure that the prototype is not used as a regular encoder, and only the encoder created by the CodecLibrary should be used for encodeToStream().

To do this, we can leave the HBitmapIterator uninitialized in the constructor, and throw an exception if the client attempts to use the encoder with an uninitialized iterator. This way, only the encoder created by the CodecLibrary will have an initialized iterator (via the clone() method or the additional constructor). Take a look at WindowsBitmapEncoder.h:

#pragma once

#include "IBitmapEncoder.h"

namespace BitmapGraphics
{
    class WindowsBitmapEncoder final : public IBitmapEncoder
    {
    public:
        // This constructor is used to create the prototype.
        // myBitmapIterator will be unitialized.
        // If a client attempts to use an encoder with an
        // unitialized iterator, we'll throw an exception.
        //
        WindowsBitmapEncoder() = default;
        explicit WindowsBitmapEncoder(HBitmapIterator bitmapIterator);

        const std::string& getMimeType() const override;
        HBitmapEncoder clone(HBitmapIterator bitmapIterator) override;
        void encodeToStream(std::ostream& destinationStream) override;

    private:
        HBitmapIterator myBitmapIterator;

        void writePadBytes(std::ostream& destinationStream) const;
    };
}

And here's the implementation in WindowsBitmapEncoder.cpp:

#include "WindowsBitmapEncoder.h"
#include "WindowsBitmapHeader.h"
#include "WindowsBitmapCommon.h"
#include "Color.h"

namespace BitmapGraphics
{
    WindowsBitmapEncoder::WindowsBitmapEncoder(HBitmapIterator bitmapIterator)
        : myBitmapIterator{ std::move(bitmapIterator) }
    {
    }

    const std::string& WindowsBitmapEncoder::getMimeType() const
    {
        return WindowsBitmapMimeType;
    }

    HBitmapEncoder WindowsBitmapEncoder::clone(HBitmapIterator bitmapIterator)
    {
        return std::make_unique<WindowsBitmapEncoder>(std::move(bitmapIterator));
    }

    void WindowsBitmapEncoder::writePadBytes(std::ostream& destinationStream) const
    {
        const Binary::Byte padByte{};
        for (auto i = 0; i < numberOfPadBytes(myBitmapIterator->getBitmapWidth()); ++i)
        {
            padByte.write(destinationStream);
        }
    }

    void WindowsBitmapEncoder::encodeToStream(std::ostream& destinationStream)
    {
        if (!myBitmapIterator.get())
        {
            throw std::runtime_error{ "Invalid encoder: null iterator" };
        }

        // Write the bitmap header
        const WindowsBitmapHeader bitmapHeader{ myBitmapIterator->getBitmapWidth(), myBitmapIterator->getBitmapHeight() };
        bitmapHeader.write(destinationStream);

        // Write the bitmap data
        while (!myBitmapIterator->isEndOfImage())
        {
            while (!myBitmapIterator->isEndOfScanLine())
            {
                myBitmapIterator->getColor().write(destinationStream);
                myBitmapIterator->nextPixel();
            }

            writePadBytes(destinationStream);
            myBitmapIterator->nextScanLine();
        }
    }
}

encodeToStream() writes the BMP header and then each pixel's color information into an output stream, padding as necessary. writePadBytes() is a helper function that writes padding bytes to ensure that each scan line meets the BMP file format specifications.

3.3 Shared Properties of Windows Bitmap Encoders and Decoders

Note that in the WindowsBitmapEncoder.cpp, we come across two things that are not defined: WindowsBitmapHeader and numberOfPadBytes(). These are shared properties of Windows Bitmap encoders and decoders, so we will define them in WindowsBitmapCommon.h:

#pragma once

namespace BitmapGraphics
{
    const std::string WindowsBitmapMimeType{ "image/x-ms-bmp" };

    inline int numberOfPadBytes(const int widthInPixels)
    {
        const auto remainder{ (widthInPixels * 3) % 4 };
        return (remainder == 0) ? 0 : (4 - remainder);
    }
}

Here, "image/x-ms-bmp" is the MIME type for Windows Bitmap files.

numberOfPadBytes() calculates the number of padding bytes needed to ensure that each scan line meets the BMP file format specifications. It calculates the remainder when the total number of bytes in the row (width multiplied by 3, since each pixel in BMP takes up 3 bytes for RGB) is divided by 4. Then, if the remainder is zero, no padding is needed. Otherwise, the padding needed is 4 - remainder.


4. Implementing Decoders

4.1 IBitmapDecoder Interface

Here's the interface class IBitmapDecoder:

#pragma once

#include "IBitmapIterator.h"
#include <memory>
#include <string>

namespace BitmapGraphics
{
    class BitmapIteratorDecorator;
    class IBitmapDecoder;
    using HBitmapDecoder = std::unique_ptr<IBitmapDecoder>;

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

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

        virtual const std::string& getMimeType() const = 0;
        virtual HBitmapDecoder clone(std::istream& sourceStream) = 0;
        virtual bool isSupported(const std::string& firstChunk) const = 0;
        virtual HBitmapIterator createIterator() = 0;
    };
}

Notice that we have already seen getMimeType(), clone(), and isSupported() in CodecLibrary::createDecoder().

Here we see the design of the decoder class. Instead of decoding the whole bitmap at once, we will use an iterator (created by createIterator()) to iterate over the bitmap, and return the pixel's color at the current position. This way, not only do we allow more flexibility in the decoding process, but we also allow the code to defer the decoding process until the bitmap is actually needed. This is called "lazy evaluation", and it's a common technique in C++.

4.2 WindowsBitmapDecoder Class

Similar to the WindowsBitmapEncoder class, we will leave the myBitmap uninitialized in the default constructor, and throw an exception if the client attempts to use the decoder with an uninitialized bitmap. This way, only the decoder created by the CodecLibrary will have an initialized bitmap (via the clone() method or the additional constructor). Take a look at WindowsBitmapDecoder.h:

#pragma once

#include "IBitmapDecoder.h"
#include "Bitmap.h"

namespace BitmapGraphics
{
    class WindowsBitmapDecoder final : public IBitmapDecoder
    {
    public:
        WindowsBitmapDecoder();
        explicit WindowsBitmapDecoder(std::istream& sourceStream);

        const std::string& getMimeType() const override;
        HBitmapDecoder clone(std::istream& sourceStream) override;
        bool isSupported(const std::string& firstChunk) const override;
        HBitmapIterator createIterator() override;

    private:
        HBitmap myBitmap;
        void decodeIntoBitmap(std::istream &sourceStream);
    };
}

And here's the implementation in WindowsBitmapDecoder.cpp:

#include "WindowsBitmapDecoder.h"
#include "BitmapIterator.h"
#include "WindowsBitmapHeader.h"
#include "WindowsBitmapCommon.h"

namespace BitmapGraphics
{
    // This constructor is used to create the prototype.
    // Initialize mySourceStream to an empty stringstream,
    // and set its state to "bad".
    // If a client attempts to use a decoder with bad stream,
    // we'll throw an exception.
    //
    WindowsBitmapDecoder::WindowsBitmapDecoder() = default;

    WindowsBitmapDecoder::WindowsBitmapDecoder(std::istream& sourceStream)
    {
        decodeIntoBitmap(sourceStream);
    }

    const std::string& WindowsBitmapDecoder::getMimeType() const
    {
        return WindowsBitmapMimeType;
    }

    HBitmapDecoder WindowsBitmapDecoder::clone(std::istream& sourceStream)
    {
        return std::make_unique<WindowsBitmapDecoder>(sourceStream);
    }

    bool WindowsBitmapDecoder::isSupported(const std::string& firstChunk) const
    {
        if (firstChunk.size() < 2)
        {
            return false;
        }
        return firstChunk[0] == 'B' && firstChunk[1] == 'M';
    }

    HBitmapIterator WindowsBitmapDecoder::createIterator()
    {
        if (myBitmap)
        {
            return myBitmap->createIterator();
        }
        throw std::runtime_error("No decoded bitmap present");
    }

    void WindowsBitmapDecoder::decodeIntoBitmap(std::istream& sourceStream)
    {
        try
        {
            // Read the bitmap header
            const WindowsBitmapHeader bitmapHeader{ sourceStream };

            // Initialize the bitmap
            myBitmap = std::make_unique<Bitmap>(static_cast<uint32_t>(bitmapHeader.getBitmapWidth()), static_cast<uint32_t>(bitmapHeader.getBitmapHeight()));

            for (auto row = 0; row < myBitmap->getHeight(); ++row)
            {
                Bitmap::ScanLine scanLine;
                scanLine.reserve(myBitmap->getWidth());

                // Read row of pixels
                for (auto column = 0; column < myBitmap->getWidth(); ++column)
                {
                    scanLine.emplace_back(Color::read(sourceStream));
                }

                // Read and ignore pad bytes (if any)
                for (auto pad = 0; pad < numberOfPadBytes(myBitmap->getWidth()); ++pad)
                {
                    Binary::Byte::read(sourceStream);
                }

                myBitmap->addScanLine(std::move(scanLine));
            }
        }
        catch(const std::exception &)
        {
            // If we fail parsing, discard the bitmap.
            myBitmap.reset();
        }
    }
}

5. Implementing Decorators

5.1 BitmapIteratorDecorator Class

The BitmapIteratorDecorator class is a base class for all decorators. It implements the IBitmapIterator interface, and delegates all calls to the original iterator. And yes, although it is a base class, it does implement some of the methods that are common to all decorators. Take a look at BitmapIteratorDecorator.h:

#pragma once

#include "IBitmapIterator.h"

namespace BitmapGraphics
{
    class BitmapIteratorDecorator : public IBitmapIterator
    {
    public:
        explicit BitmapIteratorDecorator(HBitmapIterator originalIterator);

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

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

    protected:
        HBitmapIterator myOriginalIterator;
    };
}

Here is the implementation in BitmapIteratorDecorator.cpp:

#include "BitmapIteratorDecorator.h"
#include "Color.h"

namespace BitmapGraphics
{
    BitmapIteratorDecorator::BitmapIteratorDecorator(HBitmapIterator originalIterator) :
        myOriginalIterator{ std::move(originalIterator) }
    {
    }

    void BitmapIteratorDecorator::nextScanLine()
    {
        myOriginalIterator->nextScanLine();
    }

    bool BitmapIteratorDecorator::isEndOfImage() const
    {
        return myOriginalIterator->isEndOfImage();
    }

    void BitmapIteratorDecorator::nextPixel()
    {
        myOriginalIterator->nextPixel();
    }

    bool BitmapIteratorDecorator::isEndOfScanLine() const
    {
        return myOriginalIterator->isEndOfScanLine();
    }

    int BitmapIteratorDecorator::getBitmapWidth() const
    {
        return myOriginalIterator->getBitmapWidth();
    }

    int BitmapIteratorDecorator::getBitmapHeight() const
    {
        return myOriginalIterator->getBitmapHeight();
    }

}

How do we use this class? Recall that the CodecLibrary uses the following functions to create a new encoder or decoder:

HBitmapEncoder CodecLibrary::createEncoder(
    const std::string& mimeType,
    HBitmapIterator bitmapIterator)
{
    for (const auto& encoder : myEncoders)
    {
        if (encoder->getMimeType() == mimeType)
        {
            return encoder->clone(std::move(bitmapIterator));
        }
    }

    throw std::runtime_error{ "No encoder for " + mimeType };
}

When creating an encoder, we can pass in the iterator of our choice. In this case, a decorator iterator can be passed to this method, and the encoder will use the decorator iterator to encode the bitmap. For example, we can create a ColorInversionDecorator iterator, and pass it to the encoder:

auto colorInvertIterator = std::make_unique<ColorInversionDecorator>(std::move(iterator));
auto encoder{ theCodecLibrary->createEncoder(msBmp, std::move(colorInvertIterator)) };
std::ofstream outFile{ "output_basicColorInvert.bmp", std::ios::binary };
encoder->encodeToStream(outFile);
outFile.close();

5.2 ColorInversionDecorator Class

To implement the ColorInversionDecorator class, we will override the getColor() method, and invert the color of the original iterator. Take a look at ColorInversionDecorator.h:

#pragma once

#include "BitmapIteratorDecorator.h"

namespace BitmapGraphics
{
    class ColorInversionDecorator final : public BitmapIteratorDecorator
    {
    public:
        explicit ColorInversionDecorator(HBitmapIterator originalIterator);

        Color getColor() const override;
    };
}

And here is how getColor() is implemented in ColorInversionDecorator.cpp:

#include "ColorInversionDecorator.h"
#include "Color.h"

namespace BitmapGraphics
{
    ColorInversionDecorator::ColorInversionDecorator(
        HBitmapIterator originalIterator)
        : BitmapIteratorDecorator{ std::move(originalIterator) }
    {
    }

    Color ColorInversionDecorator::getColor() const
    {
        const auto oldColor{ myOriginalIterator->getColor() };

        return Color{ 
            Binary::Byte(255 - static_cast<uint8_t>(oldColor.getRed())),
            Binary::Byte(255 - static_cast<uint8_t>(oldColor.getGreen())),
            Binary::Byte(255 - static_cast<uint8_t>(oldColor.getBlue())) };
    }
}

5.3 BrightnessDecorator Class

The BrightnessDecorator class is similar to the ColorInversionDecorator class. We will override the getColor() method, and add brightness to the color of the original iterator. Take a look at BrightnessDecorator.h:

#pragma once

#include "BitmapIteratorDecorator.h"
#include "Byte.h"

namespace BitmapGraphics
{
    class BrightnessDecorator final : public BitmapIteratorDecorator
    {
    public:
        BrightnessDecorator(
            HBitmapIterator originalIterator,
            const int& brightnessAdjustment);

        void setBrightnessAdjustment(int brightnessAdjustment);
        int getBrightnessAdjustment() const;

        Color getColor() const override;

    private:
        int myBrightnessAdjustment;
    };
}

Here, we add the brightnessAdjustment specified to the color channels of the original iterator. Note that we use ranged_number to ensure that the color channels are within the range of 0 to 255. Here is how getColor() is implemented in BrightnessDecorator.cpp:

#include "BrightnessDecorator.h"
#include "Color.h"
#include "ranged_number.h"

namespace BitmapGraphics
{
    BrightnessDecorator::BrightnessDecorator(
        HBitmapIterator originalIterator,
        const int& brightnessAdjustment)
        : BitmapIteratorDecorator{ std::move(originalIterator) },
        myBrightnessAdjustment{ brightnessAdjustment }
    {
    }

    void BrightnessDecorator::setBrightnessAdjustment(const int brightnessAdjustment)
    {
        myBrightnessAdjustment = brightnessAdjustment;
    }

    int BrightnessDecorator::getBrightnessAdjustment() const
    {
        return myBrightnessAdjustment;
    }

    // Arithmetic with unsigned values can have wrap-around
    // consequences, so we want to use int here.
    using ColorComponent = ranged_number<int, 0, 255>;

    Color BrightnessDecorator::getColor() const
    {
        const auto oldColor{ myOriginalIterator->getColor() };

        const ColorComponent red(static_cast<uint8_t>(oldColor.getRed()) + myBrightnessAdjustment);
        const ColorComponent green(static_cast<uint8_t>(oldColor.getGreen()) + myBrightnessAdjustment);
        const ColorComponent blue(static_cast<uint8_t>(oldColor.getBlue()) + myBrightnessAdjustment);

        return Color{ Binary::Byte(static_cast<int>(red)), Binary::Byte(static_cast<int>(green)), Binary::Byte(static_cast<int>(blue)) };
    }
}