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
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;
}