# -*- Mode: Python; py-indent-offset: 4 -*-
# vim: tabstop=4 shiftwidth=4 expandtab

from __future__ import division

# overrides for pygobject gobject-introspection binding for libvips, tested 
# with python2.7 and python3.4

# copy this file to dist-packages/gi/overrides, eg.
# 
#   sudo cp Vips.py /usr/lib/python2.7/dist-packages/gi/overrides
#   sudo cp Vips.py /usr/lib/python3/dist-packages/gi/overrides
#
# Alternatively, build vips to another prefix, then copy Vips.py and Vips.pyc
# from $prefix/lib/python2.7/dist-packages/gi/overrides to /usr

# This file is part of VIPS.
# 
# VIPS is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
# 
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
# more details.
# 
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
# 
# These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

import sys
import re
import logging
import numbers

logger = logging.getLogger(__name__)

from gi.repository import GObject
from ..overrides import override
from ..module import get_introspection_module

Vips = get_introspection_module('Vips')

__all__ = []

# start up vips! 
# passing argv[0] helps vips find its data files on some platforms
Vips.init(sys.argv[0])

# need the gtypes for various vips types
vips_type_array_int = GObject.GType.from_name("VipsArrayInt")
vips_type_array_double = GObject.GType.from_name("VipsArrayDouble")
vips_type_array_image = GObject.GType.from_name("VipsArrayImage")
vips_type_blob = GObject.GType.from_name("VipsBlob")
vips_type_image = GObject.GType.from_name("VipsImage")
vips_type_operation = GObject.GType.from_name("VipsOperation")
vips_type_ref_string = GObject.GType.from_name("VipsRefString")

# 8.4 and earlier had a bug which swapped the order of const args to enum
# operations
swap_const_args = Vips.version(0) < 8 or (Vips.version(0) == 8 and 
                                          Vips.version(1) <= 4)

def is_2D(value):
    if not isinstance(value, list):
        return False

    for x in value:
        if not isinstance(x, list):
            return False

        if len(x) != len(value[0]):
            return False

    return True

def imageize(match_image, value):
    logger.debug('imageize match_image=%s, value=%s' % (match_image, value))

    # 2D arrays become array images
    if is_2D(value):
        return Vips.Image.new_from_array(value)

    # if there's nothing to match to, also make an array
    if match_image is None:
        return Vips.Image.new_from_array(value)

    # assume this is a pixel constant ... expand into an image using
    # match as a template
    return match_image.new_from_image(value)

# we'd like to use memoryview to avoid copying things like ICC profiles, but
# unfortunately pygobject does not support this ... so for blobs we just use
# bytes(). 

unpack_types = [[Vips.Blob, lambda x: bytes(x.get())],
                [Vips.RefString, lambda x: x.get()],
                [Vips.ArrayDouble, lambda x: x.get()],
                [Vips.ArrayImage, lambda x: x.get()], 
                [Vips.ArrayInt, lambda x: x.get()]]
def unpack(value):
    for t, cast in unpack_types:
        if isinstance(value, t):
            return cast(value)

    return value

def array_image_new(array):
    match_image = next((x for x in array if isinstance(x, Vips.Image)), None)
    if match_image is None:
        raise Error('Unable to make image array argument.', 
                    'Array must contain at least one image.')

    for i in range(0, len(array)):
        if not isinstance(array[i], Vips.Image):
            array[i] = imageize(match_image, array[i])

    return Vips.ArrayImage.new(array)

arrayize_types = [[vips_type_array_int, Vips.ArrayInt.new],
                  [vips_type_array_double, Vips.ArrayDouble.new],
                  [vips_type_array_image, array_image_new]]
def arrayize(gtype, value):
    for t, cast in arrayize_types:
        if GObject.type_is_a(gtype, t):
            if not isinstance(value, list):
                value = [value]
            return cast(value)

    return value

def run_cmplx(fn, image):
    """Run a complex function on a non-complex image.

    The image needs to be complex, or have an even number of bands. The input
    can be int, the output is always float or double.
    """
    original_format = image.format

    if not Vips.band_format_iscomplex(image.format):
        if image.bands % 2 != 0:
            raise "not an even number of bands"

        if not Vips.band_format_isfloat(image.format):
            image = image.cast(Vips.BandFormat.FLOAT)

        if image.format == Vips.BandFormat.DOUBLE:
            new_format = Vips.BandFormat.DPCOMPLEX
        else:
            new_format = Vips.BandFormat.COMPLEX

        image = image.copy(format = new_format, bands = image.bands / 2)

    image = fn(image)

    if not Vips.band_format_iscomplex(original_format):
        if image.format == Vips.BandFormat.DPCOMPLEX:
            new_format = Vips.BandFormat.DOUBLE
        else:
            new_format = Vips.BandFormat.FLOAT

        image = image.copy(format = new_format, bands = image.bands * 2)

    return image

class Error(Exception):
    """An error from vips.

    message -- a high-level description of the error
    detail -- a string with some detailed diagnostics
    """
    def __init__(self, message, detail = None):
        self.message = message
        if detail == None or detail == "":
            detail = Vips.error_buffer()
            Vips.error_clear()
        self.detail = detail

        logger.debug('Error %s %s', self.message, self.detail)

    def __str__(self):
        return '%s\n  %s' % (self.message, self.detail)

Vips.Error = Error

class Argument(object):
    def __init__(self, op, prop):
        self.op = op
        self.prop = prop
        self.name = re.sub("-", "_", prop.name)
        self.flags = op.get_argument_flags(self.name)
        self.priority = op.get_argument_priority(self.name)
        self.isset = op.argument_isset(self.name)

    def set_value(self, match_image, value):
        logger.debug('assigning %s to %s' % (value, self.name))
        logger.debug('%s needs a %s' % (self.name, self.prop.value_type))

        # blob-ize
        if GObject.type_is_a(self.prop.value_type, vips_type_blob):
            if not isinstance(value, Vips.Blob):
                value = Vips.Blob.new(None, value)

        # image-ize
        if GObject.type_is_a(self.prop.value_type, vips_type_image):
            if not isinstance(value, Vips.Image):
                value = imageize(match_image, value)

        # array-ize some types, if necessary
        value = arrayize(self.prop.value_type, value)

        # MODIFY input images need to be copied before assigning them
        if self.flags & Vips.ArgumentFlags.MODIFY:
            # don't use .copy(): we want to make a new pipeline with no
            # reference back to the old stuff ... this way we can free the
            # previous image earlier
            logger.debug('MODIFY argument: copying image')
            new_image = Vips.Image.new_memory()
            value.write(new_image)
            value = new_image

        logger.debug('assigning %s' % value)

        self.op.props.__setattr__(self.name, value)

    def get_value(self):
        value = self.op.props.__getattribute__(self.name)

        logger.debug('read out %s from %s' % (value, self.name))

        return unpack(value)

    def description(self):
        result = self.name
        result += " " * (10 - len(self.name)) + " -- " + self.prop.blurb
        result += ", " + self.prop.value_type.name

        return result

Vips.Argument = Argument

class Operation(Vips.Operation):

    # find all the args for this op, sort into priority order
    # we leave deprecated args in this list: for compatibility, we want users
    # to be able to set them
    # if you are (for example) generating docs, you'll need to filter out the
    # deprecated args yourself
    def get_args(self):
        args = [Argument(self, x) for x in self.props]
        args.sort(key = lambda x: x.priority)

        return args

Operation = override(Operation)
__all__.append('Operation')

# search a list recursively for a Vips.Image object
def find_image(x):
    if isinstance(x, Vips.Image):
        return x
    if isinstance(x, list):
        for i in x:
            y = find_image(i)
            if y is not None:
                return y
    return None

def _call_base(name, required, optional, self = None, option_string = None):
    logger.debug('_call_base name=%s, required=%s optional=%s' % 
                  (name, required, optional))
    if self:
        logger.debug('_call_base self=%s' % self)
    if option_string:
        logger.debug('_call_base option_string = %s' % option_string)

    try:
        op = Vips.Operation.new(name)
    except TypeError as e:
        raise Error('No such operator.')
    if op.get_flags() & Vips.OperationFlags.DEPRECATED:
        raise Error('No such operator.', 'operator "%s" is deprecated' % name)

    # set str options first so the user can't override things we set
    # deliberately and break stuff
    if option_string:
        if op.set_from_string(option_string) != 0:
            raise Error('Bad arguments.')

    args = op.get_args()

    enm = Vips.ArgumentFlags

    # find all required, unassigned, undeprecated input args 
    required_input = [x for x in args if x.flags & enm.INPUT and 
                      x.flags & enm.REQUIRED and 
                      not x.flags & enm.DEPRECATED and
                      not x.isset]

    # do we have a non-None self pointer? this is used to set the first
    # compatible input arg
    if self is not None:
        found = False
        for x in required_input:
            if GObject.type_is_a(self, x.prop.value_type):
                x.set_value(None, self)
                required_input.remove(x)
                found = True
                break

        if not found:
            raise Error('Bad arguments.', 'No %s argument to %s.' %
                        (str(self.__class__), name))

    if len(required_input) != len(required):
        raise Error('Wrong number of arguments.', 
                    '%s needs %d arguments, you supplied %d.' % 
                    (name, len(required_input), len(required)))

    # if we need an image arg but the user supplied a number or list of 
    # numbers, we expand it into an image automatically ... the number is
    # expanded to match self, or if that's None, the first image we can find in
    # the required or optional arguments
    match_image = self
    if match_image is None:
        for arg in required:
            match_image = find_image(arg)
            if match_image is not None:
                break

    if match_image is None:
        for arg_name in optional:
            match_image = find_image(optional[arg_name])
            if match_image is not None:
                break

    for i in range(len(required_input)):
        required_input[i].set_value(match_image, required[i])

    # find all optional, unassigned input args ... make a hash from name to
    # Argument
    # we let deprecated ones through, we want to allow assigment to them for
    # compat
    optional_input = {x.name: x for x in args if x.flags & enm.INPUT and 
                      not x.flags & enm.REQUIRED and 
                      not x.isset}

    # find all optional output args ... we use "x = True" 
    # in args to mean add that to output
    optional_output = {x.name: x for x in args if x.flags & enm.OUTPUT and 
                       not x.flags & enm.REQUIRED}

    # set optional input args
    for key in list(optional.keys()):
        if key in optional_input:
            optional_input[key].set_value(match_image, optional[key])
        elif key in optional_output:
            # must be a literal True value
            if optional[key] is not True:
                raise Error('Optional output argument must be True.',
                            'Argument %s should equal True.' % key)
        else:
            raise Error('Unknown argument.', 
                        'Operator %s has no argument %s.' % (name, key))

    # call
    logger.debug('_call_base checking cache for op %s' % op)
    op2 = Vips.cache_operation_build(op)
    logger.debug('_call_base got op2 %s' % op2)
    if op2 == None:
        raise Error('Error calling operator %s.' % name)

    # rescan args if op2 is different from op
    if op2 != op:
        logger.debug('_call_base rescanning args')
        args = op2.get_args()
        optional_output = {x.name: x for x in args if x.flags & enm.OUTPUT and 
                           not x.flags & enm.REQUIRED}

    # gather output args 
    logger.debug('_call_base fetching required output args')
    out = []

    for x in args:
        # required non-deprecated output arg
        if x.flags & enm.OUTPUT and x.flags & enm.REQUIRED and not x.flags & enm.DEPRECATED:
            out.append(x.get_value())

        # modified input arg ... this will get the memory image we made above
        if x.flags & enm.INPUT and x.flags & enm.MODIFY:
            out.append(x.get_value())

    logger.debug('_call_base fetching optional output args')
    out_dict = {}
    for x in list(optional.keys()):
        if x in optional_output:
            out_dict[x] = optional_output[x].get_value()
    if out_dict != {}:
        out.append(out_dict)

    if len(out) == 1:
        out = out[0]
    elif len(out) == 0:
        out = None

    # unref everything now we have refs to all outputs we want
    op2.unref_outputs()

    logger.debug('success')

    return out

# handy for expanding enums
def _call_enum(self, name, enum, other):
    if isinstance(other, Vips.Image):
        return _call_base(name, [other, enum], {}, self)
    elif swap_const_args:
        return _call_base(name + "_const", [other, enum], {}, self)
    else:
        return _call_base(name + "_const", [enum, other], {}, self)

# for equality style operations, we need to allow comparison with None
def _call_enum_eq(self, name, enum, other):
    if isinstance(other, Vips.Image):
        return _call_base(name, [other, enum], {}, self)
    elif isinstance(other, list) or isinstance(other, numbers.Number):
        if swap_const_args:
            return _call_base(name + "_const", [other, enum], {}, self)
        else:
            return _call_base(name + "_const", [enum, other], {}, self)
    else:
        return False

# general user entrypoint 
def call(name, *args, **kwargs):
    return _call_base(name, args, kwargs)

Vips.call = call

# here from getattr ... try to run the attr as a method
def _call_instance(self, name, args, kwargs):
    return _call_base(name, args, kwargs, self)

@classmethod
def vips_image_new_from_file(cls, vips_filename, **kwargs):
    """Create a new Image from a filename.

    Extra optional arguments depend on the loader selected by libvips. See each
    loader for details. 
    """
    filename = Vips.filename_get_filename(vips_filename)
    option_string = Vips.filename_get_options(vips_filename)
    loader = Vips.Foreign.find_load(filename)
    if loader == None:
        raise Error('No known loader for "%s".' % filename)
    logger.debug('Image.new_from_file: loader = %s' % loader)

    return _call_base(loader, [filename], kwargs, None, option_string)

setattr(Vips.Image, 'new_from_file', vips_image_new_from_file)

@classmethod
def vips_image_new_from_buffer(cls, data, option_string, **kwargs):
    """Create a new Image from binary data in a string.

    data -- binary image data
    option_string -- optional arguments in string form

    option_string can be something like "page=10" to load the 10th page of a
    tiff file. You can also give load options as keyword arguments. 
    """
    loader = Vips.Foreign.find_load_buffer(data)
    if loader == None:
        raise Error('No known loader for buffer.')
    logger.debug('Image.new_from_buffer: loader = %s' % loader)

    return _call_base(loader, [data], kwargs, None, option_string)

setattr(Vips.Image, 'new_from_buffer', vips_image_new_from_buffer)

@classmethod
def vips_image_new_from_array(cls, array, scale = 1, offset = 0):
    """Create a new image from an array.

    The array argument can be a 1D array to create a height == 1 image, or a 2D
    array to make a 2D image. Use scale and offset to set the scale factor,
    handy for integer convolutions. 
    """
    # we accept a 1D array and assume height == 1, or a 2D array and check all
    # lines are the same length
    if not isinstance(array, list):
        raise TypeError('new_from_array() takes a list argument')
    if not isinstance(array[0], list):
        height = 1
        width = len(array)
    else:
        # must copy the first row, we don't want to modify the passed-in array
        flat_array = list(array[0])
        height = len(array)
        width = len(array[0])
        for i in range(1, height):
            if len(array[i]) != width:
                raise TypeError('new_from_array() array not rectangular')
            flat_array += array[i]
        array = flat_array

    image = cls.new_matrix_from_array(width, height, array)

    # be careful to set them as double
    image.set('scale', float(scale))
    image.set('offset', float(offset))

    return image

setattr(Vips.Image, 'new_from_array', vips_image_new_from_array)

def generate_docstring(name):
    try:
        op = Vips.Operation.new(name)
    except TypeError as e:
        raise Error('No such operator.')
    if op.get_flags() & Vips.OperationFlags.DEPRECATED:
        raise Error('No such operator.', 'operator "%s" is deprecated' % name)

    # find all the args for this op, sort into priority order
    args = op.get_args()

    # we are only interested in non-deprecated args
    args = [y for y in args 
            if not y.flags & Vips.ArgumentFlags.DEPRECATED]

    enm = Vips.ArgumentFlags

    # find all required, unassigned input args
    required_input = [x for x in args if x.flags & enm.INPUT and 
                      x.flags & enm.REQUIRED and 
                      not x.isset]

    optional_input = [x for x in args if x.flags & enm.INPUT and 
                      not x.flags & enm.REQUIRED and 
                      not x.isset]

    required_output = [x for x in args if x.flags & enm.OUTPUT and 
                       x.flags & enm.REQUIRED]

    optional_output = [x for x in args if x.flags & enm.OUTPUT and 
                       not x.flags & enm.REQUIRED]

    # find the first required input image, if any ... we will be a member
    # function of this instance
    member_x = None
    for i in range(0, len(required_input)):
        x = required_input[i]
        if GObject.type_is_a(vips_type_image, x.prop.value_type):
            member_x = x
            break

    description = op.get_description()
    result = description[0].upper() + description[1:] + ".\n\n"
    result += "Usage:\n"

    result += "   " + ", ".join([x.name for x in required_output]) + " = "
    if member_x:
        result += member_x.name + "." + name + "("
    else:
        result += "Vips.Image." + name + "("

    required_input_args = [x.name for x in required_input if x != member_x]
    result += ", ".join(required_input_args)
    if len(optional_input) > 0 and len(required_input_args) > 0:
        result += ", "
    result += ", ".join([x.name + " = " + x.prop.value_type.name 
                         for x in optional_input])
    result += ")\n"

    result += "Where:\n"
    for x in required_output:
        result += "   " + x.description() + "\n"

    for x in required_input:
        result += "   " + x.description() + "\n"

    if len(optional_input) > 0:
        result += "Keyword parameters:\n"
        for x in optional_input:
            result += "   " + x.description() + "\n"

    if len(optional_output) > 0:
        result += "Extra output options:\n"
        for x in optional_output:
            result += "   " + x.description() + "\n"

    return result

# apply a function to a thing, or map over a list
# we often need to do something like (1.0 / other) and need to work for lists
# as well as scalars
def smap(func, x):
    if isinstance(x, list):
        return list(map(func, x))
    else:
        return func(x)

# decorator to set docstring
def add_doc(value):
    def _doc(func):
        func.__doc__ = value
        return func
    return _doc

class Image(Vips.Image):
    # for constructors, see class methods above

    def new_from_image(self, value):
        """Create a new image based on an existing one.

        A new image is created with the same width, height, format,
        interpretation, resolution and offset as self, but with every pixel
        having the value of value.

        You can pass an array to create a many-band image.
        """

        # we'd like to call the vips function vips_image_new_from_image() but we
        # can't call __getattr__ methods from a subclass 
        pixel = (Vips.Image.black(1, 1) + value).cast(self.format)
        image = pixel.embed(0, 0, self.width, self.height,
                            extend = Vips.Extend.COPY)
        image = image.copy(interpretation = self.interpretation,
                           xres = self.xres,
                           yres = self.yres,
                           xoffset = self.xoffset,
                           yoffset = self.yoffset)

        return image

    # output

    def write_to_file(self, vips_filename, **kwargs):
        """Write an Image to a file. 

        The filename can contain save options, for example
        "fred.tif[compression=jpeg]", or save options can be given as keyword
        arguments. Save options depend on the selected saver. 
        """
        filename = Vips.filename_get_filename(vips_filename)
        option_string = Vips.filename_get_options(vips_filename)
        saver = Vips.Foreign.find_save(filename)
        if saver == None:
            raise Error('No known saver for "%s".' % filename)
        logger.debug('Image.write_to_file: saver = %s' % saver)

        _call_base(saver, [filename], kwargs, self, option_string)

    def write_to_buffer(self, format_string, **kwargs):
        """Write an Image to memory.

        Return the image as a binary string, encoded in the selected format.
        Save options can be given in the format_string, for example
        ".jpg[Q=90]". Save options depend on the selected saver.
        """
        filename = Vips.filename_get_filename(format_string)
        option_string = Vips.filename_get_options(format_string)
        saver = Vips.Foreign.find_save_buffer(filename)
        if saver == None:
            raise Error('No known saver for "%s".' % filename)
        logger.debug('Image.write_to_buffer: saver = %s' % saver)

        return _call_base(saver, [], kwargs, self, option_string)

    # we can use Vips.Image.write_to_memory() directly

    # support with in the most trivial way
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        pass

    # operator overloads

    def __getattr__(self, name):
        logger.debug('Image.__getattr__ %s' % name)

        # look up in props first, eg. x.props.width
        if name in dir(self.props):
            return getattr(self.props, name)

        @add_doc(generate_docstring(name))
        def call_function(*args, **kwargs):
            return _call_instance(self, name, args, kwargs)

        return call_function

    def __add__(self, other):
        if isinstance(other, Vips.Image):
            return self.add(other)
        else:
            return self.linear(1, other)

    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self, other):
        if isinstance(other, Vips.Image):
            return self.subtract(other)
        else:
            return self.linear(1, smap(lambda x: -1 * x, other))

    def __rsub__(self, other):
        return self.linear(-1, other)

    def __mul__(self, other):
        if isinstance(other, Vips.Image):
            return self.multiply(other)
        else:
            return self.linear(other, 0)

    def __rmul__(self, other):
        return self.__mul__(other)

    # a / const has always been a float in vips, so div and truediv are the 
    # same
    def __div__(self, other):
        if isinstance(other, Vips.Image):
            return self.divide(other)
        else:
            return self.linear(smap(lambda x: 1.0 / x, other), 0)

    def __rdiv__(self, other):
        return (self ** -1) * other

    def __truediv__(self, other):
        return self.__div__(other)

    def __rtruediv__(self, other):
        return self.__rdiv__(other)

    def __floordiv__(self, other):
        if isinstance(other, Vips.Image):
            return self.divide(other).floor()
        else:
            return self.linear(smap(lambda x: 1.0 / x, other), 0).floor()

    def __rfloordiv__(self, other):
        return ((self ** -1) * other).floor()

    def __mod__(self, other):
        if isinstance(other, Vips.Image):
            return self.remainder(other)
        else:
            return self.remainder_const(other)

    def __pow__(self, other):
        return _call_enum(self, "math2", Vips.OperationMath2.POW, other)

    def __rpow__(self, other):
        return _call_enum(self, "math2", Vips.OperationMath2.WOP, other)

    def __abs__(self):
        return self.abs()

    def __lshift__(self, other):
        return _call_enum(self, "boolean", Vips.OperationBoolean.LSHIFT, other)

    def __rshift__(self, other):
        return _call_enum(self, "boolean", Vips.OperationBoolean.RSHIFT, other)

    def __and__(self, other):
        return _call_enum(self, "boolean", Vips.OperationBoolean.AND, other)

    def __rand__(self, other):
        return self.__and__(other)

    def __or__(self, other):
        return _call_enum(self, "boolean", Vips.OperationBoolean.OR, other)

    def __ror__(self, other):
        return self.__or__(other)

    def __xor__(self, other):
        return _call_enum(self, "boolean", Vips.OperationBoolean.EOR, other)

    def __rxor__(self, other):
        return self.__xor__(other)

    def __neg__(self):
        return -1 * self

    def __pos__(self):
        return self

    def __invert__(self):
        return self ^ -1

    def __gt__(self, other):
        return _call_enum(self, 
                          "relational", Vips.OperationRelational.MORE, other)

    def __ge__(self, other):
        return _call_enum(self, 
                          "relational", Vips.OperationRelational.MOREEQ, other)

    def __lt__(self, other):
        return _call_enum(self, 
                          "relational", Vips.OperationRelational.LESS, other)

    def __le__(self, other):
        return _call_enum(self, 
                          "relational", Vips.OperationRelational.LESSEQ, other)

    def __eq__(self, other):
        # _eq version allows comparison to None
        return _call_enum_eq(self, 
                             "relational", Vips.OperationRelational.EQUAL, other)

    def __ne__(self, other):
        # _eq version allows comparison to None
        return _call_enum_eq(self, 
                             "relational", Vips.OperationRelational.NOTEQ, other)

    def __getitem__(self, arg):
        if isinstance(arg, slice):
            i = 0
            if arg.start != None:
                i = arg.start

            n = self.bands - i
            if arg.stop != None:
                if arg.stop < 0:
                    n = self.bands + arg.stop - i
                else:
                    n = arg.stop - i
        elif isinstance(arg, int):
            i = arg
            n = 1
        else:
            raise TypeError

        if i < 0:
            i = self.bands + i

        if i < 0 or i >= self.bands:
            raise IndexError

        return self.extract_band(i, n = n)

    def __call__(self, x, y):
        return self.getpoint(x, y)

    # the cast operators int(), long() and float() must return numeric types, 
    # so we can't define them for images

    # a few useful things

    def get_value(self, field):
        """Get a named item from an Image.

        Fetch an item of metadata and convert it to a Python-friendly format.
        For example, VipsBlob values will be converted to bytes().
        """
        value = self.get(field)

        logger.debug('read out %s from %s' % (value, self))

        return unpack(value)

    def set_value(self, field, value):
        """Set a named item on an Image.

        Values are converted from Python types to something libvips can swallow.
        For example, bytes() can be used to set VipsBlob fields. 
        """
        gtype = self.get_typeof(field)
        logger.debug('%s.%s = %s' % (self, field, value))
        logger.debug('%s.%s needs a %s' % (self, field, gtype))

        # if there's a thing of this name already, convert to that type
        if gtype != GObject.TYPE_INVALID:
            # blob-ize
            if GObject.type_is_a(gtype, vips_type_blob):
                if not isinstance(value, Vips.Blob):
                    value = Vips.Blob.new(None, value)

            # image-ize
            if GObject.type_is_a(gtype, vips_type_image):
                if not isinstance(value, Vips.Image):
                    value = imageize(self, value)

            # array-ize some types, if necessary
            value = arrayize(gtype, value)

        self.set(field, value)

    def floor(self):
        """Return the largest integral value not greater than the argument."""
        return self.round(Vips.OperationRound.FLOOR)

    def ceil(self):
        """Return the smallest integral value not less than the argument."""
        return self.round(Vips.OperationRound.CEIL)

    def rint(self):
        """Return the nearest integral value."""
        return self.round(Vips.OperationRound.RINT)

    def bandand(self):
        """AND image bands together."""
        return self.bandbool(Vips.OperationBoolean.AND)

    def bandor(self):
        """OR image bands together."""
        return self.bandbool(Vips.OperationBoolean.OR)

    def bandeor(self):
        """EOR image bands together."""
        return self.bandbool(Vips.OperationBoolean.EOR)

    def bandsplit(self):
        """Split an n-band image into n separate images."""
        return [x for x in self]

    def bandjoin(self, other):
        """Append a set of images or constants bandwise."""
        if not isinstance(other, list):
            other = [other]

        # if [other] is all numbers, we can use bandjoin_const
        non_number = next((x for x in other 
                            if not isinstance(x, numbers.Number)), 
                           None)

        if non_number == None:
            return self.bandjoin_const(other)
        else:
            return _call_base("bandjoin", [[self] + other], {})

    def bandrank(self, other, **kwargs):
        """Band-wise rank filter a set of images."""
        if not isinstance(other, list):
            other = [other]

        return _call_base("bandrank", [[self] + other], kwargs)

    def maxpos(self):
        """Return the coordinates of the image maximum."""
        v, opts = self.max(x = True, y = True)
        x = opts['x']
        y = opts['y']
        return v, x, y

    def minpos(self):
        """Return the coordinates of the image minimum."""
        v, opts = self.min(x = True, y = True)
        x = opts['x']
        y = opts['y']
        return v, x, y

    def real(self):
        """Return the real part of a complex image."""
        return self.complexget(Vips.OperationComplexget.REAL)

    def imag(self):
        """Return the imaginary part of a complex image."""
        return self.complexget(Vips.OperationComplexget.IMAG)

    def polar(self):
        """Return an image converted to polar coordinates."""
        return run_cmplx(lambda x: x.complex(Vips.OperationComplex.POLAR), self)

    def rect(self):
        """Return an image converted to rectangular coordinates."""
        return run_cmplx(lambda x: x.complex(Vips.OperationComplex.RECT), self)

    def conj(self):
        """Return the complex conjugate of an image."""
        return self.complex(Vips.OperationComplex.CONJ)

    def sin(self):
        """Return the sine of an image in degrees."""
        return self.math(Vips.OperationMath.SIN)

    def cos(self):
        """Return the cosine of an image in degrees."""
        return self.math(Vips.OperationMath.COS)

    def tan(self):
        """Return the tangent of an image in degrees."""
        return self.math(Vips.OperationMath.TAN)

    def asin(self):
        """Return the inverse sine of an image in degrees."""
        return self.math(Vips.OperationMath.ASIN)

    def acos(self):
        """Return the inverse cosine of an image in degrees."""
        return self.math(Vips.OperationMath.ACOS)

    def atan(self):
        """Return the inverse tangent of an image in degrees."""
        return self.math(Vips.OperationMath.ATAN)

    def log(self):
        """Return the natural log of an image."""
        return self.math(Vips.OperationMath.LOG)

    def log10(self):
        """Return the log base 10 of an image."""
        return self.math(Vips.OperationMath.LOG10)

    def exp(self):
        """Return e ** pixel."""
        return self.math(Vips.OperationMath.EXP)

    def exp10(self):
        """Return 10 ** pixel."""
        return self.math(Vips.OperationMath.EXP10)

    def erode(self, mask):
        """Erode with a structuring element."""
        return self.morph(mask, Vips.OperationMorphology.ERODE)

    def dilate(self, mask):
        """Dilate with a structuring element."""
        return self.morph(mask, Vips.OperationMorphology.DILATE)

    def median(self, size):
        """size x size median filter."""
        return self.rank(size, size, (size * size) / 2)

    def fliphor(self):
        """Flip horizontally."""
        return self.flip(Vips.Direction.HORIZONTAL)

    def flipver(self):
        """Flip vertically."""
        return self.flip(Vips.Direction.VERTICAL)

    def rot90(self):
        """Rotate 90 degrees clockwise."""
        return self.rot(Vips.Angle.D90)

    def rot180(self):
        """Rotate 180 degrees."""
        return self.rot(Vips.Angle.D180)

    def rot270(self):
        """Rotate 270 degrees clockwise."""
        return self.rot(Vips.Angle.D270)

    # we need different imageize rules for this operator ... we need to 
    # imageize th and el to match each other first
    @add_doc(generate_docstring("ifthenelse"))
    def ifthenelse(self, th, el, **kwargs):
        for match_image in [th, el, self]:
            if isinstance(match_image, Vips.Image):
                break

        if not isinstance(th, Vips.Image):
            th = imageize(match_image, th)
        if not isinstance(el, Vips.Image):
            el = imageize(match_image, el)

        return _call_base("ifthenelse", [th, el], kwargs, self)

# add operators which needs to be class methods

# use find_class_methods.py to generate this list

# don't include "bandjoin" or "bandrank", they need to be wrapped by hand, 
# see above

class_methods = [
                    "analyzeload",
                    "arrayjoin",
                    "black",
                    "csvload",
                    "eye",
                    "fitsload",
                    "fractsurf",
                    "gaussmat",
                    "gaussnoise",
                    "gifload",
                    "gifload_buffer",
                    "grey",
                    "identity",
                    "jpegload",
                    "jpegload_buffer",
                    "logmat",
                    "magickload",
                    "magickload_buffer",
                    "mask_butterworth",
                    "mask_butterworth_band",
                    "mask_butterworth_ring",
                    "mask_fractal",
                    "mask_gaussian",
                    "mask_gaussian_band",
                    "mask_gaussian_ring",
                    "mask_ideal",
                    "mask_ideal_band",
                    "mask_ideal_ring",
                    "matload",
                    "matrixload",
                    "openexrload",
                    "openslideload",
                    "pdfload",
                    "pdfload_buffer",
                    "perlin",
                    "pngload",
                    "pngload_buffer",
                    "ppmload",
                    "radload",
                    "rawload",
                    "sines",
                    "sum",
                    "svgload",
                    "svgload_buffer",
                    "system",
                    "text",
                    "thumbnail",
                    "thumbnail_buffer",
                    "tiffload",
                    "tiffload_buffer",
                    "tonelut",
                    "vipsload",
                    "webpload",
                    "webpload_buffer",
                    "worley",
                    "xyz",
                    "zone"]

def generate_class_method(name):
    @classmethod
    @add_doc(generate_docstring(name))
    def class_method(cls, *args, **kwargs):
        return _call_base(name, args, kwargs)

    return class_method

for nickname in class_methods:
    logger.debug('adding %s as a class method' % nickname)
    # some may be missing in this vips, eg. we might not have "webpload"
    try:
        method = generate_class_method(nickname)
        setattr(Vips.Image, nickname, method)
    except Error:
        pass

Image = override(Image)
__all__.append('Image')