1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-25 02:57:22 +00:00
calibre-web/vendor/wand/image.py

3705 lines
136 KiB
Python
Raw Normal View History

2016-04-08 22:36:30 +00:00
""":mod:`wand.image` --- Image objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Opens and manipulates images. Image objects can be used in :keyword:`with`
statement, and these resources will be automatically managed (even if any
error happened)::
with Image(filename='pikachu.png') as i:
print('width =', i.width)
print('height =', i.height)
"""
import collections
import ctypes
import functools
import numbers
import weakref
from . import compat
from .api import MagickPixelPacket, libc, libmagick, library
from .color import Color
from .compat import (binary, binary_type, encode_filename, file_types,
string_type, text, xrange)
from .exceptions import MissingDelegateError, WandException
from .resource import DestroyedResourceError, Resource
from .font import Font
__all__ = ('ALPHA_CHANNEL_TYPES', 'CHANNELS', 'COLORSPACE_TYPES',
'COMPARE_METRICS', 'COMPOSITE_OPERATORS', 'COMPRESSION_TYPES',
2016-04-08 22:36:30 +00:00
'EVALUATE_OPS', 'FILTER_TYPES',
'GRAVITY_TYPES', 'IMAGE_TYPES', 'ORIENTATION_TYPES', 'UNIT_TYPES',
'FUNCTION_TYPES',
'BaseImage', 'ChannelDepthDict', 'ChannelImageDict',
'ClosedImageError', 'HistogramDict', 'Image', 'ImageProperty',
'Iterator', 'Metadata', 'OptionDict', 'manipulative')
#: (:class:`tuple`) The list of filter types.
#:
#: - ``'undefined'``
#: - ``'point'``
#: - ``'box'``
#: - ``'triangle'``
#: - ``'hermite'``
#: - ``'hanning'``
#: - ``'hamming'``
#: - ``'blackman'``
#: - ``'gaussian'``
#: - ``'quadratic'``
#: - ``'cubic'``
#: - ``'catrom'``
#: - ``'mitchell'``
#: - ``'jinc'``
#: - ``'sinc'``
#: - ``'sincfast'``
#: - ``'kaiser'``
#: - ``'welsh'``
#: - ``'parzen'``
#: - ``'bohman'``
#: - ``'bartlett'``
#: - ``'lagrange'``
#: - ``'lanczos'``
#: - ``'lanczossharp'``
#: - ``'lanczos2'``
#: - ``'lanczos2sharp'``
#: - ``'robidoux'``
#: - ``'robidouxsharp'``
#: - ``'cosine'``
#: - ``'spline'``
#: - ``'sentinel'``
#:
#: .. seealso::
#:
#: `ImageMagick Resize Filters`__
#: Demonstrates the results of resampling images using the various
#: resize filters and blur settings available in ImageMagick.
#:
#: __ http://www.imagemagick.org/Usage/resize/
FILTER_TYPES = ('undefined', 'point', 'box', 'triangle', 'hermite', 'hanning',
'hamming', 'blackman', 'gaussian', 'quadratic', 'cubic',
'catrom', 'mitchell', 'jinc', 'sinc', 'sincfast', 'kaiser',
'welsh', 'parzen', 'bohman', 'bartlett', 'lagrange', 'lanczos',
'lanczossharp', 'lanczos2', 'lanczos2sharp', 'robidoux',
'robidouxsharp', 'cosine', 'spline', 'sentinel')
#: (:class:`tuple`) The list of compare metric types
#:
#: - ``'undefined'``
#: - ``'absolute'``
#: - ``'mean_absolute'``
#: - ``'mean_error_per_pixel'``
#: - ``'mean_squared'``
#: - ``'normalized_cross_correlation'``
#: - ``'peak_absolute'``
#: - ``'peak_signal_to_noise_ratio'``
#: - ``'perceptual_hash'``
#: - ``'root_mean_square'``
#: .. seealso::
#:
#: `ImageMagick Compare Operations`__
#:
#: __ http://www.imagemagick.org/Usage/compare/
#:
#: .. versionadded:: 0.4.3
COMPARE_METRICS = ('undefined', 'absolute',
'mean_absolute', 'mean_error_per_pixel',
'mean_squared', 'normalized_cross_correlation',
'peak_absolute', 'peak_signal_to_noise_ratio',
'perceptual_hash', 'root_mean_square')
2016-04-08 22:36:30 +00:00
#: (:class:`tuple`) The list of composition operators
#:
#: - ``'undefined'``
#: - ``'no'``
#: - ``'add'``
#: - ``'atop'``
#: - ``'blend'``
#: - ``'bumpmap'``
#: - ``'change_mask'``
#: - ``'clear'``
#: - ``'color_burn'``
#: - ``'color_dodge'``
#: - ``'colorize'``
#: - ``'copy_black'``
#: - ``'copy_blue'``
#: - ``'copy'``
#: - ``'copy_cyan'``
#: - ``'copy_green'``
#: - ``'copy_magenta'``
#: - ``'copy_opacity'``
#: - ``'copy_red'``
#: - ``'copy_yellow'``
#: - ``'darken'``
#: - ``'dst_atop'``
#: - ``'dst'``
#: - ``'dst_in'``
#: - ``'dst_out'``
#: - ``'dst_over'``
#: - ``'difference'``
#: - ``'displace'``
#: - ``'dissolve'``
#: - ``'exclusion'``
#: - ``'hard_light'``
#: - ``'hue'``
#: - ``'in'``
#: - ``'lighten'``
#: - ``'linear_light'``
#: - ``'luminize'``
#: - ``'minus'``
#: - ``'modulate'``
#: - ``'multiply'``
#: - ``'out'``
#: - ``'over'``
#: - ``'overlay'``
#: - ``'plus'``
#: - ``'replace'``
#: - ``'saturate'``
#: - ``'screen'``
#: - ``'soft_light'``
#: - ``'src_atop'``
#: - ``'src'``
#: - ``'src_in'``
#: - ``'src_out'``
#: - ``'src_over'``
#: - ``'subtract'``
#: - ``'threshold'``
#: - ``'xor'``
#: - ``'divide'``
#:
#: .. versionchanged:: 0.3.0
#: Renamed from :const:`COMPOSITE_OPS` to :const:`COMPOSITE_OPERATORS`.
#:
#: .. seealso::
#:
#: `Compositing Images`__ ImageMagick v6 Examples
#: Image composition is the technique of combining images that have,
#: or do not have, transparency or an alpha channel.
#: This is usually performed using the IM :program:`composite` command.
#: It may also be performed as either part of a larger sequence of
#: operations or internally by other image operators.
#:
#: `ImageMagick Composition Operators`__
#: Demonstrates the results of applying the various composition
#: composition operators.
#:
#: __ http://www.imagemagick.org/Usage/compose/
#: __ http://www.rubblewebs.co.uk/imagemagick/operators/compose.php
COMPOSITE_OPERATORS = (
'undefined', 'no', 'add', 'atop', 'blend', 'bumpmap', 'change_mask',
'clear', 'color_burn', 'color_dodge', 'colorize', 'copy_black',
'copy_blue', 'copy', 'copy_cyan', 'copy_green', 'copy_magenta',
'copy_opacity', 'copy_red', 'copy_yellow', 'darken', 'dst_atop', 'dst',
'dst_in', 'dst_out', 'dst_over', 'difference', 'displace', 'dissolve',
'exclusion', 'hard_light', 'hue', 'in', 'lighten', 'linear_light',
'luminize', 'minus', 'modulate', 'multiply', 'out', 'over', 'overlay',
'plus', 'replace', 'saturate', 'screen', 'soft_light', 'src_atop', 'src',
'src_in', 'src_out', 'src_over', 'subtract', 'threshold', 'xor', 'divide'
)
#: (:class:`dict`) The dictionary of channel types.
#:
#: - ``'undefined'``
#: - ``'red'``
#: - ``'gray'``
#: - ``'cyan'``
#: - ``'green'``
#: - ``'magenta'``
#: - ``'blue'``
#: - ``'yellow'``
#: - ``'alpha'``
#: - ``'opacity'``
#: - ``'black'``
#: - ``'index'``
#: - ``'composite_channels'``
#: - ``'all_channels'``
#: - ``'true_alpha'``
#: - ``'rgb_channels'``
#: - ``'gray_channels'``
#: - ``'sync_channels'``
#: - ``'default_channels'``
#:
#: .. seealso::
#:
#: `ImageMagick Color Channels`__
#: Lists the various channel types with descriptions of each
#:
#: __ http://www.imagemagick.org/Magick++/Enumerations.html#ChannelType
CHANNELS = dict(undefined=0, red=1, gray=1, cyan=1, green=2, magenta=2,
blue=4, yellow=4, alpha=8, opacity=8, black=32, index=32,
composite_channels=47, all_channels=134217727, true_alpha=64,
rgb_channels=128, gray_channels=128, sync_channels=256,
default_channels=134217719)
#: (:class:`tuple`) The list of evaluation operators
#:
#: - ``'undefined'``
#: - ``'add'``
#: - ``'and'``
#: - ``'divide'``
#: - ``'leftshift'``
#: - ``'max'``
#: - ``'min'``
#: - ``'multiply'``
#: - ``'or'``
#: - ``'rightshift'``
#: - ``'set'``
#: - ``'subtract'``
#: - ``'xor'``
#: - ``'pow'``
#: - ``'log'``
#: - ``'threshold'``
#: - ``'thresholdblack'``
#: - ``'thresholdwhite'``
#: - ``'gaussiannoise'``
#: - ``'impulsenoise'``
#: - ``'laplaciannoise'``
#: - ``'multiplicativenoise'``
#: - ``'poissonnoise'``
#: - ``'uniformnoise'``
#: - ``'cosine'``
#: - ``'sine'``
#: - ``'addmodulus'``
#: - ``'mean'``
#: - ``'abs'``
#: - ``'exponential'``
#: - ``'median'``
#: - ``'sum'``
#:
#: .. seealso::
#:
#: `ImageMagick Image Evaluation Operators`__
#: Describes the MagickEvaluateImageChannel method and lists the
#: various evaluations operators
#:
#: __ http://www.magickwand.org/MagickEvaluateImage.html
EVALUATE_OPS = ('undefined', 'add', 'and', 'divide', 'leftshift', 'max',
'min', 'multiply', 'or', 'rightshift', 'set', 'subtract',
'xor', 'pow', 'log', 'threshold', 'thresholdblack',
'thresholdwhite', 'gaussiannoise', 'impulsenoise',
'laplaciannoise', 'multiplicativenoise', 'poissonnoise',
'uniformnoise', 'cosine', 'sine', 'addmodulus', 'mean',
'abs', 'exponential', 'median', 'sum', 'rootmeansquare')
#: (:class:`tuple`) The list of colorspaces.
#:
#: - ``'undefined'``
#: - ``'rgb'``
#: - ``'gray'``
#: - ``'transparent'``
#: - ``'ohta'``
#: - ``'lab'``
#: - ``'xyz'``
#: - ``'ycbcr'``
#: - ``'ycc'``
#: - ``'yiq'``
#: - ``'ypbpr'``
#: - ``'yuv'``
#: - ``'cmyk'``
#: - ``'srgb'``
#: - ``'hsb'``
#: - ``'hsl'``
#: - ``'hwb'``
#: - ``'rec601luma'``
#: - ``'rec601ycbcr'``
#: - ``'rec709luma'``
#: - ``'rec709ycbcr'``
#: - ``'log'``
#: - ``'cmy'``
#: - ``'luv'``
#: - ``'hcl'``
#: - ``'lch'``
#: - ``'lms'``
#: - ``'lchab'``
#: - ``'lchuv'``
#: - ``'scrgb'``
#: - ``'hsi'``
#: - ``'hsv'``
#: - ``'hclp'``
#: - ``'ydbdr'``
#:
#: .. seealso::
#:
#: `ImageMagick Color Management`__
#: Describes the ImageMagick color management operations
#:
#: __ http://www.imagemagick.org/script/color-management.php
#:
#: .. versionadded:: 0.3.4
COLORSPACE_TYPES = ('undefined', 'rgb', 'gray', 'transparent', 'ohta', 'lab',
'xyz', 'ycbcr', 'ycc', 'yiq', 'ypbpr', 'yuv', 'cmyk',
'srgb', 'hsb', 'hsl', 'hwb', 'rec601luma', 'rec601ycbcr',
'rec709luma', 'rec709ycbcr', 'log', 'cmy', 'luv', 'hcl',
'lch', 'lms', 'lchab', 'lchuv', 'scrgb', 'hsi', 'hsv',
'hclp', 'ydbdr')
#: (:class:`tuple`) The list of alpha channel types
#:
#: - ``'undefined'``
#: - ``'activate'``
#: - ``'background'``
#: - ``'copy'``
#: - ``'deactivate'``
#: - ``'extract'``
#: - ``'opaque'``
#: - ``'reset'``
#: - ``'set'``
#: - ``'shape'``
#: - ``'transparent'``
#: - ``'flatten'``
#: - ``'remove'``
#:
#: .. seealso::
#: `ImageMagick Image Channel`__
#: Describes the SetImageAlphaChannel method which can be used
#: to modify alpha channel. Also describes AlphaChannelType
#:
#: __ http://www.imagemagick.org/api/channel.php#SetImageAlphaChannel
ALPHA_CHANNEL_TYPES = ('undefined', 'activate', 'background', 'copy',
'deactivate', 'extract', 'opaque', 'reset', 'set',
'shape', 'transparent', 'flatten', 'remove')
#: (:class:`tuple`) The list of image types
#:
#: - ``'undefined'``
#: - ``'bilevel'``
#: - ``'grayscale'``
#: - ``'grayscalematte'``
#: - ``'palette'``
#: - ``'palettematte'``
#: - ``'truecolor'``
#: - ``'truecolormatte'``
#: - ``'colorseparation'``
#: - ``'colorseparationmatte'``
#: - ``'optimize'``
#: - ``'palettebilevelmatte'``
#:
#: .. seealso::
#:
#: `ImageMagick Image Types`__
#: Describes the MagickSetImageType method which can be used
#: to set the type of an image
#:
#: __ http://www.imagemagick.org/api/magick-image.php#MagickSetImageType
IMAGE_TYPES = ('undefined', 'bilevel', 'grayscale', 'grayscalematte',
'palette', 'palettematte', 'truecolor', 'truecolormatte',
'colorseparation', 'colorseparationmatte', 'optimize',
'palettebilevelmatte')
#: (:class:`tuple`) The list of resolution unit types.
#:
#: - ``'undefined'``
#: - ``'pixelsperinch'``
#: - ``'pixelspercentimeter'``
#:
#: .. seealso::
#:
#: `ImageMagick Image Units`__
#: Describes the MagickSetImageUnits method which can be used
#: to set image units of resolution
#:
#: __ http://www.imagemagick.org/api/magick-image.php#MagickSetImageUnits
UNIT_TYPES = 'undefined', 'pixelsperinch', 'pixelspercentimeter'
#: (:class:`tuple`) The list of :attr:`~BaseImage.gravity` types.
#:
#: .. versionadded:: 0.3.0
GRAVITY_TYPES = ('forget', 'north_west', 'north', 'north_east', 'west',
'center', 'east', 'south_west', 'south', 'south_east',
'static')
#: (:class:`tuple`) The list of :attr:`~BaseImage.orientation` types.
#:
#: .. versionadded:: 0.3.0
ORIENTATION_TYPES = ('undefined', 'top_left', 'top_right', 'bottom_right',
'bottom_left', 'left_top', 'right_top', 'right_bottom',
'left_bottom')
#: (:class:`collections.Set`) The set of available :attr:`~BaseImage.options`.
#:
#: .. versionadded:: 0.3.0
#:
#: .. versionchanged:: 0.3.4
#: Added ``'jpeg:sampling-factor'`` option.
#:
#: .. versionchanged:: 0.3.9
#: Added ``'pdf:use-cropbox'`` option.
OPTIONS = frozenset(['fill', 'jpeg:sampling-factor', 'pdf:use-cropbox'])
#: (:class:`tuple`) The list of :attr:`Image.compression` types.
#:
#: .. versionadded:: 0.3.6
COMPRESSION_TYPES = (
'undefined', 'b44a', 'b44', 'bzip', 'dxt1', 'dxt3', 'dxt5', 'fax',
'group4',
'jbig1', # ISO/IEC std 11544 / ITU-T rec T.82
'jbig2', # ISO/IEC std 14492 / ITU-T rec T.88
'jpeg2000', # ISO/IEC std 15444-1
'jpeg', 'losslessjpeg',
'lzma', # Lempel-Ziv-Markov chain algorithm
'lzw', 'no', 'piz', 'pxr24', 'rle', 'zip', 'zips'
)
#: (:class:`tuple`) The list of :attr:`Image.function` types.
#:
#: - ``'undefined'``
#: - ``'polynomial'``
#: - ``'sinusoid'``
#: - ``'arcsin'``
#: - ``'arctan'``
FUNCTION_TYPES = ('undefined', 'polynomial', 'sinusoid', 'arcsin', 'arctan')
#: (:class:`tuple`) The list of :method:`Image.distort` methods.
#:
#: - ``'undefined'``
#: - ``'affine'``
#: - ``'affine_projection'``
#: - ``'scale_rotate_translate'``
#: - ``'perspective'``
#: - ``'perspective_projection'``
#: - ``'bilinear_forward'``
#: - ``'bilinear_reverse'``
#: - ``'polynomial'``
#: - ``'arc'``
#: - ``'polar'``
#: - ``'depolar'``
#: - ``'cylinder_2_plane'``
#: - ``'plane_2_cylinder'``
#: - ``'barrel'``
#: - ``'barrel_inverse'``
#: - ``'shepards'``
#: - ``'resize'``
#: - ``'sentinel'``
#:
#: .. versionadded:: 0.4.1
DISTORTION_METHODS = (
'undefined', 'affine', 'affine_projection', 'scale_rotate_translate',
'perspective', 'perspective_projection', 'bilinear_forward',
'bilinear_reverse', 'polynomial', 'arc', 'polar', 'depolar',
'cylinder_2_plane', 'plane_2_cylinder', 'barrel', 'barrel_inverse',
'shepards', 'resize', 'sentinel'
)
#: (:class:`tuple`) The list of :attr:`~BaseImage.virtual_pixel` types.
#: - ``'undefined'``
#: - ``'background'``
#: - ``'constant'``
#: - ``'dither'``
#: - ``'edge'``
#: - ``'mirror'``
#: - ``'random'``
#: - ``'tile'``
#: - ``'transparent'``
#: - ``'mask'``
#: - ``'black'``
#: - ``'gray'``
#: - ``'white'``
#: - ``'horizontal_tile'``
#: - ``'vertical_tile'``
#: - ``'horizontal_tile_edge'``
#: - ``'vertical_tile_edge'``
#: - ``'checker_tile'``
#:
#: .. versionadded:: 0.4.1
VIRTUAL_PIXEL_METHOD = ('undefined', 'background', 'constant', 'dither',
'edge', 'mirror', 'random', 'tile', 'transparent',
'mask', 'black', 'gray', 'white', 'horizontal_tile',
'vertical_tile', 'horizontal_tile_edge',
'vertical_tile_edge', 'checker_tile')
#: (:class:`tuple`) The list of :attr:`~BaseImage.layer_method` types.
#: - ``'undefined'``
#: - ``'coalesce'``
#: - ``'compareany'``
#: - ``'compareclear'``
#: - ``'compareoverlay'``
#: - ``'dispose'``
#: - ``'optimize'``
#: - ``'optimizeimage'``
#: - ``'optimizeplus'``
#: - ``'optimizetrans'``
#: - ``'removedups'``
#: - ``'removezero'``
#: - ``'composite'``
#: - ``'merge'``
#: - ``'flatten'``
#: - ``'mosaic'``
#: - ``'trimbounds'``
#: .. versionadded:: 0.4.3
IMAGE_LAYER_METHOD = ('undefined', 'coalesce', 'compareany', 'compareclear',
'compareoverlay', 'dispose', 'optimize', 'optimizeimage',
'optimizeplus', 'optimizetrans', 'removedups',
'removezero', 'composite', 'merge', 'flatten', 'mosaic',
'trimbounds')
2016-04-08 22:36:30 +00:00
def manipulative(function):
"""Mark the operation manipulating itself instead of returning new one."""
@functools.wraps(function)
def wrapped(self, *args, **kwargs):
result = function(self, *args, **kwargs)
self.dirty = True
return result
return wrapped
class BaseImage(Resource):
"""The abstract base of :class:`Image` (container) and
:class:`~wand.sequence.SingleImage`. That means the most of
operations, defined in this abstract classs, are possible for
both :class:`Image` and :class:`~wand.sequence.SingleImage`.
.. versionadded:: 0.3.0
"""
#: (:class:`OptionDict`) The mapping of internal option settings.
#:
#: .. versionadded:: 0.3.0
#:
#: .. versionchanged:: 0.3.4
#: Added ``'jpeg:sampling-factor'`` option.
#:
#: .. versionchanged:: 0.3.9
#: Added ``'pdf:use-cropbox'`` option.
options = None
#: (:class:`collections.Sequence`) The list of
#: :class:`~wand.sequence.SingleImage`\ s that the image contains.
#:
#: .. versionadded:: 0.3.0
sequence = None
#: (:class:`bool`) Whether the image is changed or not.
dirty = None
c_is_resource = library.IsMagickWand
c_destroy_resource = library.DestroyMagickWand
c_get_exception = library.MagickGetException
c_clear_exception = library.MagickClearException
__slots__ = '_wand',
def __init__(self, wand):
self.wand = wand
self.channel_images = ChannelImageDict(self)
self.channel_depths = ChannelDepthDict(self)
self.options = OptionDict(self)
self.dirty = False
@property
def wand(self):
"""Internal pointer to the MagickWand instance. It may raise
:exc:`ClosedImageError` when the instance has destroyed already.
"""
try:
return self.resource
except DestroyedResourceError:
raise ClosedImageError(repr(self) + ' is closed already')
@wand.setter
def wand(self, wand):
try:
self.resource = wand
except TypeError:
raise TypeError(repr(wand) + ' is not a MagickWand instance')
@wand.deleter
def wand(self):
del self.resource
def clone(self):
"""Clones the image. It is equivalent to call :class:`Image` with
``image`` parameter. ::
with img.clone() as cloned:
# manipulate the cloned image
pass
:returns: the cloned new image
:rtype: :class:`Image`
.. versionadded:: 0.1.1
"""
return Image(image=self)
def __len__(self):
return self.height
def __iter__(self):
return Iterator(image=self)
def __getitem__(self, idx):
if (not isinstance(idx, string_type) and
isinstance(idx, collections.Iterable)):
idx = tuple(idx)
d = len(idx)
if not (1 <= d <= 2):
raise ValueError('index cannot be {0}-dimensional'.format(d))
elif d == 2:
x, y = idx
x_slice = isinstance(x, slice)
y_slice = isinstance(y, slice)
if x_slice and not y_slice:
y = slice(y, y + 1)
elif not x_slice and y_slice:
x = slice(x, x + 1)
elif not (x_slice or y_slice):
if not (isinstance(x, numbers.Integral) and
isinstance(y, numbers.Integral)):
raise TypeError('x and y must be integral, not ' +
repr((x, y)))
if x < 0:
x += self.width
if y < 0:
y += self.height
if x >= self.width:
raise IndexError('x must be less than width')
elif y >= self.height:
raise IndexError('y must be less than height')
elif x < 0:
raise IndexError('x cannot be less than 0')
elif y < 0:
raise IndexError('y cannot be less than 0')
with iter(self) as iterator:
iterator.seek(y)
return iterator.next(x)
if not (x.step is None and y.step is None):
raise ValueError('slicing with step is unsupported')
elif (x.start is None and x.stop is None and
y.start is None and y.stop is None):
return self.clone()
cloned = self.clone()
try:
cloned.crop(x.start, y.start, x.stop, y.stop)
except ValueError as e:
raise IndexError(str(e))
return cloned
else:
return self[idx[0]]
elif isinstance(idx, numbers.Integral):
if idx < 0:
idx += self.height
elif idx >= self.height:
raise IndexError('index must be less than height, but got ' +
repr(idx))
elif idx < 0:
raise IndexError('index cannot be less than zero, but got ' +
repr(idx))
with iter(self) as iterator:
iterator.seek(idx)
return iterator.next()
elif isinstance(idx, slice):
return self[:, idx]
raise TypeError('unsupported index type: ' + repr(idx))
def __eq__(self, other):
if isinstance(other, type(self)):
return self.signature == other.signature
return False
def __ne__(self, other):
return not (self == other)
def __hash__(self):
return hash(self.signature)
@property
def animation(self):
"""(:class:`bool`) Whether the image is animation or not.
It doesn't only mean that the image has two or more images (frames),
but all frames are even the same size. It's about image format,
not content. It's :const:`False` even if :mimetype:`image/ico`
consits of two or more images of the same size.
For example, it's :const:`False` for :mimetype:`image/jpeg`,
:mimetype:`image/gif`, :mimetype:`image/ico`.
If :mimetype:`image/gif` has two or more frames, it's :const:`True`.
If :mimetype:`image/gif` has only one frame, it's :const:`False`.
.. versionadded:: 0.3.0
.. versionchanged:: 0.3.8
Became to accept :mimetype:`image/x-gif` as well.
"""
return False
@property
def gravity(self):
"""(:class:`basestring`) The text placement gravity used when
annotating with text. It's a string from :const:`GRAVITY_TYPES`
list. It also can be set.
"""
gravity_index = library.MagickGetGravity(self.wand)
if not gravity_index:
self.raise_exception()
return GRAVITY_TYPES[gravity_index]
@gravity.setter
@manipulative
def gravity(self, value):
if not isinstance(value, string_type):
raise TypeError('expected a string, not ' + repr(value))
if value not in GRAVITY_TYPES:
raise ValueError('expected a string from GRAVITY_TYPES, not ' +
repr(value))
2016-04-08 22:36:30 +00:00
library.MagickSetGravity(self.wand, GRAVITY_TYPES.index(value))
@property
def font_path(self):
"""(:class:`basestring`) The path of the current font.
It also can be set.
"""
return text(library.MagickGetFont(self.wand))
@font_path.setter
@manipulative
def font_path(self, font):
font = binary(font)
if library.MagickSetFont(self.wand, font) is False:
raise ValueError('font is invalid')
@property
def font_size(self):
"""(:class:`numbers.Real`) The font size. It also can be set."""
return library.MagickGetPointsize(self.wand)
@font_size.setter
@manipulative
def font_size(self, size):
if not isinstance(size, numbers.Real):
raise TypeError('expected a numbers.Real, but got ' + repr(size))
elif size < 0.0:
raise ValueError('cannot be less then 0.0, but got ' + repr(size))
elif library.MagickSetPointsize(self.wand, size) is False:
raise ValueError('unexpected error is occur')
@property
def font_antialias(self):
return bool(library.MagickGetAntialias(self.wand))
@font_antialias.setter
@manipulative
def font_antialias(self, antialias):
if not isinstance(antialias, bool):
raise TypeError('font_antialias must be a bool, not ' +
repr(antialias))
library.MagickSetAntialias(self.wand, antialias)
@property
def font(self):
"""(:class:`wand.font.Font`) The current font options."""
return Font(
path=text(self.font_path),
size=self.font_size,
color=self.font_color,
antialias=self.font_antialias
)
@font.setter
@manipulative
def font(self, font):
if not isinstance(font, Font):
raise TypeError('font must be a wand.font.Font, not ' + repr(font))
self.font_path = font.path
self.font_size = font.size
self.font_color = font.color
self.font_antialias = font.antialias
@property
def page(self):
"""The dimensions and offset of this Wand's page as a 4-tuple:
``(width, height, x, y)``.
Note that since it is based on the virtual canvas, it may not equal the
dimensions of an image. See the ImageMagick documentation on the
virtual canvas for more information.
.. versionadded:: 0.4.3
"""
w = ctypes.c_uint()
h = ctypes.c_uint()
x = ctypes.c_int()
y = ctypes.c_int()
r = library.MagickGetImagePage(self.wand, w, h, x, y)
if not r:
self.raise_exception()
return int(w.value), int(h.value), int(x.value), int(y.value)
@page.setter
@manipulative
def page(self, newpage):
if isinstance(newpage, collections.Sequence):
w, h, x, y = newpage
else:
raise TypeError("page layout must be 4-tuple")
r = library.MagickSetImagePage(self.wand, w, h, x, y)
if not r:
self.raise_exception()
@property
def page_width(self):
"""(:class:`numbers.Integral`) The width of the page for this wand.
.. versionadded:: 0.4.3
"""
return self.page[0]
@page_width.setter
@manipulative
def page_width(self, width):
newpage = list(self.page)
newpage[0] = width
self.page = newpage
@property
def page_height(self):
"""(:class:`numbers.Integral`) The height of the page for this wand.
.. versionadded:: 0.4.3
"""
return self.page[1]
@page_height.setter
@manipulative
def page_height(self, height):
newpage = list(self.page)
newpage[1] = height
self.page = newpage
@property
def page_x(self):
"""(:class:`numbers.Integral`) The X-offset of the page for this wand.
.. versionadded:: 0.4.3
"""
return self.page[2]
@page_x.setter
@manipulative
def page_x(self, x):
newpage = list(self.page)
newpage[2] = x
self.page = newpage
@property
def page_y(self):
"""(:class:`numbers.Integral`) The Y-offset of the page for this wand.
.. versionadded:: 0.4.3
"""
return self.page[3]
@page_y.setter
@manipulative
def page_y(self, y):
newpage = list(self.page)
newpage[3] = y
self.page = newpage
2016-04-08 22:36:30 +00:00
@property
def width(self):
"""(:class:`numbers.Integral`) The width of this image."""
return library.MagickGetImageWidth(self.wand)
@width.setter
@manipulative
def width(self, width):
if width is not None and not isinstance(width, numbers.Integral):
raise TypeError('width must be a integral, not ' + repr(width))
library.MagickSetSize(self.wand, width, self.height)
@property
def height(self):
"""(:class:`numbers.Integral`) The height of this image."""
return library.MagickGetImageHeight(self.wand)
@height.setter
@manipulative
def height(self, height):
if height is not None and not isinstance(height, numbers.Integral):
raise TypeError('height must be a integral, not ' + repr(height))
library.MagickSetSize(self.wand, self.width, height)
@property
def orientation(self):
"""(:class:`basestring`) The image orientation. It's a string from
:const:`ORIENTATION_TYPES` list. It also can be set.
.. versionadded:: 0.3.0
"""
orientation_index = library.MagickGetImageOrientation(self.wand)
return ORIENTATION_TYPES[orientation_index]
@orientation.setter
@manipulative
def orientation(self, value):
if not isinstance(value, string_type):
raise TypeError('expected a string, not ' + repr(value))
if value not in ORIENTATION_TYPES:
raise ValueError('expected a string from ORIENTATION_TYPES, not ' +
repr(value))
2016-04-08 22:36:30 +00:00
index = ORIENTATION_TYPES.index(value)
library.MagickSetImageOrientation(self.wand, index)
@property
def font_color(self):
return Color(self.options['fill'])
@font_color.setter
@manipulative
def font_color(self, color):
if not isinstance(color, Color):
raise TypeError('font_color must be a wand.color.Color, not ' +
repr(color))
self.options['fill'] = color.string
@manipulative
def caption(self, text, left=0, top=0, width=None, height=None, font=None,
gravity=None):
"""Writes a caption ``text`` into the position.
:param text: text to write
:type text: :class:`basestring`
:param left: x offset in pixels
:type left: :class:`numbers.Integral`
:param top: y offset in pixels
:type top: :class:`numbers.Integral`
:param width: width of caption in pixels.
default is :attr:`width` of the image
:type width: :class:`numbers.Integral`
:param height: height of caption in pixels.
default is :attr:`height` of the image
:type height: :class:`numbers.Integral`
:param font: font to use. default is :attr:`font` of the image
:type font: :class:`wand.font.Font`
:param gravity: text placement gravity.
uses the current :attr:`gravity` setting of the image
by default
:type gravity: :class:`basestring`
.. versionadded:: 0.3.0
"""
if not isinstance(left, numbers.Integral):
raise TypeError('left must be an integer, not ' + repr(left))
elif not isinstance(top, numbers.Integral):
raise TypeError('top must be an integer, not ' + repr(top))
elif width is not None and not isinstance(width, numbers.Integral):
raise TypeError('width must be an integer, not ' + repr(width))
elif height is not None and not isinstance(height, numbers.Integral):
raise TypeError('height must be an integer, not ' + repr(height))
elif font is not None and not isinstance(font, Font):
raise TypeError('font must be a wand.font.Font, not ' + repr(font))
elif gravity is not None and compat.text(gravity) not in GRAVITY_TYPES:
raise ValueError('invalid gravity value')
if width is None:
width = self.width - left
if height is None:
height = self.height - top
if not font:
try:
font = self.font
except TypeError:
raise TypeError('font must be specified or existing in image')
2016-04-08 22:36:30 +00:00
with Image() as textboard:
library.MagickSetSize(textboard.wand, width, height)
textboard.font = font
2016-04-08 22:36:30 +00:00
textboard.gravity = gravity or self.gravity
with Color('transparent') as background_color:
library.MagickSetBackgroundColor(textboard.wand,
background_color.resource)
textboard.read(filename=b'caption:' + text.encode('utf-8'))
self.composite(textboard, left, top)
@property
def resolution(self):
"""(:class:`tuple`) Resolution of this image.
.. versionadded:: 0.3.0
"""
x = ctypes.c_double()
y = ctypes.c_double()
r = library.MagickGetImageResolution(self.wand, x, y)
if not r:
self.raise_exception()
return int(x.value), int(y.value)
@resolution.setter
@manipulative
def resolution(self, geometry):
if isinstance(geometry, collections.Sequence):
x, y = geometry
elif isinstance(geometry, numbers.Integral):
x, y = geometry, geometry
else:
raise TypeError('resolution must be a (x, y) pair or an integer '
'of the same x/y')
if self.size == (0, 0):
r = library.MagickSetResolution(self.wand, x, y)
else:
r = library.MagickSetImageResolution(self.wand, x, y)
if not r:
self.raise_exception()
@property
def size(self):
"""(:class:`tuple`) The pair of (:attr:`width`, :attr:`height`)."""
return self.width, self.height
@property
def units(self):
"""(:class:`basestring`) The resolution units of this image."""
r = library.MagickGetImageUnits(self.wand)
return UNIT_TYPES[text(r)]
@units.setter
@manipulative
def units(self, units):
if not isinstance(units, string_type) or units not in UNIT_TYPES:
raise TypeError('Unit value must be a string from wand.images.'
'UNIT_TYPES, not ' + repr(units))
r = library.MagickSetImageUnits(self.wand, UNIT_TYPES.index(units))
if not r:
self.raise_exception()
@property
def virtual_pixel(self):
"""(:class:`basestring`) The virtual pixel of image.
This can also be set with a value from :const:`VIRTUAL_PIXEL_METHOD`
... versionadded:: 0.4.1
"""
method_index = library.MagickGetImageVirtualPixelMethod(self.wand)
return VIRTUAL_PIXEL_METHOD[method_index]
@virtual_pixel.setter
def virtual_pixel(self, method):
if method not in VIRTUAL_PIXEL_METHOD:
raise ValueError('expected method from VIRTUAL_PIXEL_METHOD,'
' not ' + repr(method))
library.MagickSetImageVirtualPixelMethod(
self.wand,
VIRTUAL_PIXEL_METHOD.index(method)
)
@property
def colorspace(self):
"""(:class:`basestring`) The image colorspace.
Defines image colorspace as in :const:`COLORSPACE_TYPES` enumeration.
It may raise :exc:`ValueError` when the colorspace is unknown.
.. versionadded:: 0.3.4
"""
colorspace_type_index = library.MagickGetImageColorspace(self.wand)
if not colorspace_type_index:
self.raise_exception()
return COLORSPACE_TYPES[text(colorspace_type_index)]
@colorspace.setter
@manipulative
def colorspace(self, colorspace_type):
if (not isinstance(colorspace_type, string_type) or
colorspace_type not in COLORSPACE_TYPES):
raise TypeError('Colorspace value must be a string from '
'COLORSPACE_TYPES, not ' + repr(colorspace_type))
r = library.MagickSetImageColorspace(
self.wand,
COLORSPACE_TYPES.index(colorspace_type)
)
if not r:
self.raise_exception()
@property
def depth(self):
"""(:class:`numbers.Integral`) The depth of this image.
.. versionadded:: 0.2.1
"""
return library.MagickGetImageDepth(self.wand)
@depth.setter
@manipulative
def depth(self, depth):
r = library.MagickSetImageDepth(self.wand, depth)
if not r:
raise self.raise_exception()
@property
def type(self):
"""(:class:`basestring`) The image type.
Defines image type as in :const:`IMAGE_TYPES` enumeration.
It may raise :exc:`ValueError` when the type is unknown.
.. versionadded:: 0.2.2
"""
image_type_index = library.MagickGetImageType(self.wand)
if not image_type_index:
self.raise_exception()
return IMAGE_TYPES[text(image_type_index)]
@type.setter
@manipulative
def type(self, image_type):
if (not isinstance(image_type, string_type) or
image_type not in IMAGE_TYPES):
raise TypeError('Type value must be a string from IMAGE_TYPES'
', not ' + repr(image_type))
r = library.MagickSetImageType(self.wand,
IMAGE_TYPES.index(image_type))
if not r:
self.raise_exception()
@property
def compression_quality(self):
"""(:class:`numbers.Integral`) Compression quality of this image.
.. versionadded:: 0.2.0
"""
return library.MagickGetImageCompressionQuality(self.wand)
@compression_quality.setter
@manipulative
def compression_quality(self, quality):
"""Set compression quality for the image.
:param quality: new compression quality setting
:type quality: :class:`numbers.Integral`
"""
if not isinstance(quality, numbers.Integral):
raise TypeError('compression quality must be a natural '
'number, not ' + repr(quality))
r = library.MagickSetImageCompressionQuality(self.wand, quality)
if not r:
raise ValueError('Unable to set compression quality to ' +
repr(quality))
@property
def signature(self):
"""(:class:`str`) The SHA-256 message digest for the image pixel
stream.
.. versionadded:: 0.1.9
"""
signature = library.MagickGetImageSignature(self.wand)
return text(signature.value)
@property
def alpha_channel(self):
"""(:class:`bool`) Get state of image alpha channel.
It can also be used to enable/disable alpha channel, but with different
behavior new, copied, or existing.
Behavior of setting :attr:`alpha_channel` is defined with the
following values:
- ``'activate'``, ``'on'``, or :const:`True` will enable an images
alpha channel. Existing alpha data is preserved.
- ``'deactivate'``, ``'off'``, or :const:`False` will disable an images
alpha channel. Any data on the alpha will be preserved.
- ``'associate'`` & ``'disassociate'`` toggle alpha channel flag in
certain image-file specifications.
- ``'set'`` enables and resets any data in an images alpha channel.
- ``'opaque'`` enables alpha/matte channel, and forces full opaque
image.
- ``'transparent'`` enables alpha/matte channel, and forces full
transparent image.
- ``'extract'`` copies data in alpha channel across all other channels,
and disables alpha channel.
- ``'copy'`` calculates the gray-scale of RGB channels,
and applies it to alpha channel.
- ``'shape'`` is identical to ``'copy'``, but will color the resulting
image with the value defined with :attr:`background_color`.
- ``'remove'`` will composite :attr:`background_color` value.
- ``'background'`` replaces full-transparent color with background
color.
.. versionadded:: 0.2.1
.. versionchanged:: 0.4.1
Support for additional setting values.
However :attr:`Image.alpha_channel` will continue to return
:class:`bool` if the current alpha/matte state is enabled.
"""
return bool(library.MagickGetImageAlphaChannel(self.wand))
@alpha_channel.setter
@manipulative
def alpha_channel(self, alpha_type):
# Map common aliases for ``'deactivate'``
if alpha_type is False or alpha_type == 'off':
alpha_type = 'deactivate'
# Map common aliases for ``'activate'``
elif alpha_type is True or alpha_type == 'on':
alpha_type = 'activate'
if alpha_type in ALPHA_CHANNEL_TYPES:
alpha_index = ALPHA_CHANNEL_TYPES.index(alpha_type)
library.MagickSetImageAlphaChannel(self.wand,
alpha_index)
self.raise_exception()
else:
raise ValueError('expecting string from ALPHA_CHANNEL_TYPES, '
'not ' + repr(alpha_type))
@property
def background_color(self):
"""(:class:`wand.color.Color`) The image background color.
It can also be set to change the background color.
.. versionadded:: 0.1.9
"""
pixel = library.NewPixelWand()
result = library.MagickGetImageBackgroundColor(self.wand, pixel)
if result:
size = ctypes.sizeof(MagickPixelPacket)
buffer = ctypes.create_string_buffer(size)
library.PixelGetMagickColor(pixel, buffer)
return Color(raw=buffer)
self.raise_exception()
@background_color.setter
@manipulative
def background_color(self, color):
if not isinstance(color, Color):
raise TypeError('color must be a wand.color.Color object, not ' +
repr(color))
with color:
result = library.MagickSetImageBackgroundColor(self.wand,
color.resource)
if not result:
self.raise_exception()
@property
def matte_color(self):
"""(:class:`wand.color.Color`) The color value of the matte channel.
This can also be set.
..versionadded:: 0.4.1
"""
pixel = library.NewPixelWand()
result = library.MagickGetImageMatteColor(self.wand, pixel)
if result:
pixel_size = ctypes.sizeof(MagickPixelPacket)
pixel_buffer = ctypes.create_string_buffer(pixel_size)
library.PixelGetMagickColor(pixel, pixel_buffer)
return Color(raw=pixel_buffer)
self.raise_exception()
@matte_color.setter
@manipulative
def matte_color(self, color):
if not isinstance(color, Color):
raise TypeError('color must be a wand.color.Color object, not ' +
repr(color))
with color:
result = library.MagickSetImageMatteColor(self.wand,
color.resource)
if not result:
self.raise_exception()
@property
def quantum_range(self):
"""(:class:`int`) The maxumim value of a color channel that is
supported by the imagemagick library.
.. versionadded:: 0.2.0
"""
result = ctypes.c_size_t()
library.MagickGetQuantumRange(ctypes.byref(result))
return result.value
@property
def histogram(self):
"""(:class:`HistogramDict`) The mapping that represents the histogram.
Keys are :class:`~wand.color.Color` objects, and values are
the number of pixels.
.. versionadded:: 0.3.0
"""
return HistogramDict(self)
@manipulative
def distort(self, method, arguments, best_fit=False):
"""Distorts an image using various distorting methods.
:param method: Distortion method name from :const:`DISTORTION_METHODS`
:type method: :class:`basestring`
:param arguments: List of distorting float arguments
unique to distortion method
:type arguments: :class:`collections.Sequence`
:param best_fit: Attempt to resize resulting image fit distortion.
Defaults False
:type best_fit: :class:`bool`
.. versionadded:: 0.4.1
"""
if method not in DISTORTION_METHODS:
raise ValueError('expected string from DISTORTION_METHODS, not ' +
repr(method))
if not isinstance(arguments, collections.Sequence):
raise TypeError('expected sequence of doubles, not ' +
repr(arguments))
argc = len(arguments)
argv = (ctypes.c_double * argc)(*arguments)
library.MagickDistortImage(self.wand,
DISTORTION_METHODS.index(method),
argc, argv, bool(best_fit))
self.raise_exception()
@manipulative
def crop(self, left=0, top=0, right=None, bottom=None,
width=None, height=None, reset_coords=True,
gravity=None):
"""Crops the image in-place.
.. sourcecode:: text
+--------------------------------------------------+
| ^ ^ |
| | | |
| top | |
| | | |
| v | |
| <-- left --> +-------------------+ bottom |
| | ^ | | |
| | <-- width --|---> | | |
| | height | | |
| | | | | |
| | v | | |
| +-------------------+ v |
| <--------------- right ----------> |
+--------------------------------------------------+
:param left: x-offset of the cropped image. default is 0
:type left: :class:`numbers.Integral`
:param top: y-offset of the cropped image. default is 0
:type top: :class:`numbers.Integral`
:param right: second x-offset of the cropped image.
default is the :attr:`width` of the image.
this parameter and ``width`` parameter are exclusive
each other
:type right: :class:`numbers.Integral`
:param bottom: second y-offset of the cropped image.
default is the :attr:`height` of the image.
this parameter and ``height`` parameter are exclusive
each other
:type bottom: :class:`numbers.Integral`
:param width: the :attr:`width` of the cropped image.
default is the :attr:`width` of the image.
this parameter and ``right`` parameter are exclusive
each other
:type width: :class:`numbers.Integral`
:param height: the :attr:`height` of the cropped image.
default is the :attr:`height` of the image.
this parameter and ``bottom`` parameter are exclusive
each other
:type height: :class:`numbers.Integral`
:param reset_coords:
optional flag. If set, after the rotation, the coordinate frame
will be relocated to the upper-left corner of the new image.
By default is `True`.
:type reset_coords: :class:`bool`
:param gravity: optional flag. If set, will calculate the :attr:`top`
and :attr:`left` attributes. This requires both
:attr:`width` and :attr:`height` parameters to be
included.
:type gravity: :const:`GRAVITY_TYPES`
:raises ValueError: when one or more arguments are invalid
.. note::
If you want to crop the image but not in-place, use slicing
operator.
.. versionchanged:: 0.4.1
Added ``gravity`` option. Using ``gravity`` along with
``width`` & ``height`` to auto-adjust ``left`` & ``top``
attributes.
.. versionchanged:: 0.1.8
Made to raise :exc:`~exceptions.ValueError` instead of
:exc:`~exceptions.IndexError` for invalid ``width``/``height``
arguments.
.. versionadded:: 0.1.7
"""
if not (right is None or width is None):
raise TypeError('parameters right and width are exclusive each '
'other; use one at a time')
elif not (bottom is None or height is None):
raise TypeError('parameters bottom and height are exclusive each '
'other; use one at a time')
# Define left & top if gravity is given.
if gravity:
if width is None or height is None:
raise TypeError(
'both width and height must be defined with gravity'
)
if gravity not in GRAVITY_TYPES:
raise ValueError('expected a string from GRAVITY_TYPES, not ' +
repr(gravity))
2016-04-08 22:36:30 +00:00
# Set `top` based on given gravity
if gravity in ('north_west', 'north', 'north_east'):
top = 0
elif gravity in ('west', 'center', 'east'):
top = int(self.height / 2) - int(height / 2)
elif gravity in ('south_west', 'south', 'south_east'):
top = self.height - height
# Set `left` based on given gravity
if gravity in ('north_west', 'west', 'south_west'):
left = 0
elif gravity in ('north', 'center', 'south'):
left = int(self.width / 2) - int(width / 2)
elif gravity in ('north_east', 'east', 'south_east'):
left = self.width - width
def abs_(n, m, null=None):
if n is None:
return m if null is None else null
elif not isinstance(n, numbers.Integral):
raise TypeError('expected integer, not ' + repr(n))
elif n > m:
raise ValueError(repr(n) + ' > ' + repr(m))
return m + n if n < 0 else n
left = abs_(left, self.width, 0)
top = abs_(top, self.height, 0)
if width is None:
right = abs_(right, self.width)
width = right - left
if height is None:
bottom = abs_(bottom, self.height)
height = bottom - top
if width < 1:
raise ValueError('image width cannot be zero')
elif height < 1:
raise ValueError('image width cannot be zero')
elif (left == top == 0 and width == self.width and
height == self.height):
return
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(0, n + 1):
library.MagickSetIteratorIndex(self.wand, i)
library.MagickCropImage(self.wand, width, height, left, top)
if reset_coords:
library.MagickResetImagePage(self.wand, None)
else:
library.MagickCropImage(self.wand, width, height, left, top)
self.raise_exception()
if reset_coords:
self.reset_coords()
def reset_coords(self):
"""Reset the coordinate frame of the image so to the upper-left corner
is (0, 0) again (crop and rotate operations change it).
.. versionadded:: 0.2.0
"""
library.MagickResetImagePage(self.wand, None)
@manipulative
def resize(self, width=None, height=None, filter='undefined', blur=1):
"""Resizes the image.
:param width: the width in the scaled image. default is the original
width
:type width: :class:`numbers.Integral`
:param height: the height in the scaled image. default is the original
height
:type height: :class:`numbers.Integral`
:param filter: a filter type to use for resizing. choose one in
:const:`FILTER_TYPES`. default is ``'undefined'``
which means IM will try to guess best one to use
:type filter: :class:`basestring`, :class:`numbers.Integral`
:param blur: the blur factor where > 1 is blurry, < 1 is sharp.
default is 1
:type blur: :class:`numbers.Real`
.. versionchanged:: 0.2.1
The default value of ``filter`` has changed from ``'triangle'``
to ``'undefined'`` instead.
.. versionchanged:: 0.1.8
The ``blur`` parameter changed to take :class:`numbers.Real`
instead of :class:`numbers.Rational`.
.. versionadded:: 0.1.1
"""
if width is None:
width = self.width
if height is None:
height = self.height
if not isinstance(width, numbers.Integral):
raise TypeError('width must be a natural number, not ' +
repr(width))
elif not isinstance(height, numbers.Integral):
raise TypeError('height must be a natural number, not ' +
repr(height))
elif width < 1:
raise ValueError('width must be a natural number, not ' +
repr(width))
elif height < 1:
raise ValueError('height must be a natural number, not ' +
repr(height))
elif not isinstance(blur, numbers.Real):
raise TypeError('blur must be numbers.Real , not ' + repr(blur))
elif not isinstance(filter, (string_type, numbers.Integral)):
raise TypeError('filter must be one string defined in wand.image.'
'FILTER_TYPES or an integer, not ' + repr(filter))
if isinstance(filter, string_type):
try:
filter = FILTER_TYPES.index(filter)
except IndexError:
raise ValueError(repr(filter) + ' is an invalid filter type; '
'choose on in ' + repr(FILTER_TYPES))
elif (isinstance(filter, numbers.Integral) and
not (0 <= filter < len(FILTER_TYPES))):
raise ValueError(repr(filter) + ' is an invalid filter type')
blur = ctypes.c_double(float(blur))
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(n + 1):
library.MagickSetIteratorIndex(self.wand, i)
library.MagickResizeImage(self.wand, width, height,
filter, blur)
library.MagickSetSize(self.wand, width, height)
else:
r = library.MagickResizeImage(self.wand, width, height,
filter, blur)
library.MagickSetSize(self.wand, width, height)
if not r:
self.raise_exception()
@manipulative
def sample(self, width=None, height=None):
"""Resizes the image by sampling the pixels. It's basically quicker
than :meth:`resize()` except less quality as a tradeoff.
:param width: the width in the scaled image. default is the original
width
:type width: :class:`numbers.Integral`
:param height: the height in the scaled image. default is the original
height
:type height: :class:`numbers.Integral`
.. versionadded:: 0.3.4
"""
if width is None:
width = self.width
if height is None:
height = self.height
if not isinstance(width, numbers.Integral):
raise TypeError('width must be a natural number, not ' +
repr(width))
elif not isinstance(height, numbers.Integral):
raise TypeError('height must be a natural number, not ' +
repr(height))
elif width < 1:
raise ValueError('width must be a natural number, not ' +
repr(width))
elif height < 1:
raise ValueError('height must be a natural number, not ' +
repr(height))
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(n + 1):
library.MagickSetIteratorIndex(self.wand, i)
library.MagickSampleImage(self.wand, width, height)
library.MagickSetSize(self.wand, width, height)
else:
r = library.MagickSampleImage(self.wand, width, height)
library.MagickSetSize(self.wand, width, height)
if not r:
self.raise_exception()
@manipulative
def transform(self, crop='', resize=''):
"""Transforms the image using :c:func:`MagickTransformImage`,
which is a convenience function accepting geometry strings to
perform cropping and resizing. Cropping is performed first,
followed by resizing. Either or both arguments may be omitted
or given an empty string, in which case the corresponding action
will not be performed. Geometry specification strings are
defined as follows:
A geometry string consists of a size followed by an optional offset.
The size is specified by one of the options below,
where **bold** terms are replaced with appropriate integer values:
**scale**\ ``%``
Height and width both scaled by specified percentage
**scale-x**\ ``%x``\ \ **scale-y**\ ``%``
Height and width individually scaled by specified percentages.
Only one % symbol is needed.
**width**
Width given, height automagically selected to preserve aspect ratio.
``x``\ \ **height**
Height given, width automagically selected to preserve aspect ratio.
**width**\ ``x``\ **height**
Maximum values of width and height given; aspect ratio preserved.
**width**\ ``x``\ **height**\ ``!``
Width and height emphatically given; original aspect ratio ignored.
**width**\ ``x``\ **height**\ ``>``
Shrinks images with dimension(s) larger than the corresponding
width and/or height dimension(s).
**width**\ ``x``\ **height**\ ``<``
Enlarges images with dimensions smaller than the corresponding
width and/or height dimension(s).
**area**\ ``@``
Resize image to have the specified area in pixels.
Aspect ratio is preserved.
The offset, which only applies to the cropping geometry string,
is given by ``{+-}``\ **x**\ ``{+-}``\ **y**\ , that is,
one plus or minus sign followed by an **x** offset,
followed by another plus or minus sign, followed by a **y** offset.
Offsets are in pixels from the upper left corner of the image.
Negative offsets will cause the corresponding number of pixels to
be removed from the right or bottom edge of the image, meaning the
cropped size will be the computed size minus the absolute value
of the offset.
For example, if you want to crop your image to 300x300 pixels
and then scale it by 2x for a final size of 600x600 pixels,
you can call::
image.transform('300x300', '200%')
This method is a fairly thing wrapper for the C API, and does not
perform any additional checking of the parameters except insofar as
verifying that they are of the correct type. Thus, like the C
API function, the method is very permissive in terms of what
it accepts for geometry strings; unrecognized strings and
trailing characters will be ignored rather than raising an error.
:param crop: A geometry string defining a subregion of the image
to crop to
:type crop: :class:`basestring`
:param resize: A geometry string defining the final size of the image
:type resize: :class:`basestring`
.. seealso::
`ImageMagick Geometry Specifications`__
Cropping and resizing geometry for the ``transform`` method are
specified according to ImageMagick's geometry string format.
The ImageMagick documentation provides more information about
geometry strings.
__ http://www.imagemagick.org/script/command-line-processing.php#geometry
.. versionadded:: 0.2.2
""" # noqa
# Check that the values given are the correct types. ctypes will do
# this automatically, but we can make the error message more friendly
# here.
if not isinstance(crop, string_type):
raise TypeError("crop must be a string, not " + repr(crop))
if not isinstance(resize, string_type):
raise TypeError("resize must be a string, not " + repr(resize))
# Also verify that only ASCII characters are included
try:
crop = crop.encode('ascii')
except UnicodeEncodeError:
raise ValueError('crop must only contain ascii-encodable ' +
'characters.')
try:
resize = resize.encode('ascii')
except UnicodeEncodeError:
raise ValueError('resize must only contain ascii-encodable ' +
'characters.')
if self.animation:
new_wand = library.MagickCoalesceImages(self.wand)
length = len(self.sequence)
for i in xrange(length):
library.MagickSetIteratorIndex(new_wand, i)
if i:
library.MagickAddImage(
new_wand,
library.MagickTransformImage(new_wand, crop, resize)
)
else:
new_wand = library.MagickTransformImage(new_wand,
crop,
resize)
self.sequence.instances = []
else:
new_wand = library.MagickTransformImage(self.wand, crop, resize)
if not new_wand:
self.raise_exception()
self.wand = new_wand
@manipulative
def liquid_rescale(self, width, height, delta_x=0, rigidity=0):
"""Rescales the image with `seam carving`_, also known as
image retargeting, content-aware resizing, or liquid rescaling.
:param width: the width in the scaled image
:type width: :class:`numbers.Integral`
:param height: the height in the scaled image
:type height: :class:`numbers.Integral`
:param delta_x: maximum seam transversal step.
0 means straight seams. default is 0
:type delta_x: :class:`numbers.Real`
:param rigidity: introduce a bias for non-straight seams.
default is 0
:type rigidity: :class:`numbers.Real`
:raises wand.exceptions.MissingDelegateError:
when ImageMagick isn't configured ``--with-lqr`` option.
.. note::
This feature requires ImageMagick to be configured
``--with-lqr`` option. Or it will raise
:exc:`~wand.exceptions.MissingDelegateError`:
.. seealso::
`Seam carving`_ --- Wikipedia
The article which explains what seam carving is
on Wikipedia.
.. _Seam carving: http://en.wikipedia.org/wiki/Seam_carving
"""
if not isinstance(width, numbers.Integral):
raise TypeError('width must be an integer, not ' + repr(width))
elif not isinstance(height, numbers.Integral):
raise TypeError('height must be an integer, not ' + repr(height))
elif not isinstance(delta_x, numbers.Real):
raise TypeError('delta_x must be a float, not ' + repr(delta_x))
elif not isinstance(rigidity, numbers.Real):
raise TypeError('rigidity must be a float, not ' + repr(rigidity))
library.MagickLiquidRescaleImage(self.wand, int(width), int(height),
float(delta_x), float(rigidity))
try:
self.raise_exception()
except MissingDelegateError as e:
raise MissingDelegateError(
str(e) + '\n\nImageMagick in the system is likely to be '
'impossible to load liblqr. You might not install liblqr, '
'or ImageMagick may not compiled with liblqr.'
)
@manipulative
def rotate(self, degree, background=None, reset_coords=True):
"""Rotates the image right. It takes a ``background`` color
for ``degree`` that isn't a multiple of 90.
:param degree: a degree to rotate. multiples of 360 affect nothing
:type degree: :class:`numbers.Real`
:param background: an optional background color.
default is transparent
:type background: :class:`wand.color.Color`
:param reset_coords: optional flag. If set, after the rotation, the
coordinate frame will be relocated to the upper-left corner of
the new image. By default is `True`.
:type reset_coords: :class:`bool`
.. versionadded:: 0.2.0
The ``reset_coords`` parameter.
.. versionadded:: 0.1.8
"""
if background is None:
background = Color('transparent')
elif not isinstance(background, Color):
raise TypeError('background must be a wand.color.Color instance, '
'not ' + repr(background))
if not isinstance(degree, numbers.Real):
raise TypeError('degree must be a numbers.Real value, not ' +
repr(degree))
with background:
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in range(0, n + 1):
library.MagickSetIteratorIndex(self.wand, i)
library.MagickRotateImage(self.wand,
background.resource,
degree)
if reset_coords:
library.MagickResetImagePage(self.wand, None)
else:
result = library.MagickRotateImage(self.wand,
background.resource,
degree)
if not result:
self.raise_exception()
if reset_coords:
self.reset_coords()
@manipulative
def evaluate(self, operator=None, value=0.0, channel=None):
"""Apply arithmetic, relational, or logical expression to an image.
Percent values must be calculated against the quantum range of the
image::
fifty_percent = img.quantum_range * 0.5
img.evaluate(operator='set', value=fifty_percent)
:param operator: Type of operation to calculate
:type operator: :const:`EVALUATE_OPS`
:param value: Number to calculate with ``operator``
:type value: :class:`numbers.Real`
:param channel: Optional channel to apply operation on.
:type channel: :const:`CHANNELS`
:raises TypeError: When ``value`` is not numeric.
:raises ValueError: When ``operator``, or ``channel`` are not defined
in constants.
.. versionadded:: 0.4.1
"""
if operator not in EVALUATE_OPS:
raise ValueError('expected value from EVALUATE_OPS, not ' +
repr(operator))
if not isinstance(value, numbers.Real):
raise TypeError('value must be real number, not ' + repr(value))
if channel:
if channel not in CHANNELS:
raise ValueError('expected value from CHANNELS, not ' +
repr(channel))
library.MagickEvaluateImageChannel(self.wand,
CHANNELS[channel],
EVALUATE_OPS.index(operator),
value)
else:
library.MagickEvaluateImage(self.wand,
EVALUATE_OPS.index(operator), value)
self.raise_exception()
@manipulative
def flip(self):
"""Creates a vertical mirror image by reflecting the pixels around
the central x-axis. It manipulates the image in place.
.. versionadded:: 0.3.0
"""
result = library.MagickFlipImage(self.wand)
if not result:
self.raise_exception()
@manipulative
def flop(self):
"""Creates a horizontal mirror image by reflecting the pixels around
the central y-axis. It manipulates the image in place.
.. versionadded:: 0.3.0
"""
result = library.MagickFlopImage(self.wand)
if not result:
self.raise_exception()
@manipulative
def frame(self, matte=None, width=1, height=1, inner_bevel=0,
outer_bevel=0):
"""Creates a bordered frame around image.
Inner & outer bevel can simulate a 3D effect.
:param matte: color of the frame
:type matte: :class:`wand.color.Color`
:param width: total size of frame on x-axis
:type width: :class:`numbers.Integral`
:param height: total size of frame on y-axis
:type height: :class:`numbers.Integral`
:param inner_bevel: inset shadow length
:type inner_bevel: :class:`numbers.Real`
:param outer_bevel: outset highlight length
:type outer_bevel: :class:`numbers.Real`
.. versionadded:: 0.4.1
"""
if matte is None:
matte = Color('gray')
if not isinstance(matte, Color):
raise TypeError('Expecting instance of Color for matte, not ' +
repr(matte))
if not isinstance(width, numbers.Integral):
raise TypeError('Expecting integer for width, not ' + repr(width))
if not isinstance(height, numbers.Integral):
raise TypeError('Expecting integer for height, not ' +
repr(height))
if not isinstance(inner_bevel, numbers.Real):
raise TypeError('Expecting real number, not ' + repr(inner_bevel))
if not isinstance(outer_bevel, numbers.Real):
raise TypeError('Expecting real number, not ' + repr(outer_bevel))
with matte:
library.MagickFrameImage(self.wand,
matte.resource,
width, height,
inner_bevel, outer_bevel)
@manipulative
def function(self, function, arguments, channel=None):
"""Apply an arithmetic, relational, or logical expression to an image.
Defaults entire image, but can isolate affects to single color channel
by passing :const:`CHANNELS` value to ``channel`` parameter.
.. note::
Support for function methods added in the following versions
of ImageMagick.
- ``'polynomial'`` >= 6.4.8-8
- ``'sinusoid'`` >= 6.4.8-8
- ``'arcsin'`` >= 6.5.3-1
- ``'arctan'`` >= 6.5.3-1
:param function: a string listed in :const:`FUNCTION_TYPES`
:type function: :class:`basestring`
:param arguments: a sequence of doubles to apply against ``function``
:type arguments: :class:`collections.Sequence`
:param channel: optional :const:`CHANNELS`, defaults all
:type channel: :class:`basestring`
:raises ValueError: when a ``function``, or ``channel`` is not
defined in there respected constant
:raises TypeError: if ``arguments`` is not a sequence
.. versionadded:: 0.4.1
"""
if function not in FUNCTION_TYPES:
raise ValueError('expected string from FUNCTION_TYPES, not ' +
repr(function))
if not isinstance(arguments, collections.Sequence):
raise TypeError('expecting sequence of arguments, not ' +
repr(arguments))
argc = len(arguments)
argv = (ctypes.c_double * argc)(*arguments)
index = FUNCTION_TYPES.index(function)
if channel is None:
library.MagickFunctionImage(self.wand, index, argc, argv)
elif channel in CHANNELS:
library.MagickFunctionImageChannel(self.wand, CHANNELS[channel],
index, argc, argv)
else:
raise ValueError('expected string from CHANNELS, not ' +
repr(channel))
self.raise_exception()
@manipulative
def fx(self, expression, channel=None):
"""Manipulate each pixel of an image by given expression.
FX will preserver current wand instance, and return a new instance of
:class:`Image` containing affected pixels.
Defaults entire image, but can isolate affects to single color channel
by passing :const:`CHANNELS` value to ``channel`` parameter.
.. seealso:: The anatomy of FX expressions can be found at
http://www.imagemagick.org/script/fx.php
:param expression: The entire FX expression to apply
:type expression: :class:`basestring`
:param channel: Optional channel to target.
:type channel: :const:`CHANNELS`
:returns: A new instance of an image with expression applied
:rtype: :class:`Image`
.. versionadded:: 0.4.1
"""
if not isinstance(expression, string_type):
raise TypeError('expected basestring for expression, not' +
repr(expression))
c_expression = binary(expression)
if channel is None:
new_wand = library.MagickFxImage(self.wand, c_expression)
elif channel in CHANNELS:
new_wand = library.MagickFxImageChannel(self.wand,
CHANNELS[channel],
c_expression)
else:
raise ValueError('expected string from CHANNELS, not ' +
repr(channel))
if new_wand:
return Image(image=BaseImage(new_wand))
self.raise_exception()
@manipulative
def transparentize(self, transparency):
"""Makes the image transparent by subtracting some percentage of
the black color channel. The ``transparency`` parameter specifies the
percentage.
:param transparency: the percentage fade that should be performed on
the image, from 0.0 to 1.0
:type transparency: :class:`numbers.Real`
.. versionadded:: 0.2.0
"""
if transparency:
t = ctypes.c_double(float(self.quantum_range *
float(transparency)))
if t.value > self.quantum_range or t.value < 0:
raise ValueError('transparency must be a numbers.Real value ' +
'between 0.0 and 1.0')
# Set the wand to image zero, in case there are multiple images
# in it
library.MagickSetIteratorIndex(self.wand, 0)
# Change the pixel representation of the image
# to RGB with an alpha channel
library.MagickSetImageType(self.wand,
IMAGE_TYPES.index('truecolormatte'))
# Perform the black channel subtraction
library.MagickEvaluateImageChannel(self.wand,
CHANNELS['opacity'],
EVALUATE_OPS.index('subtract'),
t)
self.raise_exception()
@manipulative
def transparent_color(self, color, alpha, fuzz=0, invert=False):
"""Makes the color ``color`` a transparent color with a tolerance of
fuzz. The ``alpha`` parameter specify the transparency level and the
parameter ``fuzz`` specify the tolerance.
:param color: The color that should be made transparent on the image,
color object
:type color: :class:`wand.color.Color`
:param alpha: the level of transparency: 1.0 is fully opaque
and 0.0 is fully transparent.
:type alpha: :class:`numbers.Real`
:param fuzz: By default target must match a particular pixel color
exactly. However, in many cases two colors may differ
by a small amount. The fuzz member of image defines how
much tolerance is acceptable to consider two colors as the
same. For example, set fuzz to 10 and the color red at
intensities of 100 and 102 respectively are now
interpreted as the same color for the color.
:type fuzz: :class:`numbers.Integral`
:param invert: Boolean to tell to paint the inverse selection.
:type invert: :class:`bool`
.. versionadded:: 0.3.0
"""
if not isinstance(alpha, numbers.Real):
raise TypeError('alpha must be an float, not ' + repr(alpha))
elif not isinstance(fuzz, numbers.Integral):
raise TypeError('fuzz must be an integer, not ' + repr(fuzz))
elif not isinstance(color, Color):
raise TypeError('color must be a wand.color.Color object, not ' +
repr(color))
library.MagickTransparentPaintImage(self.wand, color.resource,
alpha, fuzz, invert)
self.raise_exception()
def compare(self, image, metric='undefined'):
"""Compares an image to a reconstructed image.
:param image: The reference image
:type image: :class:`wand.image.Image`
:param metric: The metric type to use for comparing.
:type metric: :class:`basestring`
:returns: The difference image(:class:`wand.image.Image`),
the computed distortion between the images
(:class:`numbers.Integral`)
:rtype: :class:`tuple`
..versionadded:: 0.4.3
"""
if not isinstance(metric, string_type):
raise TypeError('metric must be a string, not ' + repr(metric))
metric = COMPARE_METRICS.index(metric)
distortion = ctypes.c_double()
compared_image = library.MagickCompareImages(self.wand, image.wand,
metric,
ctypes.byref(distortion))
return Image(BaseImage(compared_image)), distortion.value
2016-04-08 22:36:30 +00:00
@manipulative
def composite(self, image, left, top):
"""Places the supplied ``image`` over the current image, with the top
left corner of ``image`` at coordinates ``left``, ``top`` of the
current image. The dimensions of the current image are not changed.
:param image: the image placed over the current image
:type image: :class:`wand.image.Image`
:param left: the x-coordinate where `image` will be placed
:type left: :class:`numbers.Integral`
:param top: the y-coordinate where `image` will be placed
:type top: :class:`numbers.Integral`
.. versionadded:: 0.2.0
"""
if not isinstance(left, numbers.Integral):
raise TypeError('left must be an integer, not ' + repr(left))
elif not isinstance(top, numbers.Integral):
raise TypeError('top must be an integer, not ' + repr(left))
op = COMPOSITE_OPERATORS.index('over')
library.MagickCompositeImage(self.wand, image.wand, op,
int(left), int(top))
self.raise_exception()
@manipulative
def composite_channel(self, channel, image, operator, left=0, top=0):
"""Composite two images using the particular ``channel``.
:param channel: the channel type. available values can be found
in the :const:`CHANNELS` mapping
:param image: the composited source image.
(the receiver image becomes the destination)
:type image: :class:`Image`
:param operator: the operator that affects how the composite
is applied to the image. available values
can be found in the :const:`COMPOSITE_OPERATORS`
list
:param left: the column offset of the composited source image
:type left: :class:`numbers.Integral`
:param top: the row offset of the composited source image
:type top: :class:`numbers.Integral`
:raises ValueError: when the given ``channel`` or
``operator`` is invalid
.. versionadded:: 0.3.0
"""
if not isinstance(channel, string_type):
raise TypeError('channel must be a string, not ' +
repr(channel))
elif not isinstance(operator, string_type):
raise TypeError('operator must be a string, not ' +
repr(operator))
elif not isinstance(left, numbers.Integral):
raise TypeError('left must be an integer, not ' + repr(left))
elif not isinstance(top, numbers.Integral):
raise TypeError('top must be an integer, not ' + repr(left))
try:
ch_const = CHANNELS[channel]
except KeyError:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
try:
op = COMPOSITE_OPERATORS.index(operator)
except IndexError:
raise IndexError(repr(operator) + ' is an invalid composite '
'operator type; see wand.image.COMPOSITE_'
'OPERATORS dictionary')
library.MagickCompositeImageChannel(self.wand, ch_const, image.wand,
op, int(left), int(top))
self.raise_exception()
@manipulative
def equalize(self):
"""Equalizes the image histogram
.. versionadded:: 0.3.10
"""
result = library.MagickEqualizeImage(self.wand)
if not result:
self.raise_exception()
@manipulative
def modulate(self, brightness=100.0, saturation=100.0, hue=100.0):
"""Changes the brightness, saturation and hue of an image.
We modulate the image with the given ``brightness``, ``saturation``
and ``hue``.
:param brightness: percentage of brightness
:type brightness: :class:`numbers.Real`
:param saturation: percentage of saturation
:type saturation: :class:`numbers.Real`
:param hue: percentage of hue rotation
:type hue: :class:`numbers.Real`
:raises ValueError: when one or more arguments are invalid
.. versionadded:: 0.3.4
"""
if not isinstance(brightness, numbers.Real):
raise TypeError('brightness has to be a numbers.Real, not ' +
repr(brightness))
elif not isinstance(saturation, numbers.Real):
raise TypeError('saturation has to be a numbers.Real, not ' +
repr(saturation))
elif not isinstance(hue, numbers.Real):
raise TypeError('hue has to be a numbers.Real, not ' + repr(hue))
r = library.MagickModulateImage(
self.wand,
brightness,
saturation,
hue
)
if not r:
self.raise_exception()
@manipulative
def merge_layers(self, method):
"""Composes all the image layers from the current given image onward
to produce a single image of the merged layers.
The inital canvas's size depends on the given ImageLayerMethod, and is
initialized using the first images background color. The images
are then compositied onto that image in sequence using the given
composition that has been assigned to each individual image.
The method must be set with a value from :const:`IMAGE_LAYER_METHOD`
that is acceptable to this operation. (See ImageMagick documentation
for more details.)
:param method: the method of selecting the size of the initial canvas.
:type method: :class:`basestring`
.. versionadded:: 0.4.3
"""
if method not in ['merge', 'flatten', 'mosaic']:
raise TypeError('method must be one of: merge, flatten, mosaic')
m = IMAGE_LAYER_METHOD.index(method)
r = library.MagickMergeImageLayers(self.wand, m)
if not r:
self.raise_exception()
self.wand = r
2016-04-08 22:36:30 +00:00
@manipulative
def threshold(self, threshold=0.5, channel=None):
"""Changes the value of individual pixels based on the intensity
of each pixel compared to threshold. The result is a high-contrast,
two color image. It manipulates the image in place.
:param threshold: threshold as a factor of quantum
:type threshold: :class:`numbers.Real`
:param channel: the channel type. available values can be found
in the :const:`CHANNELS` mapping. If ``None``,
threshold all channels.
:type channel: :class:`basestring`
.. versionadded:: 0.3.10
"""
if not isinstance(threshold, numbers.Real):
raise TypeError('threshold has to be a numbers.Real, not ' +
repr(threshold))
if channel:
try:
ch_const = CHANNELS[channel]
except KeyError:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
r = library.MagickThresholdImageChannel(
self.wand, ch_const,
threshold * self.quantum_range
)
else:
r = library.MagickThresholdImage(self.wand,
threshold * self.quantum_range)
if not r:
self.raise_exception()
def negate(self, grayscale=False, channel=None):
"""Negate the colors in the reference image.
:param grayscale: if set, only negate grayscale pixels in the image.
:type grayscale: :class:`bool`
:param channel: the channel type. available values can be found
in the :const:`CHANNELS` mapping. If ``None``,
negate all channels.
:type channel: :class:`basestring`
.. versionadded:: 0.3.8
"""
if channel:
try:
ch_const = CHANNELS[channel]
except KeyError:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
r = library.MagickNegateImageChannel(self.wand, ch_const,
grayscale)
else:
r = library.MagickNegateImage(self.wand, grayscale)
if not r:
self.raise_exception()
@manipulative
def gaussian_blur(self, radius, sigma):
"""Blurs the image. We convolve the image with a gaussian operator
of the given ``radius`` and standard deviation (``sigma``).
For reasonable results, the ``radius`` should be larger
than ``sigma``. Use a ``radius`` of 0 and :meth:`blur()` selects
a suitable ``radius`` for you.
:param radius: the radius of the, in pixels,
not counting the center pixel
:type radius: :class:`numbers.Real`
:param sigma: the standard deviation of the, in pixels
:type sigma: :class:`numbers.Real`
.. versionadded:: 0.3.3
"""
if not isinstance(radius, numbers.Real):
raise TypeError('radius has to be a numbers.Real, not ' +
repr(radius))
elif not isinstance(sigma, numbers.Real):
raise TypeError('sigma has to be a numbers.Real, not ' +
repr(sigma))
r = library.MagickGaussianBlurImage(self.wand, radius, sigma)
if not r:
self.raise_exception()
@manipulative
def unsharp_mask(self, radius, sigma, amount, threshold):
"""Sharpens the image using unsharp mask filter. We convolve the image
with a Gaussian operator of the given ``radius`` and standard deviation
(``sigma``). For reasonable results, ``radius`` should be larger than
``sigma``. Use a radius of 0 and :meth:`unsharp_mask()` selects
a suitable radius for you.
:param radius: the radius of the Gaussian, in pixels,
not counting the center pixel
:type radius: :class:`numbers.Real`
:param sigma: the standard deviation of the Gaussian, in pixels
:type sigma: :class:`numbers.Real`
:param amount: the percentage of the difference between the original
and the blur image that is added back into the original
:type amount: :class:`numbers.Real`
:param threshold: the threshold in pixels needed to apply
the diffence amount
:type threshold: :class:`numbers.Real`
.. versionadded:: 0.3.4
"""
if not isinstance(radius, numbers.Real):
raise TypeError('radius has to be a numbers.Real, not ' +
repr(radius))
elif not isinstance(sigma, numbers.Real):
raise TypeError('sigma has to be a numbers.Real, not ' +
repr(sigma))
elif not isinstance(amount, numbers.Real):
raise TypeError('amount has to be a numbers.Real, not ' +
repr(amount))
elif not isinstance(threshold, numbers.Real):
raise TypeError('threshold has to be a numbers.Real, not ' +
repr(threshold))
r = library.MagickUnsharpMaskImage(self.wand, radius, sigma,
amount, threshold)
if not r:
self.raise_exception()
@manipulative
def watermark(self, image, transparency=0.0, left=0, top=0):
"""Transparentized the supplied ``image`` and places it over the
current image, with the top left corner of ``image`` at coordinates
``left``, ``top`` of the current image. The dimensions of the
current image are not changed.
:param image: the image placed over the current image
:type image: :class:`wand.image.Image`
:param transparency: the percentage fade that should be performed on
the image, from 0.0 to 1.0
:type transparency: :class:`numbers.Real`
:param left: the x-coordinate where `image` will be placed
:type left: :class:`numbers.Integral`
:param top: the y-coordinate where `image` will be placed
:type top: :class:`numbers.Integral`
.. versionadded:: 0.2.0
"""
with image.clone() as watermark_image:
watermark_image.transparentize(transparency)
self.composite(watermark_image, left=left, top=top)
self.raise_exception()
@manipulative
def quantize(self, number_colors, colorspace_type,
treedepth, dither, measure_error):
"""`quantize` analyzes the colors within a sequence of images and
chooses a fixed number of colors to represent the image. The goal of
the algorithm is to minimize the color difference between the input and
output image while minimizing the processing time.
:param number_colors: the number of colors.
:type number_colors: :class:`numbers.Integral`
:param colorspace_type: colorspace_type. available value can be found
in the :const:`COLORSPACE_TYPES`
:type colorspace_type: :class:`basestring`
:param treedepth: normally, this integer value is zero or one.
a zero or one tells :meth:`quantize` to choose
a optimal tree depth of ``log4(number_colors)``.
a tree of this depth generally allows the best
representation of the reference image
with the least amount of memory and
the fastest computational speed.
in some cases, such as an image with low color
dispersion (a few number of colors), a value other
than ``log4(number_colors)`` is required.
to expand the color tree completely,
use a value of 8
:type treedepth: :class:`numbers.Integral`
:param dither: a value other than zero distributes the difference
between an original image and the corresponding
color reduced algorithm to neighboring pixels along
a Hilbert curve
:type dither: :class:`bool`
:param measure_error: a value other than zero measures the difference
between the original and quantized images.
this difference is the total quantization error.
The error is computed by summing over all pixels
in an image the distance squared in RGB space
between each reference pixel value and
its quantized value
:type measure_error: :class:`bool`
.. versionadded:: 0.4.2
"""
if not isinstance(number_colors, numbers.Integral):
raise TypeError('number_colors must be integral, '
'not ' + repr(number_colors))
if not isinstance(colorspace_type, string_type) \
or colorspace_type not in COLORSPACE_TYPES:
raise TypeError('Colorspace value must be a string from '
'COLORSPACE_TYPES, not ' + repr(colorspace_type))
if not isinstance(treedepth, numbers.Integral):
raise TypeError('treedepth must be integral, '
'not ' + repr(treedepth))
if not isinstance(dither, bool):
raise TypeError('dither must be a bool, not ' +
repr(dither))
if not isinstance(measure_error, bool):
raise TypeError('measure_error must be a bool, not ' +
repr(measure_error))
r = library.MagickQuantizeImage(
self.wand, number_colors,
COLORSPACE_TYPES.index(colorspace_type),
treedepth, dither, measure_error
)
if not r:
self.raise_exception()
@manipulative
def transform_colorspace(self, colorspace_type):
"""Transform image's colorspace.
:param colorspace_type: colorspace_type. available value can be found
in the :const:`COLORSPACE_TYPES`
:type colorspace_type: :class:`basestring`
.. versionadded:: 0.4.2
"""
if not isinstance(colorspace_type, string_type) \
or colorspace_type not in COLORSPACE_TYPES:
raise TypeError('Colorspace value must be a string from '
'COLORSPACE_TYPES, not ' + repr(colorspace_type))
r = library.MagickTransformImageColorspace(
self.wand,
COLORSPACE_TYPES.index(colorspace_type)
)
if not r:
self.raise_exception()
def __repr__(self, extra_format=' ({self.width}x{self.height})'):
2016-04-08 22:36:30 +00:00
cls = type(self)
typename = '{0}.{1}'.format(
cls.__module__,
getattr(cls, '__qualname__', cls.__name__)
)
2016-04-08 22:36:30 +00:00
if getattr(self, 'c_resource', None) is None:
return '<{0}: (closed)>'.format(typename)
sig = self.signature
if not sig:
return '<{0}: (empty)>'.format(typename)
return '<{0}: {1}{2}>'.format(
typename, sig[:7], extra_format.format(self=self)
2016-04-08 22:36:30 +00:00
)
class Image(BaseImage):
"""An image object.
:param image: makes an exact copy of the ``image``
:type image: :class:`Image`
:param blob: opens an image of the ``blob`` byte array
:type blob: :class:`bytes`
:param file: opens an image of the ``file`` object
:type file: file object
:param filename: opens an image of the ``filename`` string
:type filename: :class:`basestring`
:param format: forces filename to buffer. ``format`` to help
imagemagick detect the file format. Used only in
``blob`` or ``file`` cases
:type format: :class:`basestring`
:param width: the width of new blank image or an image loaded from raw
data.
:type width: :class:`numbers.Integral`
:param height: the height of new blank imgage or an image loaded from
raw data.
:type height: :class:`numbers.Integral`
:param depth: the depth used when loading raw data.
:type depth: :class:`numbers.Integral`
:param background: an optional background color.
default is transparent
:type background: :class:`wand.color.Color`
:param resolution: set a resolution value (dpi),
useful for vectorial formats (like pdf)
:type resolution: :class:`collections.Sequence`,
:Class:`numbers.Integral`
.. versionadded:: 0.1.5
The ``file`` parameter.
.. versionadded:: 0.1.1
The ``blob`` parameter.
.. versionadded:: 0.2.1
The ``format`` parameter.
.. versionadded:: 0.2.2
The ``width``, ``height``, ``background`` parameters.
.. versionadded:: 0.3.0
The ``resolution`` parameter.
.. versionadded:: 0.4.2
The ``depth`` parameter.
.. versionchanged:: 0.4.2
The ``depth``, ``width`` and ``height`` parameters can be used
with the ``filename``, ``file`` and ``blob`` parameters to load
raw pixel data.
.. describe:: [left:right, top:bottom]
Crops the image by its ``left``, ``right``, ``top`` and ``bottom``,
and then returns the cropped one. ::
with img[100:200, 150:300] as cropped:
# manipulated the cropped image
pass
Like other subscriptable objects, default is 0 or its width/height::
img[:, :] #--> just clone
img[:100, 200:] #--> equivalent to img[0:100, 200:img.height]
Negative integers count from the end (width/height)::
img[-70:-50, -20:-10]
#--> equivalent to img[width-70:width-50, height-20:height-10]
:returns: the cropped image
:rtype: :class:`Image`
.. versionadded:: 0.1.2
"""
#: (:class:`Metadata`) The metadata mapping of the image. Read only.
#:
#: .. versionadded:: 0.3.0
metadata = None
#: (:class:`ChannelImageDict`) The mapping of separated channels
#: from the image. ::
#:
#: with image.channel_images['red'] as red_image:
#: display(red_image)
channel_images = None
#: (:class:`ChannelDepthDict`) The mapping of channels to their depth.
#: Read only.
#:
#: .. versionadded:: 0.3.0
channel_depths = None
def __init__(self, image=None, blob=None, file=None, filename=None,
format=None, width=None, height=None, depth=None,
background=None, resolution=None):
new_args = width, height, background, depth
open_args = blob, file, filename
if any(a is not None for a in new_args) and image is not None:
raise TypeError("blank image parameters can't be used with image "
'parameter')
if sum(a is not None for a in open_args + (image,)) > 1:
raise TypeError(', '.join(open_args) +
' and image parameters are exclusive each other; '
'use only one at once')
if not (format is None):
if not isinstance(format, string_type):
raise TypeError('format must be a string, not ' + repr(format))
if not any(a is not None for a in open_args):
raise TypeError('format can only be used with the blob, file '
'or filename parameter')
if depth not in [None, 8, 16, 32]:
raise ValueError('Depth must be 8, 16 or 32')
with self.allocate():
if image is None:
wand = library.NewMagickWand()
super(Image, self).__init__(wand)
if image is not None:
if not isinstance(image, BaseImage):
raise TypeError('image must be a wand.image.Image '
'instance, not ' + repr(image))
wand = library.CloneMagickWand(image.wand)
super(Image, self).__init__(wand)
elif any(a is not None for a in open_args):
if format:
format = binary(format)
with Color('transparent') as bg: # FIXME: parameterize this
result = library.MagickSetBackgroundColor(self.wand,
bg.resource)
if not result:
self.raise_exception()
# allow setting the width, height and depth
# (needed for loading raw data)
if width is not None and height is not None:
if not isinstance(width, numbers.Integral) or width < 1:
raise TypeError('width must be a natural number, '
'not ' + repr(width))
if not isinstance(height, numbers.Integral) or height < 1:
raise TypeError('height must be a natural number, '
'not ' + repr(height))
library.MagickSetSize(self.wand, width, height)
if depth is not None:
library.MagickSetDepth(self.wand, depth)
if format:
library.MagickSetFormat(self.wand, format)
if not filename:
library.MagickSetFilename(self.wand,
b'buffer.' + format)
if file is not None:
self.read(file=file, resolution=resolution)
elif blob is not None:
self.read(blob=blob, resolution=resolution)
elif filename is not None:
self.read(filename=filename, resolution=resolution)
# clear the wand format, otherwise any subsequent call to
# MagickGetImageBlob will silently change the image to this
# format again.
library.MagickSetFormat(self.wand, binary(""))
elif width is not None and height is not None:
self.blank(width, height, background)
if depth:
r = library.MagickSetImageDepth(self.wand, depth)
if not r:
raise self.raise_exception()
self.metadata = Metadata(self)
from .sequence import Sequence
self.sequence = Sequence(self)
self.raise_exception()
def read(self, file=None, filename=None, blob=None, resolution=None):
"""Read new image into Image() object.
:param blob: reads an image from the ``blob`` byte array
:type blob: :class:`bytes`
:param file: reads an image from the ``file`` object
:type file: file object
:param filename: reads an image from the ``filename`` string
:type filename: :class:`basestring`
:param resolution: set a resolution value (DPI),
useful for vectorial formats (like PDF)
:type resolution: :class:`collections.Sequence`,
:class:`numbers.Integral`
.. versionadded:: 0.3.0
"""
r = None
# Resolution must be set after image reading.
if resolution is not None:
if (isinstance(resolution, collections.Sequence) and
len(resolution) == 2):
library.MagickSetResolution(self.wand, *resolution)
elif isinstance(resolution, numbers.Integral):
library.MagickSetResolution(self.wand, resolution, resolution)
else:
raise TypeError('resolution must be a (x, y) pair or an '
'integer of the same x/y')
if file is not None:
if (isinstance(file, file_types) and
hasattr(libc, 'fdopen') and hasattr(file, 'mode')):
fd = libc.fdopen(file.fileno(), file.mode)
r = library.MagickReadImageFile(self.wand, fd)
elif not callable(getattr(file, 'read', None)):
raise TypeError('file must be a readable file object'
', but the given object does not '
'have read() method')
else:
blob = file.read()
file = None
if blob is not None:
if not isinstance(blob, collections.Iterable):
raise TypeError('blob must be iterable, not ' +
repr(blob))
if not isinstance(blob, binary_type):
blob = b''.join(blob)
r = library.MagickReadImageBlob(self.wand, blob, len(blob))
elif filename is not None:
filename = encode_filename(filename)
r = library.MagickReadImage(self.wand, filename)
if not r:
self.raise_exception()
def close(self):
"""Closes the image explicitly. If you use the image object in
:keyword:`with` statement, it was called implicitly so don't have to
call it.
.. note::
It has the same functionality of :attr:`destroy()` method.
"""
self.destroy()
def clear(self):
"""Clears resources associated with the image, leaving the image blank,
and ready to be used with new image.
.. versionadded:: 0.3.0
"""
library.ClearMagickWand(self.wand)
def level(self, black=0.0, white=None, gamma=1.0, channel=None):
"""Adjusts the levels of an image by scaling the colors falling
between specified black and white points to the full available
quantum range.
If only ``black`` is given, ``white`` will be adjusted inward.
:param black: Black point, as a percentage of the system's quantum
range. Defaults to 0.
:type black: :class:`numbers.Real`
:param white: White point, as a percentage of the system's quantum
range. Defaults to 1.0.
:type white: :class:`numbers.Real`
:param gamma: Optional gamma adjustment. Values > 1.0 lighten the
image's midtones while values < 1.0 darken them.
:type gamma: :class:`numbers.Real`
:param channel: The channel type. Available values can be found
in the :const:`CHANNELS` mapping. If ``None``,
normalize all channels.
:type channel: :const:`CHANNELS`
.. versionadded:: 0.4.1
"""
if not isinstance(black, numbers.Real):
raise TypeError('expecting real number, not' + repr(black))
# If white is not given, mimic CLI behavior by reducing top point
if white is None:
white = 1.0 - black
if not isinstance(white, numbers.Real):
raise TypeError('expecting real number, not' + repr(white))
if not isinstance(gamma, numbers.Real):
raise TypeError('expecting real number, not' + repr(gamma))
bp = float(self.quantum_range * black)
wp = float(self.quantum_range * white)
if channel:
try:
ch_const = CHANNELS[channel]
except KeyError:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
library.MagickLevelImageChannel(self.wand, ch_const, bp, gamma, wp)
else:
library.MagickLevelImage(self.wand, bp, gamma, wp)
self.raise_exception()
@property
def format(self):
"""(:class:`basestring`) The image format.
If you want to convert the image format, just reset this property::
assert isinstance(img, wand.image.Image)
img.format = 'png'
It may raise :exc:`ValueError` when the format is unsupported.
.. seealso::
`ImageMagick Image Formats`__
ImageMagick uses an ASCII string known as *magick* (e.g. ``GIF``)
to identify file formats, algorithms acting as formats,
built-in patterns, and embedded profile types.
__ http://www.imagemagick.org/script/formats.php
.. versionadded:: 0.1.6
"""
fmt = library.MagickGetImageFormat(self.wand)
if bool(fmt):
return text(fmt.value)
self.raise_exception()
@format.setter
def format(self, fmt):
if not isinstance(fmt, string_type):
raise TypeError("format must be a string like 'png' or 'jpeg'"
', not ' + repr(fmt))
fmt = fmt.strip()
r = library.MagickSetImageFormat(self.wand, binary(fmt.upper()))
if not r:
raise ValueError(repr(fmt) + ' is unsupported format')
r = library.MagickSetFilename(self.wand,
b'buffer.' + binary(fmt.lower()))
if not r:
self.raise_exception()
@property
def mimetype(self):
"""(:class:`basestring`) The MIME type of the image
e.g. ``'image/jpeg'``, ``'image/png'``.
.. versionadded:: 0.1.7
"""
rp = libmagick.MagickToMime(binary(self.format))
if not bool(rp):
self.raise_exception()
mimetype = rp.value
return text(mimetype)
@property
def animation(self):
return (self.mimetype in ('image/gif', 'image/x-gif') and
len(self.sequence) > 1)
2016-04-08 22:36:30 +00:00
@property
def compression(self):
"""(:class:`basestring`) The type of image compression.
It's a string from :const:`COMPRESSION_TYPES` list.
It also can be set.
.. versionadded:: 0.3.6
"""
compression_index = library.MagickGetImageCompression(self.wand)
return COMPRESSION_TYPES[compression_index]
@compression.setter
def compression(self, value):
if not isinstance(value, string_type):
raise TypeError('expected a string, not ' + repr(value))
if value not in COMPRESSION_TYPES:
raise ValueError('expected a string from COMPRESSION_TYPES, not ' +
repr(value))
2016-04-08 22:36:30 +00:00
library.MagickSetImageCompression(
self.wand,
COMPRESSION_TYPES.index(value)
)
def blank(self, width, height, background=None):
"""Creates blank image.
:param width: the width of new blank image.
:type width: :class:`numbers.Integral`
:param height: the height of new blank imgage.
:type height: :class:`numbers.Integral`
:param background: an optional background color.
default is transparent
:type background: :class:`wand.color.Color`
:returns: blank image
:rtype: :class:`Image`
.. versionadded:: 0.3.0
"""
if not isinstance(width, numbers.Integral) or width < 1:
raise TypeError('width must be a natural number, not ' +
repr(width))
if not isinstance(height, numbers.Integral) or height < 1:
raise TypeError('height must be a natural number, not ' +
repr(height))
if background is not None and not isinstance(background, Color):
raise TypeError('background must be a wand.color.Color '
'instance, not ' + repr(background))
if background is None:
background = Color('transparent')
with background:
r = library.MagickNewImage(self.wand, width, height,
background.resource)
if not r:
self.raise_exception()
return self
def convert(self, format):
"""Converts the image format with the original image maintained.
It returns a converted image instance which is new. ::
with img.convert('png') as converted:
converted.save(filename='converted.png')
:param format: image format to convert to
:type format: :class:`basestring`
:returns: a converted image
:rtype: :class:`Image`
:raises ValueError: when the given ``format`` is unsupported
.. versionadded:: 0.1.6
"""
cloned = self.clone()
cloned.format = format
return cloned
def save(self, file=None, filename=None):
"""Saves the image into the ``file`` or ``filename``. It takes
only one argument at a time.
:param file: a file object to write to
:type file: file object
:param filename: a filename string to write to
:type filename: :class:`basestring`
.. versionadded:: 0.1.5
The ``file`` parameter.
.. versionadded:: 0.1.1
"""
if file is None and filename is None:
raise TypeError('expected an argument')
elif file is not None and filename is not None:
raise TypeError('expected only one argument; but two passed')
elif file is not None:
if isinstance(file, string_type):
raise TypeError('file must be a writable file object, '
'but {0!r} is a string; did you want '
'.save(filename={0!r})?'.format(file))
elif isinstance(file, file_types) and hasattr(libc, 'fdopen'):
fd = libc.fdopen(file.fileno(), file.mode)
if len(self.sequence) > 1:
r = library.MagickWriteImagesFile(self.wand, fd)
else:
r = library.MagickWriteImageFile(self.wand, fd)
libc.fflush(fd)
if not r:
self.raise_exception()
else:
if not callable(getattr(file, 'write', None)):
raise TypeError('file must be a writable file object, '
'but it does not have write() method: ' +
repr(file))
file.write(self.make_blob())
else:
if not isinstance(filename, string_type):
raise TypeError('filename must be a string, not ' +
repr(filename))
filename = encode_filename(filename)
if len(self.sequence) > 1:
r = library.MagickWriteImages(self.wand, filename, True)
else:
r = library.MagickWriteImage(self.wand, filename)
if not r:
self.raise_exception()
def make_blob(self, format=None):
"""Makes the binary string of the image.
:param format: the image format to write e.g. ``'png'``, ``'jpeg'``.
it is omittable
:type format: :class:`basestring`
:returns: a blob (bytes) string
:rtype: :class:`bytes`
:raises ValueError: when ``format`` is invalid
.. versionchanged:: 0.1.6
Removed a side effect that changes the image :attr:`format`
silently.
.. versionadded:: 0.1.5
The ``format`` parameter became optional.
.. versionadded:: 0.1.1
"""
if format is not None:
with self.convert(format) as converted:
return converted.make_blob()
library.MagickResetIterator(self.wand)
length = ctypes.c_size_t()
blob_p = None
if len(self.sequence) > 1:
blob_p = library.MagickGetImagesBlob(self.wand,
ctypes.byref(length))
else:
blob_p = library.MagickGetImageBlob(self.wand,
ctypes.byref(length))
if blob_p and length.value:
blob = ctypes.string_at(blob_p, length.value)
library.MagickRelinquishMemory(blob_p)
return blob
self.raise_exception()
def strip(self):
"""Strips an image of all profiles and comments.
.. versionadded:: 0.2.0
"""
result = library.MagickStripImage(self.wand)
if not result:
self.raise_exception()
def trim(self, color=None, fuzz=0):
"""Remove solid border from image. Uses top left pixel as a guide
by default, or you can also specify the ``color`` to remove.
:param color: the border color to remove.
if it's omitted top left pixel is used by default
:type color: :class:`~wand.color.Color`
:param fuzz: Defines how much tolerance is acceptable to consider
two colors as the same.
:type fuzz: :class:`numbers.Integral`
.. versionadded:: 0.3.0
Optional ``color`` and ``fuzz`` parameters.
.. versionadded:: 0.2.1
"""
with color or self[0, 0] as color:
self.border(color, 1, 1)
result = library.MagickTrimImage(self.wand, fuzz)
if not result:
self.raise_exception()
@manipulative
def transpose(self):
"""Creates a vertical mirror image by reflecting the pixels around
the central x-axis while rotating them 90-degrees.
.. versionadded:: 0.4.1
"""
result = library.MagickTransposeImage(self.wand)
if not result:
self.raise_exception()
@manipulative
def transverse(self):
"""Creates a horizontal mirror image by reflecting the pixels around
the central y-axis while rotating them 270-degrees.
.. versionadded:: 0.4.1
"""
result = library.MagickTransverseImage(self.wand)
if not result:
self.raise_exception()
@manipulative
def _auto_orient(self):
"""Fallback for :attr:`auto_orient()` method
(which wraps :c:func:`MagickAutoOrientImage`),
fixes orientation by checking EXIF data.
.. versionadded:: 0.4.1
"""
exif_orientation = self.metadata.get('exif:orientation')
if not exif_orientation:
return
orientation_type = ORIENTATION_TYPES[int(exif_orientation)]
fn_lookup = {
'undefined': None,
'top_left': None,
'top_right': self.flop,
'bottom_right': functools.partial(self.rotate, degree=180.0),
'bottom_left': self.flip,
'left_top': self.transpose,
'right_top': functools.partial(self.rotate, degree=90.0),
'right_bottom': self.transverse,
'left_bottom': functools.partial(self.rotate, degree=270.0)
}
fn = fn_lookup.get(orientation_type)
if not fn:
return
fn()
self.orientation = 'top_left'
@manipulative
def auto_orient(self):
"""Adjusts an image so that its orientation is suitable
for viewing (i.e. top-left orientation). If available it uses
:c:func:`MagickAutoOrientImage` (was added in ImageMagick 6.8.9+)
if you have an older magick library,
it will use :attr:`_auto_orient()` method for fallback.
.. versionadded:: 0.4.1
"""
try:
result = library.MagickAutoOrientImage(self.wand)
if not result:
self.raise_exception()
except AttributeError:
self._auto_orient()
def border(self, color, width, height):
"""Surrounds the image with a border.
:param bordercolor: the border color pixel wand
:type image: :class:`~wand.color.Color`
:param width: the border width
:type width: :class:`numbers.Integral`
:param height: the border height
:type height: :class:`numbers.Integral`
.. versionadded:: 0.3.0
"""
if not isinstance(color, Color):
raise TypeError('color must be a wand.color.Color object, not ' +
repr(color))
with color:
result = library.MagickBorderImage(self.wand, color.resource,
width, height)
if not result:
self.raise_exception()
@manipulative
def contrast_stretch(self, black_point=0.0, white_point=None,
channel=None):
"""Enhance contrast of image by adjusting the span of the available
colors.
If only ``black_point`` is given, match the CLI behavior by assuming
the ``white_point`` has the same delta percentage off the top
e.g. contrast stretch of 15% is calculated as ``black_point`` = 0.15
and ``white_point`` = 0.85.
:param black_point: black point between 0.0 and 1.0. default is 0.0
:type black_point: :class:`numbers.Real`
:param white_point: white point between 0.0 and 1.0.
default value of 1.0 minus ``black_point``
:type white_point: :class:`numbers.Real`
:param channel: optional color channel to apply contrast stretch
:type channel: :const:`CHANNELS`
:raises ValueError: if ``channel`` is not in :const:`CHANNELS`
.. versionadded:: 0.4.1
"""
if not isinstance(black_point, numbers.Real):
raise TypeError('expecting float, not ' + repr(black_point))
if not (white_point is None or isinstance(white_point, numbers.Real)):
raise TypeError('expecting float, not ' + repr(white_point))
# If only black-point is given, match CLI behavior by
# calculating white point
if white_point is None:
white_point = 1.0 - black_point
contrast_range = float(self.width * self.height)
black_point *= contrast_range
white_point *= contrast_range
if channel in CHANNELS:
library.MagickContrastStretchImageChannel(self.wand,
CHANNELS[channel],
black_point,
white_point)
elif channel is None:
library.MagickContrastStretchImage(self.wand,
black_point,
white_point)
else:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
self.raise_exception()
@manipulative
def gamma(self, adjustment_value, channel=None):
"""Gamma correct image.
Specific color channels can be correct individual. Typical values
range between 0.8 and 2.3.
:param adjustment_value: value to adjust gamma level
:type adjustment_value: :class:`numbers.Real`
:param channel: optional channel to apply gamma correction
:type channel: :class:`basestring`
:raises TypeError: if ``gamma_point`` is not a :class:`numbers.Real`
:raises ValueError: if ``channel`` is not in :const:`CHANNELS`
.. versionadded:: 0.4.1
"""
if not isinstance(adjustment_value, numbers.Real):
raise TypeError('expecting float, not ' + repr(adjustment_value))
if channel in CHANNELS:
library.MagickGammaImageChannel(self.wand,
CHANNELS[channel],
adjustment_value)
elif channel is None:
library.MagickGammaImage(self.wand, adjustment_value)
else:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
self.raise_exception()
@manipulative
def linear_stretch(self, black_point=0.0, white_point=1.0):
"""Enhance saturation intensity of an image.
:param black_point: Black point between 0.0 and 1.0. Default 0.0
:type black_point: :class:`numbers.Real`
:param white_point: White point between 0.0 and 1.0. Default 1.0
:type white_point: :class:`numbers.Real`
.. versionadded:: 0.4.1
"""
if not isinstance(black_point, numbers.Real):
raise TypeError('expecting float, not ' + repr(black_point))
if not isinstance(white_point, numbers.Real):
raise TypeError('expecting float, not ' + repr(white_point))
linear_range = float(self.width * self.height)
library.MagickLinearStretchImage(self.wand,
linear_range * black_point,
linear_range * white_point)
def normalize(self, channel=None):
"""Normalize color channels.
:param channel: the channel type. available values can be found
in the :const:`CHANNELS` mapping. If ``None``,
normalize all channels.
:type channel: :class:`basestring`
"""
if channel:
try:
ch_const = CHANNELS[channel]
except KeyError:
raise ValueError(repr(channel) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
r = library.MagickNormalizeImageChannel(self.wand, ch_const)
else:
r = library.MagickNormalizeImage(self.wand)
if not r:
self.raise_exception()
def _repr_png_(self):
with self.convert('png') as cloned:
return cloned.make_blob()
def __repr__(self):
return super(Image, self).__repr__(
extra_format=' {self.format!r} ({self.width}x{self.height})'
2016-04-08 22:36:30 +00:00
)
class Iterator(Resource, collections.Iterator):
"""Row iterator for :class:`Image`. It shouldn't be instantiated
directly; instead, it can be acquired through :class:`Image` instance::
assert isinstance(image, wand.image.Image)
iterator = iter(image)
It doesn't iterate every pixel, but rows. For example::
for row in image:
for col in row:
assert isinstance(col, wand.color.Color)
print(col)
Every row is a :class:`collections.Sequence` which consists of
one or more :class:`wand.color.Color` values.
:param image: the image to get an iterator
:type image: :class:`Image`
.. versionadded:: 0.1.3
"""
c_is_resource = library.IsPixelIterator
c_destroy_resource = library.DestroyPixelIterator
c_get_exception = library.PixelGetIteratorException
c_clear_exception = library.PixelClearIteratorException
def __init__(self, image=None, iterator=None):
if image is not None and iterator is not None:
raise TypeError('it takes only one argument at a time')
with self.allocate():
if image is not None:
if not isinstance(image, Image):
raise TypeError('expected a wand.image.Image instance, '
'not ' + repr(image))
self.resource = library.NewPixelIterator(image.wand)
self.height = image.height
else:
if not isinstance(iterator, Iterator):
raise TypeError('expected a wand.image.Iterator instance, '
'not ' + repr(iterator))
self.resource = library.ClonePixelIterator(iterator.resource)
self.height = iterator.height
self.raise_exception()
self.cursor = 0
def __iter__(self):
return self
def seek(self, y):
if not isinstance(y, numbers.Integral):
raise TypeError('expected an integer, but got ' + repr(y))
elif y < 0:
raise ValueError('cannot be less than 0, but got ' + repr(y))
elif y > self.height:
raise ValueError('canot be greater than height')
self.cursor = y
if y == 0:
library.PixelSetFirstIteratorRow(self.resource)
else:
if not library.PixelSetIteratorRow(self.resource, y - 1):
self.raise_exception()
def __next__(self, x=None):
if self.cursor >= self.height:
self.destroy()
raise StopIteration()
self.cursor += 1
width = ctypes.c_size_t()
pixels = library.PixelGetNextIteratorRow(self.resource,
ctypes.byref(width))
get_color = library.PixelGetMagickColor
struct_size = ctypes.sizeof(MagickPixelPacket)
if x is None:
r_pixels = [None] * width.value
for x in xrange(width.value):
pc = pixels[x]
packet_buffer = ctypes.create_string_buffer(struct_size)
get_color(pc, packet_buffer)
r_pixels[x] = Color(raw=packet_buffer)
return r_pixels
packet_buffer = ctypes.create_string_buffer(struct_size)
get_color(pixels[x], packet_buffer)
return Color(raw=packet_buffer)
next = __next__ # Python 2 compatibility
def clone(self):
"""Clones the same iterator.
"""
return type(self)(iterator=self)
class ImageProperty(object):
"""The mixin class to maintain a weak reference to the parent
:class:`Image` object.
.. versionadded:: 0.3.0
"""
def __init__(self, image):
if not isinstance(image, BaseImage):
raise TypeError('expected a wand.image.BaseImage instance, '
'not ' + repr(image))
self._image = weakref.ref(image)
@property
def image(self):
"""(:class:`Image`) The parent image.
It ensures that the parent :class:`Image`, which is held in a weak
reference, still exists. Returns the dereferenced :class:`Image`
if it does exist, or raises a :exc:`ClosedImageError` otherwise.
:exc: `ClosedImageError` when the parent Image has been destroyed
"""
# Dereference our weakref and check that the parent Image stil exists
image = self._image()
if image is not None:
return image
raise ClosedImageError(
'parent Image of {0!r} has been destroyed'.format(self)
)
class OptionDict(ImageProperty, collections.MutableMapping):
"""Mutable mapping of the image internal options. See available
options in :const:`OPTIONS` constant.
.. versionadded:: 0.3.0
"""
def __iter__(self):
return iter(OPTIONS)
def __len__(self):
return len(OPTIONS)
def __getitem__(self, key):
if not isinstance(key, string_type):
raise TypeError('option name must be a string, not ' + repr(key))
if key not in OPTIONS:
raise ValueError('invalid option: ' + repr(key))
image = self.image
return text(library.MagickGetOption(image.wand, binary(key)))
def __setitem__(self, key, value):
if not isinstance(key, string_type):
raise TypeError('option name must be a string, not ' + repr(key))
if not isinstance(value, string_type):
raise TypeError('option value must be a string, not ' +
repr(value))
if key not in OPTIONS:
raise ValueError('invalid option: ' + repr(key))
image = self.image
library.MagickSetOption(image.wand, binary(key), binary(value))
def __delitem__(self, key):
self[key] = ''
class Metadata(ImageProperty, collections.Mapping):
"""Class that implements dict-like read-only access to image metadata
like EXIF or IPTC headers.
:param image: an image instance
:type image: :class:`Image`
.. note::
You don't have to use this by yourself.
Use :attr:`Image.metadata` property instead.
.. versionadded:: 0.3.0
"""
def __init__(self, image):
if not isinstance(image, Image):
raise TypeError('expected a wand.image.Image instance, '
'not ' + repr(image))
super(Metadata, self).__init__(image)
def __getitem__(self, k):
"""
:param k: Metadata header name string.
:type k: :class:`basestring`
:returns: a header value string
:rtype: :class:`str`
"""
image = self.image
if not isinstance(k, string_type):
raise TypeError('k must be a string, not ' + repr(k))
v = library.MagickGetImageProperty(image.wand, binary(k))
if bool(v) is False:
raise KeyError(k)
value = v.value
return text(value)
def __iter__(self):
image = self.image
num = ctypes.c_size_t()
props_p = library.MagickGetImageProperties(image.wand, b'', num)
props = [text(props_p[i]) for i in xrange(num.value)]
library.MagickRelinquishMemory(props_p)
return iter(props)
def __len__(self):
image = self.image
num = ctypes.c_size_t()
props_p = library.MagickGetImageProperties(image.wand, b'', num)
library.MagickRelinquishMemory(props_p)
return num.value
class ChannelImageDict(ImageProperty, collections.Mapping):
"""The mapping table of separated images of the particular channel
from the image.
:param image: an image instance
:type image: :class:`Image`
.. note::
You don't have to use this by yourself.
Use :attr:`Image.channel_images` property instead.
.. versionadded:: 0.3.0
"""
def __iter__(self):
return iter(CHANNELS)
def __len__(self):
return len(CHANNELS)
def __getitem__(self, channel):
c = CHANNELS[channel]
img = self.image.clone()
succeeded = library.MagickSeparateImageChannel(img.wand, c)
if not succeeded:
try:
img.raise_exception()
except WandException:
img.close()
raise
return img
class ChannelDepthDict(ImageProperty, collections.Mapping):
"""The mapping table of channels to their depth.
:param image: an image instance
:type image: :class:`Image`
.. note::
You don't have to use this by yourself.
Use :attr:`Image.channel_depths` property instead.
.. versionadded:: 0.3.0
"""
def __iter__(self):
return iter(CHANNELS)
def __len__(self):
return len(CHANNELS)
def __getitem__(self, channel):
c = CHANNELS[channel]
depth = library.MagickGetImageChannelDepth(self.image.wand, c)
return int(depth)
class HistogramDict(collections.Mapping):
"""Specialized mapping object to represent color histogram.
Keys are colors, and values are the number of pixels.
:param image: the image to get its histogram
:type image: :class:`BaseImage`
.. versionadded:: 0.3.0
"""
def __init__(self, image):
self.size = ctypes.c_size_t()
self.pixels = library.MagickGetImageHistogram(
image.wand,
ctypes.byref(self.size)
)
self.counts = None
def __len__(self):
if self.counts is None:
return self.size.value
return len(self.counts)
def __iter__(self):
if self.counts is None:
pixels = self.pixels
string = library.PixelGetColorAsString
return (Color(string(pixels[i]).value)
for i in xrange(self.size.value))
return iter(Color(string=c) for c in self.counts)
def __getitem__(self, color):
if self.counts is None:
string = library.PixelGetColorAsNormalizedString
pixels = self.pixels
count = library.PixelGetColorCount
self.counts = dict(
(text(string(pixels[i]).value), count(pixels[i]))
for i in xrange(self.size.value)
)
del self.size, self.pixels
return self.counts[color.normalized_string]
class ClosedImageError(DestroyedResourceError):
"""An error that rises when some code tries access to an already closed
image.
"""