2.2 Thumbnail Class

2.2 Thumbnail Class

In our design, the image class is a container for image pixels and nothing else. Since the purpose of this application is to create thumbnail images, a thumbnail class is needed to handle file I/O and the thumbnail algorithm. The thumbnail class has the following properties:

• Reads the input image from a file.

• Computes the thumbnail image given the input file and the reduction factor to use (i.e., how small a thumbnail image to create).

• Throws a C++ exception, invalid , if any errors are encountered . If the image class throws a rangeError error, this exception is caught and the invalid exception is thrown instead.

• Writes the thumbnail image to a file.

The complete definition of the thumbnail class is shown in Section 2.3.2 on page 16.

2.2.1 Thumbnail Algorithm

Each pixel in the thumbnail image is found by averaging a number of pixels in the input image. The pixel value in the thumbnail image, T(x0,y0) , is computed as shown in Figure 2.1, where factor is the desired reduction value.

Figure 2.1. Computing the Thumbnail Pixel Value

A picture helps clarify this equation. Each pixel in the original image P is reduced, by averaging pixel values in image P , to a corresponding point in the thumbnail image T using the equation from Figure 2.1. In Figure 2.2, we show how a group of pixels in the original image is reduced to the group of pixels shown in the thumbnail image.

Figure 2.2. Pictorial Representation of Thumbnail Computation

To further simplify our application, we ignore the condition created by integer arithmetic where the division by the reduction factor results in a fraction. For example, if the original image is 640x480 pixels in size, and the desired reduction factor is 6 , the thumbnail image will be (640/6)x(480/6) pixels in size or 106.67x80 . To avoid the fractional result, we ignore the last 4 pixels in each line, effectively making the thumbnail image (636/6)x(480/6) pixels in size or 106x80 .

2.3 Implementation

This section shows the implementation of both the image class and the thumbnail classes. For brevity in the book, we have omitted many of the comments in the code snippets. We have chosen instead to explain our design choices in a narrative fashion. The full implementation, including complete comments, can be found on the CD-ROM.

2.3.1 Image Class

We define our image class in image.h as follows :

```class apImage
{
public:
apImage  ();
apImage  (int width, int height);
~apImage ();

apImage            (const apImage& src);
apImage& operator= (const apImage& src);

void swap (apImage& src);

void          setPixel (int x, int y, unsigned char pixel);
unsigned char getPixel (int x, int y) const;

bool isValid () const { return pixels_ != 0;}
// Tests if the image data exists, and presumably valid.

int width ()  const { return width_;}    // Image width
int height () const { return height_;}   // Image height

class rangeError {};   // Exception class

private:
void init    ();
void cleanup ();
// Initialize or cleanup the allocated image data

int width_;              // Image width
int height_;             // Image height
```

The implementation for the apImage class is straightforward. We'll start with memory allocation and deallocation.

MEMORY ALLOCATION AND DEALLOCATION

Instead of duplicating code every time the image data is allocated or deleted, we define init() and cleanup() functions.

The init() function uses the new operator to allocate memory for storing image data of size width x height .

The orthogonal function is cleanup() which deletes any memory allocated to the apImage class, as shown here.

```void apImage::cleanup ()
{
// Put the object back into its original, null state.
delete [] pixels_;

width_  = 0;
height_ = 0;
pixels_ = 0;
}

void apImage::init ()
{
// All memory allocation passes through this function.
if (width_ > 0 && height_ > 0)
pixels_ = new unsigned char [width_ * height_];
}
```

 Create orthogonal functions whenever possible. For example, if your class has an open () function, you should also have a close() function.

CONSTRUCTORS AND DESTRUCTORS

By using init() and cleanup() , our constructors and destructors become very simple.

```apImage::apImage () : width_ (0), height_ (0), pixels_ (0)
{}
apImage::apImage (int width, int height)
: width_ (width), height_ (height), pixels_ (0)
{
init ();
}
apImage::~apImage ()
{
cleanup ();
}
```

 Constructors should initialize all data members to a default state.

The default constructor for apImage creates a null image which is an image with no memory allocation and zero width and height. Null images are very useful objects, and are necessary because some image processing operations can legitimately be null. For example, image processing routines often operate on a particular set of pixels present in multiple images. If there is no overlap in pixels among the images, then a null image is returned. A null image is also returned if an error occurs during an image processing operation. The isValid() member function tests whether an image is null as shown here.

```apImage input;
...
apImage output = input.arbitraryAlgorithm (args);
if (!output.isValid()) {
// Error processing. arbitraryAlgorithm failed.
}
```

ASSIGNMENT OPERATOR AND COPY CONSTRUCTOR

We define an assignment operator and copy constructor, because the one that the compiler automatically generates performs a member-wise copy of the class data, which incorrectly copies the image data pointer pixels_ . So, we use the standard C library memcpy () function to duplicate the image data:

```apImage::apImage (const apImage& src)
: width_ (0), height_ (0), pixels_ (0)
{
if (src.isValid()) {
width_  = src.width ();
height_ = src.height ();
init ();
memcpy (pixels_, src.pixels_, width_ * height_);
}
}

apImage& apImage::operator= (const apImage& src)
{
if (&src != this) {
// Delete any existing data and recreate the source image
cleanup ();
width_  = src.width ();
height_ = src.height ();
init ();

memcpy (pixels_, src.pixels_, width_ * height_);
}

return *this;
}
```

 Use the version of memcpy() or std::copy() supplied on your system. It will be difficult to write a faster version.

As shown, our copy constructor and assignment operator use much of the same code. One way to eliminate this duplication is to use Sutter's technique for writing safe assignment operators. See [Sutter00]. In a nutshell , his technique requires that you define the assignment operator by calling the copy constructor. In addition, his technique requires that you define a swap() function, which switches the data of two apImage objects as shown here.

```template<class T> void swap (T& a, T& b)
{
T copy(a);
a = b;
b = copy;
}

void apImage::swap (apImage& src)
{
::swap (width_,  src.width_);
::swap (height_, src.height_);
::swap (pixels_, src.pixels_);
}

apImage& apImage::operator= (const apImage& src)
{
apImage temp (src);
swap (temp);

return *this;
}
```

The swap<> template function simply exchanges the data of two objects of the same type. It allows us to implement a swap() method for the apImage class that exchanges all of the data in the object with another apImage object. The assignment operator uses the copy constructor to create a copy of src , which is then exchanged with the existing data members of the object. When the assignment operator returns, the temporary apImage object is automatically destroyed . This behavior is especially useful because it guarantees that if any exceptions are thrown, the object will not be left in a partially constructed state.

Our apImage class is complete once we define the functions that read and write image pixels. Both will throw rangeError if the coordinates do not specify a pixel within the image, or if the image is null.

```void apImage::setPixel (int x, int y, unsigned char pixel)
{
if (x < 0  y < 0
x >= width_  y >= height_
!isValid())
throw rangeError ();
unsigned char* p = pixels_ + y*width_ + x;
*p = pixel;
}

unsigned char apImage::getPixel (int x, int y) const
{
if (x < 0  y < 0
x >= width_  y >= height_
!isValid())
throw rangeError ();
// Image data is stored a row at a time.
unsigned char* p = pixels_ + y*width_ + x;
return *p;
}
```

2.3.2 Thumbnail Class

Now we are ready to define the thumbnail image class, apThumbNail , which computes the thumbnail image from an input image. As with apImage , we initially keep the definition in apThumbnail.h as simple as possible, as shown here.

```class apThumbNail
{
public:
apThumbNail ();
~apThumbNail ();
// The default copy constructor and assignment operator are ok

void createThumbNail (const char* inputFile,
const char* outputFile, int factor);
class invalid {};   // Exception class

private:
void writeImage (const char* outputFile) const;
unsigned char averagePixels (int x0, int y0, int factor);

apImage image_;         // input image
apImage thumbnail_;     // thumbnail image
};
```

In this class, we considered whether a copy/assignment operator is necessary, but decided it is not. Because it is a common oversight to not define a copy/assignment operator, we explicitly include a comment to let others know that this is a deliberate choice.

 If the default copy constructor and assignment operator are acceptable, add a comment stating this to your class declaration.

CONSTRUCTOR AND DESTRUCTOR

Let's discuss the implementation of the thumbnail class, starting with the constructor and destructor.

```apThumbNail::apThumbNail ()
{}
apThumbNail::~apThumbNail ()
{}
```

Although they are both empty, they do serve a purpose. A common mistake during development is to add a new data member to an object and forget to initialize it in the constructor. Defining a constructor makes it more likely that you will remember to initialize new data members.

In addition, you may wonder why these trivial functions are not placed in the header file as an inline definition. Keeping these definitions in the source file is appropriate, because the constructor and destructor are apt to be modified frequently during development and maintenance. We have also found that on some platforms, especially embedded, cross-compiled platforms, inlined constructors cause the memory footprint of an application to increase, because the compiler adds additional housekeeping code. This isn't a problem if the object is referenced only a few times in the code, but, if used frequently, it can dramatically increase the memory footprint.

 Except in very trivial objects, never define a constructor or destructor in the header file. These functions are apt to change frequently and inlining these definitions can increase the memory footprint on some platforms.

R EADING AND W RITING I MAGES

Let's skip ahead to the implementation of the readImage() and writeImage() functions. These functions are designed to read an image from a file and convert it into an apImage class, or to write an apImage to a file, respectively. There are numerous file formats designed to store image data, and we deal with them later. For now, we simulate these functions to quickly verify that the design is working properly, before getting bogged down in the details of supporting numerous file formats. readImage() creates an apImage and populates it with image pixels, and writeImage() simply displays the apImage . readImage() also demonstrates how we catch the rangeError thrown by apImage and rethrow it as an invalid error.

```void apThumbNail::readImage  (const char* /*inputFile*/)
{
// Create a synthetic 64x64 image
image_ = apImage (64, 64);
try {
for (int y=0; y<image_.height(); y++)
for (int x=0; x<image_.width(); x++)
image_.setPixel (x, y, (unsigned char)(y % 255));
}
catch (apImage::rangeError) {
throw invalid ();
}
}

void apThumbNail::writeImage  (const char* /*outputFile*/) const
{
// Save formatting state of stream
std::ios_base::fmtflags flags = std::cout.flags (std::cout.hex);
int width = std::cout.width (2);

for (int y=0; y< thumbnail_.height(); y++) {
for (int x=0; x< thumbnail_.width(); x++)
std::cout << (int) thumbnail_.getPixel (x, y) << " ";
std::cout << std::endl;
}
std::cout.flags (flags);
std::cout.width (width);
}
```

CREATING THUMBNAIL IMAGES

With that out of the way, we can look at the function that actually does the work. createThumbNail() takes the name of an input file, the name of the output thumbnail file to create, and how much reduction is desired. Once the input image is read and the thumbnail image is allocated, the function loops through each pixel in the thumbnail image and computes its value:

```void apThumbNail::createThumbNail (const char* inputFile,
const char* outputFile,
int factor)
{
// Validate the arguments
if (inputFile == 0  outputFile == 0
factor <= 1)
throw invalid ();

if (!image_.isValid())
throw invalid ();

// Create our internal thumbnail image
thumbnail_ = apImage (image_.width()  / factor,
image_.height() / factor);

// Turn any rangeErrors from apImage into our invalid error
unsigned char pixel;
try {
for (int y=0; y<thumbnail_.height(); y++) {
for (int x=0; x<thumbnail_.width(); x++) {
// Convert to image_ coordinates to find the average
pixel = averagePixels (x*factor, y*factor, factor);
thumbnail_.setPixel (x, y, pixel);
}
}
}
catch (apImage::rangeError) {
throw invalid ();
}

writeImage (outputFile);
}
```

createThumbNail() calls averagePixels() to compute the average of pixels in the input image needed to compute a single pixel in the thumbnail image. To be precise, the pixels in a small factor x factor subset of image_ are summed and averaged as shown:

```unsigned char apThumbNail::averagePixels (int x0, int y0,
int factor)
{
int sum = 0;

// Average factor x factor pixels in the input image
try {
for (int y=0; y<factor; y++) {
for (int x=0; x<factor; x++)
sum += image_.getPixel (x+x0, y+y0);
}
}
catch (apImage::rangeError) {
throw invalid ();
}

// This cast (an int to an unsigned char) is very safe
return static_cast<unsigned char>(sum / (factor * factor));
}
```

Although apThumbNail is a simple class, it can also be confusing, because it contains two images with different coordinate systems. In our example, the input image has dimensions width x height , while the thumbnail image has dimensions width/factor x height/factor . Coding mistakes can be made when these coordinate systems are accidentally swapped or blended together. Even though the getPixel() and setPixel() methods in apImage will throw an error if the coordinates are out of bounds, these problems will not be found until a unit test is written (or worse , if there is no unit test, until the integration and test stage of the project).

 Functions should not mix or blend coordinate systems, because this greatly increases the chances of introducing bugs .

To decrease the likelihood of bugs, the coordinates used in averagePixels() are all in terms of the input image, image_ . Likewise, the coordinates used in createThumbNail() are mostly in terms of the thumbnail image.

The following line creates the thumbnail image, given the dimensions of the input image, and is written to be self-documenting :

```thumbnail_ = apImage (image_.width()  / factor,
image_.height() / factor);
```

The line that computes the average isn't as obvious, so we add a comment:

```// Convert to image_ coordinates to find the average
pixel = averagePixels (x*factor, y*factor, factor);
```