1. Edd Dawson
  2. pngxx

Wiki

Clone wiki

pngxx / load_into_custom_image

Loading an image in to a custom image object

One of the major design goals of pngxx and jpegxx was to provide the ability to load images directly in to 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
{
    public:
        // Constructs a blank image of the given size.
        rgb_image(unsigned width, unsigned height);

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

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

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

    // ...
};

Our aim is now to read the data from a PNG or JPEG file in to such an rgb_image object. We will assume that a PNG image is being loaded and will therefore use pngxx. The process is entirely analogous for loading JPEG images with jpegxx.

Let's suppose we're handed an std::string containing the file name of an image. We'll first need to read the header of the image so that we can construct a blank rgb_image of the correct dimensions:

typedef std::istreambuf_iterator<char> it_t;

std::ifstream src(filename.c_str(), std::ios::binary); // binary flag is important
if (!src) throw std::runtime_error("failed to open file: " + filename);

it_t b(src), e; // beginning and end of the stream

pngxx::loader ldr;
it_t resume = ldr.read_header(b, e);
const imagexx::raster_details &d = ldr.details();

// construct the image
rgb_image img(d.width(), d.height());

We've now read the header of the image file and constructed an image of the appropriate size. But it's still blank. Let's fill it in. We first need to define a little class, called a pixel composer. It must look like the following (though the name of the class can be anything you like; I'm using rgb_pixel_composer here because it will compose pixels for an rgb_image):

class rgb_pixel_composer
{
    public:
        void set_rgb(unsigned char r, unsigned char g, unsigned char b);
        void set_grey(unsigned char g);
        void set_rgba(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
        void set_grey_alpha(unsigned char g, unsigned char a);
        void next_pixel();

        // copy construction and assignment must be supported
};

The set_rgb, set_grey, set_rgba and set_grey_alpha functions must be implemented to assign a colour to the "current pixel" of an image. If we were loading a JPEG image using jpegxx, we wouldn't need to have set_rgba or set_grey_alpha functions in our composer because the JPEG compression scheme does not support transparency.

So let's implement the rgb_pixel_composer functions. They're all short and one of them will be called to fill in each pixel of an rgb_image, meaning that they have the potential to be called millions of times per image (depending on the size of the image). So I'll define the functions inline:

class rgb_pixel_composer
{
    public:
        typedef unsigned char uchar;
        rgb_pixel_composer(rgb_image &img) : img(&img), i(0), j(0) { }

        void set_rgb(uchar r, uchar g, uchar b) { (*img)(i, j) = rgb(r, g, b); }
        void set_grey(uchar g) { set_rgb(g, g, g); }
        void set_rgba(uchar r, uchar g, uchar b, uchar a) { set_rgb(r, g, b); }
        void set_grey_alpha(uchar g, uchar a) { set_rgb(g, g, g); }
        void next_pixel()
        {
            // update i and j, so that the next set_xxx() call will 
            // fill in the next pixel
            i = (i + 1) % img->width();
            if (i == 0) ++j;
        }

    private:
        rgb_image *img;
        unsigned i, j;
};

Since the image we're reading in to is only able to hold RGB pixels, each function forwards its data on to set_rgb(). If you require more sophisticated conversions, you are free to implement them however you like.

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

Ok, now we complete the loading of the image. To do this we need to construct an output iterator. imagexx provides a template class called pixel_compose_iterator that can use a pixel composer, such as the one we've defined, for output.

using imagexx::pixel_compose_iterator;
using imagexx::rgb_forwarder;
using imagexx::rgba_forwarder;
using imagexx::grey_forwarder;
using imagexx::grey_alpha_forwarder;

rgb_pixel_composer c(img); // create our pixel composer

if (d.type() == imagexx::rgba)
{
    pixel_compose_iterator<rgb_pixel_composer, rgba_forwarder> out(c);
    ldr.read_raster(resume, e, out);
}
else if (d.type() == imagexx::rgb)
{
    pixel_compose_iterator<rgb_pixel_composer, rgb_forwarder> out(c);
    ldr.read_raster(resume, e, out);
}
else if (d.type() == imagexx::grey)
{
    pixel_compose_iterator<rgb_pixel_composer, grey_forwarder> out(c);
    ldr.read_raster(resume, e, out);
}
else
{
    assert(d.type() == imagexx::grey_alpha);
    pixel_compose_iterator<rgb_pixel_composer, grey_alpha_forwarder> out(c);
    ldr.read_raster(resume, e, out);
}

Here, we're looking at the type of image being read and constructing an appropriate pixel_compose_iterator that uses an rgb_pixel_composer. The 2nd template parameter tells the pixel_compose_iterator which member function of the pixel composer to call. If you're reading a JPEG image with jpegxx, you won't need to worry about the two formats that have an alpha component.

The PNG image has now been read in to an rgb_image, assuming an error didn't occur, in which case an exception would have been thrown.

Updated