1. Edd Dawson
  2. pngxx


Clone wiki

pngxx / save_from_custom_image

Saving an image from a custom image object

One of the major design goals of pngxx and jpegxx was to provide the ability to save images directly from your own image object.

You don't need to use one provided by pngxx or jpegxx. Indeed, it is impossible to guess what every possible user might need of an image container.

So, let us suppose that there is a class rgb_image that looks something like this:

struct rgb
    rgb(unsigned char r, unsigned char g, unsigned char b);
    unsigned char r, g, b;

class rgb_image
        // Get the width of the image (in pixels)
        unsigned width() const;

        // Get the height of the image (in pixels)
        unsigned height() const;

        // Get the pixel in the ith column, jth row
        const rgb &operator() (unsigned i, unsigned j) const;

    // ...

Our aim is now to save the image as a PNG using pngxx. The process is entirely analogous for saving a JPEG image with jpegxx.

We'd like to be able to write something like:

imagexx::details d(imagexx::rgb, my_rgb_image.width(), my_rgb_image.height());

some_kind_of_iterator b = ???;
some_kind_of_iterator e = ???;

pngxx::write_image(d, "my_image.png", b, e);

imagexx provides a pixel_decompose_iterator, which will be the "some_kind_of_iterator" in the above code. To create a pair of pixel_decompose_iterators, we first need to create a pixel decomposer for an rgb_image, which must look like the following (though the name of the class can be anything; ours is rgb_pixel_decomposer because it will decompose pixels from an rgb_image).

class rgb_pixel_decomposer
        unsigned char get_red();
        unsigned char get_green();
        unsigned char get_blue();
        unsigned char get_alpha();
        unsigned char get_grey();
        void next_pixel();

        bool operator== (const rgb_pixel_decomposer &other) const;

        // copy construction and assignment must be supported

The member functions of a pixel decomposer are called very often (perhaps millions of times for large images) and they're usually very short, so I'll define them inline:

class rgb_pixel_decomposer
        typedef unsigned char uchar;

        rgb_pixel_decomposer(const rgb_image &img) : img(&img), i(0), j(0) { }

        uchar get_red() { return (*img)(i, j).r; }
        uchar get_green() { return (*img)(i, j).g; }
        uchar get_blue() { return (*img)(i, j).b; }
        uchar get_alpha() { return 255; }

        uchar get_grey()
            double weighted_sum = 0.3 * get_red() + 0.59 * get_green() + 0.11 * get_blue();
            return (weighted_sum > 255.0) ? uchar(255) : uchar(weighted_sum);

        void next_pixel()
            // update i and j, so that the next get_xxx() call will
            // grab data from the next pixel
            i = (i + 1) % img->width();
            if (i == 0) ++j;

        bool operator== (const rgb_pixel_decomposer &other) const
            // Strictly speaking, we should check img == other.img, too, but in practice,
            // this check isn't really needed.
            return i == other.i && j == other.j;

        // The compiler generated copy constructor and assignment operator are both fine 

        const rgb_image *img;
        unsigned i, j;

Since the image container we're reading from doesn't support alpha, the get_alpha() implementation always returns 255, meaning "full opacity". If the image class you're using does support alpha, you'll obviously want something else in there.

You don't need to define the get_grey() or get_alpha() functions if you're saving to an RGB image, but they're shown above for completeness.

As an aside, a lot of image classes store their pixels contiguously in an array. In this case, the pixel decomposer would only need to store a single pointer-to-pixel. next_pixel() would simply increment that pointer and our operator== would simply do a pointer comparison.

Now that we've defined our pixel decomposer, we can create the iterators needed to save the image:

using imagexx::rgb_forwarder;
using imagexx::pixel_decompose_iterator;

imagexx::details d(imagexx::rgb, my_rgb_image.width(), my_rgb_image.height());

rgb_pixel_decomposer decomp(my_rgb_image);
pixel_decompose_iterator<rgb_pixel_decomposer, rgb_forwarder> b(decomp, d);
pixel_decompose_iterator<rgb_pixel_decomposer, rgb_forwarder> e;

// save it!
pngxx::write_image(d, "my_image.png", b, e);

The 2nd template parameter for the pixel_decompose_iterator template class tells the iterator which member functions in the pixel decomposer to call in order to extract the components of each pixel in the source image. It is important that the type() of the imagexx::raster_details object you use corresponds to this 2nd template parameter; there is also imagexx::rgba_forwarder, imagexx::grey_forwarder and imagexx::grey_alpha_forwarder, which we would have used if d.type() was imagexx::rgba, imagexx::grey or imagexx::grey_alpha respectively.

In fact, only a small change is needed to save the image as a grey-scale PNG, for example:

using imagexx::grey_forwarder;
using imagexx::pixel_decompose_iterator;

imagexx::details d(imagexx::grey, my_rgb_image.width(), my_rgb_image.height());

rgb_pixel_decomposer decomp(my_rgb_image);
pixel_decompose_iterator<rgb_pixel_decomposer, grey_forwarder> b(decomp, d);
pixel_decompose_iterator<rgb_pixel_decomposer, grey_forwarder> e;

// save it!
pngxx::write_image(d, "my_image.png", b, e);

The same applies for grey-alpha and rgba images too (though they would be silly in this example because there's no alpha component in the source image and saving the extra channel would be a waste of space).