Homework 5: Integrating Bitmap and Vector Graphics

1. Instructions

This week, our focus shifts to integrating the BitmapGraphics and Binary namespaces we've been working on for the past two weeks with the untouched VG namespace.

1.1 Double Buffering: The Right Rendering Approach

The Problem

Until now, our VG namespace—comprising Scene, Layer, PlacedGraphic, VectorGraphic, and Point classes—lacks the capability to generate actual images. As we don't have GUI support, the question arises: Should the rendering be instantaneous or buffered?

The Solution

  • Single Buffering: Renders images on-the-fly, but can lead to poor user experience.
  • Double Buffering: Our choice for smoother rendering.

In this approach, we first draw on a temporary canvas using BasicCanvas. It employs a new iterator, BasicCanvasBitmapIterator. After that, we convert this canvas to an image file via WindowsBitmapFileProjector.

1.2 Drawing Mechanism: Strokes and Pens

The Draw Method

To draw graphics, we will add a draw() method to our Vector Graphics classes. These methods are hierarchically linked—invoking draw() in a Scene class will trigger draw() methods in its child layers, placed graphics, and so on.

Strokes and Pens

We need to decide how exactly these drawings will appear on the canvas. We will use a "Square Stroke" to lay down the stroke on the canvas. To draw between points, we will create a "Square Pen" that uses a LineIterator class to iterate through pixels.

1.3 Scene Descriptors: Incorporating Strokes

With strokes now a part of each vector graphic, our SceneReader and SceneWriter must be updated. We'll use XML format for stroke descriptors, like so:

<VectorGraphic closed="false">
    <Stroke tip="square" size="5" color="00FF00" />
    <Point x="0" y="0" />
    <Point x="100" y="100" />
</VectorGraphic>

1.4 Classes to Implement or Modify

New Classes

  • Canvas: ICanvas, BasicCanvas, BasicCanvasBitmapIterator
  • Projector: IProjector, WindowsBitmapFileProjector
  • VectorGraphics: IStroke, SquareStroke, IPen, SquarePen, LineIterator

Modified Classes

  • SceneReader: SceneReader
  • VectorGraphics: Existing classes to include draw() methods

Homework 5 UML


2. Implement Canvas Classes

2.1 Implement ICanvas and BasicCanvas

Again we will use the interface/implementation pattern to implement our canvas classes. The ICanvas interface is defined as follows:

#pragma once

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

namespace VG { class Point; }

namespace BitmapGraphics
{
    class ICanvas
    {
    public:
        virtual void setPixelColor(const VG::Point& location, Color const& color) = 0;
        virtual Color getPixelColor(const VG::Point& location) const = 0;
        virtual int getWidth() const = 0;
        virtual int getHeight() const = 0;
        virtual HBitmapIterator createBitmapIterator() const = 0;
    };

    using HCanvas = std::unique_ptr<ICanvas>;
}

Now onto our BasicCanvas. Even though we are implementing technically a 2D container, we don't actually need it to be 2D. A clear way to implement this is to use a std::unordered_map to store the pixels corresponding to their locations, and if a pixel is not found, we return the background color.

#pragma once

#include "ICanvas.h"
#include "Point.h"
#include "Color.h"

#include <functional>
#include <unordered_map>

namespace BitmapGraphics
{
    class BasicCanvas final : public ICanvas
    {
        using PixelMap = std::unordered_map<VG::Point, Color>;

    public:
        BasicCanvas(int width, int height, const Color& backgroundColor = Color(0_byte, 0_byte, 0_byte));

        void setPixelColor(const VG::Point& location, const Color& color) override;
        Color getPixelColor(const VG::Point& location) const override;
        int getWidth() const override;
        int getHeight() const override;
        HBitmapIterator createBitmapIterator() const override;

    private:
        int myWidth;
        int myHeight;
        Color myBackgroundColor;
        PixelMap myPixelMap;

        bool outOfRange(const VG::Point& point) const;
    };
}

The implementation is quite simple, so I will not go into detail here. The only thing to note is that the createBitmapIterator() method returns a pointer to BasicCanvasBitmapIterator object:

    HBitmapIterator BasicCanvas::createBitmapIterator() const
    {
        return std::make_unique<BasicCanvasBitmapIterator>(*this);
    }

2.2 Implement BasicCanvasBitmapIterator

Let's see how this BasicCanvasBitmapIterator iterate this std::unordered_map as if it were a 2D container.

#pragma once

#include "IBitmapIterator.h"
#include "ICanvas.h"
#include "Point.h"

namespace BitmapGraphics
{
    enum class BitmapIterationDirection
    {
        Forward,
        Reverse
    };

    class BasicCanvasBitmapIterator final : public IBitmapIterator
    {
    public:
        explicit BasicCanvasBitmapIterator(const ICanvas& canvas);

        void setDirection(BitmapIterationDirection direction) override;
        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:
        const ICanvas& myCanvas;

        BitmapIterationDirection myRowDirectionTraveral = BitmapIterationDirection::Forward;

        VG::Point myCurrentPoint;
        const int myCanvasWidth;
        const int myCanvasHeight;
    };
}

The implementation is a bit tricky. We need to keep track of the current point, the direction of traversal (up or down, but always left to right), and the canvas width and height. We also need to keep track of the next point to move to. We can do this by defining a nextPoint variable in an anonymous namespace. This variable is initialized to {1,0}. This means that we will move to the right by default. If we are traversing in reverse, we will move to the left instead.

#include "BasicCanvasBitmapIterator.h"

namespace
{
    VG::Point nextPoint{ 1,0 };
}

namespace BitmapGraphics
{
    BasicCanvasBitmapIterator::BasicCanvasBitmapIterator(const ICanvas& canvas) :
        myCanvas(canvas),
        myCurrentPoint(VG::Point(0, 0)),
        myCanvasWidth{ canvas.getWidth() },
        myCanvasHeight{ canvas.getHeight() }
    {
    }

    void BasicCanvasBitmapIterator::setDirection(const BitmapIterationDirection direction)
    {
        myRowDirectionTraveral = direction;
        myCurrentPoint = direction == BitmapIterationDirection::Forward ? 
            VG::Point(0, 0) : 
            VG::Point(0, myCanvasHeight - 1);
    }


    void BasicCanvasBitmapIterator::nextScanLine()
    {
        myCurrentPoint = VG::Point(0, myRowDirectionTraveral == BitmapIterationDirection::Forward ? 
            (myCurrentPoint.getY() + 1) : 
            (myCurrentPoint.getY() - 1));
    }

    bool BasicCanvasBitmapIterator::isEndOfImage() const
    {
        return myRowDirectionTraveral == BitmapIterationDirection::Forward ?
            myCurrentPoint.getY() >= myCanvasHeight :
            myCurrentPoint.getY() < 0;
    }

    void BasicCanvasBitmapIterator::nextPixel()
    {
        myCurrentPoint += nextPoint;
    }

    bool BasicCanvasBitmapIterator::isEndOfScanLine() const
    {
        return myCurrentPoint.getX() >= myCanvasWidth;
    }

    Color BasicCanvasBitmapIterator::getColor() const
    {
        return myCanvas.getPixelColor(myCurrentPoint);
    }

    // Other methods omitted for brevity
}

3. Implement Bitmap Projector Classes

3.1 Implement IProjector and WindowsBitmapFileProjector

Projector is a simple interface with a single method:

#pragma once

#include "ICanvas.h"

namespace BitmapGraphics
{
    class IProjector
    {
    public:
        virtual void projectCanvas(const HCanvas& canvas) = 0;
    };
}

The WindowsBitmapFileProjector class is also straightforward. It takes in a filename and a CodecLibrary object. It then creates a BitmapIterator from the canvas and uses the CodecLibrary to create an encoder. Finally, it encodes the image to a stream.

#pragma once

#include "IProjector.h"
#include "CodecLibrary.h"

namespace BitmapGraphics
{
    class WindowsBitmapFileProjector final : public IProjector
    {
    public:
        WindowsBitmapFileProjector(std::string filename, std::shared_ptr<CodecLibrary> codecLibrary);
        void projectCanvas(const HCanvas& canvas) override;

    private:
        std::string myFilename;
        std::shared_ptr<CodecLibrary> myCodecLibrary;
    };
}

Here is the implementation:

#include "WindowsBitmapFileProjector.h"
#include "Bitmap.h"

#include <fstream>
#include <memory>
#include <utility>

namespace BitmapGraphics
{
    WindowsBitmapFileProjector::WindowsBitmapFileProjector(
        std::string filename,
        std::shared_ptr<CodecLibrary> codecLibrary)
        : myFilename(std::move(filename)),
        myCodecLibrary(std::move(codecLibrary))
    {
    }

    void WindowsBitmapFileProjector::projectCanvas(const HCanvas& canvas)
    {
        auto canvasItr = canvas->createBitmapIterator();
        canvasItr->setDirection(BitmapIterationDirection::Reverse);

        auto encoder = myCodecLibrary->createEncoder("image/x-ms-bmp", std::move(canvasItr));

        std::ofstream outputStream(myFilename.c_str(), std::ios::binary);
        encoder->encodeToStream(outputStream);
    }
}

4. Implement Stroke and Pen Classes

4.1 Implement IStroke and SquareStroke

IStroke is a just a simple container of the stroke attributes. It also has a createPen() method that returns a HPen object, which will be discussed later.

#pragma once

#include "ICanvas.h"
#include "IPen.h"

namespace BitmapGraphics
{
    class IStroke
    {
    public:
        virtual void setSize(int size) = 0;
        virtual int getSize() const = 0;
        virtual void setColor(const Color& color) = 0;
        virtual Color getColor() const = 0;
        virtual HPen createPen() const = 0;
    };

    using HStroke = std::shared_ptr<IStroke>;
}

The SquareStroke class is also simple. It just stores the stroke attributes and returns a SquarePen object when createPen() is called.

#pragma once

#include "IStroke.h"

namespace BitmapGraphics
{
    class SquareStroke final : public IStroke
    {
    public:
        SquareStroke(const int& size, const Color& color);

        void setSize(int size) override;
        int getSize() const override;
        void setColor(const Color& color) override;
        Color getColor() const override;
        HPen createPen() const override;

    protected:
        int mySize;
        Color myColor;
    };
}

Like so:

    HPen SquareStroke::createPen() const
    {
        return std::make_unique<SquarePen>(getSize(), getColor());
    }

4.2 Implement IPen and SquarePen

IPen is a simple interface with a single method:

#pragma once

#include "ICanvas.h"
#include <memory>

namespace BitmapGraphics
{
    class IPen
    {
    public:
        virtual void drawPoint(const HCanvas& canvas, const VG::Point& point) = 0;
    };

    using HPen = std::unique_ptr<IPen>;
}

The SquarePen class is where the magic happens. It uses a LineIterator to iterate through pixels and draw them on the canvas.

#pragma once

#include "IPen.h"
#include "ICanvas.h"
#include "Color.h"

namespace BitmapGraphics
{
    class SquarePen final : public IPen
    {
    public:
        SquarePen(int size, const Color& color);
        void drawPoint(const HCanvas& canvas, const VG::Point& point) override;

    private:
        int mySize;
        Color myColor;
    };
}

drawPoint() simply just iterates through the pixels and calls setPixelColor() on the canvas:

#include "SquarePen.h"
#include "Point.h"

namespace BitmapGraphics
{
    SquarePen::SquarePen(const int size, const Color& color) :
        mySize(size),
        myColor(color)
    {
    }

    void SquarePen::drawPoint(const HCanvas& canvas, const VG::Point& point)
    {
        const VG::Point upperLeft(point.getX() - (mySize / 2), point.getY() - (mySize / 2));

        for (auto i = 0; i < mySize; ++i)
        {
            auto p = upperLeft + VG::Point(0, i);

            for (auto j = 0; j < mySize; ++j)
            {
                p += VG::Point(1, 0);

                if (inRect(p, 0, 0, canvas->getWidth(), canvas->getHeight()))
                {
                    canvas->setPixelColor(p, myColor);
                }
            }
        }
    }
}

The LineIterator class is provided for us. We will not go into detail here.


5. Implement SceneReader

5.1 Implement SceneReader

The SceneReader class is responsible for reading a scene from an XML file. It uses the XmlReader class to parse the XML file and create a Scene object. Note the only thing new here is the readStroke() method, which reads the stroke attributes from the XML file.

    VG::VectorGraphic readVectorGraphic(const Xml::Element& vgElement)
    {
        VG::VectorGraphic vg;

        const auto closed = vgElement.getAttribute("closed");
        if (closed == "true")
        {
            vg.closeShape();
        }
        else if (closed == "false")
        {
            vg.openShape();
        }
        else
        {
            throw std::runtime_error("Invalid VectorGraphic attribute");
        }

        auto elements = vgElement.getChildElements();
        for (const auto& element : elements)
        {
            if (element->getName() == "Point")
            {
                const auto x = toInt(element->getAttribute("x"));
                const auto y = toInt(element->getAttribute("y"));
                vg.addPoint({ x, y });
            }
            else if (element->getName() == "Stroke") // New code
            {
                const auto tip = element->getAttribute("tip");
                const auto size = toInt(element->getAttribute("size"));
                auto color = toColor(element->getAttribute("color"));
                addStroke(vg, tip, size, color);
            }
        }

        return vg;
    }