/* -*- mode: c++; c-basic-offset: 4 -*- */

/* Python API mandates Python.h is included *first* */
#include "Python.h"
#include <string>

#include <iostream>
#include <fstream>
#include <cmath>
#include <cstdio>

#include "numpy/arrayobject.h"

#include "agg_color_rgba.h"
#include "agg_conv_transform.h"
#include "agg_image_accessors.h"
#include "agg_path_storage.h"
#include "agg_pixfmt_rgb.h"
#include "agg_pixfmt_rgba.h"
#include "agg_rasterizer_scanline_aa.h"
#include "agg_renderer_scanline.h"
#include "agg_rendering_buffer.h"
#include "agg_scanline_bin.h"
#include "agg_scanline_bin.h"
#include "agg_scanline_u.h"
#include "agg_span_allocator.h"
#include "agg_span_image_filter_rgb.h"
#include "agg_span_image_filter_rgba.h"
#include "agg_span_interpolator_linear.h"
#include "agg_rasterizer_sl_clip.h"
#include "util/agg_color_conv_rgb8.h"
#include "_image.h"
#include "mplutils.h"


typedef agg::pixfmt_rgba32 pixfmt;
typedef agg::pixfmt_rgba32_pre pixfmt_pre;
typedef agg::renderer_base<pixfmt> renderer_base;
typedef agg::span_interpolator_linear<> interpolator_type;
typedef agg::rasterizer_scanline_aa<agg::rasterizer_sl_clip_dbl> rasterizer;


Image::Image() :
        bufferIn(NULL), rbufIn(NULL), colsIn(0), rowsIn(0),
        bufferOut(NULL), rbufOut(NULL), colsOut(0), rowsOut(0),  BPP(4),
        interpolation(BILINEAR), aspect(ASPECT_FREE), bg(1, 1, 1, 0), resample(true)
{
    _VERBOSE("Image::Image");
}

Image::~Image()
{
    _VERBOSE("Image::~Image");
    delete [] bufferIn;
    bufferIn = NULL;
    delete rbufIn;
    rbufIn = NULL;
    delete rbufOut;
    rbufOut = NULL;
    delete [] bufferOut;
    bufferOut = NULL;
}

int
Image::setattr(const char * name, const Py::Object & value)
{
    _VERBOSE("Image::setattr");
    __dict__[name] = value;
    return 0;
}

Py::Object
Image::getattr(const char * name)
{
    _VERBOSE("Image::getattro");
    if (__dict__.hasKey(name)) return __dict__[name];
    else return getattr_default(name);
}

char Image::apply_rotation__doc__[] =
    "apply_rotation(angle)\n"
    "\n"
    "Apply the rotation (degrees) to image"
    ;
Py::Object
Image::apply_rotation(const Py::Tuple& args)
{
    _VERBOSE("Image::apply_rotation");

    args.verify_length(1);
    double r = Py::Float(args[0]);


    agg::trans_affine M = agg::trans_affine_rotation(r * agg::pi / 180.0);
    srcMatrix *= M;
    imageMatrix *= M;
    return Py::Object();
}

char Image::flipud_out__doc__[] =
    "flipud()\n"
    "\n"
    "Flip the output image upside down"
    ;

char Image::flipud_in__doc__[] =
    "flipud()\n"
    "\n"
    "Flip the input image upside down"
    ;
Py::Object
Image::flipud_in(const Py::Tuple& args)
{
    _VERBOSE("Image::flipud_in");

    args.verify_length(0);
    int stride = rbufIn->stride();
    rbufIn->attach(bufferIn, colsIn, rowsIn, -stride);

    return Py::Object();
}

char Image::set_bg__doc__[] =
    "set_bg(r,g,b,a)\n"
    "\n"
    "Set the background color"
    ;

Py::Object
Image::set_bg(const Py::Tuple& args)
{
    _VERBOSE("Image::set_bg");

    args.verify_length(4);
    bg.r = Py::Float(args[0]);
    bg.g = Py::Float(args[1]);
    bg.b = Py::Float(args[2]);
    bg.a = Py::Float(args[3]);
    return Py::Object();
}

char Image::apply_scaling__doc__[] =
    "apply_scaling(sx, sy)\n"
    "\n"
    "Apply the scale factors sx, sy to the transform matrix"
    ;

Py::Object
Image::apply_scaling(const Py::Tuple& args)
{
    _VERBOSE("Image::apply_scaling");

    args.verify_length(2);
    double sx = Py::Float(args[0]);
    double sy = Py::Float(args[1]);

    //printf("applying scaling %1.2f, %1.2f\n", sx, sy);
    agg::trans_affine M = agg::trans_affine_scaling(sx, sy);
    srcMatrix *= M;
    imageMatrix *= M;

    return Py::Object();
}

char Image::apply_translation__doc__[] =
    "apply_translation(tx, ty)\n"
    "\n"
    "Apply the translation tx, ty to the transform matrix"
    ;

Py::Object
Image::apply_translation(const Py::Tuple& args)
{
    _VERBOSE("Image::apply_translation");

    args.verify_length(2);
    double tx = Py::Float(args[0]);
    double ty = Py::Float(args[1]);

    //printf("applying translation %1.2f, %1.2f\n", tx, ty);
    agg::trans_affine M = agg::trans_affine_translation(tx, ty);
    srcMatrix *= M;
    imageMatrix *= M;

    return Py::Object();
}

char Image::as_rgba_str__doc__[] =
    "numrows, numcols, s = as_rgba_str()"
    "\n"
    "Call this function after resize to get the data as string\n"
    "The string is a numrows by numcols x 4 (RGBA) unsigned char buffer\n"
    ;

Py::Object
Image::as_rgba_str(const Py::Tuple& args, const Py::Dict& kwargs)
{
    _VERBOSE("Image::as_rgba_str");

    args.verify_length(0);

    std::pair<agg::int8u*, bool> bufpair = _get_output_buffer();

    #if PY3K
    Py::Object ret =  Py::asObject(Py_BuildValue("lly#", rowsOut, colsOut,
                                   bufpair.first, colsOut * rowsOut * 4));
    #else
    Py::Object ret =  Py::asObject(Py_BuildValue("lls#", rowsOut, colsOut,
                                   bufpair.first, colsOut * rowsOut * 4));
    #endif

    if (bufpair.second) delete [] bufpair.first;
    return ret;
}


char Image::color_conv__doc__[] =
    "numrows, numcols, buffer = color_conv(format)"
    "\n"
    "format 0(BGRA) or 1(ARGB)\n"
    "Convert image to format and return in a writable buffer\n"
    ;
Py::Object
Image::color_conv(const Py::Tuple& args)
{
    _VERBOSE("Image::color_conv");

    args.verify_length(1);
    int format = Py::Int(args[0]);
    PyObject* py_buffer = NULL;
    int row_len = colsOut * 4;
#if PY3K
    unsigned char* buf = (unsigned char *)malloc(row_len * rowsOut);
    if (buf == NULL)
        throw Py::MemoryError("Image::color_conv could not allocate memory");
#else
    py_buffer = PyBuffer_New(row_len * rowsOut);
    if (py_buffer == NULL)
        throw Py::MemoryError("Image::color_conv could not allocate memory");

    void* buf;
    Py_ssize_t buffer_len;
    int ret = PyObject_AsWriteBuffer(py_buffer, &buf, &buffer_len);
    if (ret != 0)
    {
        Py_XDECREF(py_buffer);
        throw Py::MemoryError("Image::color_conv could not allocate memory");
    }
#endif

    agg::rendering_buffer rtmp;
    rtmp.attach(reinterpret_cast<unsigned char*>(buf), colsOut, rowsOut,
                row_len);

    switch (format)
    {
    case 0:
        agg::color_conv(&rtmp, rbufOut, agg::color_conv_rgba32_to_bgra32());
        break;
    case 1:
        agg::color_conv(&rtmp, rbufOut, agg::color_conv_rgba32_to_argb32());
        break;
    default:
        Py_XDECREF(py_buffer);
        throw Py::ValueError("Image::color_conv unknown format");
    }

#if PY3K
    py_buffer = PyByteArray_FromStringAndSize((char *)buf, row_len * rowsOut);
    if (py_buffer == NULL) {
        free(buf);
    }
#endif

    PyObject* o = Py_BuildValue("llN", rowsOut, colsOut, py_buffer);
    return Py::asObject(o);
}

char Image::buffer_rgba__doc__[] =
    "buffer = buffer_rgba()"
    "\n"
    "Return the image buffer as rgba32\n"
    ;
Py::Object
Image::buffer_rgba(const Py::Tuple& args)
{
    //"Return the image object as rgba";

    _VERBOSE("RendererAgg::buffer_rgba");

    args.verify_length(0);
    int row_len = colsOut * 4;
    PyObject* o = Py_BuildValue("lls#", rowsOut, colsOut,
                                rbufOut, row_len * rowsOut);
    return Py::asObject(o);
}

char Image::reset_matrix__doc__[] =
    "reset_matrix()"
    "\n"
    "Reset the transformation matrix"
    ;

Py::Object
Image::reset_matrix(const Py::Tuple& args)
{
    _VERBOSE("Image::reset_matrix");

    args.verify_length(0);
    srcMatrix.reset();
    imageMatrix.reset();

    return Py::Object();
}

char Image::get_matrix__doc__[] =
    "(m11,m21,m12,m22,m13,m23) = get_matrix()\n"
    "\n"
    "Get the affine transformation matrix\n"
    "  /m11,m12,m13\\\n"
    "  /m21,m22,m23|\n"
    "  \\ 0 , 0 , 1 /"
    ;

Py::Object
Image::get_matrix(const Py::Tuple& args)
{
    _VERBOSE("Image::get_matrix");

    args.verify_length(0);

    double m[6];
    srcMatrix.store_to(m);
    Py::Tuple ret(6);
    for (int i = 0;i < 6;i++)
    {
        ret[i] = Py::Float(m[i]);
    }
    return ret;
}

char Image::resize__doc__[] =
    "resize(width, height, norm=1, radius=4.0)\n"
    "\n"
    "Resize the image to width, height using interpolation\n"
    "norm and radius are optional args for some of the filters and must be\n"
    "passed as kwargs\n"
    ;

Py::Object
Image::resize(const Py::Tuple& args, const Py::Dict& kwargs)
{
    _VERBOSE("Image::resize");

    args.verify_length(2);

    int norm = 1;
    if (kwargs.hasKey("norm"))
    {
        norm = Py::Int(kwargs["norm"]);
    }

    double radius = 4.0;
    if (kwargs.hasKey("radius"))
    {
        radius = Py::Float(kwargs["radius"]);
    }

    if (bufferIn == NULL)
    {
        throw Py::RuntimeError("You must first load the image");
    }

    int numcols = Py::Int(args[0]);
    int numrows = Py::Int(args[1]);

    colsOut = numcols;
    rowsOut = numrows;

    size_t NUMBYTES(numrows * numcols * BPP);

    delete [] bufferOut;
    bufferOut = new agg::int8u[NUMBYTES];
    if (bufferOut == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("Image::resize could not allocate memory");
    }

    delete rbufOut;
    rbufOut = new agg::rendering_buffer;
    rbufOut->attach(bufferOut, numcols, numrows, numcols * BPP);

    // init the output rendering/rasterizing stuff
    pixfmt pixf(*rbufOut);
    renderer_base rb(pixf);
    rb.clear(bg);
    rasterizer ras;
    agg::scanline_u8 sl;

    ras.clip_box(0, 0, numcols, numrows);

    //srcMatrix *= resizingMatrix;
    //imageMatrix *= resizingMatrix;
    imageMatrix.invert();
    interpolator_type interpolator(imageMatrix);

    typedef agg::span_allocator<agg::rgba8> span_alloc_type;
    span_alloc_type sa;
    agg::rgba8 background(agg::rgba8(int(255*bg.r),
                                     int(255*bg.g),
                                     int(255*bg.b),
                                     int(255*bg.a)));

    // the image path
    agg::path_storage path;
    agg::rendering_buffer rbufPad;

    double x0, y0, x1, y1;

    x0 = 0.0;
    x1 = colsIn;
    y0 = 0.0;
    y1 = rowsIn;

    path.move_to(x0, y0);
    path.line_to(x1, y0);
    path.line_to(x1, y1);
    path.line_to(x0, y1);
    path.close_polygon();
    agg::conv_transform<agg::path_storage> imageBox(path, srcMatrix);
    ras.add_path(imageBox);

    typedef agg::wrap_mode_reflect reflect_type;
    typedef agg::image_accessor_wrap<pixfmt_pre, reflect_type, reflect_type> img_accessor_type;

    pixfmt_pre pixfmtin(*rbufIn);
    img_accessor_type ia(pixfmtin);
    switch (interpolation)
    {

    case NEAREST:
    {
        if (colsIn == numcols && rowsIn == numrows) {
            memcpy(bufferOut, bufferIn, colsIn * rowsIn * 4);
        } else {
            typedef agg::span_image_filter_rgba_nn<img_accessor_type, interpolator_type> span_gen_type;
            typedef agg::renderer_scanline_aa<renderer_base, span_alloc_type, span_gen_type> renderer_type;
            span_gen_type sg(ia, interpolator);
            renderer_type ri(rb, sa, sg);
            agg::render_scanlines(ras, sl, ri);
        }
    }
    break;

    case HANNING:
    case HAMMING:
    case HERMITE:
    {
        agg::image_filter_lut filter;
        switch (interpolation)
        {
        case HANNING:
            filter.calculate(agg::image_filter_hanning(), norm);
            break;
        case HAMMING:
            filter.calculate(agg::image_filter_hamming(), norm);
            break;
        case HERMITE:
            filter.calculate(agg::image_filter_hermite(), norm);
            break;
        }
        if (resample)
        {
            typedef agg::span_image_resample_rgba_affine<img_accessor_type> span_gen_type;
            typedef agg::renderer_scanline_aa<renderer_base, span_alloc_type, span_gen_type> renderer_type;
            span_gen_type sg(ia, interpolator, filter);
            renderer_type ri(rb, sa, sg);
            agg::render_scanlines(ras, sl, ri);
        }
        else
        {
            typedef agg::span_image_filter_rgba_2x2<img_accessor_type, interpolator_type> span_gen_type;
            typedef agg::renderer_scanline_aa<renderer_base, span_alloc_type, span_gen_type> renderer_type;
            span_gen_type sg(ia, interpolator, filter);
            renderer_type ri(rb, sa, sg);
            agg::render_scanlines(ras, sl, ri);
        }
    }
    break;
    case BILINEAR:
    case BICUBIC:
    case SPLINE16:
    case SPLINE36:
    case KAISER:
    case QUADRIC:
    case CATROM:
    case GAUSSIAN:
    case BESSEL:
    case MITCHELL:
    case SINC:
    case LANCZOS:
    case BLACKMAN:
    {
        agg::image_filter_lut filter;
        switch (interpolation)
        {
        case BILINEAR:
            filter.calculate(agg::image_filter_bilinear(), norm);
            break;
        case BICUBIC:
            filter.calculate(agg::image_filter_bicubic(), norm);
            break;
        case SPLINE16:
            filter.calculate(agg::image_filter_spline16(), norm);
            break;
        case SPLINE36:
            filter.calculate(agg::image_filter_spline36(), norm);
            break;
        case KAISER:
            filter.calculate(agg::image_filter_kaiser(), norm);
            break;
        case QUADRIC:
            filter.calculate(agg::image_filter_quadric(), norm);
            break;
        case CATROM:
            filter.calculate(agg::image_filter_catrom(), norm);
            break;
        case GAUSSIAN:
            filter.calculate(agg::image_filter_gaussian(), norm);
            break;
        case BESSEL:
            filter.calculate(agg::image_filter_bessel(), norm);
            break;
        case MITCHELL:
            filter.calculate(agg::image_filter_mitchell(), norm);
            break;
        case SINC:
            filter.calculate(agg::image_filter_sinc(radius), norm);
            break;
        case LANCZOS:
            filter.calculate(agg::image_filter_lanczos(radius), norm);
            break;
        case BLACKMAN:
            filter.calculate(agg::image_filter_blackman(radius), norm);
            break;
        }
        if (resample)
        {
            typedef agg::span_image_resample_rgba_affine<img_accessor_type> span_gen_type;
            typedef agg::renderer_scanline_aa<renderer_base, span_alloc_type, span_gen_type> renderer_type;
            span_gen_type sg(ia, interpolator, filter);
            renderer_type ri(rb, sa, sg);
            agg::render_scanlines(ras, sl, ri);
        }
        else
        {
            typedef agg::span_image_filter_rgba<img_accessor_type, interpolator_type> span_gen_type;
            typedef agg::renderer_scanline_aa<renderer_base, span_alloc_type, span_gen_type> renderer_type;
            span_gen_type sg(ia, interpolator, filter);
            renderer_type ri(rb, sa, sg);
            agg::render_scanlines(ras, sl, ri);
        }
    }
    break;

    }

    return Py::Object();
}



char Image::get_interpolation__doc__[] =
    "get_interpolation()\n"
    "\n"
    "Get the interpolation scheme to one of the module constants, "
    "one of image.NEAREST, image.BILINEAR, etc..."
    ;

Py::Object
Image::get_interpolation(const Py::Tuple& args)
{
    _VERBOSE("Image::get_interpolation");

    args.verify_length(0);
    return Py::Int((int)interpolation);
}


char Image::get_aspect__doc__[] =
    "get_aspect()\n"
    "\n"
    "Get the aspect constraint constants"
    ;

Py::Object
Image::get_aspect(const Py::Tuple& args)
{
    _VERBOSE("Image::get_aspect");

    args.verify_length(0);
    return Py::Int((int)aspect);
}

char Image::get_size__doc__[] =
    "numrows, numcols = get_size()\n"
    "\n"
    "Get the number or rows and columns of the input image"
    ;

Py::Object
Image::get_size(const Py::Tuple& args)
{
    _VERBOSE("Image::get_size");

    args.verify_length(0);

    Py::Tuple ret(2);
    ret[0] = Py::Int((long)rowsIn);
    ret[1] = Py::Int((long)colsIn);
    return ret;

}

char Image::get_resample__doc__[] =
    "get_resample()\n"
    "\n"
    "Get the resample flag."
    ;

Py::Object
Image::get_resample(const Py::Tuple& args)
{
    _VERBOSE("Image::get_resample");

    args.verify_length(0);
    return Py::Int((int)resample);
}

char Image::get_size_out__doc__[] =
    "numrows, numcols = get_size()\n"
    "\n"
    "Get the number or rows and columns of the output image"
    ;

Py::Object
Image::get_size_out(const Py::Tuple& args)
{
    _VERBOSE("Image::get_size_out");

    args.verify_length(0);

    Py::Tuple ret(2);
    ret[0] = Py::Int((long)rowsOut);
    ret[1] = Py::Int((long)colsOut);
    return ret;
}

//get the output buffer, flipped if necessary.  The second element of
//the pair is a bool that indicates whether you need to free the
//memory
std::pair<agg::int8u*, bool>
Image::_get_output_buffer()
{
    _VERBOSE("Image::_get_output_buffer");
    std::pair<agg::int8u*, bool> ret;
    bool flipy = rbufOut->stride() < 0;
    if (flipy)
    {
        agg::int8u* buffer = new agg::int8u[rowsOut*colsOut*4];
        agg::rendering_buffer rb;
        rb.attach(buffer, colsOut, rowsOut, colsOut*4);
        rb.copy_from(*rbufOut);
        ret.first = buffer;
        ret.second = true;
    }
    else
    {
        ret.first = bufferOut;
        ret.second = false;
    }
    return ret;

}

char Image::set_interpolation__doc__[] =
    "set_interpolation(scheme)\n"
    "\n"
    "Set the interpolation scheme to one of the module constants, "
    "eg, image.NEAREST, image.BILINEAR, etc..."
    ;

Py::Object
Image::set_interpolation(const Py::Tuple& args)
{
    _VERBOSE("Image::set_interpolation");

    args.verify_length(1);

    size_t method = (long)Py::Int(args[0]);
    interpolation = (unsigned)method;
    return Py::Object();
}

char Image::set_resample__doc__[] =
    "set_resample(boolean)\n"
    "\n"
    "Set the resample flag."
    ;

Py::Object
Image::set_resample(const Py::Tuple& args)
{
    _VERBOSE("Image::set_resample");
    args.verify_length(1);
    int flag = Py::Int(args[0]);
    resample = (bool)flag;
    return Py::Object();
}


char Image::set_aspect__doc__[] =
    "set_aspect(scheme)\n"
    "\n"
    "Set the aspect ration to one of the image module constant."
    "eg, one of image.ASPECT_PRESERVE, image.ASPECT_FREE"
    ;
Py::Object
Image::set_aspect(const Py::Tuple& args)
{
    _VERBOSE("Image::set_aspect");

    args.verify_length(1);
    size_t method = (long)Py::Int(args[0]);
    aspect = (unsigned)method;
    return Py::Object();

}

void
Image::init_type()
{
    _VERBOSE("Image::init_type");

    behaviors().name("Image");
    behaviors().doc("Image");
    behaviors().supportGetattr();
    behaviors().supportSetattr();

    add_varargs_method("apply_rotation", &Image::apply_rotation, Image::apply_rotation__doc__);
    add_varargs_method("apply_scaling",  &Image::apply_scaling, Image::apply_scaling__doc__);
    add_varargs_method("apply_translation", &Image::apply_translation, Image::apply_translation__doc__);
    add_keyword_method("as_rgba_str", &Image::as_rgba_str, Image::as_rgba_str__doc__);
    add_varargs_method("color_conv", &Image::color_conv, Image::color_conv__doc__);
    add_varargs_method("buffer_rgba", &Image::buffer_rgba, Image::buffer_rgba__doc__);
    add_varargs_method("get_aspect", &Image::get_aspect, Image::get_aspect__doc__);
    add_varargs_method("get_interpolation", &Image::get_interpolation, Image::get_interpolation__doc__);
    add_varargs_method("get_resample", &Image::get_resample, Image::get_resample__doc__);
    add_varargs_method("get_size", &Image::get_size, Image::get_size__doc__);
    add_varargs_method("get_size_out", &Image::get_size_out, Image::get_size_out__doc__);
    add_varargs_method("reset_matrix", &Image::reset_matrix, Image::reset_matrix__doc__);
    add_varargs_method("get_matrix", &Image::get_matrix, Image::get_matrix__doc__);
    add_keyword_method("resize", &Image::resize, Image::resize__doc__);
    add_varargs_method("set_interpolation", &Image::set_interpolation, Image::set_interpolation__doc__);
    add_varargs_method("set_resample", &Image::set_resample, Image::set_resample__doc__);
    add_varargs_method("set_aspect", &Image::set_aspect, Image::set_aspect__doc__);
    add_varargs_method("set_bg", &Image::set_bg, Image::set_bg__doc__);
    add_varargs_method("flipud_out", &Image::flipud_out, Image::flipud_out__doc__);
    add_varargs_method("flipud_in", &Image::flipud_in, Image::flipud_in__doc__);
}




char _image_module_from_images__doc__[] =
    "from_images(numrows, numcols, seq)\n"
    "\n"
    "return an image instance with numrows, numcols from a seq of image\n"
    "instances using alpha blending.  seq is a list of (Image, ox, oy)"
    ;
Py::Object
_image_module::from_images(const Py::Tuple& args)
{
    _VERBOSE("_image_module::from_images");

    args.verify_length(3);

    size_t numrows = (long)Py::Int(args[0]);
    size_t numcols = (long)Py::Int(args[1]);

    if (numrows >= 32768 || numcols >= 32768)
    {
        throw Py::RuntimeError("numrows and numcols must both be less than 32768");
    }

    Py::SeqBase<Py::Object> tups = args[2];
    size_t N = tups.length();

    if (N == 0)
    {
        throw Py::RuntimeError("Empty list of images");
    }

    Py::Tuple tup;

    size_t ox(0), oy(0), thisx(0), thisy(0);

    //copy image 0 output buffer into return images output buffer
    Image* imo = new Image;
    imo->rowsOut  = numrows;
    imo->colsOut  = numcols;

    size_t NUMBYTES(numrows * numcols * imo->BPP);
    imo->bufferOut = new agg::int8u[NUMBYTES];
    if (imo->bufferOut == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("_image_module::from_images could not allocate memory");
    }

    delete imo->rbufOut;
    imo->rbufOut = new agg::rendering_buffer;
    imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);

    pixfmt pixf(*imo->rbufOut);
    renderer_base rb(pixf);

    rb.clear(agg::rgba(1, 1, 1, 1));
    for (size_t imnum = 0; imnum < N; imnum++)
    {
        tup = Py::Tuple(tups[imnum]);
        Image* thisim = static_cast<Image*>(tup[0].ptr());
        ox = (long)Py::Int(tup[1]);
        oy = (long)Py::Int(tup[2]);
        bool isflip = (thisim->rbufOut->stride()) < 0;
        //std::cout << "from images " << isflip << "; stride=" << thisim->rbufOut->stride() << std::endl;
        size_t ind = 0;
        for (size_t j = 0; j < thisim->rowsOut; j++)
        {
            for (size_t i = 0; i < thisim->colsOut; i++)
            {
                thisx = i + ox;

                if (isflip)
                {
                    thisy = thisim->rowsOut - j + oy;
                }
                else
                {
                    thisy = j + oy;
                }

                if (thisx >= numcols || thisy >= numrows)
                {
                    ind += 4;
                    continue;
                }

                pixfmt::color_type p;
                p.r = *(thisim->bufferOut + ind++);
                p.g = *(thisim->bufferOut + ind++);
                p.b = *(thisim->bufferOut + ind++);
                p.a = *(thisim->bufferOut + ind++);
                pixf.blend_pixel(thisx, thisy, p, 255);
            }
        }
    }

    return Py::asObject(imo);
}


char _image_module_fromarray__doc__[] =
    "fromarray(A, isoutput)\n"
    "\n"
    "Load the image from a numpy array\n"
    "By default this function fills the input buffer, which can subsequently\n"
    "be resampled using resize.  If isoutput=1, fill the output buffer.\n"
    "This is used to support raw pixel images w/o resampling"
    ;
Py::Object
_image_module::fromarray(const Py::Tuple& args)
{
    _VERBOSE("_image_module::fromarray");

    args.verify_length(2);

    Py::Object x = args[0];
    int isoutput = Py::Int(args[1]);
    PyArrayObject *A = (PyArrayObject *) PyArray_FromObject(x.ptr(), PyArray_DOUBLE, 2, 3);
    if (A == NULL)
    {
        throw Py::ValueError("Array must be rank 2 or 3 of doubles");
    }
    Py::Object A_obj((PyObject *)A, true);

    Image* imo = new Image;

    imo->rowsIn  = A->dimensions[0];
    imo->colsIn  = A->dimensions[1];

    size_t NUMBYTES(imo->colsIn * imo->rowsIn * imo->BPP);
    agg::int8u *buffer = new agg::int8u[NUMBYTES];
    if (buffer == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("_image_module::fromarray could not allocate memory");
    }

    if (isoutput)
    {
        // make the output buffer point to the input buffer
        imo->rowsOut  = imo->rowsIn;
        imo->colsOut  = imo->colsIn;

        imo->rbufOut = new agg::rendering_buffer;
        imo->bufferOut = buffer;
        imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);
    }
    else
    {
        imo->bufferIn = buffer;
        imo->rbufIn = new agg::rendering_buffer;
        imo->rbufIn->attach(buffer, imo->colsIn, imo->rowsIn, imo->colsIn*imo->BPP);
    }

    if (A->nd == 2)     //assume luminance for now;
    {
        agg::int8u gray;
        for (size_t rownum = 0; rownum < imo->rowsIn; rownum++)
        {
            for (size_t colnum = 0; colnum < imo->colsIn; colnum++)
            {
                double val = *(double *)(A->data + rownum * A->strides[0] + colnum * A->strides[1]);

                gray = int(255 * val);
                *buffer++ = gray;       // red
                *buffer++ = gray;       // green
                *buffer++ = gray;       // blue
                *buffer++   = 255;        // alpha
            }
        }
    }
    else if (A->nd == 3)     // assume RGB
    {

        if (A->dimensions[2] != 3 && A->dimensions[2] != 4)
        {
            throw Py::ValueError(Printf("3rd dimension must be length 3 (RGB) or 4 (RGBA); found %d", A->dimensions[2]).str());
        }

        int rgba = A->dimensions[2] == 4;
        double r, g, b, alpha;
        size_t offset = 0;

        for (size_t rownum = 0; rownum < imo->rowsIn; rownum++)
        {
            for (size_t colnum = 0; colnum < imo->colsIn; colnum++)
            {
                offset = rownum * A->strides[0] + colnum * A->strides[1];
                r = *(double *)(A->data + offset);
                g = *(double *)(A->data + offset + A->strides[2]);
                b = *(double *)(A->data + offset + 2 * A->strides[2]);

                if (rgba)
                {
                    alpha = *(double *)(A->data + offset + 3 * A->strides[2]);
                }
                else
                {
                    alpha = 1.0;
                }

                *buffer++ = int(255 * r);       // red
                *buffer++ = int(255 * g);       // green
                *buffer++ = int(255 * b);       // blue
                *buffer++ = int(255 * alpha);   // alpha
            }
        }
    }
    else     // error
    {
        throw Py::ValueError("Illegal array rank; must be rank; must 2 or 3");
    }
    buffer -= NUMBYTES;

    return Py::asObject(imo);
}

char _image_module_fromarray2__doc__[] =
    "fromarray2(A, isoutput)\n"
    "\n"
    "Load the image from a numpy array\n"
    "By default this function fills the input buffer, which can subsequently\n"
    "be resampled using resize.  If isoutput=1, fill the output buffer.\n"
    "This is used to support raw pixel images w/o resampling"
    ;
Py::Object
_image_module::fromarray2(const Py::Tuple& args)
{
    _VERBOSE("_image_module::fromarray2");

    args.verify_length(2);

    Py::Object x = args[0];
    int isoutput = Py::Int(args[1]);
    PyArrayObject *A = (PyArrayObject *) PyArray_ContiguousFromObject(x.ptr(), PyArray_DOUBLE, 2, 3);
    if (A == NULL)
    {
        throw Py::ValueError("Array must be rank 2 or 3 of doubles");
    }
    Py::Object A_obj((PyObject*)A, true);

    Image* imo = new Image;

    imo->rowsIn  = A->dimensions[0];
    imo->colsIn  = A->dimensions[1];

    size_t NUMBYTES(imo->colsIn * imo->rowsIn * imo->BPP);
    agg::int8u *buffer = new agg::int8u[NUMBYTES];
    if (buffer == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("_image_module::fromarray could not allocate memory");
    }

    if (isoutput)
    {
        // make the output buffer point to the input buffer
        imo->rowsOut  = imo->rowsIn;
        imo->colsOut  = imo->colsIn;

        imo->rbufOut = new agg::rendering_buffer;
        imo->bufferOut = buffer;
        imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);
    }
    else
    {
        imo->bufferIn = buffer;
        imo->rbufIn = new agg::rendering_buffer;
        imo->rbufIn->attach(buffer, imo->colsIn, imo->rowsIn, imo->colsIn*imo->BPP);
    }

    if (A->nd == 2)     //assume luminance for now;
    {
        agg::int8u gray;
        const size_t N = imo->rowsIn * imo->colsIn;
        size_t i = 0;
        while (i++ < N)
        {
            double val = *(double *)(A->data++);

            gray = int(255 * val);
            *buffer++ = gray;       // red
            *buffer++ = gray;       // green
            *buffer++ = gray;       // blue
            *buffer++   = 255;        // alpha
        }

    }
    else if (A->nd == 3)     // assume RGB
    {
        if (A->dimensions[2] != 3 && A->dimensions[2] != 4)
        {
            throw Py::ValueError(Printf("3rd dimension must be length 3 (RGB) or 4 (RGBA); found %d", A->dimensions[2]).str());

        }

        int rgba = A->dimensions[2] == 4;
        double r, g, b, alpha;
        const size_t N = imo->rowsIn * imo->colsIn;
        for (size_t i = 0; i < N; ++i)
        {
            r = *(double *)(A->data++);
            g = *(double *)(A->data++);
            b = *(double *)(A->data++);

            if (rgba)
                alpha = *(double *)(A->data++);
            else
                alpha = 1.0;

            *buffer++ = int(255 * r);       // red
            *buffer++ = int(255 * g);       // green
            *buffer++ = int(255 * b);       // blue
            *buffer++ = int(255 * alpha);   // alpha

        }
    }
    else     // error
    {
        throw Py::ValueError("Illegal array rank; must be rank; must 2 or 3");
    }
    buffer -= NUMBYTES;

    return Py::asObject(imo);
}

char _image_module_frombyte__doc__[] =
    "frombyte(A, isoutput)\n"
    "\n"
    "Load the image from a byte array.\n"
    "By default this function fills the input buffer, which can subsequently\n"
    "be resampled using resize.  If isoutput=1, fill the output buffer.\n"
    "This is used to support raw pixel images w/o resampling."
    ;
Py::Object
_image_module::frombyte(const Py::Tuple& args)
{
    _VERBOSE("_image_module::frombyte");

    args.verify_length(2);

    Py::Object x = args[0];
    int isoutput = Py::Int(args[1]);

    PyArrayObject *A = (PyArrayObject *) PyArray_FromObject(x.ptr(), PyArray_UBYTE, 3, 3);
    if (A == NULL)
    {
        throw Py::ValueError("Array must have 3 dimensions");
    }
    Py::Object A_obj((PyObject*)A, true);

    if (A->dimensions[2] < 3 || A->dimensions[2] > 4)
    {
        throw Py::ValueError("Array dimension 3 must have size 3 or 4");
    }

    Image* imo = new Image;

    imo->rowsIn = A->dimensions[0];
    imo->colsIn = A->dimensions[1];

    agg::int8u *arrbuf;
    agg::int8u *buffer;
    agg::int8u *dstbuf;

    arrbuf = reinterpret_cast<agg::int8u *>(A->data);

    size_t NUMBYTES(imo->colsIn * imo->rowsIn * imo->BPP);
    buffer = dstbuf = new agg::int8u[NUMBYTES];

    if (buffer == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("_image_module::frombyte could not allocate memory");
    }

    if PyArray_ISCONTIGUOUS(A)
    {
        if (A->dimensions[2] == 4)
        {
            memmove(dstbuf, arrbuf, imo->rowsIn * imo->colsIn * 4);
        }
        else
        {
            size_t i = imo->rowsIn * imo->colsIn;
            while (i--)
            {
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = 255;
            }
        }
    }
    else if ((A->strides[1] == 4) && (A->strides[2] == 1))
    {
        const size_t N = imo->colsIn * 4;
        const size_t stride = A->strides[0];
        for (size_t rownum = 0; rownum < imo->rowsIn; rownum++)
        {
            memmove(dstbuf, arrbuf, N);
            arrbuf += stride;
            dstbuf += N;
        }
    }
    else if ((A->strides[1] == 3) && (A->strides[2] == 1))
    {
        const size_t stride = A->strides[0] - imo->colsIn * 3;
        for (size_t rownum = 0; rownum < imo->rowsIn; rownum++)
        {
            for (size_t colnum = 0; colnum < imo->colsIn; colnum++)
            {
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = *arrbuf++;
                *dstbuf++ = 255;
            }
            arrbuf += stride;
        }
    }
    else
    {
        PyArrayIterObject *iter;
        iter = (PyArrayIterObject *)PyArray_IterNew((PyObject *)A);
        if (A->dimensions[2] == 4)
        {
            while (iter->index < iter->size) {
                *dstbuf++ = *((unsigned char *)iter->dataptr);
                PyArray_ITER_NEXT(iter);
            }
        }
        else
        {
            while (iter->index < iter->size) {
                *dstbuf++ = *((unsigned char *)iter->dataptr);
                PyArray_ITER_NEXT(iter);
                *dstbuf++ = *((unsigned char *)iter->dataptr);
                PyArray_ITER_NEXT(iter);
                *dstbuf++ = *((unsigned char *)iter->dataptr);
                PyArray_ITER_NEXT(iter);
                *dstbuf++ = 255;
            }
        }
        Py_DECREF(iter);
    }

    if (isoutput)
    {
        // make the output buffer point to the input buffer

        imo->rowsOut  = imo->rowsIn;
        imo->colsOut  = imo->colsIn;

        imo->rbufOut = new agg::rendering_buffer;
        imo->bufferOut = buffer;
        imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);

    }
    else
    {
        imo->bufferIn = buffer;
        imo->rbufIn = new agg::rendering_buffer;
        imo->rbufIn->attach(buffer, imo->colsIn, imo->rowsIn, imo->colsIn*imo->BPP);
    }

    return Py::asObject(imo);
}

char _image_module_frombuffer__doc__[] =
    "frombuffer(buffer, width, height, isoutput)\n"
    "\n"
    "Load the image from a character buffer\n"
    "By default this function fills the input buffer, which can subsequently\n"
    "be resampled using resize.  If isoutput=1, fill the output buffer.\n"
    "This is used to support raw pixel images w/o resampling."
    ;
Py::Object
_image_module::frombuffer(const Py::Tuple& args)
{
    _VERBOSE("_image_module::frombuffer");

    args.verify_length(4);

    PyObject *bufin = new_reference_to(args[0]);
    size_t x = (long)Py::Int(args[1]);
    size_t y = (long)Py::Int(args[2]);

    if (x >= 32768 || y >= 32768)
    {
        throw Py::ValueError("x and y must both be less than 32768");
    }

    int isoutput = Py::Int(args[3]);

    if (PyObject_CheckReadBuffer(bufin) != 1)
        throw Py::ValueError("First argument must be a buffer.");

    Image* imo = new Image;

    imo->rowsIn = y;
    imo->colsIn = x;
    Py_ssize_t NUMBYTES(imo->colsIn * imo->rowsIn * imo->BPP);

    Py_ssize_t buflen;
    const agg::int8u *rawbuf;
    if (PyObject_AsReadBuffer(bufin, reinterpret_cast<const void**>(&rawbuf), &buflen) != 0)
    {
        throw Py::ValueError("Cannot get buffer from object.");
    }

    // Check buffer is required size.
    if (buflen != NUMBYTES)
    {
        throw Py::ValueError("Buffer length must be width * height * 4.");
    }

    // Copy from input buffer to new buffer for agg.
    agg::int8u* buffer = new agg::int8u[NUMBYTES];
    if (buffer == NULL) //todo: also handle allocation throw
    {
        throw Py::MemoryError("_image_module::frombuffer could not allocate memory");
    }
    memmove(buffer, rawbuf, NUMBYTES);

    if (isoutput)
    {
        // make the output buffer point to the input buffer
        imo->rowsOut  = imo->rowsIn;
        imo->colsOut  = imo->colsIn;

        imo->rbufOut = new agg::rendering_buffer;
        imo->bufferOut = buffer;
        imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);

    }
    else
    {
        imo->bufferIn = buffer;
        imo->rbufIn = new agg::rendering_buffer;
        imo->rbufIn->attach(buffer, imo->colsIn, imo->rowsIn, imo->colsIn*imo->BPP);
    }

    return Py::asObject(imo);
}

// utilities for irregular grids
void _bin_indices_middle(unsigned int *irows, int nrows, float *ys1, int ny, float dy, float y_min)
{
    int  i, j, j_last;
    unsigned  int * rowstart = irows;
    float *ys2 = ys1 + 1;
    float *yl = ys1 + ny ;
    float yo = y_min + dy / 2.0;
    float ym = 0.5f * (*ys1 + *ys2);
    // y/rows
    j = 0;
    j_last = j;
    for (i = 0;i < nrows;i++, yo += dy, rowstart++)
    {
        while (ys2 != yl && yo > ym)
        {
            ys1 = ys2;
            ys2 = ys1 + 1;
            ym = 0.5f * (*ys1 + *ys2);
            j++;
        }
        *rowstart = j - j_last;
        j_last = j;
    }
}

void _bin_indices_middle_linear(float *arows, unsigned int *irows, int nrows, float *y, int ny, float dy, float y_min)
{
    int i;
    int ii = 0;
    int iilast = ny - 1;
    float sc = 1 / dy;
    int iy0 = (int)floor(sc * (y[ii]  - y_min));
    int iy1 = (int)floor(sc * (y[ii+1]  - y_min));
    float invgap = 1.0f / (iy1 - iy0);
    for (i = 0; i < nrows && i <= iy0; i++)
    {
        irows[i] = 0;
        arows[i] = 1.0;
        //std::cerr<<"i="<<i<<"  ii="<<0<<" a="<< arows[i]<< std::endl;
    }
    for (; i < nrows; i++)
    {
        while (i > iy1 && ii < iilast)
        {
            ii++;
            iy0 = iy1;
            iy1 = (int)floor(sc * (y[ii+1] - y_min));
            invgap = 1.0f / (iy1 - iy0);
        }
        if (i >= iy0 && i <= iy1)
        {
            irows[i] = ii;
            arows[i] = (iy1 - i) * invgap;
            //std::cerr<<"i="<<i<<"  ii="<<ii<<" a="<< arows[i]<< std::endl;
        }
        else break;
    }
    for (; i < nrows; i++)
    {
        irows[i] = iilast - 1;
        arows[i] = 0.0;
        //std::cerr<<"i="<<i<<"  ii="<<iilast-1<<" a="<< arows[i]<< std::endl;
    }
}

void _bin_indices(int *irows, int nrows, double *y, int ny,
                  double sc, double offs)
{
    int i;
    if (sc*(y[ny-1] - y[0]) > 0)
    {
        int ii = 0;
        int iilast = ny - 1;
        int iy0 = (int)floor(sc * (y[ii]  - offs));
        int iy1 = (int)floor(sc * (y[ii+1]  - offs));
        for (i = 0; i < nrows && i < iy0; i++)
        {
            irows[i] = -1;
        }
        for (; i < nrows; i++)
        {
            while (i > iy1 && ii < iilast)
            {
                ii++;
                iy0 = iy1;
                iy1 = (int)floor(sc * (y[ii+1] - offs));
            }
            if (i >= iy0 && i <= iy1) irows[i] = ii;
            else break;
        }
        for (; i < nrows; i++)
        {
            irows[i] = -1;
        }
    }
    else
    {
        int iilast = ny - 1;
        int ii = iilast;
        int iy0 = (int)floor(sc * (y[ii]  - offs));
        int iy1 = (int)floor(sc * (y[ii-1]  - offs));
        for (i = 0; i < nrows && i < iy0; i++)
        {
            irows[i] = -1;
        }
        for (; i < nrows; i++)
        {
            while (i > iy1 && ii > 1)
            {
                ii--;
                iy0 = iy1;
                iy1 = (int)floor(sc * (y[ii-1] - offs));
            }
            if (i >= iy0 && i <= iy1) irows[i] = ii - 1;
            else break;
        }
        for (; i < nrows; i++)
        {
            irows[i] = -1;
        }
    }
}

void _bin_indices_linear(float *arows, int *irows, int nrows, double *y, int ny,
                         double sc, double offs)
{
    int i;
    if (sc*(y[ny-1] - y[0]) > 0)
    {
        int ii = 0;
        int iilast = ny - 1;
        int iy0 = (int)floor(sc * (y[ii]  - offs));
        int iy1 = (int)floor(sc * (y[ii+1]  - offs));
        float invgap = 1.0 / (iy1 - iy0);
        for (i = 0; i < nrows && i < iy0; i++)
        {
            irows[i] = -1;
        }
        for (; i < nrows; i++)
        {
            while (i > iy1 && ii < iilast)
            {
                ii++;
                iy0 = iy1;
                iy1 = (int)floor(sc * (y[ii+1] - offs));
                invgap = 1.0 / (iy1 - iy0);
            }
            if (i >= iy0 && i <= iy1)
            {
                irows[i] = ii;
                arows[i] = (iy1 - i) * invgap;
            }
            else break;
        }
        for (; i < nrows; i++)
        {
            irows[i] = -1;
        }
    }
    else
    {
        int iilast = ny - 1;
        int ii = iilast;
        int iy0 = (int)floor(sc * (y[ii]  - offs));
        int iy1 = (int)floor(sc * (y[ii-1]  - offs));
        float invgap = 1.0 / (iy1 - iy0);
        for (i = 0; i < nrows && i < iy0; i++)
        {
            irows[i] = -1;
        }
        for (; i < nrows; i++)
        {
            while (i > iy1 && ii > 1)
            {
                ii--;
                iy0 = iy1;
                iy1 = (int)floor(sc * (y[ii-1] - offs));
                invgap = 1.0 / (iy1 - iy0);
            }
            if (i >= iy0 && i <= iy1)
            {
                irows[i] = ii - 1;
                arows[i] = (i - iy0) * invgap;
            }
            else break;
        }
        for (; i < nrows; i++)
        {
            irows[i] = -1;
        }
    }
}



char __image_module_pcolor__doc__[] =
    "pcolor(x, y, data, rows, cols, bounds)\n"
    "\n"
    "Generate a pseudo-color image from data on a non-uniform grid using\n"
    "nearest neighbour or linear interpolation.\n"
    "bounds = (x_min, x_max, y_min, y_max)\n"
    "interpolation = NEAREST or BILINEAR \n"
    ;

void _pcolor_cleanup(PyArrayObject* x, PyArrayObject* y,  PyArrayObject *d,
                     unsigned int * rowstarts , unsigned int*colstarts ,
                     float *acols , float *arows)
{
    Py_XDECREF(x);
    Py_XDECREF(y);
    Py_XDECREF(d);
    if (rowstarts)
    {
        PyMem_Free(rowstarts);
    }
    if (colstarts)
    {
        PyMem_Free(colstarts);
    }
    if (acols)
    {
        PyMem_Free(acols);
    }
    if (arows)
    {
        PyMem_Free(arows);
    }
    return;
}

Py::Object
_image_module::pcolor(const Py::Tuple& args)
{
    _VERBOSE("_image_module::pcolor");


    if (args.length() != 7)
    {
        throw Py::TypeError("Incorrect number of arguments (7 expected)");
    }

    Py::Object xp = args[0];
    Py::Object yp = args[1];
    Py::Object dp = args[2];
    unsigned int rows = (unsigned long)Py::Int(args[3]);
    unsigned int cols = (unsigned long)Py::Int(args[4]);
    Py::Tuple bounds = args[5];
    unsigned int interpolation = (unsigned long)Py::Int(args[6]);

    if (rows >= 32768 || cols >= 32768)
    {
        throw Py::ValueError("rows and cols must both be less than 32768");
    }

    if (bounds.length() != 4)
    {
        throw Py::TypeError("Incorrect number of bounds (4 expected)");
    }

    float x_min = Py::Float(bounds[0]);
    float x_max = Py::Float(bounds[1]);
    float y_min = Py::Float(bounds[2]);
    float y_max = Py::Float(bounds[3]);
    float width = x_max - x_min;
    float height = y_max - y_min;
    float dx = width / ((float) cols);
    float dy = height / ((float) rows);

    // Check we have something to output to
    if (rows == 0 || cols == 0)
    {
        throw Py::ValueError("Cannot scale to zero size");
    }

    PyArrayObject *x = NULL;
    PyArrayObject *y = NULL;
    PyArrayObject *d = NULL;
    unsigned int *rowstarts = NULL;
    unsigned int *colstarts = NULL;
    float *acols = NULL;
    float *arows = NULL;

    // Get numpy arrays
    x = (PyArrayObject *) PyArray_ContiguousFromObject(xp.ptr(), PyArray_FLOAT, 1, 1);
    if (x == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::ValueError("x is of incorrect type (wanted 1D float)");
    }
    y = (PyArrayObject *) PyArray_ContiguousFromObject(yp.ptr(), PyArray_FLOAT, 1, 1);
    if (y == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::ValueError("y is of incorrect type (wanted 1D float)");
    }
    d = (PyArrayObject *) PyArray_ContiguousFromObject(dp.ptr(), PyArray_UBYTE, 3, 3);
    if (d == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::ValueError("data is of incorrect type (wanted 3D UInt8)");
    }
    if (d->dimensions[2] != 4)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::ValueError("data must be in RGBA format");
    }

    // Check dimensions match
    int nx = x->dimensions[0];
    int ny = y->dimensions[0];
    if (nx != d->dimensions[1] || ny != d->dimensions[0])
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::ValueError("data and axis dimensions do not match");
    }

    // Allocate memory for pointer arrays
    rowstarts = reinterpret_cast<unsigned int*>(PyMem_Malloc(sizeof(unsigned int) * rows));
    if (rowstarts == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::MemoryError("Cannot allocate memory for lookup table");
    }
    colstarts = reinterpret_cast<unsigned int*>(PyMem_Malloc(sizeof(unsigned int) * cols));
    if (colstarts == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::MemoryError("Cannot allocate memory for lookup table");
    }

    // Create output
    Image* imo = new Image;
    imo->rowsIn = rows;
    imo->colsIn = cols;
    imo->rowsOut = rows;
    imo->colsOut = cols;
    size_t NUMBYTES(rows * cols * 4);
    agg::int8u *buffer = new agg::int8u[NUMBYTES];
    if (buffer == NULL)
    {
        _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
        throw Py::MemoryError("Could not allocate memory for image");
    }


    // Calculate the pointer arrays to map input x to output x
    unsigned int i, j;
    unsigned int * colstart = colstarts;
    unsigned int * rowstart = rowstarts;
    float *xs1 = reinterpret_cast<float*>(x->data);
    float *ys1 = reinterpret_cast<float*>(y->data);


    // Copy data to output buffer
    unsigned char *start;
    unsigned char *inposition;
    size_t inrowsize(nx*4);
    size_t rowsize(cols*4);
    rowstart = rowstarts;
    agg::int8u * position = buffer;
    agg::int8u * oldposition = NULL;
    start = reinterpret_cast<unsigned char*>(d->data);
    int s0 = d->strides[0];
    int s1 = d->strides[1];

    if (interpolation == Image::NEAREST)
    {
        _bin_indices_middle(colstart, cols, xs1,  nx, dx, x_min);
        _bin_indices_middle(rowstart, rows, ys1,  ny, dy, y_min);
        for (i = 0;i < rows;i++, rowstart++)
        {
            if (i > 0 && *rowstart == 0)
            {
                memcpy(position, oldposition, rowsize*sizeof(agg::int8u));
                oldposition = position;
                position += rowsize;
            }
            else
            {
                oldposition = position;
                start += *rowstart * inrowsize;
                inposition = start;
                for (j = 0, colstart = colstarts;j < cols;j++, position += 4, colstart++)
                {
                    inposition += *colstart * 4;
                    memcpy(position, inposition, 4*sizeof(agg::int8u));
                }
            }
        }
    }
    else if (interpolation == Image::BILINEAR)
    {
        arows = reinterpret_cast<float *>(PyMem_Malloc(sizeof(float) * rows));
        if (arows == NULL)
        {
            _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
            throw Py::MemoryError("Cannot allocate memory for lookup table");
        }
        acols = reinterpret_cast<float*>(PyMem_Malloc(sizeof(float) * cols));
        if (acols == NULL)
        {
            _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);
            throw Py::MemoryError("Cannot allocate memory for lookup table");
        }

        _bin_indices_middle_linear(acols, colstart, cols, xs1,  nx, dx, x_min);
        _bin_indices_middle_linear(arows, rowstart, rows, ys1,  ny, dy, y_min);
        double a00, a01, a10, a11, alpha, beta;


        agg::int8u * start00;
        agg::int8u * start01;
        agg::int8u * start10;
        agg::int8u * start11;
        // Copy data to output buffer
        for (i = 0; i < rows; i++)
        {
            for (j = 0; j < cols; j++)
            {
                alpha = arows[i];
                beta = acols[j];

                a00 = alpha * beta;
                a01 = alpha * (1.0 - beta);
                a10 = (1.0 - alpha) * beta;
                a11 = 1.0 - a00 - a01 - a10;

                start00 = (agg::int8u *)(start + s0 * rowstart[i] + s1 * colstart[j]);
                start01 = start00 + s1;
                start10 = start00 + s0;
                start11 = start10 + s1;
                position[0] = (agg::int8u)(start00[0] * a00 + start01[0] * a01 + start10[0] * a10 + start11[0] * a11);
                position[1] = (agg::int8u)(start00[1] * a00 + start01[1] * a01 + start10[1] * a10 + start11[1] * a11);
                position[2] = (agg::int8u)(start00[2] * a00 + start01[2] * a01 + start10[2] * a10 + start11[2] * a11);
                position[3] = (agg::int8u)(start00[3] * a00 + start01[3] * a01 + start10[3] * a10 + start11[3] * a11);
                position += 4;
            }
        }

    }

    // Attach output buffer to output buffer
    imo->rbufOut = new agg::rendering_buffer;
    imo->bufferOut = buffer;
    imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);

    _pcolor_cleanup(x, y, d, rowstarts, colstarts, acols, arows);

    return Py::asObject(imo);

}

void _pcolor2_cleanup(PyArrayObject* x, PyArrayObject* y, PyArrayObject *d,
                      PyArrayObject* bg, int *irows, int*jcols)
{
    Py_XDECREF(x);
    Py_XDECREF(y);
    Py_XDECREF(d);
    Py_XDECREF(bg);
    if (irows)
    {
        PyMem_Free(irows);
    }
    if (jcols)
    {
        PyMem_Free(jcols);
    }
}


char __image_module_pcolor2__doc__[] =
    "pcolor2(x, y, data, rows, cols, bounds, bg)\n"
    "\n"
    "Generate a pseudo-color image from data on a non-uniform grid\n"
    "specified by its cell boundaries.\n"
    "bounds = (x_left, x_right, y_bot, y_top)\n"
    "bg = ndarray of 4 uint8 representing background rgba\n"
    ;
Py::Object
_image_module::pcolor2(const Py::Tuple& args)
{
    _VERBOSE("_image_module::pcolor2");

    if (args.length() != 7)
    {
        throw Py::TypeError("Incorrect number of arguments (6 expected)");
    }

    Py::Object xp = args[0];
    Py::Object yp = args[1];
    Py::Object dp = args[2];
    int rows = Py::Int(args[3]);
    int cols = Py::Int(args[4]);
    Py::Tuple bounds = args[5];
    Py::Object bgp = args[6];

    if (rows >= 32768 || cols >= 32768)
    {
        throw Py::ValueError("rows and cols must both be less than 32768");
    }

    if (bounds.length() != 4)
    {
        throw Py::TypeError("Incorrect number of bounds (4 expected)");
    }

    double x_left = Py::Float(bounds[0]);
    double x_right = Py::Float(bounds[1]);
    double y_bot = Py::Float(bounds[2]);
    double y_top = Py::Float(bounds[3]);

    // Check we have something to output to
    if (rows == 0 || cols == 0)
    {
        throw Py::ValueError("rows or cols is zero; there are no pixels");
    }

    PyArrayObject* x = NULL;
    PyArrayObject* y = NULL;
    PyArrayObject* d = NULL;
    PyArrayObject* bg = NULL;
    int* irows = NULL;
    int* jcols = NULL;

    // Get numpy arrays
    x = (PyArrayObject *) PyArray_ContiguousFromObject(xp.ptr(), PyArray_DOUBLE, 1, 1);
    if (x == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("x is of incorrect type (wanted 1D double)");
    }
    y = (PyArrayObject *) PyArray_ContiguousFromObject(yp.ptr(), PyArray_DOUBLE, 1, 1);
    if (y == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("y is of incorrect type (wanted 1D double)");
    }
    d = (PyArrayObject *) PyArray_ContiguousFromObject(dp.ptr(), PyArray_UBYTE, 3, 3);
    if (d == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("data is of incorrect type (wanted 3D uint8)");
    }
    if (d->dimensions[2] != 4)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("data must be in RGBA format");
    }

    // Check dimensions match
    int nx = x->dimensions[0];
    int ny = y->dimensions[0];
    if (nx != d->dimensions[1] + 1 || ny != d->dimensions[0] + 1)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("data and axis bin boundary dimensions are incompatible");
    }

    bg = (PyArrayObject *) PyArray_ContiguousFromObject(bgp.ptr(), PyArray_UBYTE, 1, 1);
    if (bg == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("bg is of incorrect type (wanted 1D uint8)");
    }
    if (bg->dimensions[0] != 4)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::ValueError("bg must be in RGBA format");
    }

    // Allocate memory for pointer arrays
    irows = reinterpret_cast<int*>(PyMem_Malloc(sizeof(int) * rows));
    if (irows == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::MemoryError("Cannot allocate memory for lookup table");
    }
    jcols = reinterpret_cast<int*>(PyMem_Malloc(sizeof(int) * cols));
    if (jcols == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::MemoryError("Cannot allocate memory for lookup table");
    }

    // Create output
    Image* imo = new Image;
    imo->rowsIn = rows;
    imo->rowsOut = rows;
    imo->colsIn = cols;
    imo->colsOut = cols;
    size_t NUMBYTES(rows * cols * 4);
    agg::int8u *buffer = new agg::int8u[NUMBYTES];
    if (buffer == NULL)
    {
        _pcolor2_cleanup(x, y, d, bg, irows, jcols);
        throw Py::MemoryError("Could not allocate memory for image");
    }

    // Calculate the pointer arrays to map input x to output x
    int i, j;
    double *x0 = reinterpret_cast<double*>(x->data);
    double *y0 = reinterpret_cast<double*>(y->data);
    double sx = cols / (x_right - x_left);
    double sy = rows / (y_top - y_bot);
    _bin_indices(jcols, cols, x0, nx, sx, x_left);
    _bin_indices(irows, rows, y0, ny, sy, y_bot);

    // Copy data to output buffer
    agg::int8u * position = buffer;
    unsigned char *start = reinterpret_cast<unsigned char*>(d->data);
    unsigned char *bgptr = reinterpret_cast<unsigned char*>(bg->data);
    int s0 = d->strides[0];
    int s1 = d->strides[1];

    for (i = 0; i < rows; i++)
    {
        for (j = 0; j < cols; j++)
        {
            if (irows[i] == -1 || jcols[j] == -1)
            {
                memcpy(position, bgptr, 4*sizeof(agg::int8u));
            }
            else
            {
                memcpy(position, (start + s0*irows[i] + s1*jcols[j]),
                       4*sizeof(agg::int8u));
            }
            position += 4;
        }
    }

    // Attach output buffer to output buffer
    imo->rbufOut = new agg::rendering_buffer;
    imo->bufferOut = buffer;
    imo->rbufOut->attach(imo->bufferOut, imo->colsOut, imo->rowsOut, imo->colsOut * imo->BPP);

    _pcolor2_cleanup(x, y, d, bg, irows, jcols);

    return Py::asObject(imo);
}

#if PY3K
PyMODINIT_FUNC
PyInit__image(void)
#else
PyMODINIT_FUNC
init_image(void)
#endif
{
    _VERBOSE("init_image");

    static _image_module* _image = new _image_module;

    import_array();
    Py::Dict d = _image->moduleDictionary();

    d["NEAREST"] = Py::Int(Image::NEAREST);
    d["BILINEAR"] = Py::Int(Image::BILINEAR);
    d["BICUBIC"] = Py::Int(Image::BICUBIC);
    d["SPLINE16"] = Py::Int(Image::SPLINE16);
    d["SPLINE36"] = Py::Int(Image::SPLINE36);
    d["HANNING"] = Py::Int(Image::HANNING);
    d["HAMMING"] = Py::Int(Image::HAMMING);
    d["HERMITE"] = Py::Int(Image::HERMITE);
    d["KAISER"]   = Py::Int(Image::KAISER);
    d["QUADRIC"]   = Py::Int(Image::QUADRIC);
    d["CATROM"]  = Py::Int(Image::CATROM);
    d["GAUSSIAN"]  = Py::Int(Image::GAUSSIAN);
    d["BESSEL"]  = Py::Int(Image::BESSEL);
    d["MITCHELL"]  = Py::Int(Image::MITCHELL);
    d["SINC"]  = Py::Int(Image::SINC);
    d["LANCZOS"]  = Py::Int(Image::LANCZOS);
    d["BLACKMAN"] = Py::Int(Image::BLACKMAN);

    d["ASPECT_FREE"] = Py::Int(Image::ASPECT_FREE);
    d["ASPECT_PRESERVE"] = Py::Int(Image::ASPECT_PRESERVE);

#if PY3K
    return _image->module().ptr();
#endif
}
