2ea5c5f7e5
we had a custom wrapper for bandjoin(), but bandrank(), a function with an almost identical interface, did not ... this was confusing bandrank() now has a custom wrapper too ... this breaks API unfortunately, but hopefully very few people were using this thing and it's better to make this change as soon as possible
1142 lines
37 KiB
Python
1142 lines
37 KiB
Python
# -*- 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")
|
|
|
|
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
|
|
pixel = (Vips.Image.black(1, 1) + value).cast(match_image.format)
|
|
image = pixel.embed(0, 0, match_image.width, match_image.height,
|
|
extend = Vips.Extend.COPY)
|
|
image = image.copy(interpretation = match_image.interpretation,
|
|
xres = match_image.xres,
|
|
yres = match_image.yres)
|
|
return image
|
|
|
|
# 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:
|
|
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
|
|
|
|
# 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
|
|
|
|
# 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):
|
|
if isinstance(other, Vips.Image):
|
|
return self.math2(other, Vips.OperationMath2.POW)
|
|
else:
|
|
return self.math2_const(other, Vips.OperationMath2.POW)
|
|
|
|
def __rpow__(self, other):
|
|
return self.math2_const(other, Vips.OperationMath2.WOP)
|
|
|
|
def __abs__(self):
|
|
return self.abs()
|
|
|
|
def __lshift__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.boolean(other, Vips.OperationBoolean.LSHIFT)
|
|
else:
|
|
return self.boolean_const(other, Vips.OperationBoolean.LSHIFT)
|
|
|
|
def __rshift__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.boolean(other, Vips.OperationBoolean.RSHIFT)
|
|
else:
|
|
return self.boolean_const(other, Vips.OperationBoolean.RSHIFT)
|
|
|
|
def __and__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.boolean(other, Vips.OperationBoolean.AND)
|
|
else:
|
|
return self.boolean_const(other, Vips.OperationBoolean.AND)
|
|
|
|
def __rand__(self, other):
|
|
return self.__and__(other)
|
|
|
|
def __or__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.boolean(other, Vips.OperationBoolean.OR)
|
|
else:
|
|
return self.boolean_const(other, Vips.OperationBoolean.OR)
|
|
|
|
def __ror__(self, other):
|
|
return self.__or__(other)
|
|
|
|
def __xor__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.boolean(other, Vips.OperationBoolean.EOR)
|
|
else:
|
|
return self.boolean_const(other, Vips.OperationBoolean.EOR)
|
|
|
|
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):
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.MORE)
|
|
else:
|
|
return self.relational_const(other, Vips.OperationRelational.MORE)
|
|
|
|
def __ge__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.MOREEQ)
|
|
else:
|
|
return self.relational_const(other, Vips.OperationRelational.MOREEQ)
|
|
|
|
def __lt__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.LESS)
|
|
else:
|
|
return self.relational_const(other, Vips.OperationRelational.LESS)
|
|
|
|
def __le__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.LESSEQ)
|
|
else:
|
|
return self.relational_const(other, Vips.OperationRelational.LESSEQ)
|
|
|
|
def __eq__(self, other):
|
|
# for == and != we need to allow comparison to None
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.EQUAL)
|
|
elif isinstance(other, list):
|
|
return self.relational_const(other, Vips.OperationRelational.EQUAL)
|
|
elif isinstance(other, numbers.Number):
|
|
return self.relational_const(other, Vips.OperationRelational.EQUAL)
|
|
else:
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
if isinstance(other, Vips.Image):
|
|
return self.relational(other, Vips.OperationRelational.NOTEQ)
|
|
elif isinstance(other, list):
|
|
return self.relational_const(other, Vips.OperationRelational.NOTEQ)
|
|
elif isinstance(other, numbers.Number):
|
|
return self.relational_const(other, Vips.OperationRelational.NOTEQ)
|
|
else:
|
|
return False
|
|
|
|
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('assigning %s to %s' % (value, self))
|
|
logger.debug('%s needs a %s' % (self, gtype))
|
|
|
|
# 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 = [
|
|
"system",
|
|
"sum",
|
|
"arrayjoin",
|
|
"black",
|
|
"gaussnoise",
|
|
"text",
|
|
"xyz",
|
|
"gaussmat",
|
|
"logmat",
|
|
"eye",
|
|
"grey",
|
|
"zone",
|
|
"sines",
|
|
"mask_ideal",
|
|
"mask_ideal_ring",
|
|
"mask_ideal_band",
|
|
"mask_butterworth",
|
|
"mask_butterworth_ring",
|
|
"mask_butterworth_band",
|
|
"mask_gaussian",
|
|
"mask_gaussian_ring",
|
|
"mask_gaussian_band",
|
|
"mask_fractal",
|
|
"tonelut",
|
|
"identity",
|
|
"fractsurf",
|
|
"radload",
|
|
"ppmload",
|
|
"csvload",
|
|
"matrixload",
|
|
"analyzeload",
|
|
"rawload",
|
|
"vipsload",
|
|
"pngload",
|
|
"pngload_buffer",
|
|
"matload",
|
|
"jpegload",
|
|
"jpegload_buffer",
|
|
"webpload",
|
|
"webpload_buffer",
|
|
"tiffload",
|
|
"tiffload_buffer",
|
|
"pdfload",
|
|
"pdfload_buffer",
|
|
"svgload",
|
|
"svgload_buffer",
|
|
"gifload",
|
|
"gifload_buffer",
|
|
"openslideload",
|
|
"magickload",
|
|
"magickload_buffer",
|
|
"fitsload",
|
|
"openexrload"]
|
|
|
|
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')
|