You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10888 lines
421 KiB
Python

""":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 ctypes
import functools
import numbers
import weakref
from . import assertions
from .api import libc, libmagick, library
from .cdefs.structures import (CCObjectInfo, CCObjectInfo70A, CCObjectInfo710,
ChannelFeature, GeometryInfo, PixelInfo,
RectangleInfo)
from .color import Color
from .compat import (abc, binary, binary_type, encode_filename, file_types,
string_type, text, to_bytes, xrange)
from .exceptions import (MissingDelegateError, WandException,
WandLibraryVersionError, WandRuntimeError)
from .font import Font
from .resource import DestroyedResourceError, Resource, genesis
from .version import MAGICK_HDRI, MAGICK_VERSION_NUMBER
__all__ = ('ALPHA_CHANNEL_TYPES', 'AUTO_THRESHOLD_METHODS', 'CHANNELS',
'COLORSPACE_TYPES', 'COMPARE_METRICS', 'COMPOSITE_OPERATORS',
'COMPRESSION_TYPES', 'DISPOSE_TYPES', 'DISTORTION_METHODS',
'DITHER_METHODS', 'EVALUATE_OPS', 'FILTER_TYPES', 'FUNCTION_TYPES',
'GRAVITY_TYPES', 'IMAGE_LAYER_METHOD', 'IMAGE_TYPES',
'INTERLACE_TYPES', 'KERNEL_INFO_TYPES', 'MORPHOLOGY_METHODS',
'NOISE_TYPES', 'ORIENTATION_TYPES', 'PAPERSIZE_MAP',
'PIXEL_INTERPOLATE_METHODS', 'RENDERING_INTENT_TYPES',
'SPARSE_COLOR_METHODS', 'STATISTIC_TYPES',
'STORAGE_TYPES', 'VIRTUAL_PIXEL_METHOD', 'UNIT_TYPES',
'BaseImage', 'ChannelDepthDict', 'ChannelImageDict',
'ClosedImageError', 'HistogramDict', 'Image', 'ImageProperty',
'Iterator', 'Metadata', 'OptionDict', 'manipulative',
'ArtifactTree', 'ProfileDict', 'ConnectedComponentObject')
#: (:class:`tuple`) The list of :attr:`~wand.image.BaseImage.alpha_channel`
#: types.
#:
#: - ``'undefined'``
#: - ``'activate'``
#: - ``'background'``
#: - ``'copy'``
#: - ``'deactivate'``
#: - ``'discrete'`` - Only available in ImageMagick-7
#: - ``'extract'``
#: - ``'off'`` - Only available in ImageMagick-7
#: - ``'on'`` - Only available in ImageMagick-7
#: - ``'opaque'``
#: - ``'reset'`` - Only available in ImageMagick-6
#: - ``'set'``
#: - ``'shape'``
#: - ``'transparent'``
#: - ``'flatten'`` - Only available in ImageMagick-6
#: - ``'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',
'associate', 'disassociate')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
ALPHA_CHANNEL_TYPES = ('undefined', 'activate', 'associate', 'background',
'copy', 'deactivate', 'discrete', 'disassociate',
'extract', 'off', 'on', 'opaque', 'remove', 'set',
'shape', 'transparent')
#: (:class:`tuple`) The list of methods used by
#: :meth:`Image.auto_threshold() <wand.image.BaseImage.auto_threshold>`
#:
#: - ``'undefined'``
#: - ``'kapur'``
#: - ``'otsu'``
#: - ``'triangle'``
#:
#: .. versionadded:: 0.5.5
AUTO_THRESHOLD_METHODS = ('undefined', 'kapur', 'otsu', 'triangle')
#: (:class:`dict`) The dictionary of channel types.
#:
#: - ``'undefined'``
#: - ``'red'``
#: - ``'gray'``
#: - ``'cyan'``
#: - ``'green'``
#: - ``'magenta'``
#: - ``'blue'``
#: - ``'yellow'``
#: - ``'alpha'``
#: - ``'opacity'``
#: - ``'black'``
#: - ``'index'``
#: - ``'composite_channels'``
#: - ``'all_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
#:
#: .. versionchanged:: 0.5.5
#: Deprecated ``true_alpha``, ``rgb_channels``, and ``gray_channels``
#: values in favor of MagickCore channel parser.
#:
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=7, rgb_channels=7, gray_channels=1, sync_channels=256,
default_channels=134217719)
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
CHANNELS = dict(undefined=0, red=1, gray=1, cyan=1, green=2, magenta=2,
blue=4, yellow=4, black=8, alpha=16, opacity=16, index=32,
readmask=0x0040, write_mask=128, meta=256,
composite_channels=31, all_channels=134217727,
true_alpha=256, rgb=7, rgb_channels=7, gray_channels=1,
sync_channels=131072, default_channels=134217727)
#: (:class:`tuple`) The list of colorspaces.
#:
#: - ``'undefined'``
#: - ``'rgb'``
#: - ``'gray'``
#: - ``'transparent'``
#: - ``'ohta'``
#: - ``'lab'``
#: - ``'xyz'``
#: - ``'ycbcr'``
#: - ``'ycc'``
#: - ``'yiq'``
#: - ``'ypbpr'``
#: - ``'yuv'``
#: - ``'cmyk'``
#: - ``'srgb'``
#: - ``'hsb'``
#: - ``'hsl'``
#: - ``'hwb'``
#: - ``'rec601luma'`` - Only available with ImageMagick-6
#: - ``'rec601ycbcr'``
#: - ``'rec709luma'`` - Only available with ImageMagick-6
#: - ``'rec709ycbcr'``
#: - ``'log'``
#: - ``'cmy'``
#: - ``'luv'``
#: - ``'hcl'``
#: - ``'lch'``
#: - ``'lms'``
#: - ``'lchab'``
#: - ``'lchuv'``
#: - ``'scrgb'``
#: - ``'hsi'``
#: - ``'hsv'``
#: - ``'hclp'``
#: - ``'xyy'`` - Only available with ImageMagick-7
#: - ``'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')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
COLORSPACE_TYPES = ('undefined', 'cmy', 'cmyk', 'gray', 'hcl', 'hclp',
'hsb', 'hsi', 'hsl', 'hsv', 'hwb', 'lab', 'lch',
'lchab', 'lchuv', 'log', 'lms', 'luv', 'ohta',
'rec601ycbcr', 'rec709ycbcr', 'rgb', 'scrgb', 'srgb',
'transparent', 'xyy', 'xyz', 'ycbcr', 'ycc', 'ydbdr',
'yiq', 'ypbpr', 'yuv')
#: (:class:`tuple`) The list of compare metric types used by
#: :meth:`Image.compare() <wand.image.BaseImage.compare>` and
#: :meth:`Image.similarity() <wand.image.BaseImage.similarity>` methods.
#:
#: - ``'undefined'``
#: - ``'absolute'``
#: - ``'fuzz'``
#: - ``'mean_absolute'``
#: - ``'mean_error_per_pixel'``
#: - ``'mean_squared'``
#: - ``'normalized_cross_correlation'``
#: - ``'peak_absolute'``
#: - ``'peak_signal_to_noise_ratio'``
#: - ``'perceptual_hash'`` - Available with ImageMagick-7
#: - ``'root_mean_square'``
#: - ``'structural_similarity'`` - Available with ImageMagick-7
#: - ``'structural_dissimilarity'`` - Available with ImageMagick-7
#:
#: .. seealso::
#:
#: `ImageMagick Compare Operations`__
#:
#: __ http://www.imagemagick.org/Usage/compare/
#:
#: .. versionadded:: 0.4.3
#:
#: .. versionchanged:: 0.5.4 - Remapped :c:data:`MetricType` enum.
COMPARE_METRICS = ('undefined', 'absolute',
'mean_absolute', 'mean_error_per_pixel',
'mean_squared', 'peak_absolute',
'peak_signal_to_noise_ratio', 'root_mean_square',
'normalized_cross_correlation', 'fuzz')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
COMPARE_METRICS = ('undefined', 'absolute', 'fuzz', 'mean_absolute',
'mean_error_per_pixel', 'mean_squared',
'normalized_cross_correlation', 'peak_absolute',
'peak_signal_to_noise_ratio', 'perceptual_hash',
'root_mean_square', 'structural_similarity',
'structural_dissimilarity')
#: (:class:`tuple`) The list of complex operators used by
#: :meth:`Image.complex() <wand.image.BaseImage.complex>`.
#:
#: - ``'undefined'``
#: - ``'add'``
#: - ``'conjugate'``
#: - ``'divide'``
#: - ``'magnitude',``
#: - ``'multiply'``
#: - ``'real_imaginary'``
#: - ``'subtract'``
#:
#: .. versionadded:: 0.5.5
COMPLEX_OPERATORS = ('undefined', 'add', 'conjugate', 'divide', 'magnitude',
'multiply', 'real_imaginary', 'subtract')
#: (:class:`tuple`) The list of composition operators
#:
#: - ``'undefined'``
#: - ``'alpha'`` - Only available with ImageMagick-7
#: - ``'atop'``
#: - ``'blend'``
#: - ``'blur'``
#: - ``'bumpmap'``
#: - ``'change_mask'``
#: - ``'clear'``
#: - ``'color_burn'``
#: - ``'color_dodge'``
#: - ``'colorize'``
#: - ``'copy_black'``
#: - ``'copy_blue'``
#: - ``'copy'``
#: - ``'copy_alpha'`` - Only available with ImageMagick-7
#: - ``'copy_cyan'``
#: - ``'copy_green'``
#: - ``'copy_magenta'``
#: - ``'copy_opacity'`` - Only available with ImageMagick-6
#: - ``'copy_red'``
#: - ``'copy_yellow'``
#: - ``'darken'``
#: - ``'darken_intensity'``
#: - ``'difference'``
#: - ``'displace'``
#: - ``'dissolve'``
#: - ``'distort'``
#: - ``'divide_dst'``
#: - ``'divide_src'``
#: - ``'dst_atop'``
#: - ``'dst'``
#: - ``'dst_in'``
#: - ``'dst_out'``
#: - ``'dst_over'``
#: - ``'exclusion'``
#: - ``'freeze'`` - Added with ImageMagick-7.0.10
#: - ``'hard_light'``
#: - ``'hard_mix'``
#: - ``'hue'``
#: - ``'in'``
#: - ``'intensity'`` - Only available with ImageMagick-7
#: - ``'interpolate'`` - Added with ImageMagick-7.0.10
#: - ``'lighten'``
#: - ``'lighten_intensity'``
#: - ``'linear_burn'``
#: - ``'linear_dodge'``
#: - ``'linear_light'``
#: - ``'luminize'``
#: - ``'mathematics'``
#: - ``'minus_dst'``
#: - ``'minus_src'``
#: - ``'modulate'``
#: - ``'modulus_add'``
#: - ``'modulus_subtract'``
#: - ``'multiply'``
#: - ``'negate'`` - Added with ImageMagick-7.0.10
#: - ``'no'``
#: - ``'out'``
#: - ``'over'``
#: - ``'overlay'``
#: - ``'pegtop_light'``
#: - ``'pin_light'``
#: - ``'plus'``
#: - ``'reflect'`` - Added with ImageMagick-7.0.10
#: - ``'replace'``
#: - ``'rmse'`` - Added with ImageMagick-7.1.0
#: - ``'saliency_blend'`` - Added with ImageMagick-7.1.1
#: - ``'saturate'``
#: - ``'screen'``
#: - ``'seamless_blend'`` - Added with ImageMagick-7.1.1
#: - ``'soft_burn'`` - Added with ImageMagick-7.0.10
#: - ``'soft_dudge'`` - Added with ImageMagick-7.0.10
#: - ``'soft_light'``
#: - ``'src_atop'``
#: - ``'src'``
#: - ``'src_in'``
#: - ``'src_out'``
#: - ``'src_over'``
#: - ``'stamp'`` - Added with ImageMagick-7.0.10
#: - ``'stereo'``
#: - ``'threshold'``
#: - ``'vivid_light'``
#: - ``'xor'``
#:
#: .. versionchanged:: 0.3.0
#: Renamed from :const:`COMPOSITE_OPS` to :const:`COMPOSITE_OPERATORS`.
#:
#: .. versionchanged:: 0.5.6
#: Operators have been updated to reflect latest changes in C-API.
#: For ImageMagick-6, ``'add'`` has been renamed to ``'modulus_add'``,
#: ``'subtract'`` has been renamed to ``'modulus_subtract'``,
#: ``'divide'`` has been split into ``'divide_dst'`` & ``'divide_src'``, and
#: ``'minus'`` has been split into ``'minus_dst'`` & ``'minus_src'``.
#:
#: .. 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', 'modulus_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_dst', 'modulate', 'multiply', 'out',
'over', 'overlay', 'plus', 'replace', 'saturate', 'screen', 'soft_light',
'src_atop', 'src', 'src_in', 'src_out', 'src_over', 'modulus_subtract',
'threshold', 'xor', 'divide_dst', 'distort', 'blur', 'pegtop_light',
'vivid_light', 'pin_light', 'linear_dodge', 'linear_burn', 'mathematics',
'divide_src', 'minus_src', 'darken_intensity', 'lighten_intensity',
'hard_mix', 'stereo'
)
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
COMPOSITE_OPERATORS = (
'undefined', 'alpha', 'atop', 'blend', 'blur', 'bumpmap',
'change_mask', 'clear', 'color_burn', 'color_dodge', 'colorize',
'copy_black', 'copy_blue', 'copy', 'copy_cyan', 'copy_green',
'copy_magenta', 'copy_alpha', 'copy_red', 'copy_yellow', 'darken',
'darken_intensity', 'difference', 'displace', 'dissolve', 'distort',
'divide_dst', 'divide_src', 'dst_atop', 'dst', 'dst_in', 'dst_out',
'dst_over', 'exclusion', 'hard_light', 'hard_mix', 'hue', 'in',
'intensity', 'lighten', 'lighten_intensity', 'linear_burn',
'linear_dodge', 'linear_light', 'luminize', 'mathematics', 'minus_dst',
'minus_src', 'modulate', 'modulus_add', 'modulus_subtract', 'multiply',
'no', 'out', 'over', 'overlay', 'pegtop_light', 'pin_light', 'plus',
'replace', 'saturate', 'screen', 'soft_light', 'src_atop', 'src',
'src_in', 'src_out', 'src_over', 'threshold', 'vivid_light', 'xor',
'stereo', 'freeze', 'interpolate', 'negate', 'reflect', 'soft_burn',
'soft_dodge', 'stamp', 'rmse', 'saliency_blend', 'seamless_blend'
)
#: (:class:`tuple`) The list of :attr:`Image.compression` types.
#:
#: - ``'undefined'``
#: - ``'b44a'``
#: - ``'b44'``
#: - ``'bzip'``
#: - ``'dxt1'``
#: - ``'dxt3'``
#: - ``'dxt5'``
#: - ``'fax'``
#: - ``'group4'``
#: - ``'jbig1'``
#: - ``'jbig2'``
#: - ``'jpeg2000'``
#: - ``'jpeg'``
#: - ``'losslessjpeg'``
#: - ``'lzma'``
#: - ``'lzw'``
#: - ``'no'``
#: - ``'piz'``
#: - ``'pxr24'``
#: - ``'rle'``
#: - ``'zip'``
#: - ``'zips'``
#:
#: .. versionadded:: 0.3.6
#: .. versionchanged:: 0.5.0
#: Support for ImageMagick-6 & ImageMagick-7
COMPRESSION_TYPES = (
'undefined', 'no', 'bzip', 'dxt1', 'dxt3', 'dxt5',
'fax', 'group4', 'jpeg', 'jpeg2000', 'losslessjpeg',
'lzw', 'rle', 'zip', 'zips', 'piz', 'pxr24', 'b44',
'b44a', 'lzma', 'jbig1', 'jbig2'
)
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
COMPRESSION_TYPES = (
'undefined', 'b44a', 'b44', 'bzip', 'dxt1', 'dxt3', 'dxt5', 'fax',
'group4', 'jbig1', 'jbig2', 'jpeg2000', 'jpeg', 'losslessjpeg',
'lzma', 'lzw', 'no', 'piz', 'pxr24', 'rle', 'zip', 'zips'
)
#: (:class:`tuple`) The list of :attr:`BaseImage.dispose` types.
#:
#: - ``'undefined'``
#: - ``'none'``
#: - ``'background'``
#: - ``'previous'``
#:
#: .. versionadded:: 0.5.0
DISPOSE_TYPES = (
'undefined',
'none',
'background',
'previous'
)
#: (:class:`tuple`) The list of :meth:`BaseImage.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'``
#: - ``'rigidaffine'`` - Only available with ImageMagick-7.0.10, or later.
#:
#: .. 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', 'rigidaffine'
)
#: (:class:`tuple`) The list of Dither methods. Used by
#: :meth:`Image.posterize() <BaseImage.posterize>` and
#: :meth:`Image.remap() <BaseImage.remap>` methods.
#:
#: - ``'undefined'``
#: - ``'no'``
#: - ``'riemersma'``
#: - ``'floyd_steinberg'``
#:
#: .. versionadded:: 0.5.0
DITHER_METHODS = ('undefined', 'no', 'riemersma', 'floyd_steinberg')
#: (:class:`tuple`) The list of evaluation operators. Used by
#: :meth:`Image.evaluate() <BaseImage.evaluate>` method.
#:
#: - ``'undefined'``
#: - ``'abs'``
#: - ``'add'``
#: - ``'addmodulus'``
#: - ``'and'``
#: - ``'cosine'``
#: - ``'divide'``
#: - ``'exponential'``
#: - ``'gaussiannoise'``
#: - ``'impulsenoise'``
#: - ``'inverse_log'`` - Added with ImageMagick-7.0.10-24
#: - ``'laplaciannoise'``
#: - ``'leftshift'``
#: - ``'log'``
#: - ``'max'``
#: - ``'mean'``
#: - ``'median'``
#: - ``'min'``
#: - ``'multiplicativenoise'``
#: - ``'multiply'``
#: - ``'or'``
#: - ``'poissonnoise'``
#: - ``'pow'``
#: - ``'rightshift'``
#: - ``'set'``
#: - ``'sine'``
#: - ``'subtract'``
#: - ``'sum'``
#: - ``'threshold'``
#: - ``'thresholdblack'``
#: - ``'thresholdwhite'``
#: - ``'uniformnoise'``
#: - ``'xor'``
#:
#: .. 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')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
EVALUATE_OPS = ('undefined', 'abs', 'add', 'addmodulus', 'and', 'cosine',
'divide', 'exponential', 'gaussiannoise', 'impulsenoise',
'laplaciannoise', 'leftshift', 'log', 'max', 'mean',
'median', 'min', 'multiplicativenoise', 'multiply', 'or',
'poissonnoise', 'pow', 'rightshift', 'rootmeansquare',
'set', 'sine', 'subtract', 'sum', 'thresholdblack',
'threshold', 'thresholdwhite', 'uniformnoise', 'xor',
'inverse_log')
#: (:class:`tuple`) The list of filter types. Used by
#: :meth:`Image.resample() <BaseImage.resample>` and
#: :meth:`Image.resize() <BaseImage.resize>` methods.
#:
#: - ``'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 :attr:`Image.function <BaseImage.function>`
#: types.
#:
#: - ``'undefined'``
#: - ``'arcsin'``
#: - ``'arctan'``
#: - ``'polynomial'``
#: - ``'sinusoid'``
FUNCTION_TYPES = ('undefined', 'polynomial', 'sinusoid', 'arcsin', 'arctan')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
FUNCTION_TYPES = ('undefined', 'arcsin', 'arctan', 'polynomial',
'sinusoid')
#: (:class:`tuple`) The list of :attr:`~BaseImage.gravity` types.
#:
#: - ``'forget'``
#: - ``'north_west'``
#: - ``'north'``
#: - ``'north_east'``
#: - ``'west'``
#: - ``'center'``
#: - ``'east'``
#: - ``'south_west'``
#: - ``'south'``
#: - ``'south_east'``
#:
#: .. 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 methods for :meth:`~BaseImage.merge_layers`
#: and :meth:`~Image.compare_layers`.
#:
#: - ``'undefined'``
#: - ``'coalesce'``
#: - ``'compareany'`` - Only used for :meth:`~Image.compare_layers`.
#: - ``'compareclear'`` - Only used for :meth:`~Image.compare_layers`.
#: - ``'compareoverlay'`` - Only used for :meth:`~Image.compare_layers`.
#: - ``'dispose'``
#: - ``'optimize'``
#: - ``'optimizeimage'``
#: - ``'optimizeplus'``
#: - ``'optimizetrans'``
#: - ``'removedups'``
#: - ``'removezero'``
#: - ``'composite'``
#: - ``'merge'`` - Only used for :meth:`~BaseImage.merge_layers`.
#: - ``'flatten'`` - Only used for :meth:`~BaseImage.merge_layers`.
#: - ``'mosaic'`` - Only used for :meth:`~BaseImage.merge_layers`.
#: - ``'trimbounds'`` - Only used for :meth:`~BaseImage.merge_layers`.
#:
#: .. versionadded:: 0.4.3
IMAGE_LAYER_METHOD = ('undefined', 'coalesce', 'compareany', 'compareclear',
'compareoverlay', 'dispose', 'optimize', 'optimizeimage',
'optimizeplus', 'optimizetrans', 'removedups',
'removezero', 'composite', 'merge', 'flatten', 'mosaic',
'trimbounds')
#: (:class:`tuple`) The list of image types
#:
#: - ``'undefined'``
#: - ``'bilevel'``
#: - ``'grayscale'``
#: - ``'grayscalealpha'`` - Only available with ImageMagick-7
#: - ``'grayscalematte'`` - Only available with ImageMagick-6
#: - ``'palette'``
#: - ``'palettealpha'`` - Only available with ImageMagick-7
#: - ``'palettematte'`` - Only available with ImageMagick-6
#: - ``'truecolor'``
#: - ``'truecoloralpha'`` - Only available with ImageMagick-7
#: - ``'truecolormatte'`` - Only available with ImageMagick-6
#: - ``'colorseparation'``
#: - ``'colorseparationalpha'`` - Only available with ImageMagick-7
#: - ``'colorseparationmatte'`` - Only available with ImageMagick-6
#: - ``'optimize'``
#: - ``'palettebilevelalpha'`` - Only available with ImageMagick-7
#: - ``'palettebilevelmatte'`` - Only available with ImageMagick-6
#:
#: .. 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')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
IMAGE_TYPES = ('undefined', 'bilevel', 'grayscale', 'grayscalealpha',
'palette', 'palettealpha', 'truecolor', 'truecoloralpha',
'colorseparation', 'colorseparationalpha', 'optimize',
'palettebilevelalpha')
#: (:class:`tuple`) The list of interlace schemes.
#:
#: - ``'undefined'``
#: - ``'no'``
#: - ``'line'``
#: - ``'plane'``
#: - ``'partition'``
#: - ``'gif'``
#: - ``'jpeg'``
#: - ``'png'``
#:
#: .. versionadded:: 0.5.2
INTERLACE_TYPES = ('undefined', 'no', 'line', 'plane', 'partition', 'gif',
'jpeg', 'png')
#: (:class:`tuple`) The list of builtin kernels.
#:
#: - ``'undefined'``
#: - ``'unity'``
#: - ``'gaussian'``
#: - ``'dog'``
#: - ``'log'``
#: - ``'blur'``
#: - ``'comet'``
#: - ``'laplacian'``
#: - ``'sobel'``
#: - ``'frei_chen'``
#: - ``'roberts'``
#: - ``'prewitt'``
#: - ``'compass'``
#: - ``'kirsch'``
#: - ``'diamond'``
#: - ``'square'``
#: - ``'rectangle'``
#: - ``'octagon'``
#: - ``'disk'``
#: - ``'plus'``
#: - ``'cross'``
#: - ``'ring'``
#: - ``'peaks'``
#: - ``'edges'``
#: - ``'corners'``
#: - ``'diagonals'``
#: - ``'line_ends'``
#: - ``'line_junctions'``
#: - ``'ridges'``
#: - ``'convex_hull'``
#: - ``'thin_se'``
#: - ``'skeleton'``
#: - ``'chebyshev'``
#: - ``'manhattan'``
#: - ``'octagonal'``
#: - ``'euclidean'``
#: - ``'user_defined'``
#: - ``'binomial'``
#:
KERNEL_INFO_TYPES = ('undefined', 'unity', 'gaussian', 'dog', 'log', 'blur',
'comet', 'laplacian', 'sobel', 'frei_chen', 'roberts',
'prewitt', 'compass', 'kirsch', 'diamond', 'square',
'rectangle', 'octagon', 'disk', 'plus', 'cross', 'ring',
'peaks', 'edges', 'corners', 'diagonals', 'line_ends',
'line_junctions', 'ridges', 'convex_hull', 'thin_se',
'skeleton', 'chebyshev', 'manhattan', 'octagonal',
'euclidean', 'user_defined', 'binomial')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
KERNEL_INFO_TYPES = ('undefined', 'unity', 'gaussian', 'dog', 'log',
'blur', 'comet', 'binomial', 'laplacian', 'sobel',
'frei_chen', 'roberts', 'prewitt', 'compass',
'kirsch', 'diamond', 'square', 'rectangle', 'octagon',
'disk', 'plus', 'cross', 'ring', 'peaks', 'edges',
'corners', 'diagonals', 'line_ends', 'line_junctions',
'ridges', 'convex_hull', 'thin_se', 'skeleton',
'chebyshev', 'manhattan', 'octagonal', 'euclidean',
'user_defined')
#: (:class:`tuple`) The list of morphology methods.
#:
#: - ``'undefined'``
#: - ``'convolve'``
#: - ``'correlate'``
#: - ``'erode'``
#: - ``'dilate'``
#: - ``'erode_intensity'``
#: - ``'dilate_intensity'``
#: - ``'distance'``
#: - ``'open'``
#: - ``'close'``
#: - ``'open_intensity'``
#: - ``'close_intensity'``
#: - ``'smooth'``
#: - ``'edgein'``
#: - ``'edgeout'``
#: - ``'edge'``
#: - ``'tophat'``
#: - ``'bottom_hat'``
#: - ``'hit_and_miss'``
#: - ``'thinning'``
#: - ``'thicken'``
#: - ``'voronoi'``
#: - ``'iterative_distance'``
#:
MORPHOLOGY_METHODS = ('undefined', 'convolve', 'correlate', 'erode', 'dilate',
'erode_intensity', 'dilate_intensity', 'distance',
'open', 'close', 'open_intensity', 'close_intensity',
'smooth', 'edgein', 'edgeout', 'edge', 'tophat',
'bottom_hat', 'hit_and_miss', 'thinning', 'thicken',
'voronoi', 'iterative_distance')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
MORPHOLOGY_METHODS = ('undefined', 'convolve', 'correlate', 'erode',
'dilate', 'erode_intensity', 'dilate_intensity',
'iterative_distance', 'open', 'close',
'open_intensity', 'close_intensity', 'smooth',
'edgein', 'edgeout', 'edge', 'tophat', 'bottom_hat',
'hit_and_miss', 'thinning', 'thicken', 'distance',
'voronoi')
#: (:class:`tuple`) The list of montage behaviors used by
#: :meth:`Image.montage()` method.
#:
#: - ``'undefined'``
#: - ``'frame'``
#: - ``'unframe'``
#: - ``'concatenate'``
#:
#: .. versionadded:: 0.6.8
MONTAGE_MODES = ('undefined', 'frame', 'unframe', 'concatenate')
#: (:class:`tuple`) The list of noise types used by
#: :meth:`Image.noise() <wand.image.BaseImage.noise>` method.
#:
#: - ``'undefined'``
#: - ``'uniform'``
#: - ``'gaussian'``
#: - ``'multiplicative_gaussian'``
#: - ``'impulse'``
#: - ``'laplacian'``
#: - ``'poisson'``
#: - ``'random'``
#:
#: .. versionadded:: 0.5.3
NOISE_TYPES = ('undefined', 'uniform', 'gaussian', 'multiplicative_gaussian',
'impulse', 'laplacian', 'poisson', 'random')
#: (:class:`collections.abc.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. Ensure you set this option *before*
#: reading the PDF document.
#:
#: .. deprecated:: 0.5.0
#: Any arbitrary key can be set to the option table. Key-Value pairs set
#: on the MagickWand stack allowing for various coders, kernels, morphology
#: (&tc) to pick and choose additional user-supplied properties/artifacts.
OPTIONS = frozenset([
'caption',
'comment',
'date:create',
'date:modify',
'exif:ColorSpace',
'exif:InteroperabilityIndex',
'fill',
'film-gamma',
'gamma',
'hdr:exposure',
'jpeg:colorspace',
'jpeg:sampling-factor',
'label',
'pdf:use-cropbox',
'png:bit-depth-written',
'png:IHDR.bit-depth-orig',
'png:IHDR.color-type-orig',
'png:tIME',
'reference-black',
'reference-white',
'signature',
'tiff:Orientation',
'tiff:photometric',
'tiff:ResolutionUnit',
'type:hinting',
'vips:metadata'
])
#: (: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:`dict`) Map of papersize names to page sizes. Each page size
#: is a width & height :class:`tuple` at a 72dpi resolution.
#:
#: .. code::
#:
#: from wand.image import Image, PAPERSIZE_MAP
#:
#: w, h = PAPERSIZE_MAP["a4"]
#: with Image(width=w, height=h, background="white") as img:
#: img.save(filename="a4_page.png")
#:
#: .. versionadded:: 0.6.4
PAPERSIZE_MAP = {
'4x6': (288, 432), '5x7': (360, 504), '7x9': (504, 648),
'8x10': (576, 720), '9x11': (648, 792), '9x12': (648, 864),
'10x13': (720, 936), '10x14': (720, 1008), '11x17': (792, 1224),
'4A0': (4768, 6741), '2A0': (3370, 4768), 'a0': (2384, 3370),
'a1': (1684, 2384), 'a2': (1191, 1684), 'a3': (842, 1191),
'a4': (595, 842), 'a4small': (595, 842), 'a5': (420, 595),
'a6': (298, 420), 'a7': (210, 298), 'a8': (147, 210), 'a9': (105, 147),
'a10': (74, 105), 'archa': (648, 864), 'archb': (864, 1296),
'archC': (1296, 1728), 'archd': (1728, 2592), 'arche': (2592, 3456),
'b0': (2920, 4127), 'b1': (2064, 2920), 'b10': (91, 127),
'b2': (1460, 2064), 'b3': (1032, 1460), 'b4': (729, 1032),
'b5': (516, 729), 'b6': (363, 516), 'b7': (258, 363), 'b8': (181, 258),
'b9': (127, 181), 'c0': (2599, 3676), 'c1': (1837, 2599),
'c2': (1298, 1837), 'c3': (918, 1296), 'c4': (649, 918), 'c5': (459, 649),
'c6': (323, 459), 'c7': (230, 323), 'csheet': (1224, 1584),
'dsheet': (1584, 2448), 'esheet': (2448, 3168), 'executive': (540, 720),
'flsa': (612, 936), 'flse': (612, 936), 'folio': (612, 936),
'halfletter': (396, 612), 'isob0': (2835, 4008), 'isob1': (2004, 2835),
'isob10': (88, 125), 'isob2': (1417, 2004), 'isob3': (1001, 1417),
'isob4': (709, 1001), 'isob5': (499, 709), 'isob6': (354, 499),
'isob7': (249, 354), 'isob8': (176, 249), 'isob9': (125, 176),
'jisb0': (1030, 1456), 'jisb1': (728, 1030), 'jisb2': (515, 728),
'jisb3': (364, 515), 'jisb4': (257, 364), 'jisb5': (182, 257),
'jisb6': (128, 182), 'ledger': (1224, 792), 'legal': (612, 1008),
'letter': (612, 792), 'lettersmall': (612, 792), 'monarch': (279, 540),
'quarto': (610, 780), 'statement': (396, 612), 'tabloid': (792, 1224)
}
#: (:class:`tuple`) List of interpolate pixel methods (ImageMagick-7 only.)
#:
#: - ``'undefined'``
#: - ``'average'``
#: - ``'average9'``
#: - ``'average16'``
#: - ``'background'``
#: - ``'bilinear'``
#: - ``'blend'``
#: - ``'catrom'``
#: - ``'integer'``
#: - ``'mesh'``
#: - ``'nearest'``
#: - ``'spline'``
#:
#: .. versionadded:: 0.5.0
PIXEL_INTERPOLATE_METHODS = ('undefined', 'average', 'average9', 'average16',
'background', 'bilinear', 'blend', 'catrom',
'integer', 'mesh', 'nearest', 'spline')
#: (:class:`tuple`) List of rendering intent types used for
#: :attr:`Image.rendering_intent <wand.image.BaseImage.rendering_intent>`
#: property.
#:
#: - ``'undefined'``
#: - ``'saturation'``
#: - ``'perceptual'``
#: - ``'absolute'``
#: - ``'relative'``
#:
#: .. versionadded:: 0.5.4
RENDERING_INTENT_TYPES = ('undefined', 'saturation', 'perceptual', 'absolute',
'relative')
#: (:class:`tuple`) List of sparse color methods used by
#: :class:`Image.sparse_color() <wand.image.BaseImage.sparse_color>`
#:
#: - ``'undefined'``
#: - ``'barycentric'``
#: - ``'bilinear'``
#: - ``'shepards'``
#: - ``'voronoi'``
#: - ``'inverse'``
#: - ``'manhattan'``
#:
#: .. versionadded:: 0.5.3
SPARSE_COLOR_METHODS = dict(undefined=0, barycentric=1, bilinear=7,
shepards=16, voronoi=18, inverse=19,
manhattan=20)
#: (:class:`tuple`) The list of statistic types used by
#: :meth:`Image.statistic() <wand.image.BaseImage.statistic>`.
#:
#: - ``'undefined'``
#: - ``'gradient'``
#: - ``'maximum'``
#: - ``'mean'``
#: - ``'median'``
#: - ``'minimum'``
#: - ``'mode'``
#: - ``'nonpeak'``
#: - ``'root_mean_square'``
#: - ``'standard_deviation'``
#:
#: .. versionadded:: 0.5.3
STATISTIC_TYPES = ('undefined', 'gradient', 'maximum', 'mean', 'median',
'minimum', 'mode', 'nonpeak', 'standard_deviation',
'root_mean_square')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
STATISTIC_TYPES = ('undefined', 'gradient', 'maximum', 'mean', 'median',
'minimum', 'mode', 'nonpeak', 'root_mean_square',
'standard_deviation')
#: (:class:`tuple`) The list of pixel storage types.
#:
#: - ``'undefined'``
#: - ``'char'``
#: - ``'double'``
#: - ``'float'``
#: - ``'integer'``
#: - ``'long'``
#: - ``'quantum'``
#: - ``'short'``
#:
#: .. versionadded:: 0.5.0
STORAGE_TYPES = ('undefined', 'char', 'double', 'float', 'integer',
'long', 'quantum', 'short')
#: (: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.virtual_pixel` types.
#:
#: - ``'undefined'``
#: - ``'background'``
#: - ``'constant'`` - Only available with ImageMagick-6
#: - ``'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')
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
VIRTUAL_PIXEL_METHOD = ('undefined', 'background', 'dither',
'edge', 'mirror', 'random', 'tile', 'transparent',
'mask', 'black', 'gray', 'white',
'horizontal_tile', 'vertical_tile',
'horizontal_tile_edge', 'vertical_tile_edge',
'checker_tile')
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
def trap_exception(function):
@functools.wraps(function)
def wrapped(self, *args, **kwargs):
result = function(self, *args, **kwargs)
if not bool(result):
self.raise_exception()
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 class, 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.abc.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
#: (:class:`numbers.Integral`) Internal placeholder for
#: :attr:`seed` property.
#:
#: .. versionadded:: 0.5.5
_seed = 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
def __eq__(self, other):
if isinstance(other, type(self)):
return self.signature == other.signature
return False
def __getitem__(self, idx):
if (not isinstance(idx, string_type) and
isinstance(idx, abc.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 __setitem__(self, idx, color):
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
if not isinstance(idx, abc.Iterable):
raise TypeError('Expecting list of x,y coordinates, not ' +
repr(idx))
idx = tuple(idx)
if len(idx) != 2:
msg = 'pixel index can not be {0}-dimensional'.format(len(idx))
raise ValueError(msg)
colorspace = self.colorspace
s_index = STORAGE_TYPES.index("double")
width, height = self.size
x1, y1 = idx
x2, y2 = 1, 1
if not (isinstance(x1, numbers.Integral) and
isinstance(y1, numbers.Integral)):
raise TypeError('Expecting x & y to be integers')
if x1 < 0:
x1 += width
if y1 < 0:
y1 += height
if x1 >= width:
raise ValueError('x must be less than image width')
elif y1 >= height:
raise ValueError('y must be less than image height')
if colorspace == 'gray':
channel_map = b'I'
pixel = (ctypes.c_double * 1)()
pixel[0] = color.red
elif colorspace == 'cmyk':
channel_map = b'CMYK'
pixel = (ctypes.c_double * 5)()
pixel[0] = color.red
pixel[1] = color.green
pixel[2] = color.blue
pixel[3] = color.black
if self.alpha_channel:
channel_map += b'A'
pixel[4] = color.alpha
else:
channel_map = b'RGB'
pixel = (ctypes.c_double * 4)()
pixel[0] = color.red
pixel[1] = color.green
pixel[2] = color.blue
if self.alpha_channel:
channel_map += b'A'
pixel[3] = color.alpha
r = library.MagickImportImagePixels(self.wand,
x1, y1, x2, y2,
channel_map,
s_index,
ctypes.byref(pixel))
if not r:
self.raise_exception()
def __hash__(self):
return hash(self.signature)
def __iter__(self):
return Iterator(image=self)
def __len__(self):
return self.height
def __ne__(self, other):
return not (self == other)
def __repr__(self, extra_format=' ({self.width}x{self.height})'):
cls = type(self)
typename = '{0}.{1}'.format(
cls.__module__,
getattr(cls, '__qualname__', cls.__name__)
)
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)
)
@property
def __array_interface__(self):
"""Allows image-data from :class:`Image <wand.image.BaseImage>`
instances to be loaded into numpy's array.
.. code::
import numpy
from wand.image import Image
with Image(filename='rose:') as img:
img_data = numpy.asarray(img)
:raises ValueError: if image has no data.
.. versionadded:: 0.5.0
.. versionchanged:: 0.6.0
The :attr:`shape` property is now ordered by ``height``, ``width``,
and ``channel``.
.. versionchanged:: 0.6.2
Color spaces ``gray`` & ``cmyk`` are now supported.
"""
if not self.signature:
raise ValueError("No image data to interface with.")
width, height = self.size
storage_type = 1 # CharPixel
cs = self.colorspace
if cs in ('gray',):
channel_format = binary('R')
elif cs in ('cmyk',):
channel_format = binary('CMYK')
else:
channel_format = binary('RGB')
if self.alpha_channel:
channel_format += binary('A')
channel_number = len(channel_format)
self._c_buffer = (width * height * channel_number * ctypes.c_char)()
# FIXME: Move to pixel-data import/export methods.
r = library.MagickExportImagePixels(self.wand,
0, 0, width, height,
channel_format, storage_type,
ctypes.byref(self._c_buffer))
if not r:
self.raise_exception()
return dict(data=(ctypes.addressof(self._c_buffer), True),
shape=(height, width, channel_number),
typestr='|u1',
version=3)
@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 such as 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.
.. note::
The :attr:`alpha_channel` attribute will always return ``True``
if alpha channel is enabled, and ``False`` otherwise. Setting
this property with a string value from :const:`ALPHA_CHANNEL_TYPES`
will resolve to a :class:`bool` after applying channel operations
listed above.
With ImageMagick-6, values ``'on'`` & ``'off'`` are aliased to
``'activate'`` & ``'deactivate'``. However in ImageMagick-7,
both ``'on'`` & ``'off'`` have their own behavior.
.. 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.
.. versionchanged:: 0.6.0
Setting the alpha channel will apply the change to all frames
in the image stack.
"""
return bool(library.MagickGetImageAlphaChannel(self.wand))
@alpha_channel.setter
@manipulative
def alpha_channel(self, alpha_type):
is_im6 = MAGICK_VERSION_NUMBER < 0x700
# Map common aliases for ``'deactivate'``
if alpha_type is False or (alpha_type == 'off' and is_im6):
alpha_type = 'deactivate'
# Map common aliases for ``'activate'``
elif alpha_type is True or (alpha_type == 'on' and is_im6):
alpha_type = 'activate'
assertions.string_in_list(ALPHA_CHANNEL_TYPES,
'wand.image.ALPHA_CHANNEL_TYPES',
alpha_channel=alpha_type)
alpha_index = ALPHA_CHANNEL_TYPES.index(alpha_type)
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.MagickSetImageAlphaChannel(self.wand, alpha_index)
@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`
consists 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 antialias(self):
"""(:class:`bool`) If vectors & fonts will use anti-aliasing.
.. versionchanged:: 0.5.0
Previously named :attr:`font_antialias`.
"""
return bool(library.MagickGetAntialias(self.wand))
@antialias.setter
@manipulative
def antialias(self, antialias):
assertions.assert_bool(antialias=antialias)
library.MagickSetAntialias(self.wand, antialias)
@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
.. versionchanged:: 0.6.7
Allow property to be set before image read.
"""
pixel = library.NewPixelWand()
if library.MagickGetNumberImages(self.wand):
result = library.MagickGetImageBackgroundColor(self.wand, pixel)
else:
pixel = library.MagickGetBackgroundColor(self.wand)
result = True
if not result: # pragma: no cover
self.raise_exception()
else:
color = Color.from_pixelwand(pixel)
pixel = library.DestroyPixelWand(pixel)
return color
@background_color.setter
@manipulative
def background_color(self, color):
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
with color:
# Only set the image background if an image was loaded.
if library.MagickGetNumberImages(self.wand):
result = library.MagickSetImageBackgroundColor(self.wand,
color.resource)
if not result: # pragma: no cover
self.raise_exception()
# Also set the image stack.
result = library.MagickSetBackgroundColor(self.wand,
color.resource)
if not result:
self.raise_exception()
@property
def blue_primary(self):
"""(:class:`tuple`) The chromatic blue primary point for the image.
With ImageMagick-6 the primary value is ``(x, y)`` coordinates;
however, ImageMagick-7 has ``(x, y, z)``.
.. versionadded:: 0.5.2
"""
x = ctypes.c_double(0.0)
y = ctypes.c_double(0.0)
r = None
p = None
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickGetImageBluePrimary(self.wand, x, y)
p = (x.value, y.value)
else: # pragma: no cover
z = ctypes.c_double(0.0)
r = library.MagickGetImageBluePrimary(self.wand, x, y, z)
p = (x.value, y.value, z.value)
if not r: # pragma: no cover
self.raise_exception()
return p
@blue_primary.setter
def blue_primary(self, coordinates):
r = None
if not isinstance(coordinates, abc.Sequence):
raise TypeError('Primary must be a tuple')
if MAGICK_VERSION_NUMBER < 0x700:
x, y = coordinates
r = library.MagickSetImageBluePrimary(self.wand, x, y)
else: # pragma: no cover
x, y, z = coordinates
r = library.MagickSetImageBluePrimary(self.wand, x, y, z)
if not r: # pragma: no cover
self.raise_exception()
@property
def border_color(self):
"""(:class:`wand.color.Color`) The image border color. Used for
special effects like :meth:`polaroid()`.
.. versionadded:: 0.5.4
"""
pixel = library.NewPixelWand()
result = library.MagickGetImageBorderColor(self.wand, pixel)
if not result: # pragma: no cover
self.raise_exception()
else:
color = Color.from_pixelwand(pixel)
pixel = library.DestroyPixelWand(pixel)
return color
@border_color.setter
def border_color(self, color):
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(border_color=color)
with color:
r = library.MagickSetImageBorderColor(self.wand, color.resource)
if not r: # pragma: no cover
self.raise_exception()
@property
def colors(self):
"""(:class:`numbers.Integral`) Count of unique colors used within the
image. This is READ ONLY property.
.. versionadded:: 0.5.3
"""
return library.MagickGetImageColors(self.wand)
@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: # pragma: no cover
self.raise_exception()
return COLORSPACE_TYPES[text(colorspace_type_index)]
@colorspace.setter
@manipulative
def colorspace(self, colorspace_type):
assertions.string_in_list(COLORSPACE_TYPES,
'wand.image.COLORSPACE_TYPES',
colorspace=colorspace_type)
r = library.MagickSetImageColorspace(
self.wand,
COLORSPACE_TYPES.index(colorspace_type)
)
if not r: # pragma: no cover
self.raise_exception()
@property
def compose(self):
"""(:class:`basestring`) The type of image compose.
It's a string from :const:`COMPOSITE_OPERATORS` list.
It also can be set.
.. versionadded:: 0.5.1
"""
compose_index = library.MagickGetImageCompose(self.wand)
return COMPOSITE_OPERATORS[compose_index]
@compose.setter
def compose(self, operator):
assertions.string_in_list(COMPOSITE_OPERATORS,
'wand.image.COMPOSITE_OPERATORS',
compose=operator)
library.MagickSetImageCompose(self.wand,
COMPOSITE_OPERATORS.index(operator))
@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
.. versionchanged:: 0.5.2
Setting :attr:`compression` now sets both `image_info`
and `images` in the internal image stack.
"""
compression_index = library.MagickGetImageCompression(self.wand)
return COMPRESSION_TYPES[compression_index]
@compression.setter
def compression(self, value):
assertions.string_in_list(COMPRESSION_TYPES,
'wand.image.COMPRESSION_TYPES',
compression=value)
library.MagickSetCompression(
self.wand,
COMPRESSION_TYPES.index(value)
)
library.MagickSetImageCompression(
self.wand,
COMPRESSION_TYPES.index(value)
)
@property
def compression_quality(self):
"""(:class:`numbers.Integral`) Compression quality of this image.
.. versionadded:: 0.2.0
.. versionchanged:: 0.5.2
Setting :attr:`compression_quality` now sets both `image_info`
and `images` in the internal image stack.
"""
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`
"""
assertions.assert_integer(compression_quality=quality)
library.MagickSetCompressionQuality(self.wand, quality)
r = library.MagickSetImageCompressionQuality(self.wand, quality)
if not r: # pragma: no cover
raise ValueError('Unable to set compression quality to ' +
repr(quality))
@property
def delay(self):
"""(:class:`numbers.Integral`) The number of ticks between frames.
.. versionadded:: 0.5.9
"""
return library.MagickGetImageDelay(self.wand)
@delay.setter
def delay(self, value):
assertions.assert_integer(delay=value)
library.MagickSetImageDelay(self.wand, value)
@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: # pragma: no cover
raise self.raise_exception()
@property
def dispose(self):
"""(:class:`basestring`) Controls how the image data is
handled during animations. Values are from :const:`DISPOSE_TYPES`
list, and can also be set.
.. seealso::
`Dispose Images`__ section in ``Animation Basics`` article.
__ https://www.imagemagick.org/Usage/anim_basics/#dispose_images
.. versionadded:: 0.5.0
"""
dispose_idx = library.MagickGetImageDispose(self.wand)
try:
return DISPOSE_TYPES[dispose_idx]
except IndexError: # pragma: no cover
return DISPOSE_TYPES[0]
@dispose.setter
def dispose(self, value):
assertions.string_in_list(DISPOSE_TYPES,
'wand.image.DISPOSE_TYPES',
dispose=value)
library.MagickSetImageDispose(self.wand, DISPOSE_TYPES.index(value))
@property
def font(self):
"""(:class:`wand.font.Font`) The current font options."""
if not self.font_path:
return None
return Font(
path=text(self.font_path),
size=self.font_size,
color=self.font_color,
antialias=self.antialias,
stroke_color=self.stroke_color,
stroke_width=self.stroke_width
)
@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.antialias = font.antialias
if font.stroke_color:
self.stroke_color = font.stroke_color
if font.stroke_width is not None:
self.stroke_width = font.stroke_width
@property
def font_antialias(self):
"""
.. deprecated:: 0.5.0
Use :attr:`antialias` instead.
"""
return self.antialias
@font_antialias.setter
def font_antialias(self, antialias):
self.antialias = antialias
@property
def font_color(self):
return Color(self.options['fill'])
@font_color.setter
@manipulative
def font_color(self, color):
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(font_color=color)
self.options['fill'] = color.string
@property
def font_path(self):
"""(:class:`basestring`) The path of the current font.
It also can be set.
"""
font_str = None
font_p = library.MagickGetFont(self.wand)
if font_p:
font_str = text(ctypes.string_at(font_p))
font_p = library.MagickRelinquishMemory(font_p)
return font_str
@font_path.setter
@manipulative
def font_path(self, font):
font = binary(font)
r = library.MagickSetFont(self.wand, font)
if not r: # pragma: no cover
self.raise_exception()
@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):
assertions.assert_real(font_size=size)
if size < 0.0:
raise ValueError('cannot be less than 0.0, but got ' + repr(size))
r = library.MagickSetPointsize(self.wand, size)
if not r: # pragma: no cover
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_str = None
fmt_p = library.MagickGetImageFormat(self.wand)
if fmt_p:
fmt_str = text(ctypes.string_at(fmt_p))
fmt_p = library.MagickRelinquishMemory(fmt_p)
return fmt_str
@format.setter
def format(self, fmt):
assertions.assert_string(format=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: # pragma: no cover
self.raise_exception()
@property
def fuzz(self):
"""(:class:`numbers.Real`) The normalized real number between ``0.0``
and :attr:`quantum_range`. This property influences the accuracy of
:meth:`compare()`.
.. versionadded:: 0.5.3
"""
return library.MagickGetImageFuzz(self.wand)
@fuzz.setter
def fuzz(self, value):
assertions.assert_real(fuzz=value)
library.MagickSetImageFuzz(self.wand, value)
@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: # pragma: no cover
self.raise_exception()
return GRAVITY_TYPES[gravity_index]
@gravity.setter
@manipulative
def gravity(self, value):
assertions.string_in_list(GRAVITY_TYPES,
'wand.image.GRAVITY_TYPES',
gravity=value)
library.MagickSetGravity(self.wand, GRAVITY_TYPES.index(value))
@property
def green_primary(self):
"""(:class:`tuple`) The chromatic green primary point for the image.
With ImageMagick-6 the primary value is ``(x, y)`` coordinates;
however, ImageMagick-7 has ``(x, y, z)``.
.. versionadded:: 0.5.2
"""
x = ctypes.c_double(0.0)
y = ctypes.c_double(0.0)
r = None
p = None
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickGetImageGreenPrimary(self.wand, x, y)
p = (x.value, y.value)
else: # pragma: no cover
z = ctypes.c_double(0.0)
r = library.MagickGetImageGreenPrimary(self.wand, x, y, z)
p = (x.value, y.value, z.value)
if not r: # pragma: no cover
self.raise_exception()
return p
@green_primary.setter
def green_primary(self, coordinates):
r = None
if not isinstance(coordinates, abc.Sequence):
raise TypeError('Primary must be a tuple')
if MAGICK_VERSION_NUMBER < 0x700:
x, y = coordinates
r = library.MagickSetImageGreenPrimary(self.wand, x, y)
else: # pragma: no cover
x, y, z = coordinates
r = library.MagickSetImageGreenPrimary(self.wand, x, y, z)
if not r: # pragma: no cover
self.raise_exception()
@property
def height(self):
"""(:class:`numbers.Integral`) The height of this image."""
return library.MagickGetImageHeight(self.wand)
@height.setter
@manipulative
def height(self, height):
assertions.assert_unsigned_integer(height=height)
library.MagickSetSize(self.wand, self.width, height)
@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.
.. tip::
True-color photos can have millions of color values. If performance
is more valuable than accuracy, remember to :meth:`quantize` the
image before generating a :class:`HistogramDict`.
with Image(filename='hd_photo.jpg') as img:
img.quantize(255, 'RGB', 0, False, False)
hist = img.histogram
.. versionadded:: 0.3.0
"""
return HistogramDict(self)
@property
def interlace_scheme(self):
"""(:class:`basestring`) The interlace used by the image.
See :const:`INTERLACE_TYPES`.
.. versionadded:: 0.5.2
.. versionchanged:: 0.6.2
The :attr:`interlace_scheme` property now points to the image.
Previously was pointing to the :c:struct:`MagickWand`.
"""
scheme_idx = library.MagickGetImageInterlaceScheme(self.wand)
return INTERLACE_TYPES[scheme_idx]
@interlace_scheme.setter
def interlace_scheme(self, scheme):
assertions.string_in_list(INTERLACE_TYPES,
'wand.image.INTERLACE_TYPES',
interlace_scheme=scheme)
scheme_idx = INTERLACE_TYPES.index(scheme)
library.MagickSetImageInterlaceScheme(self.wand, scheme_idx)
@property
def interpolate_method(self):
"""(:class:`basestring`) The interpolation method of the image.
See :const:`PIXEL_INTERPOLATE_METHODS`.
.. versionadded:: 0.5.2
"""
method_idx = library.MagickGetImageInterpolateMethod(self.wand)
return PIXEL_INTERPOLATE_METHODS[method_idx]
@interpolate_method.setter
def interpolate_method(self, method):
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
interpolate_method=method)
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
library.MagickSetImageInterpolateMethod(self.wand, method_idx)
@property
def kurtosis(self):
"""(:class:`numbers.Real`) The kurtosis of the image.
.. tip::
If you want both :attr:`kurtosis` & :attr:`skewness`, it
would be faster to call :meth:`kurtosis_channel()` directly.
.. versionadded:: 0.5.3
"""
k, _ = self.kurtosis_channel()
return k
@property
def length_of_bytes(self):
"""(:class:`numbers.Integral`) The original size, in bytes,
of the image read. This will return `0` if the image was modified in
a way that would invalidate the original length value.
.. versionadded:: 0.5.4
"""
size_ptr = ctypes.c_size_t(0)
library.MagickGetImageLength(self.wand, ctypes.byref(size_ptr))
return size_ptr.value
@property
def loop(self):
"""(:class:`numbers.Integral`) Number of frame iterations.
A value of ``0`` will loop forever."""
return library.MagickGetImageIterations(self.wand)
@loop.setter
def loop(self, iterations):
assertions.assert_unsigned_integer(loop=iterations)
library.MagickSetImageIterations(self.wand, iterations)
@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:
color = Color.from_pixelwand(pixel)
pixel = library.DestroyPixelWand(pixel)
return color
else: # pragma: no cover
self.raise_exception()
@matte_color.setter
@manipulative
def matte_color(self, color):
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(matte_color=color)
with color:
result = library.MagickSetImageMatteColor(self.wand,
color.resource)
if not result: # pragma: no cover
self.raise_exception()
@property
def maxima(self):
"""(:class:`numbers.Real`) The maximum quantum value within the image.
Value between 0.0 and :attr:`quantum_range`
.. tip::
If you want both :attr:`maxima` & :attr:`minima`,
it would be faster to call :meth:`range_channel()` directly.
.. versionadded:: 0.5.3
"""
_, max_q = self.range_channel()
return max_q
@property
def mean(self):
"""(:class:`numbers.Real`) The mean of the image, and have a value
between 0.0 and :attr:`quantum_range`
.. tip::
If you want both :attr:`mean` & :attr:`standard_deviation`, it
would be faster to call :meth:`mean_channel()` directly.
.. versionadded:: 0.5.3
"""
m, _ = self.mean_channel()
return m
@property
def minima(self):
"""(:class:`numbers.Real`) The minimum quantum value within the image.
Value between 0.0 and :attr:`quantum_range`
.. tip::
If you want both :attr:`maxima` & :attr:`minima`,
it would be faster to call :meth:`range_channel()` directly.
.. versionadded:: 0.5.3
"""
min_q, _ = self.range_channel()
return min_q
@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)
try:
return ORIENTATION_TYPES[orientation_index]
except IndexError: # pragma: no cover
return ORIENTATION_TYPES[0]
@orientation.setter
@manipulative
def orientation(self, value):
assertions.string_in_list(ORIENTATION_TYPES,
'wand.image.ORIENTATION_TYPES',
orientation=value)
index = ORIENTATION_TYPES.index(value)
library.MagickSetImageOrientation(self.wand, index)
@property
def page(self):
"""The dimensions and offset of this Wand's page as a 4-tuple:
``(width, height, x, y)``.
.. code::
with Image(filename='wizard:') as img:
img.page = (595, 842, 0, 0)
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.
This attribute can also be set by using a named papersize. For
example::
with Image(filename='wizard:') as img:
img.page = 'a4'
.. versionadded:: 0.4.3
.. versionchanged:: 0.6.4
Added support for setting by papersize.
"""
w = ctypes.c_size_t()
h = ctypes.c_size_t()
x = ctypes.c_ssize_t()
y = ctypes.c_ssize_t()
r = library.MagickGetImagePage(self.wand, w, h, x, y)
if not r: # pragma: no cover
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, string_type):
c_ptr = libmagick.GetPageGeometry(newpage.encode())
ri = RectangleInfo()
c_ptr = ctypes.cast(c_ptr, ctypes.c_char_p)
libmagick.ParseAbsoluteGeometry(c_ptr, ctypes.byref(ri))
newpage = (ri.width, ri.height, ri.x, ri.y)
libmagick.DestroyString(c_ptr)
del ri
if isinstance(newpage, abc.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: # pragma: no cover
self.raise_exception()
@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_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_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
@property
def quantum_range(self):
"""(`:class:`numbers.Integral`) The maximum 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 red_primary(self):
"""(:class:`tuple`) The chromatic red primary point for the image.
With ImageMagick-6 the primary value is ``(x, y)`` coordinates;
however, ImageMagick-7 has ``(x, y, z)``.
.. versionadded:: 0.5.2
"""
x = ctypes.c_double(0.0)
y = ctypes.c_double(0.0)
r = None
p = None
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickGetImageRedPrimary(self.wand, x, y)
p = (x.value, y.value)
else: # pragma: no cover
z = ctypes.c_double(0.0)
r = library.MagickGetImageRedPrimary(self.wand, x, y, z)
p = (x.value, y.value, z.value)
if not r: # pragma: no cover
self.raise_exception()
return p
@red_primary.setter
def red_primary(self, coordinates):
r = None
if not isinstance(coordinates, abc.Sequence):
raise TypeError('Primary must be a tuple')
if MAGICK_VERSION_NUMBER < 0x700:
x, y = coordinates
r = library.MagickSetImageRedPrimary(self.wand, x, y)
else: # pragma: no cover
x, y, z = coordinates
r = library.MagickSetImageRedPrimary(self.wand, x, y, z)
if not r: # pragma: no cover
self.raise_exception()
@property
def rendering_intent(self):
"""(:class:`basestring`) PNG rendering intent. See
:const:`RENDERING_INTENT_TYPES` for valid options.
.. versionadded:: 0.5.4
"""
ri_index = library.MagickGetImageRenderingIntent(self.wand)
return RENDERING_INTENT_TYPES[ri_index]
@rendering_intent.setter
def rendering_intent(self, value):
assertions.string_in_list(RENDERING_INTENT_TYPES,
'wand.image.RENDERING_INTENT_TYPES',
rendering_intent=value)
ri_index = RENDERING_INTENT_TYPES.index(value)
library.MagickSetImageRenderingIntent(self.wand, ri_index)
@property
def resolution(self):
"""(:class:`tuple`) Resolution of this image.
.. versionadded:: 0.3.0
.. versionchanged:: 0.5.8
Resolution returns a tuple of float values to
match ImageMagick's behavior.
"""
x = ctypes.c_double(0.0)
y = ctypes.c_double(0.0)
r = library.MagickGetImageResolution(self.wand, x, y)
if not r: # pragma: no cover
self.raise_exception()
return x.value, y.value
@resolution.setter
@manipulative
def resolution(self, geometry):
if isinstance(geometry, abc.Sequence):
x, y = geometry
elif isinstance(geometry, numbers.Real):
x, y = geometry, geometry
else:
raise TypeError('resolution must be a (x, y) pair or a float '
'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: # pragma: no cover
self.raise_exception()
@property
def sampling_factors(self):
"""(:class:`tuple`) Factors used in sampling data streams.
This can be set by given it a string ``"4:2:2"``, or tuple of numbers
``(2, 1, 1)``. However the given string value will be parsed to aspect
ratio (i.e. ``"4:2:2"`` internally becomes ``"2,1"``).
.. note::
This property is only used by YUV, DPX, & EXR encoders. For
JPEG & TIFF set ``"jpeg:sampling-factor"`` on
:attr:`Image.options` dictionary::
with Image(filename='input.jpg') as img:
img.options['jpeg:sampling-factor'] = '2x1'
.. versionadded:: 0.6.3
"""
factors_len = ctypes.c_size_t(0)
factors = library.MagickGetSamplingFactors(self.wand,
ctypes.byref(factors_len))
factors_tuple = tuple(factors[x] for x in xrange(factors_len.value))
factors = library.MagickRelinquishMemory(factors)
return factors_tuple
@sampling_factors.setter
def sampling_factors(self, factors):
if isinstance(factors, string_type):
geometry_info = GeometryInfo()
flags = libmagick.ParseGeometry(binary(factors),
ctypes.byref(geometry_info))
if (flags & geometry_info.SigmaValue) == 0:
factors = (geometry_info.rho, geometry_info.rho)
else:
factors = (geometry_info.rho, geometry_info.sigma)
elif not isinstance(factors, abc.Sequence):
raise TypeError('sampling_factors must be a sequence of real '
'numbers, not ' + repr(factors))
factors_len = len(factors)
factors_ptr = (ctypes.c_double * factors_len)(*factors)
library.MagickSetSamplingFactors(self.wand, factors_len, factors_ptr)
@property
def scene(self):
"""(:class:`numbers.Integral`) The scene number of the current frame
within an animated image.
.. versionadded:: 0.5.4
"""
return library.MagickGetImageScene(self.wand)
@scene.setter
def scene(self, value):
assertions.assert_unsigned_integer(scene=value)
library.MagickSetImageScene(self.wand, value)
@property
def seed(self):
"""(:class:`numbers.Integral`) The seed for random number generator.
.. warning::
This property is only available with ImageMagick 7.0.8-41, or
greater.
.. versionadded:: 0.5.5
"""
return self._seed
@seed.setter
def seed(self, value):
if library.MagickSetSeed is None:
msg = 'Property requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.assert_unsigned_integer(seed=value)
self._seed = value
library.MagickSetSeed(self.wand, value)
@property
def signature(self):
"""(:class:`str`) The SHA-256 message digest for the image pixel
stream.
.. versionadded:: 0.1.9
"""
sig_str = None
sig_p = library.MagickGetImageSignature(self.wand)
if sig_p:
sig_str = text(ctypes.string_at(sig_p))
sig_p = library.MagickRelinquishMemory(sig_p)
return sig_str
@property
def size(self):
"""(:class:`tuple`) The pair of (:attr:`width`, :attr:`height`).
.. note::
When working with animations, or other layer-based image formats,
the :attr:`width` & :attr:`height` properties are referencing the
last frame read into the image stack. To get the :attr:`size`
of the entire animated images, call
:meth:`Image.coalesce() <wand.image.BaseImage.coalesce>` method
immediately after reading the image.
"""
return self.width, self.height
@property
def skewness(self):
"""(:class:`numbers.Real`) The skewness of the image.
.. tip::
If you want both :attr:`kurtosis` & :attr:`skewness`, it
would be faster to call :meth:`kurtosis_channel()` directly.
.. versionadded:: 0.5.3
"""
_, s = self.kurtosis_channel()
return s
@property
def standard_deviation(self):
"""(:class:`numbers.Real`) The standard deviation of the image.
.. tip::
If you want both :attr:`mean` & :attr:`standard_deviation`, it
would be faster to call :meth:`mean_channel()` directly.
.. versionadded:: 0.5.3
"""
_, s = self.mean_channel()
return s
@property
def stroke_color(self):
stroke = self.options['stroke']
return Color(stroke) if stroke else None
@stroke_color.setter
def stroke_color(self, color):
if isinstance(color, string_type):
color = Color(color)
if isinstance(color, Color):
self.options['stroke'] = color.string
elif color is None:
del self.options['stroke']
else:
raise TypeError('stroke_color must be a wand.color.Color, not ' +
repr(color))
@property
def stroke_width(self):
strokewidth = self.options['strokewidth']
return float(strokewidth) if strokewidth else None
@stroke_width.setter
def stroke_width(self, width):
assertions.assert_real(stroke_width=width)
self.options['strokewidth'] = str(width)
@property
def ticks_per_second(self):
"""(:class:`numbers.Integral`) Internal clock for animated images.
.. versionadded:: 0.5.4
"""
return library.MagickGetImageTicksPerSecond(self.wand)
@ticks_per_second.setter
def ticks_per_second(self, value):
assertions.assert_unsigned_integer(ticks_per_second=value)
r = library.MagickSetImageTicksPerSecond(self.wand, value)
if not r: # pragma: no cover
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: # pragma: no cover
self.raise_exception()
return IMAGE_TYPES[text(image_type_index)]
@type.setter
@manipulative
def type(self, image_type):
assertions.string_in_list(IMAGE_TYPES, 'wand.image.IMAGE_TYPES',
type=image_type)
r = library.MagickSetImageType(self.wand,
IMAGE_TYPES.index(image_type))
if not r: # pragma: no cover
self.raise_exception()
@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):
assertions.string_in_list(UNIT_TYPES, 'wand.image.UNIT_TYPES',
units=units)
r = library.MagickSetImageUnits(self.wand, UNIT_TYPES.index(units))
if not r: # pragma: no cover
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):
assertions.string_in_list(VIRTUAL_PIXEL_METHOD,
'wand.image.VIRTUAL_PIXEL_METHOD',
virtual_pixel=method)
library.MagickSetImageVirtualPixelMethod(
self.wand,
VIRTUAL_PIXEL_METHOD.index(method)
)
@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
@property
def width(self):
"""(:class:`numbers.Integral`) The width of this image."""
return library.MagickGetImageWidth(self.wand)
@width.setter
@manipulative
def width(self, width):
assertions.assert_unsigned_integer(width=width)
library.MagickSetSize(self.wand, width, self.height)
@property
def white_point(self):
"""(:class:`tuple`) The chromatic white point for the image.
With ImageMagick-6 the primary value is ``(x, y)`` coordinates;
however, ImageMagick-7 has ``(x, y, z)``.
.. versionadded:: 0.5.2
"""
x = ctypes.c_double(0.0)
y = ctypes.c_double(0.0)
r = None
p = None
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickGetImageWhitePoint(self.wand, x, y)
p = (x.value, y.value)
else: # pragma: no cover
z = ctypes.c_double(0.0)
r = library.MagickGetImageWhitePoint(self.wand, x, y, z)
p = (x.value, y.value, z.value)
if not r: # pragma: no cover
self.raise_exception()
return p
@white_point.setter
def white_point(self, coordinates):
r = None
if not isinstance(coordinates, abc.Sequence):
raise TypeError('Primary must be a tuple')
if MAGICK_VERSION_NUMBER < 0x700:
x, y = coordinates
r = library.MagickSetImageWhitePoint(self.wand, x, y)
else: # pragma: no cover
x, y, z = coordinates
r = library.MagickSetImageWhitePoint(self.wand, x, y, z)
if not r: # pragma: no cover
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
"""
v_ptr = library.MagickGetImageProperty(self.wand,
b'exif:orientation')
if v_ptr:
exif_orientation = ctypes.string_at(v_ptr)
v_ptr = library.MagickRelinquishMemory(v_ptr)
else:
return
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'
def _channel_to_mask(self, value):
"""Attempts to resolve user input into a :c:type:`ChannelType`
bit-mask. User input can be an integer, a string defined in
:const:`CHANNELS`, or a string following ImageMagick's `CLI format`__.
__ https://imagemagick.org/script/command-line-options.php#channel
.. code::
# User generated bit-mask.
mask = self._channel_to_mask(CHANNELS['red'] | CHANNELS['green'])
# Defined constant.
mask = self._channel_to_mask('red')
# CLI format.
mask = self._channel_to_mask('RGB,Sync')
:param value: Mixed user input.
:type value: :class:`numbers.Integral` or :class:`basestring`
:returns: Bit-mask constant.
:rtype: :class:`int`
.. versionadded:: 0.5.5
"""
mask = -1
if isinstance(value, numbers.Integral) and not isinstance(value, bool):
mask = value
elif isinstance(value, string_type):
if value in CHANNELS:
mask = CHANNELS[value]
elif libmagick.ParseChannelOption:
mask = libmagick.ParseChannelOption(binary(value))
else:
raise TypeError(repr(value) + ' is an invalid channel type'
'; see wand.image.CHANNELS dictionary')
if mask < 0:
raise ValueError('expected value from wand.image.CHANNELS, not '
+ repr(value))
return mask
def _gravity_to_offset(self, gravity, width, height):
"""Calculate the top/left offset by a given gravity.
Some methods in MagickWand's C-API do not respect gravity, but
instead, expect a x/y offset. This is confusing to folks coming from
the CLI documentation that does respect gravity
:param gravity: Value from :const:`GRAVITY_TYPES`.
:type gravity: :class:`basestring`
:raises: :class:`ValueError` if gravity is no known.
:returns: :class:`numbers.Intergal` top, :class:`numbers.Intergal` left
.. versionadded:: 0.5.3
"""
top, left = 0, 0
assertions.string_in_list(GRAVITY_TYPES, 'wand.image.GRAVITY_TYPES',
gravity=gravity)
# 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
return top, left
@manipulative
@trap_exception
def adaptive_blur(self, radius=0.0, sigma=0.0, channel=None):
"""Adaptively blurs the image by decreasing Gaussian as the operator
approaches detected edges.
:see: Example of :ref:`adaptive_blur`.
:param radius: size of gaussian aperture.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of the gaussian filter.
:type sigma: :class:`numbers.Real`
:param channel: Apply the blur effect on a specific channel.
See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added optional ``channel`` argument
"""
assertions.assert_real(radius=radius, sigma=sigma)
if channel is None:
r = library.MagickAdaptiveBlurImage(self.wand, radius, sigma)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickAdaptiveBlurImageChannel(self.wand,
channel_ch,
radius,
sigma)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickAdaptiveBlurImage(self.wand, radius, sigma)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def adaptive_resize(self, columns=None, rows=None):
"""Adaptively resize image by applying Mesh interpolation.
:param columns: width of resized image.
:type columns: :class:`numbers.Integral`
:param rows: height of resized image.
:type rows: :class:`numbers.Integral`
.. versionadded:: 0.5.3
"""
if columns is None:
columns = self.width
if rows is None:
rows = self.height
assertions.assert_integer(columns=columns, rows=rows)
return library.MagickAdaptiveResizeImage(self.wand, columns, rows)
@manipulative
@trap_exception
def adaptive_sharpen(self, radius=0.0, sigma=0.0, channel=None):
"""Adaptively sharpens the image by sharpening more intensely near
image edges and less intensely far from edges.
:see: Example of :ref:`adaptive_sharpen`.
:param radius: size of gaussian aperture.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of the gaussian filter.
:type sigma: :class:`numbers.Real`
:param channel: Apply the sharpen effect on a specific channel.
See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added optional ``channel`` argument
"""
assertions.assert_real(radius=radius, sigma=sigma)
if channel is None:
r = library.MagickAdaptiveSharpenImage(self.wand, radius, sigma)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickAdaptiveSharpenImageChannel(self.wand,
channel_ch,
radius,
sigma)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickAdaptiveSharpenImage(self.wand,
radius,
sigma)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
def adaptive_threshold(self, width, height, offset=0.0):
"""Applies threshold for each pixel based on neighboring pixel values.
:param width: size of neighboring pixels on the X-axis.
:type width: :class:`numbers.Integral`
:param height: size of neighboring pixels on the Y-axis.
:type height: :class:`numbers.Integral`
:param offset: normalized number between `0.0` and
:attr:`quantum_range`. Forces the pixels to black if
values are below ``offset``.
:type offset: :class:`numbers.Real`
.. versionadded:: 0.5.3
"""
assertions.assert_integer(width=width, height=height)
assertions.assert_real(offset=offset)
if MAGICK_VERSION_NUMBER < 0x700:
offset = int(offset)
return library.MagickAdaptiveThresholdImage(self.wand, width,
height, offset)
@manipulative
@trap_exception
def annotate(self, text, drawing_wand, left=0, baseline=0, angle=0):
"""Draws text on an image. This method differs from :meth:`caption()`
as it uses :class:`~wand.drawing.Drawing` class to manage the
font configuration & style context.
.. code::
from wand.drawing import Drawing
from wand.image import Image
with Image(filename='input.jpg') as img:
with Drawing() as ctx:
ctx.font_family = 'Times New Roman, Nimbus Roman No9'
ctx.font_size = 18
ctx.text_decoration = 'underline'
ctx.text_kerning = -1
img.annotate('Hello World', ctx, left=20, baseline=50)
img.save(filename='output.jpg')
:param text: String to annotate on image.
:type text: :class:`basestring`
:param drawing_wand: Font configuration & style context.
:type text: :class:`wand.drawing.Drawing`
:param left: X-axis offset of the text baseline.
:type left: :class:`numbers.Real`
:param baseline: Y-axis offset of the bottom of the text.
:type baseline: :class:`numbers.Real`
:param angle: Degree rotation to draw text at.
:type angle: :class:`numbers.Real`
.. versionadded:: 0.5.6
"""
from .drawing import Drawing
if not isinstance(drawing_wand, Drawing):
raise TypeError('drawing_wand must be in instances of ' +
'wand.drawing.Drawing, not ' + repr(drawing_wand))
assertions.assert_real(left=left, baseline=baseline, angle=angle)
btext = binary(text)
return library.MagickAnnotateImage(self.wand, drawing_wand.resource,
left, baseline, angle, btext)
@manipulative
@trap_exception
def auto_gamma(self):
"""Adjust the gamma level of an image.
.. versionadded:: 0.5.4
"""
return library.MagickAutoGammaImage(self.wand)
@manipulative
@trap_exception
def auto_level(self):
"""Scale the minimum and maximum values to a full quantum range.
.. versionadded:: 0.5.4
"""
return library.MagickAutoLevelImage(self.wand)
@manipulative
@trap_exception
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:
return library.MagickAutoOrientImage(self.wand)
except AttributeError: # pragma: no cover
self._auto_orient()
return True
@manipulative
@trap_exception
def auto_threshold(self, method='kapur'):
"""Automatically performs threshold method to reduce grayscale data
down to a binary black & white image. Included algorithms are
Kapur, Otsu, and Triangle methods.
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param method: Which threshold method to apply.
See :const:`AUTO_THRESHOLD_METHODS`.
Defaults to ``'kapur'``.
:type method: :class:`basestring`
:raises WandLibraryVersionError: if function is not available on
system's library.
.. versionadded:: 0.5.5
"""
if library.MagickAutoThresholdImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.string_in_list(AUTO_THRESHOLD_METHODS,
'wand.image.AUTO_THRESHOLD_METHODS',
method=method)
method_idx = AUTO_THRESHOLD_METHODS.index(method)
return library.MagickAutoThresholdImage(self.wand, method_idx)
@manipulative
@trap_exception
def black_threshold(self, threshold):
"""Forces all pixels above a given color as black. Leaves pixels
above threshold unaltered.
:param threshold: Color to be referenced as a threshold.
:type threshold: :class:`Color`
.. versionadded:: 0.5.3
"""
if isinstance(threshold, string_type):
threshold = Color(threshold)
assertions.assert_color(threshold=threshold)
with threshold:
r = library.MagickBlackThresholdImage(self.wand,
threshold.resource)
return r
@manipulative
@trap_exception
def blue_shift(self, factor=1.5):
"""Mutes colors of the image by shifting blue values.
:see: Example of :ref:`blue_shift`
:param factor: Amount to adjust values.
:type factor: :class:`numbers.Real`
.. versionadded:: 0.5.3
"""
assertions.assert_real(factor=factor)
return library.MagickBlueShiftImage(self.wand, factor)
@manipulative
@trap_exception
def blur(self, radius=0.0, sigma=0.0, channel=None):
"""Blurs the image. 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.
:see: Example of :ref:`blur`.
:param radius: the radius of the, in pixels,
not counting the center pixel. Default is ``0.0``.
:type radius: :class:`numbers.Real`
:param sigma: the standard deviation of the, in pixels. Default value
is ``0.0``.
:type sigma: :class:`numbers.Real`
:param channel: Optional color channel to apply blur. See
:const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.4.5
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
.. versionchanged:: 0.5.7
Positional arguments ``radius`` & ``sigman`` have been converted to
key-word arguments.
"""
assertions.assert_real(radius=radius, sigma=sigma)
if channel is None:
r = library.MagickBlurImage(self.wand, radius, sigma)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickBlurImageChannel(self.wand,
channel_ch,
radius,
sigma)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickBlurImage(self.wand, radius, sigma)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@trap_exception
def border(self, color, width, height, compose="copy"):
"""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`
:param compose: Use composite operator when applying frame. Only used
if called with ImageMagick 7+.
:type compose: :class:`basestring`
.. versionadded:: 0.3.0
.. versionchanged:: 0.5.0
Added ``compose`` parameter, and ImageMagick 7 support.
"""
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
with color:
if MAGICK_VERSION_NUMBER < 0x700:
result = library.MagickBorderImage(self.wand, color.resource,
width, height)
else: # pragma: no cover
assertions.string_in_list(COMPOSITE_OPERATORS,
'wand.image.COMPOSITE_OPERATORS',
compose=compose)
compose_idx = COMPOSITE_OPERATORS.index(compose)
result = library.MagickBorderImage(self.wand, color.resource,
width, height, compose_idx)
return result
@manipulative
@trap_exception
def brightness_contrast(self, brightness=0.0, contrast=0.0, channel=None):
"""Converts ``brightness`` & ``contrast`` parameters into a slope &
intercept, and applies a polynomial function.
:param brightness: between ``-100.0`` and ``100.0``. Default is ``0.0``
for unchanged.
:type brightness: :class:`numbers.Real`
:param contrast: between ``-100.0`` and ``100.0``. Default is ``0.0``
for unchanged.
:type contrast: :class:`numbers.Real`
:param channel: Isolate a single color channel to apply contrast.
See :const:`CHANNELS`.
.. versionadded:: 0.5.4
.. versionchanged:: 0.5.5
Optional ``channel`` argument added.
"""
assertions.assert_real(brightness=brightness, contrast=contrast)
if channel is None:
r = library.MagickBrightnessContrastImage(self.wand,
brightness,
contrast)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickBrightnessContrastImageChannel(self.wand,
channel_ch,
brightness,
contrast)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickBrightnessContrastImage(self.wand,
brightness,
contrast)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def canny(self, radius=0.0, sigma=1.0, lower_percent=0.1,
upper_percent=0.3):
"""Detect edges by leveraging a multi-stage Canny algorithm.
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param radius: Size of gaussian filter.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of gaussian filter.
:type sigma: :class:`numbers.Real`
:param lower_percent: Normalized lower threshold. Values between
``0.0`` (0%) and ``1.0`` (100%). The default
value is ``0.1`` or 10%.
:type lower_percent: :class:`numbers.Real`
:param upper_percent: Normalized upper threshold. Values between
``0.0`` (0%) and ``1.0`` (100%). The default
value is ``0.3`` or 30%.
:type upper_percent: :class:`numbers.Real`
:raises WandLibraryVersionError: if function is not available on
system's library.
.. versionadded:: 0.5.5
"""
if library.MagickCannyEdgeImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.assert_real(radius=radius, sigma=sigma,
lower_percent=lower_percent,
upper_percent=upper_percent)
return library.MagickCannyEdgeImage(self.wand, radius, sigma,
lower_percent, upper_percent)
@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
"""
assertions.assert_integer(left=left, top=top)
if font is not None and not isinstance(font, Font):
raise TypeError('font must be a wand.font.Font, not ' + repr(font))
if gravity is not None:
assertions.string_in_list(GRAVITY_TYPES,
'wand.image.GRAVITY_TYPES',
gravity=gravity)
if width is None:
width = self.width - left
else:
assertions.assert_integer(width=width)
if height is None:
height = self.height - top
else:
assertions.assert_integer(height=height)
if font is None:
try:
font = self.font
if font is None:
raise TypeError()
except TypeError:
raise TypeError('font must be specified or existing in image')
with Image() as textboard:
library.MagickSetSize(textboard.wand, width, height)
textboard.font = font
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)
def cdl(self, ccc):
"""Alias for :meth:`color_decision_list`.
.. versionadded:: 0.5.7
"""
return self.color_decision_list(ccc)
@trap_exception
def charcoal(self, radius, sigma):
"""Transform an image into a simulated charcoal drawing.
:see: Example of :ref:`charcoal`.
:param radius: The size of the Gaussian operator.
:type radius: :class:`numbers.Real`
:param sigma: The standard deviation of the Gaussian.
:type sigma: :class:`numbers.Real`
.. versionadded:: 0.5.3
"""
assertions.assert_real(radius=radius, sigma=sigma)
return library.MagickCharcoalImage(self.wand, radius, sigma)
@manipulative
@trap_exception
def chop(self, width=None, height=None, x=None, y=None, gravity=None):
"""Removes a region of an image, and reduces the image size
accordingly.
:param width: Size of region.
:type width: :class:`numbers.Integral`
:param height: Size of region.
:type height: :class:`numbers.Integral`
:param x: Offset on the X-axis.
:type x: :class:`numbers.Integral`
:param y: Offset on the Y-axis.
:type y: :class:`numbers.Integral`
:param gravity: Helper to auto-calculate offset.
See :const:`GRAVITY_TYPES`.
:type gravity: :class:`basestring`
.. versionadded:: 0.5.5
.. versionchanged:: 0.6.8
Added ``gravity`` argument.
.. versionchanged:: 0.6.12
Allow zero values for ``width`` & ``height`` arguments.
"""
if width is None:
width = self.width
if height is None:
height = self.height
assertions.assert_unsigned_integer(width=width, height=height)
if gravity is None:
if x is None:
x = 0
if y is None:
y = 0
else:
if x is not None or y is not None:
raise ValueError('x & y can not be used with gravity.')
y, x = self._gravity_to_offset(gravity, width, height)
assertions.assert_integer(x=x, y=y)
return library.MagickChopImage(self.wand, width, height, x, y)
@manipulative
@trap_exception
def clahe(self, width, height, number_bins, clip_limit):
"""Contrast limited adaptive histogram equalization.
.. warning::
The CLAHE method is only available with ImageMagick-7.
:param width: Tile division width.
:type width: :class:`numbers.Integral`
:param height: Tile division height.
:type height: :class:`numbers.Integral`
:param number_bins: Histogram bins.
:type number_bins: :class:`numbers.Real`
:param clip_limit: contrast limit.
:type clip_limit: :class:`numbers.Real`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickCLAHEImage is None:
msg = 'CLAHE method not defined in ImageMagick library.'
raise WandLibraryVersionError(msg)
assertions.assert_unsigned_integer(width=width, height=height)
assertions.assert_real(number_bins=number_bins, clip_limit=clip_limit)
return library.MagickCLAHEImage(self.wand, width, height,
number_bins, clip_limit)
@trap_exception
def clamp(self, channel=None):
"""Restrict color values between 0 and quantum range. This is useful
when applying arithmetic operations that could result in color values
over/under-flowing.
:param channel: Optional color channel.
:type channel: :class:`basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
if channel is None:
r = library.MagickClampImage(self.wand)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickClampImageChannel(self.wand, channel_ch)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickClampImage(self.wand)
library.MagickSetImageChannelMask(self.wand, mask)
return r
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)
@manipulative
@trap_exception
def clut(self, image, method='undefined', channel=None):
"""Replace color values by referencing another image as a Color
Look Up Table.
:param image: Color Look Up Table image.
:type image: :class:`wand.image.BaseImage`
:param method: Pixel Interpolate method. Only available with
ImageMagick-7. See :const:`PIXEL_INTERPOLATE_METHODS`
:type method: :class:`basestring`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
"""
if not isinstance(image, BaseImage):
raise TypeError('image must be a base image, not ' + repr(image))
if MAGICK_VERSION_NUMBER < 0x700:
if channel is None:
r = library.MagickClutImage(self.wand, image.wand)
else:
channel_ch = self._channel_to_mask(channel)
r = library.MagickClutImageChannel(self.wand,
channel_ch,
image.wand)
else: # pragma: no cover
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
pixel_interpolate_method=method)
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
if channel is None:
r = library.MagickClutImage(self.wand, image.wand, method_idx)
else:
channel_ch = self._channel_to_mask(channel)
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickClutImage(self.wand, image.wand, method_idx)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def coalesce(self):
"""Rebuilds image sequence with each frame size the same as first
frame, and composites each frame atop of previous.
.. note::
Only affects GIF, and other formats with multiple pages/layers.
.. versionadded:: 0.5.0
"""
r = library.MagickCoalesceImages(self.wand)
if r:
self.wand = r
self.reset_sequence()
return bool(r)
@manipulative
@trap_exception
def color_decision_list(self, ccc):
"""Applies color correction from a Color Correction Collection (CCC)
xml string. An example of xml:
.. code-block:: xml
<ColorCorrectionCollection xmlns="urn:ASC:CDL:v1.2">
<ColorCorrection id="cc03345">
<SOPNode>
<Slope> 0.9 1.2 0.5 </Slope>
<Offset> 0.4 -0.5 0.6 </Offset>
<Power> 1.0 0.8 1.5 </Power>
</SOPNode>
<SATNode>
<Saturation> 0.85 </Saturation>
</SATNode>
</ColorCorrection>
</ColorCorrectionCollection>
:param ccc: A XML string of the CCC contents.
:type ccc: :class:`basestring`
.. versionadded:: 0.5.7
"""
return library.MagickColorDecisionListImage(self.wand, binary(ccc))
def color_map(self, index, color=None):
"""Get & Set a color at a palette index. If ``color`` is given,
the color at the index location will be set & returned. Omitting the
``color`` argument will only return the color value at index.
Valid indexes are between ``0`` and total :attr:`colors` of the image.
.. note::
Ensure the image type is set to ``'palette'`` before calling the
:meth:`color_map` method. For example::
with Image(filename='graph.png') as img:
img.type = 'palette'
palette = [img.color_map(idx) for idx in range(img.colors)]
# ...
:param index: The color position of the image palette.
:type index: :class:`numbers.Integral`
:param color: Optional color to _set_ at the given index.
:type color: :class:`wand.color.Color`
:returns: Color at index.
:rtype: :class:`wand.color.Color`
.. versionadded:: 0.5.3
"""
if not isinstance(index, numbers.Integral):
raise TypeError('index most be an integer, not ' + repr(index))
if index < 0 or index >= self.colors:
raise ValueError('index is out of palette range')
if color:
if isinstance(color, string_type):
color = Color(color)
if not isinstance(color, Color):
raise TypeError('expecting in instance of Color, not ' +
repr(color))
with color:
r = library.MagickSetImageColormapColor(self.wand,
index,
color.resource)
if not r: # pragma: no cover
self.raise_exception()
else:
color_ptr = library.NewPixelWand()
r = library.MagickGetImageColormapColor(self.wand,
index,
color_ptr)
if not r: # pragma: no cover
color_ptr = library.DestroyPixelWand(color_ptr)
self.raise_exception()
color = Color.from_pixelwand(color_ptr)
color_ptr = library.DestroyPixelWand(color_ptr)
return color
@manipulative
@trap_exception
def color_matrix(self, matrix):
"""Adjust color values by applying a matrix transform per pixel.
Matrix should be given as 2D list, with a max size of 6x6.
An example of 3x3 matrix::
matrix = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
Which would translate RGB color channels by calculating the
following:
.. math::
\\begin{aligned}
red' &= 1.0 * red + 0.0 * green + 0.0 * blue\\\\
green' &= 0.0 * red + 1.0 * green + 0.0 * blue\\\\
blue' &= 0.0 * red + 0.0 * green + 1.0 * blue\\\\
\\end{aligned}
For RGB colorspace images, the rows & columns are laid out as:
+---------+-----+-------+------+------+-------+--------+
| | Red | Green | Blue | n/a | Alpha | Offset |
+=========+=====+=======+======+======+=======+========+
| Red' | 1 | 0 | 0 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
| Green' | 0 | 1 | 0 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
| Blue' | 0 | 0 | 1 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
| n/a | 0 | 0 | 0 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
| Alpha' | 0 | 0 | 0 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
| Offset' | 0 | 0 | 0 | 0 | 0 | 0 |
+---------+-----+-------+------+------+-------+--------+
Or for a CMYK colorspace image:
+----------+------+--------+---------+-------+-------+--------+
| | Cyan | Yellow | Magenta | Black | Alpha | Offset |
+==========+======+========+=========+=======+=======+========+
| Cyan' | 1 | 0 | 0 | 0 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
| Yellow' | 0 | 1 | 0 | 0 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
| Magenta' | 0 | 0 | 1 | 0 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
| Black' | 0 | 0 | 0 | 1 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
| Alpha' | 0 | 0 | 0 | 0 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
| Offset' | 0 | 0 | 0 | 0 | 0 | 0 |
+----------+------+--------+---------+-------+-------+--------+
See `color-matrix`__ for examples.
__ https://www.imagemagick.org/Usage/color_mods/#color-matrix
:see: Example of :ref:`color_matrix`.
:param matrix: 2D List of doubles.
:type matrix: :class:`collections.abc.Sequence`
.. versionadded:: 0.5.3
"""
if not isinstance(matrix, abc.Sequence):
raise TypeError('matrix must be a sequence, not ' + repr(matrix))
rows = len(matrix)
columns = None
values = []
for row in matrix:
if not isinstance(row, abc.Sequence):
raise TypeError('nested row must be a sequence, not ' +
repr(row))
if columns is None:
columns = len(row)
elif columns != len(row):
raise ValueError('rows have different column length')
for column in row:
values.append(str(column))
kernel = binary('{0}x{1}:{2}'.format(columns,
rows,
','.join(values)))
exception_info = libmagick.AcquireExceptionInfo()
if MAGICK_VERSION_NUMBER < 0x700:
kernel_info = libmagick.AcquireKernelInfo(kernel)
else: # pragma: no cover
kernel_info = libmagick.AcquireKernelInfo(kernel, exception_info)
exception_info = libmagick.DestroyExceptionInfo(exception_info)
r = library.MagickColorMatrixImage(self.wand, kernel_info)
kernel_info = libmagick.DestroyKernelInfo(kernel_info)
return r
@manipulative
@trap_exception
def color_threshold(self, start=None, stop=None):
"""Forces all pixels in color range to white, and all other pixels to
black.
.. note::
This method is only works with ImageMagick-7.0.10, or later.
:param start: Color to begin color range.
:type start: :class:`wand.color.Color`
:param stop: Color to end color range.
:type stop: :class:`wand.color.Color`
.. versionadded:: 0.6.4
"""
if isinstance(start, string_type):
start = Color(start)
if isinstance(stop, string_type):
stop = Color(stop)
assertions.assert_color(start=start, stop=stop)
if library.MagickColorThresholdImage is None:
msg = 'Method "color_threshold" not available.'
raise WandLibraryVersionError(msg)
with start:
with stop:
r = library.MagickColorThresholdImage(self.wand,
start.resource,
stop.resource)
return r
@manipulative
@trap_exception
def colorize(self, color=None, alpha=None):
"""Blends a given fill color over the image. The amount of blend is
determined by the color channels given by the ``alpha`` argument.
:see: Example of :ref:`colorize`.
:param color: Color to paint image with.
:type color: :class:`wand.color.Color`
:param alpha: Defines how to blend color.
:type alpha: :class:`wand.color.Color`
.. versionadded:: 0.5.3
"""
if isinstance(color, string_type):
color = Color(color)
if isinstance(alpha, string_type):
alpha = Color(alpha)
assertions.assert_color(color=color, alpha=alpha)
with color:
with alpha:
r = library.MagickColorizeImage(self.wand,
color.resource,
alpha.resource)
return r
@manipulative
@trap_exception
def combine(self, channel='rgb_channels', colorspace='rgb'):
"""Creates an image where each color channel is assigned by a grayscale
image in a sequence.
.. warning::
If your using ImageMagick-6, use ``channel`` argument to control
the color-channel order. With ImageMagick-7, the ``channel``
argument has been replaced with ``colorspace``.
For example::
for wand.image import Image
with Image() as img:
img.read(filename='red_channel.png')
img.read(filename='green_channel.png')
img.read(filename='blue_channel.png')
img.combine(colorspace='rgb')
img.save(filename='output.png')
:param channel: Determines the colorchannel ordering of the
sequence. Only used for ImageMagick-6.
See :const:`CHANNELS`.
:type channel: :class:`basestring`
:param colorspace: Determines the colorchannel ordering of the
sequence. Only used for ImageMagick-7.
See :const:`COLORSPACE_TYPES`.
:type colorspace: :class:`basestring`
.. versionadded:: 0.5.9
"""
assertions.string_in_list(COLORSPACE_TYPES,
'wand.image.COLORSPACE_TYPES',
colorspace=colorspace)
library.MagickResetIterator(self.wand)
colorspace_c = COLORSPACE_TYPES.index(colorspace)
channel_c = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
new_wand = library.MagickCombineImages(self.wand, channel_c)
else: # pragma: no-cover
new_wand = library.MagickCombineImages(self.wand, colorspace_c)
if new_wand:
self.wand = new_wand
self.reset_sequence()
return bool(new_wand)
@manipulative
def compare(self, image, metric='undefined', highlight=None,
lowlight=None):
"""Compares an image with another, and returns a reconstructed
image & computed distortion. The reconstructed image will show the
differences colored with ``highlight``, and similarities with
``lowlight``.
If you need the computed distortion between to images without a
image being reconstructed, use :meth:`get_image_distortion()` method.
Set :attr:`fuzz` property to adjust pixel-compare thresholds.
For example::
from wand.image import Image
with Image(filename='input.jpg') as base:
with Image(filename='subject.jpg') as img:
base.fuzz = base.quantum_range * 0.20 # Threshold of 20%
result_image, result_metric = base.compare(img)
with result_image:
result_image.save(filename='diff.jpg')
:param image: The reference image
:type image: :class:`wand.image.Image`
:param metric: The metric type to use for comparing. See
:const:`COMPARE_METRICS`
:type metric: :class:`basestring`
:param highlight: Set the color of the delta pixels in the resulting
difference image.
:type highlight: :class:`~wand.color.Color` or :class:`basestring`
:param lowlight: Set the color of the similar pixels in the resulting
difference image.
:type lowlight: :class:`~wand.color.Color` or :class:`basestring`
:returns: The difference image(:class:`wand.image.Image`),
the computed distortion between the images
(:class:`numbers.Real`)
:rtype: :class:`tuple` ( :class:`Image`, :class:`numbers.Real` )
.. versionadded:: 0.4.3
.. versionchanged:: 0.5.3
Added support for ``highlight`` & ``lowlight``.
"""
assertions.string_in_list(COMPARE_METRICS,
'wand.image.COMPARE_METRICS',
metric=metric)
if highlight:
if isinstance(highlight, Color):
highlight = highlight.string
library.MagickSetImageArtifact(self.wand,
b'compare:highlight-color',
binary(highlight))
if lowlight:
if isinstance(lowlight, Color):
lowlight = lowlight.string
library.MagickSetImageArtifact(self.wand,
b'compare:lowlight-color',
binary(lowlight))
metric = COMPARE_METRICS.index(metric)
distortion = ctypes.c_double(0.0)
compared_image = library.MagickCompareImages(self.wand, image.wand,
metric,
ctypes.byref(distortion))
return Image(BaseImage(compared_image)), distortion.value
@manipulative
def complex(self, operator='undefined', snr=None):
"""Performs `complex`_ mathematics against two images in a sequence,
and generates a new image with two results.
.. seealso::
:meth:`forward_fourier_transform` &
:meth:`inverse_fourier_transform`
.. code::
from wand.image import Image
with Image(filename='real_part.png') as imgA:
with Image(filename='imaginary_part.png') as imgB:
imgA.sequence.append(imgB)
with imgA.complex('conjugate') as results:
results.save(filename='output-%02d.png')
.. _complex: https://en.wikipedia.org/wiki/Complex_number
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param operator: Define which mathematic operator to perform. See
:const:`COMPLEX_OPERATORS`.
:type operator: :class:`basestring`
:param snr: Optional ``SNR`` parameter for ``'divide'`` operator.
:type snr: :class:`basestring`
:raises WandLibraryVersionError: If ImageMagick library does not
support this function.
.. versionadded:: 0.5.5
"""
if library.MagickComplexImages is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.string_in_list(COMPLEX_OPERATORS,
'wand.image.COMPLEX_OPERATORS',
operator=operator)
if snr is not None:
key = b'complex:snr=float'
val = to_bytes(snr)
library.MagickSetImageArtifact(self.wand, key, val)
operator_idx = COMPLEX_OPERATORS.index(operator)
wand = library.MagickComplexImages(self.wand, operator_idx)
if not bool(wand):
self.raise_exception()
return Image(BaseImage(wand))
@trap_exception
def composite(self, image, left=None, top=None, operator='over',
arguments=None, gravity=None):
"""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`
: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. Default is ``'over'``.
:type operator: :class:`basestring`
:param arguments: Additional numbers given as a geometry string, or
comma delimited values. This is needed for
``'blend'``, ``'displace'``, ``'dissolve'``, and
``'modulate'`` operators.
:type arguments: :class:`basestring`
:param gravity: Calculate the ``top`` & ``left`` values based on
gravity value from :const:`GRAVITY_TYPES`.
:type: gravity: :class:`basestring`
.. versionadded:: 0.2.0
.. versionchanged:: 0.5.3
The operator can be set, as well as additional composite arguments.
.. versionchanged:: 0.5.3
Optional ``gravity`` argument was added.
"""
if top is None and left is None:
if gravity is None:
gravity = self.gravity
top, left = self._gravity_to_offset(gravity,
image.width,
image.height)
elif gravity is not None:
raise TypeError('Can not use gravity if top & left are given')
elif top is None:
top = 0
elif left is None:
left = 0
assertions.assert_integer(left=left, top=top)
try:
op = COMPOSITE_OPERATORS.index(operator)
except IndexError:
raise ValueError(repr(operator) + ' is an invalid composite '
'operator type; see wand.image.COMPOSITE_'
'OPERATORS dictionary')
if arguments:
assertions.assert_string(arguments=arguments)
r = library.MagickSetImageArtifact(image.wand,
binary('compose:args'),
binary(arguments))
if not r:
self.raise_exception()
r = library.MagickSetImageArtifact(self.wand,
binary('compose:args'),
binary(arguments))
if not r: # pragma: no cover
self.raise_exception()
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickCompositeImage(self.wand, image.wand, op,
int(left), int(top))
else: # pragma: no cover
r = library.MagickCompositeImage(self.wand, image.wand, op, True,
int(left), int(top))
return r
@manipulative
@trap_exception
def composite_channel(self, channel, image, operator, left=None, top=None,
arguments=None, gravity=None):
"""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
:type operator: :class:`basestring`
: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`
:param arguments: Additional numbers given as a geometry string, or
comma delimited values. This is needed for
``'blend'``, ``'displace'``, ``'dissolve'``, and
``'modulate'`` operators.
:type arguments: :class:`basestring`
:param gravity: Calculate the ``top`` & ``left`` values based on
gravity value from :const:`GRAVITY_TYPES`.
:type: gravity: :class:`basestring`
:raises ValueError: when the given ``channel`` or
``operator`` is invalid
.. versionadded:: 0.3.0
.. versionchanged:: 0.5.3
Support for optional composite arguments has been added.
.. versionchanged:: 0.5.3
Optional ``gravity`` argument was added.
"""
assertions.assert_string(operator=operator)
ch_const = self._channel_to_mask(channel)
if gravity:
if left is None and top is None:
top, left = self._gravity_to_offset(gravity,
image.width,
image.height)
else:
raise TypeError('Can not use gravity if top & left are given')
if top is None:
top = 0
if left is None:
left = 0
assertions.assert_integer(left=left, top=top)
try:
op = COMPOSITE_OPERATORS.index(operator)
except IndexError:
raise IndexError(repr(operator) + ' is an invalid composite '
'operator type; see wand.image.COMPOSITE_'
'OPERATORS dictionary')
if arguments:
assertions.assert_string(arguments=arguments)
library.MagickSetImageArtifact(image.wand,
binary('compose:args'),
binary(arguments))
library.MagickSetImageArtifact(self.wand,
binary('compose:args'),
binary(arguments))
if library.MagickCompositeImageChannel:
r = library.MagickCompositeImageChannel(self.wand, ch_const,
image.wand, op, int(left),
int(top))
else: # pragma: no cover
ch_mask = library.MagickSetImageChannelMask(self.wand, ch_const)
r = library.MagickCompositeImage(self.wand, image.wand, op, True,
int(left), int(top))
library.MagickSetImageChannelMask(self.wand, ch_mask)
return r
@manipulative
@trap_exception
def concat(self, stacked=False):
"""Concatenates images in stack into a single image. Left-to-right
by default, top-to-bottom if ``stacked`` is True.
:param stacked: stack images in a column, or in a row (default)
:type stacked: :class:`bool`
.. versionadded:: 0.5.0
"""
assertions.assert_bool(stacked=stacked)
r = library.MagickAppendImages(self.wand, stacked)
if r:
self.wand = r
self.reset_sequence()
return bool(r)
def connected_components(self, **kwargs):
"""Evaluates binary image, and groups connected pixels into objects.
This method will also return a list of
:class:`ConnectedComponentObject` instances that will describe an
object's features.
.. code::
from wand.image import Image
with Image(filename='objects.gif') as img:
objects = img.connected_components()
for cc_obj in objects:
print("{0._id}: {0.size} {0.offset}".format(cc_obj))
#=> 0: (256, 171) (0, 0)
#=> 2: (120, 135) (104, 18)
#=> 3: (50, 36) (129, 44)
#=> 4: (21, 23) (0, 45)
#=> 1: (4, 10) (252, 0)
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
.. tip::
Set :attr:`fuzz` property to increase pixel matching by reducing
tolerance of color-value comparisons::
from wand.image import Image
from wand.version import QUANTUM_RANGE
with Image(filename='objects.gif') as img:
img.fuzz = 0.1 * QUANTUM_RANGE # 10%
objects = img.connected_components()
:param angle_threshold: Optional argument that merges any region with
an equivalent ellipse smaller than a given
value. Requires ImageMagick-7.0.9-24, or
greater.
:type angle_threshold: :class:`basestring`
:param area_threshold: Optional argument to merge objects under an
area size.
:type area_threshold: :class:`basestring`
:param background_id: Optional argument to identify which object
should be the background. Requires
ImageMagick-7.0.9-24, or greater.
:type background_id: :class:`basestring`
:param circularity_threshold: Optional argument that merges any region
smaller than value defined as:
``4*pi*area/perimeter^2``. Requires
ImageMagick-7.0.9-24, or greater.
:type circularity_threshold: :class:`basestring`
:param connectivity: Either ``4``, or ``8``. A value of ``4`` will
evaluate each pixels top-bottom, & left-right
neighbors. A value of ``8`` will use the same
pixels as with ``4``, but will also include the
four corners of each pixel. Default value of ``4``.
:type connectivity: :class:`numbers.Integral`
:param diameter_threshold: Optional argument to merge any region under
a given value. A region is defined as:
``sqr(4*area/pi)``. Requires
ImageMagick-7.0.9-24.
:type diameter_threshold: :class:`basestring`
:param eccentricity_threshold: Optional argument to merge any region
with ellipse eccentricity under a given
value. Requires ImageMagick-7.0.9-24,
or greater.
:param keep: Comma separated list of object IDs to isolate, the reset
are converted to transparent.
:type keep: :class:`basestring`
:param keep_colors: Semicolon separated list of objects to keep by
their color value. Requires ImageMagick-7.0.9-24,
or greater.
:type keep_colors: :class:`basestring`
:param keep_top: Keeps only the top number of objects by area.
Requires ImageMagick-7.0.9-24, or greater.
:type keep_top: :class:`basestring`
:param major_axis_threshold: Optional argument to merge any ellipse
with a major axis smaller then given
value. Requires ImageMagick-7.0.9-24,
or greater.
:type major_axis_threshold: :class:`basestring`
:param mean_color: Optional argument. Replace object color with mean
color of the source image.
:type mean_color: :class:`bool`
:param minor_axis_threshold: Optional argument to merge any ellipse
with a minor axis smaller then given
value. Requires ImageMagick-7.0.9-24,
or greater.
:type minor_axis_threshold: :class:`basestring`
:param perimeter_threshold: Optional argument to merge any region with
a perimeter smaller than the given value.
Requires ImageMagick-7.0.9-24, or greater.
:param remove: Comma separated list of object IDs to ignore, and
convert to transparent.
:type remove: :class:`basestring`
:param remove_colors: Semicolon separated list of objects to remove
by there color. Requires ImageMagick-7.0.9-24,
or greater.
:type remove_colors: :class:`basestring`
:returns: A list of :class:`ConnectedComponentObject`.
:rtype: :class:`list` [:class:`ConnectedComponentObject`]
:raises WandLibraryVersionError: If ImageMagick library
does not support this method.
.. versionadded:: 0.5.5
.. versionchanged:: 0.5.6
Added ``mean_color``, ``keep``, & ``remove`` optional arguments.
.. versionchanged:: 0.6.4
Added ``angle_threshold``, ``circularity_threshold``,
``diameter_threshold``, ``eccentricity_threshold``,
``keep_colors``, ``major_axis_threshold``, ``minor_axis_threshold``,
``perimeter_threshold``, and ``remove_colors`` optional arguments.
"""
angle_threshold = kwargs.get('angle_threshold', None)
area_threshold = kwargs.get('area_threshold', None)
background_id = kwargs.get('background_id', None)
circularity_threshold = kwargs.get('circularity_threshold', None)
connectivity = kwargs.get('connectivity', 4)
diameter_threshold = kwargs.get('diameter_threshold', None)
eccentricity_threshold = kwargs.get('eccentricity_threshold', None)
keep = kwargs.get('keep', None)
keep_colors = kwargs.get('keep_colors', None)
keep_top = kwargs.get('keep_top', None)
major_axis_threshold = kwargs.get('major_axis_threshold', None)
mean_color = kwargs.get('mean_color', False)
minor_axis_threshold = kwargs.get('minor_axis_threshold', None)
perimeter_threshold = kwargs.get('perimeter_threshold', None)
remove = kwargs.get('remove', None)
remove_colors = kwargs.get('remove_colors', None)
if library.MagickConnectedComponentsImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
if connectivity not in (4, 8):
raise ValueError('connectivity must be 4, or 8.')
if angle_threshold is not None:
key = b'connected-components:angle-threshold'
val = to_bytes(angle_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if area_threshold is not None:
key = b'connected-components:area-threshold'
val = to_bytes(area_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if background_id is not None:
key = b'connected-components:background-id'
val = to_bytes(background_id)
library.MagickSetImageArtifact(self.wand, key, val)
if circularity_threshold is not None:
key = b'connected-components:circularity-threshold'
val = to_bytes(circularity_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if diameter_threshold is not None:
key = b'connected-components:diameter-threshold'
val = to_bytes(diameter_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if eccentricity_threshold is not None:
key = b'connected-components:eccentricity-threshold'
val = to_bytes(eccentricity_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if keep is not None:
key = b'connected-components:keep'
val = to_bytes(keep)
library.MagickSetImageArtifact(self.wand, key, val)
if keep_colors is not None:
key = b'connected-components:keep-colors'
val = to_bytes(keep_colors)
library.MagickSetImageArtifact(self.wand, key, val)
if keep_top is not None:
key = b'connected-components:keep-top'
val = to_bytes(keep_top)
library.MagickSetImageArtifact(self.wand, key, val)
if major_axis_threshold is not None:
key = b'connected-components:major-axis-threshold'
val = to_bytes(major_axis_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if mean_color:
key = b'connected-components:mean-color'
val = b'true'
library.MagickSetImageArtifact(self.wand, key, b'true')
if minor_axis_threshold is not None:
key = b'connected-components:minor-axis-threshold'
val = to_bytes(minor_axis_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if perimeter_threshold is not None:
key = b'connected-components:perimeter-threshold'
val = to_bytes(perimeter_threshold)
library.MagickSetImageArtifact(self.wand, key, val)
if remove is not None:
key = b'connected-components:remove'
val = to_bytes(remove)
library.MagickSetImageArtifact(self.wand, key, val)
if remove_colors is not None:
key = b'connected-components:remove-colors'
val = to_bytes(remove_colors)
library.MagickSetImageArtifact(self.wand, key, val)
objects_ptr = ctypes.c_void_p(0)
CCObjectInfoStructure = CCObjectInfo
if MAGICK_VERSION_NUMBER > 0x70B:
CCObjectInfoStructure = CCObjectInfo710
elif MAGICK_VERSION_NUMBER > 0x709:
CCObjectInfoStructure = CCObjectInfo70A
ccoi_mem_size = ctypes.sizeof(CCObjectInfoStructure)
r = library.MagickConnectedComponentsImage(self.wand, connectivity,
ctypes.byref(objects_ptr))
objects = []
if r and objects_ptr.value:
for i in xrange(self.colors):
temp = CCObjectInfoStructure()
src_addr = objects_ptr.value + (i * ccoi_mem_size)
ctypes.memmove(ctypes.addressof(temp), src_addr, ccoi_mem_size)
objects.append(ConnectedComponentObject(temp))
del temp
objects_ptr = libmagick.RelinquishMagickMemory(objects_ptr)
else:
self.raise_exception()
return objects
@manipulative
@trap_exception
def contrast(self, sharpen=True):
"""Enhances the difference between lighter & darker values of the
image. Set ``sharpen`` to ``False`` to reduce contrast.
:param sharpen: Increase, or decrease, contrast. Default is ``True``
for increased contrast.
:type sharpen: :class:`bool`
.. versionadded:: 0.5.7
"""
assertions.assert_bool(sharpen=sharpen)
return library.MagickContrastImage(self.wand, sharpen)
@manipulative
@trap_exception
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.
: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.
Defaults to the same value given to the
``black_point`` argument.
: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
.. versionchanged:: 0.5.5
The ``white_point`` argument will now default to the value given
by the ``black_point`` argument.
"""
assertions.assert_real(black_point=black_point)
# If only black-point is given, match CLI behavior by
# calculating white point
if white_point is None:
white_point = black_point
assertions.assert_real(white_point=white_point)
contrast_range = float(self.width * self.height)
if 0.0 < black_point <= 1.0:
black_point *= contrast_range
if 0.0 < white_point <= 1.0:
white_point *= contrast_range
white_point = contrast_range - white_point
if channel is None:
r = library.MagickContrastStretchImage(self.wand,
black_point,
white_point)
else:
ch_const = self._channel_to_mask(channel)
if library.MagickContrastStretchImageChannel:
r = library.MagickContrastStretchImageChannel(self.wand,
ch_const,
black_point,
white_point)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickContrastStretchImage(self.wand,
black_point,
white_point)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
def convex_hull(self, background=None):
"""Find the smallest convex polygon, and returns a list of points.
.. note:: Requires ImageMagick-7.0.10 or greater.
You can pass the list of points directly to
:meth:`Drawing.polygon() <wand.drawing.Drawing.polygon>` method
to draw the convex hull shape on the image.
.. code::
from wand.image import Image
from wand.drawing import Drawing
with Image(filename='kdf_black.png') as img:
points = img.convex_hull()
with Drawing() as ctx:
ctx.fill_color = 'transparent'
ctx.stroke_color = 'red'
ctx.polygon(points=points)
ctx(img)
img.save(filename='kdf_black_convex_hull.png')
.. image:: ../_images/wand/image/kdf_black.png
.. image:: ../_images/wand/image/kdf_black_convex_hull.png
:param background: Define which color value to evaluate as the
background.
:type background: :class:`basestring` or :class:`~wand.color.Color`
:returns: list of points
:rtype: :class:`list` [ :class:`tuple` ( :class:`float`,
:class:`float` ) ]
.. versionadded:: 0.6.4
"""
r = []
if MAGICK_VERSION_NUMBER < 0x70A:
msg = 'ImageMagick-7.0.10 is required to use convex_hull().'
raise WandLibraryVersionError(msg)
with self.clone() as tmp:
if background is not None:
if isinstance(background, Color):
background = background.string
assertions.assert_string(background=background)
key = b'convex-hull:background-color'
val = to_bytes(background)
library.MagickSetImageArtifact(tmp.wand, key, val)
library.MagickSetOption(tmp.wand, b'format', b'%[convex-hull]')
library.MagickSetImageFormat(tmp.wand, b'INFO')
length = ctypes.c_size_t()
blob_p = library.MagickGetImageBlob(tmp.wand,
ctypes.byref(length))
if blob_p:
blob = ctypes.string_at(blob_p, length.value)
blob_p = library.MagickRelinquishMemory(blob_p)
pts = blob.decode('ascii', 'ignore').strip().split(' ')
r = [tuple(map(lambda x: float(x), p.split(','))) for p in pts]
else:
self.raise_exception()
return r
@manipulative
@trap_exception
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')
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
# 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'
)
top, left = self._gravity_to_offset(gravity, width, height)
else:
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
assertions.assert_counting_number(width=width, height=height)
if (
left == top == 0 and
width == self.width and
height == self.height
):
return True
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
self.reset_sequence()
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)
r = library.MagickCropImage(self.wand,
width, height,
left, top)
if reset_coords:
self.reset_coords()
else:
r = library.MagickCropImage(self.wand, width, height, left, top)
if reset_coords:
self.reset_coords()
return r
@trap_exception
def cycle_color_map(self, offset=1):
"""Shift the image color-map by a given offset.
:param offset: number of steps to rotate index by.
:type offset: :class:`numbers.Integral`
.. versionadded:: 0.5.3
"""
assertions.assert_integer(offset=offset)
return library.MagickCycleColormapImage(self.wand, offset)
@manipulative
@trap_exception
def decipher(self, passphrase):
"""Decrypt ciphered pixels into original values.
.. note::
:class:`~wand.exceptions.ImageError` will be thrown if the system's
ImageMagick library was compiled without cipher support.
:param passphrase: the secret passphrase to decrypt with.
:type passphrase: :class:`basestring`
.. versionadded:: 0.6.3
"""
assertions.assert_string(passphrase=passphrase)
return library.MagickDecipherImage(self.wand, binary(passphrase))
@manipulative
@trap_exception
def deconstruct(self):
"""Iterates over internal image stack, and adjust each frame size to
minimum bounding region of any changes from the previous frame.
.. versionadded:: 0.5.0
"""
r = library.MagickDeconstructImages(self.wand)
if r:
self.wand = r
self.reset_sequence()
return bool(r)
@manipulative
@trap_exception
def deskew(self, threshold):
"""Attempts to remove skew artifacts common with most
scanning & optical import devices.
:params threshold: limit between foreground & background. Use a real
number between `0.0` & `1.0` to match CLI's percent
argument.
:type threshold: :class:`numbers.Real`
.. versionadded:: 0.5.0
"""
assertions.assert_real(threshold=threshold)
if 0 < threshold <= 1.0:
threshold *= self.quantum_range
return library.MagickDeskewImage(self.wand, threshold)
@manipulative
@trap_exception
def despeckle(self):
"""Applies filter to reduce noise in image.
:see: Example of :ref:`despeckle`.
.. versionadded:: 0.5.0
"""
return library.MagickDespeckleImage(self.wand)
@manipulative
@trap_exception
def distort(self, method, arguments, best_fit=False, filter=None):
"""Distorts an image using various distorting methods.
.. code:: python
from wand.image import Image
from wand.color import Color
with Image(filename='checks.png') as img:
img.virtual_pixel = 'background'
img.background_color = Color('green')
img.matte_color = Color('skyblue')
arguments = (0, 0, 20, 60,
90, 0, 70, 63,
0, 90, 5, 83,
90, 90, 85, 88)
img.distort('perspective', arguments)
img.save(filename='checks_perspective.png')
.. image:: ../_images/wand/image/checks.png
.. image:: ../_images/wand/image/checks_perspective.png
Use :attr:`virtual_pixel`, :attr:`background_color`, and
:attr:`matte_color` properties to control the behavior of pixels
rendered outside of the image boundaries.
Use :attr:`interpolate_method` to control how images scale-up.
Distortion viewport, and scale, can be defined by using
:attr:`Image.artifacts` dictionary. For example::
img.artifacts['distort:viewport'] = '44x44+15+0'
img.artifacts['distort:scale'] = '10'
:see: Additional examples of :ref:`distort`.
: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.abc.Sequence`
:param best_fit: Attempt to resize resulting image fit distortion.
Defaults False
:type best_fit: :class:`bool`
:param filter: Optional resampling filter used when calculating
pixel-value. Defaults to ``'mitchell'``, or
``'lanczos'`` based on image type & operation.
:type filter: :class:`basestring`
.. versionadded:: 0.4.1
.. versionchanged:: 0.6.11
Included `filter=` parameter.
"""
assertions.string_in_list(DISTORTION_METHODS,
'wand.image.DISTORTION_METHODS',
method=method)
if not isinstance(arguments, abc.Sequence):
raise TypeError('expected sequence of doubles, not ' +
repr(arguments))
argc = len(arguments)
argv = (ctypes.c_double * argc)(*arguments)
method_idx = DISTORTION_METHODS.index(method)
if filter is not None:
assertions.string_in_list(FILTER_TYPES,
'wand.image.FILTER_TYPES',
filter=filter)
ok = False
if library.MagickSetImageFilter:
filter_idx = FILTER_TYPES.index(filter)
ok = library.MagickSetImageFilter(self.wand,
filter_idx)
else:
img_info_ptr = libmagick.AcquireImageInfo()
exp_ptr = libmagick.AcquireExceptionInfo()
img_ptr = library.GetImageFromMagickWand(self.wand)
if all([img_info_ptr, exp_ptr, img_ptr]):
libmagick.SetImageOption(img_info_ptr,
b'filter',
filter.encode())
ok = libmagick.SyncImageSettings(img_info_ptr,
img_ptr,
exp_ptr)
if img_info_ptr:
img_info_ptr = libmagick.DestroyImageInfo(img_info_ptr)
if exp_ptr:
exp_ptr = libmagick.DestroyExceptionInfo(exp_ptr)
if not ok:
raise AttributeError('Unable to set filter for ' +
filter)
return library.MagickDistortImage(self.wand, method_idx,
argc, argv, bool(best_fit))
@manipulative
@trap_exception
def edge(self, radius=0.0):
"""Applies convolution filter to detect edges.
:see: Example of :ref:`edge`.
:param radius: aperture of detection filter.
:type radius: :class:`numbers.Real`
.. versionadded:: 0.5.0
"""
assertions.assert_real(radius=radius)
return library.MagickEdgeImage(self.wand, radius)
@manipulative
@trap_exception
def emboss(self, radius=0.0, sigma=0.0):
"""Applies convolution filter against Gaussians filter.
.. note::
The `radius` value should be larger than `sigma` for best results.
:see: Example of :ref:`emboss`.
:param radius: filter aperture size.
:type radius: :class:`numbers.Real`
:param sigma: standard deviation.
:type sigma: :class:`numbers.Real`
.. versionadded:: 0.5.0
"""
assertions.assert_real(radius=radius, sigma=sigma)
return library.MagickEmbossImage(self.wand, radius, sigma)
@manipulative
@trap_exception
def encipher(self, passphrase):
"""Encrypt plain pixels into ciphered values.
.. note::
:class:`~wand.exceptions.ImageError` will be thrown if the system's
ImageMagick library was compiled without cipher support.
:param passphrase: the secret passphrase to encrypt with.
:type passphrase: :class:`basestring`
.. versionadded:: 0.6.3
.. versionchanged:: 0.6.8
Fixed C-API call.
"""
assertions.assert_string(passphrase=passphrase)
return library.MagickEncipherImage(self.wand, binary(passphrase))
@manipulative
@trap_exception
def enhance(self):
"""Applies digital filter to reduce noise.
:see: Example of :ref:`enhance`.
.. versionadded:: 0.5.0
"""
return library.MagickEnhanceImage(self.wand)
@manipulative
@trap_exception
def equalize(self, channel=None):
"""Equalizes the image histogram
:param channel: Optional channel. See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.3.10
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
"""
if channel is None:
r = library.MagickEqualizeImage(self.wand)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickEqualizeImageChannel(self.wand, channel_ch)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickEqualizeImage(self.wand)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
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)
:see: Example of :ref:`evaluate`.
: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
"""
assertions.string_in_list(EVALUATE_OPS, 'wand.image.EVALUATE_OPS',
operator=operator)
assertions.assert_real(value=value)
idx_op = EVALUATE_OPS.index(operator)
if channel is None:
r = library.MagickEvaluateImage(self.wand, idx_op, value)
else:
ch_const = self._channel_to_mask(channel)
# Use channel method if IM6, else create channel mask for IM7.
if library.MagickEvaluateImageChannel:
r = library.MagickEvaluateImageChannel(self.wand,
ch_const,
idx_op,
value)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickEvaluateImage(self.wand, idx_op, value)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
def export_pixels(self, x=0, y=0, width=None, height=None,
channel_map="RGBA", storage='char'):
"""Export pixel data from a raster image to
a list of values.
The ``channel_map`` tells ImageMagick which color
channels to export, and what order they should be
written as -- per pixel. Valid entries for
``channel_map`` are:
- ``'R'`` - Red channel
- ``'G'`` - Green channel
- ``'B'`` - Blue channel
- ``'A'`` - Alpha channel (``0`` is transparent)
- ``'O'`` - Alpha channel (``0`` is opaque)
- ``'C'`` - Cyan channel
- ``'Y'`` - Yellow channel
- ``'M'`` - Magenta channel
- ``'K'`` - Black channel
- ``'I'`` - Intensity channel (only for grayscale)
- ``'P'`` - Padding
See :const:`STORAGE_TYPES` for a list of valid
``storage`` options. This tells ImageMagick
what type of data it should calculate & write to.
For example; a storage type of ``'char'`` will write
a 8-bit value between 0 ~ 255, a storage type
of ``'short'`` will write a 16-bit value between
0 ~ 65535, and a ``'integer'`` will write a
32-bit value between 0 ~ 4294967295.
.. note::
By default, the entire image will be exported
as ``'char'`` storage with each pixel mapping
Red, Green, Blue, & Alpha channels.
:param x: horizontal starting coordinate of raster.
:type x: :class:`numbers.Integral`
:param y: vertical starting coordinate of raster.
:type y: :class:`numbers.Integral`
:param width: horizontal length of raster.
:type width: :class:`numbers.Integral`
:param height: vertical length of raster.
:type height: :class:`numbers.Integral`
:param channel_map: a string listing the channel data
format for each pixel.
:type channel_map: :class:`basestring`
:param storage: what data type each value should
be calculated as.
:type storage: :class:`basestring`
:returns: list of values.
:rtype: :class:`collections.abc.Sequence`
.. versionadded:: 0.5.0
.. versionchanged:: 0.6.11
Update storage type size for `"long"` & `"quantum"` values.
"""
_w, _h = self.size
if width is None:
width = _w
if height is None:
height = _h
assertions.assert_integer(x=x, y=y, width=width, height=height)
assertions.assert_string(channel_map=channel_map)
assertions.string_in_list(STORAGE_TYPES, 'wand.image.STORAGE_TYPES',
storage=storage)
channel_map = channel_map.upper()
valid_channels = 'RGBAOCYMKIP'
for channel in channel_map:
if channel not in valid_channels:
raise ValueError('Unknown channel label: ' +
repr(channel))
c_storage_types = [
None, # undefined
ctypes.c_ubyte, # char
ctypes.c_double, # double
ctypes.c_float, # float
ctypes.c_uint, # integer
ctypes.c_uint64, # long
library.PixelGetRedQuantum.restype, # quantum
ctypes.c_ushort # short
]
s_index = STORAGE_TYPES.index(storage)
c_storage = c_storage_types[s_index]
total_pixels = width * height
c_buffer_size = total_pixels * len(channel_map)
c_buffer = (c_buffer_size * c_storage)()
r = library.MagickExportImagePixels(self.wand,
x, y, width, height,
binary(channel_map),
s_index,
ctypes.byref(c_buffer))
if not r: # pragma: no cover
self.raise_exception()
return c_buffer[:c_buffer_size]
@manipulative
@trap_exception
def extent(self, width=None, height=None, x=None, y=None, gravity=None):
"""Adjust the canvas size of the image. Use ``x`` & ``y`` to offset
the image's relative placement in the canvas, or ``gravity`` helper
for quick position placement.
:param width: the target width of the extended image.
Default is the :attr:`width` of the image.
:type width: :class:`numbers.Integral`
:param height: the target height of the extended image.
Default is the :attr:`height` of the image.
:type height: :class:`numbers.Integral`
:param x: the x-axis offset of the extended image.
Default is 0, and can not be used with ``gravity``.
:type x: :class:`numbers.Integral`
:param y: the :attr:`y` offset of the extended image.
Default is 0, and can not be used with ``gravity``.
:type y: :class:`numbers.Integral`
:param gravity: position of the item extent when not using ``x`` &
``y``. See :const:`GRAVITY_TYPES`.
:type gravity: :class:`basestring`
.. versionadded:: 0.4.5
.. versionchanged:: 0.6.8
Added ``gravity`` argument.
"""
if width is None or width == 0:
width = self.width
if height is None or height == 0:
height = self.height
assertions.assert_unsigned_integer(width=width, height=height)
if gravity is None:
if x is None:
x = 0
if y is None:
y = 0
else:
if x is not None or y is not None:
raise ValueError('x & y can not be used with gravity.')
y, x = self._gravity_to_offset(gravity, width, height)
assertions.assert_integer(x=x, y=y)
return library.MagickExtentImage(self.wand, width, height, x, y)
def features(self, distance):
"""Calculate directional image features for each color channel.
Feature metrics including:
- angular second moment
- contrast
- correlation
- variance sum of squares
- inverse difference moment
- sum average
- sum variance
- sum entropy
- entropy
- difference variance
- difference entropy
- information measures of correlation 1
- information measures of correlation 2
- maximum correlation coefficient
With each metric containing horizontal, vertical, left & right
diagonal values.
.. code::
from wand.image import Image
with Image(filename='rose:') as img:
channel_features = img.features(distance=32)
for channels, features in channel_features.items():
print(channels)
for feature, directions in features.items():
print(' ', feature)
for name, value in directions.items():
print(' ', name, value)
:param distance: Define the distance if pixels to calculate.
:type distance: :class:`numbers.Integral`
:returns: a dict mapping each color channel with a dict of each
feature.
:rtype: :class:`dict`
.. versionadded:: 0.5.5
"""
def build_channel(address, channel):
feature = ChannelFeature()
size = ctypes.sizeof(feature)
ctypes.memmove(ctypes.addressof(feature),
feature_ptr + (CHANNELS[channel] * size),
size)
keys = ('horizontal', 'vertical',
'left_diagonal', 'right_diagonal')
feature_dict = {}
for k in feature._fields_:
a = k[0]
feature_dict[a] = dict(zip(keys, getattr(feature, a)))
return feature_dict
if MAGICK_VERSION_NUMBER < 0x700:
method = library.MagickGetImageChannelFeatures
else: # pragma: no cover
method = library.MagickGetImageFeatures
assertions.assert_unsigned_integer(distance=distance)
feature_ptr = method(self.wand, distance)
response = {}
if feature_ptr:
colorspace = self.colorspace
if self.alpha_channel:
response['alpha'] = build_channel(feature_ptr, 'alpha')
if colorspace == 'gray':
response['gray'] = build_channel(feature_ptr, 'gray')
elif colorspace == 'cmyk':
response['cyan'] = build_channel(feature_ptr, 'cyan')
response['magenta'] = build_channel(feature_ptr, 'magenta')
response['yellow'] = build_channel(feature_ptr, 'yellow')
response['black'] = build_channel(feature_ptr, 'black')
else:
response['red'] = build_channel(feature_ptr, 'red')
response['green'] = build_channel(feature_ptr, 'green')
response['blue'] = build_channel(feature_ptr, 'blue')
feature_ptr = library.MagickRelinquishMemory(feature_ptr)
return response
def fft(self, magnitude=True):
"""Alias for :meth:`forward_fourier_transform`.
.. versionadded:: 0.5.7
"""
return self.forward_fourier_transform(magnitude)
@manipulative
@trap_exception
def flip(self):
"""Creates a vertical mirror image by reflecting the pixels around
the central x-axis. It manipulates the image in place.
:see: Example of :ref:`flip_flop`.
.. versionadded:: 0.3.0
"""
return library.MagickFlipImage(self.wand)
@manipulative
@trap_exception
def flop(self):
"""Creates a horizontal mirror image by reflecting the pixels around
the central y-axis. It manipulates the image in place.
:see: Example of :ref:`flip_flop`.
.. versionadded:: 0.3.0
"""
return library.MagickFlopImage(self.wand)
@trap_exception
def forward_fourier_transform(self, magnitude=True):
"""Performs a discrete Fourier transform. The image stack is replaced
with the results. Either a pair of magnitude & phase images, or
real & imaginary (HDRI).
.. code::
from wand.image import Image
from wand.version import QUANTUM_RANGE
with Image(filename='source.png') as img:
img.forward_fourier_transform()
img.depth = QUANTUM_RANGE
img.save(filename='fft_%02d.png')
.. seealso:: :meth:`inverse_fourier_transform` & :meth:`complex`
.. note::
ImageMagick must have HDRI support to compute real & imaginary
components (i.e. ``magnitude=False``).
:param magnitude: If ``True``, generate magnitude & phase, else
real & imaginary. Default ``True``
:type magnitude: :class:`bool`
.. versionadded:: 0.5.5
"""
assertions.assert_bool(magnitude=magnitude)
return library.MagickForwardFourierTransformImage(self.wand, magnitude)
@manipulative
@trap_exception
def frame(self, matte=None, width=1, height=1, inner_bevel=0,
outer_bevel=0, compose='over'):
"""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`
:param compose: Optional composite operator. Default ``'over'``, and
only available with ImageMagick-7.
:type compose: :class:`basestring`
.. versionadded:: 0.4.1
.. versionchanged:: 0.5.6
Added optional ``compose`` parameter.
"""
if matte is None:
matte = Color('gray')
if isinstance(matte, string_type):
matte = Color(matte)
assertions.assert_color(matte=matte)
assertions.assert_integer(width=width, height=height)
assertions.assert_real(inner_bevel=inner_bevel,
outer_bevel=outer_bevel)
with matte:
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickFrameImage(self.wand,
matte.resource,
width, height,
inner_bevel, outer_bevel)
else: # pragma: no cover
assertions.string_in_list(COMPOSITE_OPERATORS,
'wand.image.COMPOSITE_OPERATORS',
compose=compose)
op = COMPOSITE_OPERATORS.index(compose)
r = library.MagickFrameImage(self.wand,
matte.resource,
width, height,
inner_bevel, outer_bevel,
op)
return r
@manipulative
@trap_exception
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
:see: Example of :ref:`function`.
: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.abc.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
"""
assertions.string_in_list(FUNCTION_TYPES, 'wand.image.FUNCTION_TYPES',
function=function)
if not isinstance(arguments, abc.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:
r = library.MagickFunctionImage(self.wand, index, argc, argv)
else:
ch_channel = self._channel_to_mask(channel)
# Use channel method if IM6, else create channel mask for IM7.
if library.MagickFunctionImageChannel:
r = library.MagickFunctionImageChannel(self.wand,
ch_channel,
index,
argc,
argv)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
r = library.MagickFunctionImage(self.wand, index, argc, argv)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@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
:see: Example of :ref:`fx`.
: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
.. versionchanged:: 0.6.9
Will raise :class:`WandRuntimeError` if method is unable to generate
a new image & doesn't throw an exception.
"""
assertions.assert_string(expression=expression)
c_expression = binary(expression)
if channel is None:
new_wand = library.MagickFxImage(self.wand, c_expression)
else:
ch_channel = self._channel_to_mask(channel)
if library.MagickFxImageChannel:
new_wand = library.MagickFxImageChannel(self.wand,
ch_channel,
c_expression)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
new_wand = library.MagickFxImage(self.wand, c_expression)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
if new_wand:
return Image(image=BaseImage(new_wand))
else: # pragma: no cover
self.raise_exception()
# If no exception is on the stack, then raise a generic run-time
# error. This can happen naturally if the source image is null,
# or the FX expression was unable to generate a new raster.
raise WandRuntimeError('No FX Image was generated by expression.')
@manipulative
@trap_exception
def gamma(self, adjustment_value=1.0, channel=None):
"""Gamma correct image.
Specific color channels can be correct individual. Typical values
range between 0.8 and 2.3.
:see: Example of :ref:`gamma`.
:param adjustment_value: value to adjust gamma level. Default `1.0`
: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
"""
assertions.assert_real(adjustment_value=adjustment_value)
if channel is None:
r = library.MagickGammaImage(self.wand, adjustment_value)
else:
ch_const = self._channel_to_mask(channel)
if library.MagickGammaImageChannel:
r = library.MagickGammaImageChannel(self.wand,
ch_const,
adjustment_value)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickGammaImage(self.wand, adjustment_value)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def gaussian_blur(self, radius=0.0, sigma=0.0, channel=None):
"""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.
:see: Example of :ref:`gaussian_blur`.
: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`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.3.3
.. versionchanged:: 0.5.5
Added ``channel`` argument.
.. versionchanged:: 0.5.7
Positional arguments ``radius`` & ``sigma`` have been converted
to keyword arguments.
"""
assertions.assert_real(radius=radius, sigma=sigma)
if channel is None:
r = library.MagickGaussianBlurImage(self.wand, radius, sigma)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickGaussianBlurImageChannel(self.wand,
channel_ch,
radius,
sigma)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickGaussianBlurImage(self.wand, radius, sigma)
library.MagickSetImageChannelMask(self.wand, mask)
return r
def get_image_distortion(self, image, metric='undefined'):
"""Compares two images, and return the specified distortion metric.
This method is faster than :meth:`compare()` method as ImageMagick
will not need to reconstruct an image.
:param image: Image to reference.
:type image: :class:`wand.image.BaseImage`
:param metric: Compare disortion metric to use. See
:const:`COMPARE_METRICS`.
:type metric: :class:`basestring`
:returns: Computed value of the distortion metric used.
:rtype: :class:`numbers.Real`
.. versionadded:: 0.6.6
"""
if not isinstance(image, BaseImage):
raise TypeError('expecting a base image, not ' + repr(image))
assertions.string_in_list(COMPARE_METRICS,
'wand.image.COMPARE_METRICS',
metric=metric)
metric_idx = COMPARE_METRICS.index(metric)
dist = ctypes.c_double(0.0)
ok = library.MagickGetImageDistortion(self.wand, image.wand,
metric_idx, dist)
if not ok:
self.raise_exception()
return dist.value
@manipulative
@trap_exception
def hald_clut(self, image, channel=None):
"""Replace color values by referencing a Higher And Lower Dimension
(HALD) Color Look Up Table (CLUT). You can generate a HALD image
by using ImageMagick's `hald:` protocol. ::
with Image(filename='rose:') as img:
with Image(filename='hald:3') as hald:
hald.gamma(1.367)
img.hald_clut(hald)
:param image: The HALD color matrix.
:type image: :class:`wand.image.BaseImage`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
if not isinstance(image, BaseImage):
raise TypeError('expecting a base image, not ' + repr(image))
if channel is None:
r = library.MagickHaldClutImage(self.wand, image.wand)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickHaldClutImageChannel(self.wand, channel_ch,
image.wand)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickHaldClutImage(self.wand, image.wand)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def hough_lines(self, width, height=None, threshold=40):
"""Identify lines within an image. Use :meth:`canny` to reduce image
to a binary edge before calling this method.
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param width: Local maxima of neighboring pixels.
:type width: :class:`numbers.Integral`
:param height: Local maxima of neighboring pixels.
:type height: :class:`numbers.Integral`
:param threshold: Line count to limit. Default to 40.
:type threshold: :class:`numbers.Integral`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickHoughLineImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
if height is None:
height = width
assertions.assert_unsigned_integer(width=width, height=height,
threshold=threshold)
return library.MagickHoughLineImage(self.wand, width, height,
threshold)
def ift(self, phase, magnitude=True):
"""Alias for :meth:`inverse_fourier_transform`.
.. versionadded:: 0.5.7
"""
return self.inverse_fourier_transform(phase, magnitude)
@trap_exception
def implode(self, amount=0.0, method="undefined"):
"""Creates a "imploding" effect by pulling pixels towards the center
of the image.
:see: Example of :ref:`implode`.
:param amount: Normalized degree of effect between `0.0` & `1.0`.
:type amount: :class:`numbers.Real`
:param method: Which interpolate method to apply to effected pixels.
See :const:`PIXEL_INTERPOLATE_METHODS` for a list of
options. Only available with ImageMagick-7.
:type method: :class:`basestring`
.. versionadded:: 0.5.2
"""
assertions.assert_real(amount=amount)
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
method=method)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickImplodeImage(self.wand, amount)
else: # pragma: no cover
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
r = library.MagickImplodeImage(self.wand, amount, method_idx)
return r
@trap_exception
def import_pixels(self, x=0, y=0, width=None, height=None,
channel_map='RGB', storage='char', data=None):
"""Import pixel data from a byte-string to
the image. The instance of :class:`Image` must already
be allocated with the correct size.
The ``channel_map`` tells ImageMagick which color
channels to export, and what order they should be
written as -- per pixel. Valid entries for
``channel_map`` are:
- ``'R'`` - Red channel
- ``'G'`` - Green channel
- ``'B'`` - Blue channel
- ``'A'`` - Alpha channel (``0`` is transparent)
- ``'O'`` - Alpha channel (``0`` is opaque)
- ``'C'`` - Cyan channel
- ``'Y'`` - Yellow channel
- ``'M'`` - Magenta channel
- ``'K'`` - Black channel
- ``'I'`` - Intensity channel (only for grayscale)
- ``'P'`` - Padding
See :const:`STORAGE_TYPES` for a list of valid
``storage`` options. This tells ImageMagick
what type of data it should calculate & write to.
For example; a storage type of ``'char'`` will write
a 8-bit value between 0 ~ 255, a storage type
of ``'short'`` will write a 16-bit value between
0 ~ 65535, and a ``'integer'`` will write a
32-bit value between 0 ~ 4294967295.
.. note::
By default, the entire image will be exported
as ``'char'`` storage with each pixel mapping
Red, Green, Blue, & Alpha channels.
:param x: horizontal starting coordinate of raster.
:type x: :class:`numbers.Integral`
:param y: vertical starting coordinate of raster.
:type y: :class:`numbers.Integral`
:param width: horizontal length of raster.
:type width: :class:`numbers.Integral`
:param height: vertical length of raster.
:type height: :class:`numbers.Integral`
:param channel_map: a string listing the channel data
format for each pixel.
:type channel_map: :class:`basestring`
:param storage: what data type each value should
be calculated as.
:type storage: :class:`basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.6.11
Update storage type size for `"long"` & `"quantum"` values.
"""
_w, _h = self.size
if width is None:
width = _w
if height is None:
height = _h
assertions.assert_integer(x=x, y=y, width=width, height=height)
assertions.string_in_list(STORAGE_TYPES, 'wand.image.STORAGE_TYPES',
storage=storage)
assertions.assert_string(channel_map=channel_map)
channel_map = channel_map.upper()
valid_channels = 'RGBAOCYMKIP'
for channel in channel_map:
if channel not in valid_channels:
raise ValueError('Unknown channel label: ' +
repr(channel))
if not isinstance(data, abc.Sequence):
raise TypeError('data must list of values, not' +
repr(data))
# Ensure enough data was given.
expected_len = width * height * len(channel_map)
given_len = len(data)
if expected_len != given_len:
msg = 'data length should be {0}, not {1}.'.format(
expected_len,
given_len
)
raise ValueError(msg)
c_storage_types = [
None, # undefined
ctypes.c_ubyte, # char
ctypes.c_double, # double
ctypes.c_float, # float
ctypes.c_uint, # integer
ctypes.c_uint64, # long
library.PixelGetRedQuantum.restype, # quantum
ctypes.c_ushort # short
]
s_index = STORAGE_TYPES.index(storage)
c_type = c_storage_types[s_index]
c_buffer = (len(data) * c_type)(*data)
r = library.MagickImportImagePixels(self.wand,
x, y, width, height,
binary(channel_map),
s_index,
ctypes.byref(c_buffer))
return r
@trap_exception
def inverse_fourier_transform(self, phase, magnitude=True):
"""Applies the inverse of a discrete Fourier transform. The image stack
is replaced with the results. Either a pair of magnitude & phase
images, or real & imaginary (HDRI).
.. code::
from wand.image import Image
with Image(filename='magnitude.png') as img:
with Image(filename='phase.png') as phase:
img.inverse_fourier_transform(phase)
img.save(filename='output.png')
.. seealso:: :meth:`forward_fourier_transform` & :meth:`complex`
.. note::
ImageMagick must have HDRI support to compute real & imaginary
components (i.e. ``magnitude=False``).
:param phase: Second part (image) of the transform. Either the phase,
or the imaginary part.
:type phase: :class:`BaseImage`
:param magnitude: If ``True``, accept magnitude & phase input, else
real & imaginary. Default ``True``
:type magnitude: :class:`bool`
.. versionadded:: 0.5.5
"""
if not isinstance(phase, BaseImage):
raise TypeError('phase must be an image, not ' + repr(phase))
assertions.assert_bool(magnitude=magnitude)
return library.MagickInverseFourierTransformImage(self.wand,
phase.wand,
magnitude)
def iterator_first(self):
"""Sets the internal image-stack iterator to the first image.
Useful for prepending an image at the start of the stack.
.. versionadded:: 0.6.2
"""
library.MagickSetFirstIterator(self.wand)
def iterator_get(self):
"""Returns the position of the internal image-stack index.
:rtype: :class:`int`
.. versionadded:: 0.6.2
"""
return library.MagickGetIteratorIndex(self.wand)
def iterator_last(self):
"""Sets the internal image-stack iterator to the last image.
Useful for appending an image to the end of the stack.
.. versionadded:: 0.6.2
"""
library.MagickSetLastIterator(self.wand)
def iterator_length(self):
"""Get the count of images in the image-stack.
:rtype: :class:`int`
.. versionadded:: 0.6.2
"""
return library.MagickGetNumberImages(self.wand)
def iterator_next(self):
"""Steps the image-stack index forward by one
:rtype: :class:`bool`
.. versionadded:: 0.6.2
"""
has_next = library.MagickHasNextImage(self.wand)
if has_next:
idx = library.MagickGetIteratorIndex(self.wand)
has_next = library.MagickSetIteratorIndex(self.wand, idx + 1)
return has_next
def iterator_previous(self):
"""Steps the image-stack index back by one.
:rtype: :class:`bool`
.. versionadded:: 0.6.2
"""
has_prev = library.MagickHasPreviousImage(self.wand)
if has_prev:
idx = library.MagickGetIteratorIndex(self.wand)
has_prev = library.MagickSetIteratorIndex(self.wand, idx - 1)
return has_prev
def iterator_reset(self):
"""Reset internal image-stack iterator. Useful for iterating over the
image-stack without allocating :class:`~wand.sequence.Sequence`.
.. versionadded:: 0.6.2
"""
library.MagickResetIterator(self.wand)
def iterator_set(self, index):
"""Sets the index of the internal image-stack.
:rtype: :class:`bool`
.. versionadded:: 0.6.2
"""
assertions.assert_integer(index=index)
return library.MagickSetIteratorIndex(self.wand, index)
@manipulative
@trap_exception
def kmeans(self, number_colors=None, max_iterations=100, tolerance=0.01):
"""Reduces the number of colors in an image by applying the K-means
clustering algorithm.
.. note::
Requires ImageMagick-7.0.10-37, or later.
:param number_colors: the target number of colors to use as seeds.
:type number_colors: :class:`numbers.Integral`
:param max_iterations: maximum number of iterations needed until
convergence. Default ``100``.
:type max_iterations: :class:`numbers.Integral`
:param tolerance: maximum tolerance between distrotion iterations.
Default ``0.01``
:type tolerance: :class:`numbers.Real`
.. versionadded:: 0.6.4
"""
if MAGICK_VERSION_NUMBER < 0x70A or library.MagickKmeansImage is None:
msg = "Kmeans requires ImageMagick-7.0.10-37 or later."
raise WandLibraryVersionError(msg)
assertions.assert_unsigned_integer(number_colors=number_colors,
max_iterations=max_iterations)
assertions.assert_real(tolerance=tolerance)
return library.MagickKmeansImage(self.wand, number_colors,
max_iterations, tolerance)
def kurtosis_channel(self, channel='default_channels'):
"""Calculates the kurtosis and skewness of the image.
.. code:: python
from wand.image import Image
with Image(filename='input.jpg') as img:
kurtosis, skewness = img.kurtosis_channel()
:param channel: Select which color channel to evaluate. See
:const:`CHANNELS`. Default ``'default_channels'``.
:type channel: :class:`basestring`
:returns: Tuple of :attr:`kurtosis` & :attr:`skewness`
values.
:rtype: :class:`tuple`
.. versionadded:: 0.5.3
"""
ch_channel = self._channel_to_mask(channel)
k = ctypes.c_double(0.0)
s = ctypes.c_double(0.0)
if MAGICK_VERSION_NUMBER < 0x700:
library.MagickGetImageChannelKurtosis(self.wand, ch_channel,
ctypes.byref(k),
ctypes.byref(s))
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
library.MagickGetImageKurtosis(self.wand,
ctypes.byref(k),
ctypes.byref(s))
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return k.value, s.value
@manipulative
@trap_exception
def kuwahara(self, radius=1.0, sigma=None):
"""Edge preserving noise reduction filter.
https://en.wikipedia.org/wiki/Kuwahara_filter
If ``sigma`` is not given, the value will be calculated as:
sigma = radius - 0.5
To match original algorithm's behavior, increase ``radius`` value by
one:
myImage.kuwahara(myRadius + 1, mySigma)
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:see: Example of :ref:`kuwahara`.
:param radius: Size of the filter aperture.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of Gaussian filter.
:type sigma: :class:`numbers.Real`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickKuwaharaImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
if sigma is None:
sigma = radius - 0.5
assertions.assert_real(radius=radius, sigma=sigma)
return library.MagickKuwaharaImage(self.wand, radius, sigma)
@manipulative
def label(self, text, left=None, top=None, font=None, gravity=None,
background_color='transparent'):
"""Writes a label ``text`` into the position on top of the existing
canvas. This method doesn't autofit text like :meth:`caption`. Use
``left`` & ``top``, or ``gravity``, to position the text.
: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 font: font to use. default is :attr:`font` of the image.
:type font: :class:`wand.font.Font`
:param gravity: text placement gravity.
:type gravity: :class:`basestring`
.. versionadded:: 0.6.8
"""
if font is not None and not isinstance(font, Font):
raise TypeError('font must be a wand.font.Font, not ' + repr(font))
if gravity is not None:
assertions.string_in_list(GRAVITY_TYPES,
'wand.image.GRAVITY_TYPES',
gravity=gravity)
if font is None:
try:
font = self.font
if font is None:
raise TypeError()
except TypeError:
raise TypeError('font must be specified or existing in image')
with Image() as textboard:
textboard.font = font
textboard.background_color = background_color
textboard.read(filename=b'label:' + text.encode('utf-8'))
self.composite(textboard, left=left, top=top, gravity=gravity)
@trap_exception
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.
:see: Example of :ref:`level`.
: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`
.. note::
Images may not be affected if the ``white`` value is equal to or
less than the ``black`` value.
.. versionadded:: 0.4.1
"""
assertions.assert_real(black=black)
# If white is not given, mimic CLI behavior by reducing top point
if white is None:
white = 1.0 - black
assertions.assert_real(white=white, gamma=gamma)
bp = float(self.quantum_range * black)
wp = float(self.quantum_range * white)
if MAGICK_HDRI: # pragma: no cover
bp -= 0.5 # TODO: Document why HDRI requires 0.5 adjustments.
wp -= 0.5
if channel is None:
r = library.MagickLevelImage(self.wand, bp, gamma, wp)
else:
ch_const = self._channel_to_mask(channel)
if library.MagickLevelImageChannel:
r = library.MagickLevelImageChannel(self.wand,
ch_const,
bp,
gamma,
wp)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickLevelImage(self.wand, bp, gamma, wp)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def level_colors(self, black_color, white_color, channel=None):
"""Maps given colors to "black" & "white" values.
.. warning::
This class method is only available with ImageMagick 7.0.8-54, or
greater.
:param black_color: linearly map given color as "black" point.
:type black_color: :class:`Color`
:param white_color: linearly map given color as "white" point.
:type white_color: :class:`Color`
:param channel: target a specific color-channel to levelize.
:type channel: :class:`basestring`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.6
"""
if library.MagickLevelImageColors is None:
msg = 'Method requires ImageMagick version 7.0.8-54 or greater.'
raise WandLibraryVersionError(msg)
if isinstance(black_color, string_type):
black_color = Color(black_color)
if isinstance(white_color, string_type):
white_color = Color(white_color)
assertions.assert_color(black_color=black_color,
white_color=white_color)
channel_mask = None
if channel is not None:
ch_const = self._channel_to_mask(channel)
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
with black_color:
with white_color:
r = library.MagickLevelImageColors(self.wand,
black_color.resource,
white_color.resource,
False)
if channel is not None:
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def levelize(self, black=0.0, white=None, gamma=1.0, channel=None):
"""Reverse of :meth:`level()`, this method compresses the range of
colors between ``black`` & ``white`` values.
If only ``black`` is given, ``white`` will be adjusted inward.
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
: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`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickLevelizeImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
if white is None:
white = float(self.quantum_range)
assertions.assert_real(black=black, white=white, gamma=gamma)
if 0 < black <= 1.0:
black *= self.quantum_range
if 0 < white <= 1.0:
white *= self.quantum_range
if channel is None:
r = library.MagickLevelizeImage(self.wand, black, gamma, white)
else:
ch_const = self._channel_to_mask(channel)
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickLevelizeImage(self.wand, black, gamma, white)
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def levelize_colors(self, black_color, white_color, channel=None):
"""Reverse of :meth:`level_colors()`, and creates a de-contrasting
gradient of given colors. This works best with grayscale images.
.. warning::
This class method is only available with ImageMagick 7.0.8-54, or
greater.
:param black_color: tint map given color as "black" point.
:type black_color: :class:`Color`
:param white_color: tint map given color as "white" point.
:type white_color: :class:`Color`
:param channel: target a specific color-channel to levelize.
:type channel: :class:`basestring`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.6
"""
if library.MagickLevelImageColors is None:
msg = 'Method requires ImageMagick version 7.0.8-54 or greater.'
raise WandLibraryVersionError(msg)
if isinstance(black_color, string_type):
black_color = Color(black_color)
if isinstance(white_color, string_type):
white_color = Color(white_color)
assertions.assert_color(black_color=black_color,
white_color=white_color)
channel_mask = None
ch_const = None
if channel is not None:
ch_const = self._channel_to_mask(channel)
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
with black_color:
with white_color:
r = library.MagickLevelImageColors(self.wand,
black_color.resource,
white_color.resource,
True)
if channel is not None:
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
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
"""
assertions.assert_real(black_point=black_point,
white_point=white_point)
linear_range = float(self.width * self.height)
return library.MagickLinearStretchImage(self.wand,
linear_range * black_point,
linear_range * white_point)
@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
"""
assertions.assert_integer(width=width, height=height)
assertions.assert_real(delta_x=delta_x, rigidity=rigidity)
library.MagickLiquidRescaleImage(self.wand, width, height,
delta_x, rigidity)
try:
self.raise_exception()
except MissingDelegateError as e: # pragma: no cover
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
@trap_exception
def local_contrast(self, radius=10, strength=12.5):
"""Increase light-dark transitions within image.
.. warning::
This class method is only available with ImageMagick 6.9.3, or
greater.
:param radius: The size of the Gaussian operator. Default value is
``10.0``.
:type radius: :class:`numbers.Real`
:param strength: Percentage of blur mask to apply. Values can be
between ``0.0`` and ``100`` with a default of
``12.5``.
:type strength: :class:`numbers.Real`
.. versionadded:: 0.5.7
"""
if library.MagickLocalContrastImage is None: # pragma: no cover
msg = 'Method requires ImageMagick version 6.9.3 or greater.'
raise WandLibraryVersionError(msg)
assertions.assert_real(radius=radius, strength=strength)
return library.MagickLocalContrastImage(self.wand, radius, strength)
@manipulative
@trap_exception
def magnify(self):
"""Quickly double an image in size. This is a convenience method.
Use :meth:`resize()`, :meth:`resample()`, or :meth:`sample()` for
more control.
.. versionadded:: 0.5.5
"""
return library.MagickMagnifyImage(self.wand)
def mean_channel(self, channel='default_channels'):
"""Calculates the mean and standard deviation of the image.
.. code:: python
from wand.image import Image
with Image(filename='input.jpg') as img:
mean, stddev = img.mean_channel()
:param channel: Select which color channel to evaluate. See
:const:`CHANNELS`. Default ``'default_channels'``.
:type channel: :class:`basestring`
:returns: Tuple of :attr:`mean` & :attr:`standard_deviation`
values. The ``mean`` value will be between 0.0 &
:attr:`quantum_range`
:rtype: :class:`tuple`
.. versionadded:: 0.5.3
"""
ch_channel = self._channel_to_mask(channel)
m = ctypes.c_double(0.0)
s = ctypes.c_double(0.0)
if MAGICK_VERSION_NUMBER < 0x700:
library.MagickGetImageChannelMean(self.wand, ch_channel,
ctypes.byref(m),
ctypes.byref(s))
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
library.MagickGetImageMean(self.wand,
ctypes.byref(m),
ctypes.byref(s))
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return m.value, s.value
@manipulative
@trap_exception
def mean_shift(self, width, height, color_distance=0.1):
"""Recalculates pixel value by comparing neighboring pixels within a
color distance, and replacing with a mean value. Works best with
Gray, YCbCr, YIQ, or YUV colorspaces.
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param width: Size of the neighborhood window in pixels.
:type width: :class:`numbers.Integral`
:param height: Size of the neighborhood window in pixels.
:type height: :class:`numbers.Integral`
:param color_distance: Include pixel values within this color distance.
:type color_distance: :class:`numbers.Real`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickMeanShiftImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.assert_counting_number(width=width, height=height)
assertions.assert_real(color_distance=color_distance)
if 0 < color_distance <= 1.0:
color_distance *= self.quantum_range
return library.MagickMeanShiftImage(self.wand, width, height,
color_distance)
@manipulative
@trap_exception
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 initial canvas's size depends on the given ImageLayerMethod, and is
initialized using the first images background color. The images
are then composited 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
"""
assertions.assert_string(method=method)
if method not in ('merge', 'flatten', 'mosaic', 'trimbounds'):
raise ValueError('method can only be \'merge\', \'flatten\', '
'\'mosaic\', or \'trimbounds\'')
m = IMAGE_LAYER_METHOD.index(method)
r = library.MagickMergeImageLayers(self.wand, m)
if r:
self.wand = r
self.reset_sequence()
return bool(r)
def minimum_bounding_box(self, orientation=None):
"""Find the minimum bounding box within the image. Use
properties :attr:`fuzz` & :attr:`background_color` to influence
bounding box thresholds.
.. code::
from wand.image import Image
from wand.drawing import Drawing
with Image(filename='kdf_black.png') as img:
img.fuzz = img.quantum_range * 0.1
img.background_color = 'black'
mbr = img.minimum_bounding_box()
with Drawing() as ctx:
ctx.fill_color = 'transparent'
ctx.stroke_color = 'red'
ctx.polygon(points=mbr['points'])
ctx.fill_color = 'red'
ctx.stroke_color = 'transparent'
ctx.text(1, 10, '{0:.4g}\u00B0'.format(mbr['angle']))
ctx(img)
img.save(filename='kdf_black_mbr.png')
.. image:: ../_images/wand/image/kdf_black.png
.. image:: ../_images/wand/image/kdf_black_mbr.png
.. note::
Requires ImageMagick-7.0.10 or later.
:param orientation: sets the image orientation. Values can be
``'landscape'``, or ``'portrait'``.
:type orientation: :class:`basestring`
:returns: a directory of MBR properties & corner points.
:rtype: :class:`dict` { "points": :class:`list` [ :class:`tuple` (
:class:`float`, :class:`float` ) ], "area": :class:`float`,
"width": :class:`float`, "height": :class:`float`,
"angle": :class:`float`, "unrotate": :class:`float` }
.. versionadded:: 0.6.4
"""
r = {}
if MAGICK_VERSION_NUMBER < 0x70A:
msg = 'ImageMagick-7.0.10 is required to use convex_hull().'
raise WandLibraryVersionError(msg)
with self.clone() as tmp:
if orientation is not None:
if orientation not in ('landscape', 'portrait'):
msg = 'orientation can only be landscape, or portrait, not'
msg += ' ' + repr(orientation)
raise ValueError(msg)
key = b'minimum-bounding-box:orientation'
val = to_bytes(orientation)
library.MagickSetImageArtifact(tmp.wand, key, val)
mbr_str = b'%[minimum-bounding-box]'
mbr_str += b'|%[minimum-bounding-box:area]'
mbr_str += b'|%[minimum-bounding-box:width]'
mbr_str += b'|%[minimum-bounding-box:height]'
mbr_str += b'|%[minimum-bounding-box:angle]'
mbr_str += b'|%[minimum-bounding-box:unrotate]'
library.MagickSetOption(tmp.wand, b'format', mbr_str)
library.MagickSetImageFormat(tmp.wand, b'INFO')
length = ctypes.c_size_t()
blob_p = library.MagickGetImageBlob(tmp.wand,
ctypes.byref(length))
if blob_p:
blob = ctypes.string_at(blob_p, length.value)
blob_p = library.MagickRelinquishMemory(blob_p)
parts = blob.decode('ascii', 'ignore').split('|')
pts = parts[0].strip().split(' ')
r = [tuple(map(lambda x: float(x), p.split(','))) for p in pts]
attr = list(map(lambda x: float(x.strip()), parts[1:]))
keys = ['area', 'width', 'height', 'angle', 'unrotate']
r = dict(zip(keys, attr), points=r)
else:
self.raise_exception()
return r
@manipulative
def mode(self, width, height=None):
"""Replace each pixel with the mathematical mode of the neighboring
colors. This is an alias of the :meth:`statistic` method.
:param width: Number of neighboring pixels to include in mode.
:type width: :class:`numbers.Integral`
:param height: Optional height of neighboring pixels, defaults to the
same value as ``width``.
:type height: :class:`numbers.Integral`
.. versionadded:: 0.5.4
"""
if height is None:
height = width
self.statistic('mode', width, height)
@manipulative
@trap_exception
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
"""
assertions.assert_real(brightness=brightness, saturation=saturation,
hue=hue)
return library.MagickModulateImage(
self.wand,
brightness,
saturation,
hue
)
@manipulative
@trap_exception
def morphology(self, method=None, kernel=None, iterations=1, channel=None):
"""Manipulate pixels based on the shape of neighboring pixels.
The ``method`` determines what type of effect to apply to matching
``kernel`` shapes. Common methods can be add/remove,
or lighten/darken pixel values.
The ``kernel`` describes the shape of the matching neighbors. Common
shapes are provided as "built-in" kernels. See
:const`KERNEL_INFO_TYPES` for examples. The format for built-in kernels
is:
.. sourcecode:: text
label:geometry
Where `label` is the kernel name defined in :const:`KERNEL_INFO_TYPES`,
and `:geometry` is an optional geometry size. For example::
with Image(filename='rose:') as img:
img.morphology(method='dilate', kernel='octagon:3x3')
# or simply
img.morphology(method='edgein', kernel='octagon')
Custom kernels can be applied by following a similar format:
.. sourcecode:: text
geometry:args
Where `geometry` is the size of the custom kernel, and `args`
list a comma separated list of values. For example::
custom_kernel='5x3:nan,1,1,1,nan 1,1,1,1,1 nan,1,1,1,nan'
with Image(filename='rose:') as img:
img.morphology(method='dilate', kernel=custom_kernel)
:param method: effect function to apply. See
:const:`MORPHOLOGY_METHODS` for a list of
methods.
:type method: :class:`basestring`
:param kernel: shape to evaluate surrounding pixels. See
:const:`KERNEL_INFO_TYPES` for a list of
built-in shapes.
:type kernel: :class:`basestring`
:param iterations: Number of times a morphology method should be
applied to the image. Default ``1``. Use ``-1`` for
unlimited iterations until the image is unchanged
by the method operator.
:type iterations: :class:`numbers.Integral`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: `basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
assertions.assert_string(method=method, kernel=kernel)
assertions.assert_integer(iterations=iterations)
builtin = None
geometry = ''
parts = kernel.split(':')
if parts[0] in KERNEL_INFO_TYPES:
builtin = parts[0]
if len(parts) == 2:
geometry = parts[1]
exception_info = libmagick.AcquireExceptionInfo()
if builtin:
kernel_idx = KERNEL_INFO_TYPES.index(builtin)
geometry_info = GeometryInfo()
flags = libmagick.ParseGeometry(binary(geometry),
ctypes.byref(geometry_info))
if builtin in ('unity',):
if (flags & geometry_info.RhoValue) == 0:
geometry_info.rho = 1.0
elif builtin in ('square', 'diamond', 'octagon', 'disk',
'plus', 'cross'):
if (flags & geometry_info.SigmaValue) == 0:
geometry_info.sigma = 1.0
elif builtin in ('ring',):
if (flags & geometry_info.XiValue) == 0:
geometry_info.xi = 1.0
elif builtin in ('rectangle',):
if (flags & geometry_info.RhoValue) == 0:
geometry_info.rho = geometry_info.sigma
if geometry_info.rho < 1.0:
geometry_info.rho = 3.0
if geometry_info.sigma < 1.0:
geometry_info.sigma = geometry_info.rho
if (flags & geometry_info.XiValue) == 0:
geometry_info.xi = (geometry_info.rho - 1.0) / 2.0
if (flags & geometry_info.PsiValue) == 0:
geometry_info.psi = (geometry_info.sigma - 1.0) / 2.0
elif builtin in ('chebyshev', 'manhattan', 'octagonal',
'euclidean'):
if (flags & geometry_info.SigmaValue) == 0:
geometry_info.sigma = 100.0
elif (flags & geometry_info.AspectValue) != 0:
geometry_info.sigma = (float(self.quantum_range) /
(geometry_info.sigma + 1.0))
elif (flags & geometry_info.PercentValue) != 0:
geometry_info.sigma *= float(self.quantum_range) / 100.0
if MAGICK_VERSION_NUMBER < 0x700:
kernel_info = libmagick.AcquireKernelBuiltIn(
kernel_idx,
ctypes.byref(geometry_info)
)
else: # pragma: no cover
kernel_info = libmagick.AcquireKernelBuiltIn(
kernel_idx,
ctypes.byref(geometry_info),
exception_info
)
elif kernel:
if MAGICK_VERSION_NUMBER < 0x700:
kernel_info = libmagick.AcquireKernelInfo(
binary(kernel)
)
else: # pragma: no cover
kernel_info = libmagick.AcquireKernelInfo(
binary(kernel),
exception_info
)
r = None
exception_info = libmagick.DestroyExceptionInfo(exception_info)
if kernel_info:
method_idx = MORPHOLOGY_METHODS.index(method)
if channel is None:
r = library.MagickMorphologyImage(self.wand, method_idx,
iterations, kernel_info)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickMorphologyImageChannel(self.wand,
channel_ch,
method_idx,
iterations,
kernel_info)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickMorphologyImage(self.wand, method_idx,
iterations, kernel_info)
library.MagickSetImageChannelMask(self.wand, mask)
kernel_info = libmagick.DestroyKernelInfo(kernel_info)
else:
raise ValueError('Unable to parse kernel info for ' +
repr(kernel))
return r
@manipulative
@trap_exception
def motion_blur(self, radius=0.0, sigma=0.0, angle=0.0, channel=None):
"""Apply a Gaussian blur along an ``angle`` direction. This
simulates motion movement.
:see: Example of :ref:`motion_blur`.
:param radius: Aperture size of the Gaussian operator.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of the Gaussian operator.
:type sigma: :class:`numbers.Real`
:param angle: Apply the effect along this angle.
:type angle: :class:`numbers.Real`
.. versionadded:: 0.5.4
"""
assertions.assert_real(radius=radius, sigma=sigma, angle=angle)
if channel is None:
r = library.MagickMotionBlurImage(self.wand, radius, sigma, angle)
else:
ch_const = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickMotionBlurImageChannel(self.wand,
ch_const,
radius,
sigma,
angle)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickMotionBlurImage(self.wand, radius, sigma,
angle)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_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 is None:
r = library.MagickNegateImage(self.wand, grayscale)
else:
ch_const = self._channel_to_mask(channel)
if library.MagickNegateImageChannel:
r = library.MagickNegateImageChannel(self.wand, ch_const,
grayscale)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_const)
r = library.MagickNegateImage(self.wand, grayscale)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def noise(self, noise_type='uniform', attenuate=1.0, channel=None):
"""Adds noise to image.
:see: Example of :ref:`noise`.
:param noise_type: type of noise to apply. See :const:`NOISE_TYPES`.
:type noise_type: :class:`basestring`
:param attenuate: rate of distribution. Only available in
ImageMagick-7. Default is ``1.0``.
:type attenuate: :class:`numbers.Real`
:param channel: Optionally target a color channel to apply noise to.
See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
"""
assertions.string_in_list(NOISE_TYPES, 'wand.image.NOISE_TYPES',
noise_type=noise_type)
assertions.assert_real(attenuate=attenuate)
noise_type_idx = NOISE_TYPES.index(noise_type)
if MAGICK_VERSION_NUMBER < 0x700:
if channel is None:
r = library.MagickAddNoiseImage(self.wand, noise_type_idx)
else:
channel_ch = self._channel_to_mask(channel)
r = library.MagickAddNoiseImageChannel(self.wand,
channel_ch,
noise_type_idx)
else: # pragma: no cover
if channel is None:
r = library.MagickAddNoiseImage(self.wand, noise_type_idx,
attenuate)
else:
channel_ch = self._channel_to_mask(channel)
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickAddNoiseImage(self.wand, noise_type_idx,
attenuate)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
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 is None:
r = library.MagickNormalizeImage(self.wand)
else:
ch_const = self._channel_to_mask(channel)
if library.MagickNormalizeImageChannel:
r = library.MagickNormalizeImageChannel(self.wand, ch_const)
else: # pragma: no cover
with Image(image=self) as mask:
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(mask.wand,
ch_const)
r = library.MagickNormalizeImage(mask.wand)
# Restore original state of channels.
library.MagickSetImageChannelMask(mask.wand,
channel_mask)
# Copy adjusted mask over original value.
copy_mask = COMPOSITE_OPERATORS.index('copy_' + channel)
library.MagickCompositeImage(self.wand,
mask.wand,
copy_mask,
False,
0,
0)
return r
@manipulative
@trap_exception
def oil_paint(self, radius=0.0, sigma=0.0):
"""Simulates an oil painting by replace each pixel with most frequent
surrounding color.
:param radius: The size of the surrounding neighbors.
:type radius: :class:`numbers.Real`
:param sigma: The standard deviation used by the Gaussian operator.
This is only available with ImageMagick-7.
:type sigma: :class:`numbers.Real`
.. versionadded:: 0.5.4
"""
assertions.assert_real(radius=radius, sigma=sigma)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickOilPaintImage(self.wand, radius)
else: # pragma: no cover
r = library.MagickOilPaintImage(self.wand, radius, sigma)
return r
@manipulative
@trap_exception
def opaque_paint(self, target=None, fill=None, fuzz=0.0, invert=False,
channel=None):
"""Replace any color that matches ``target`` with ``fill``. Use
``fuzz`` to control the threshold of the target match.
The ``invert`` will replace all colors *but* the pixels matching
the ``target`` color.
:param target: The color to match.
:type target: :class:`wand.color.Color`
:param fill: The color to paint with.
:type fill: :class:`wand.color.Color`
:param fuzz: Normalized real number between `0.0` and
:attr:`quantum_range`. Default is `0.0`.
:type fuzz: class:`numbers.Real`
:param invert: Replace all colors that do not match target.
Default is ``False``.
:type invert: :class:`bool`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.4
.. versionchanged:: 0.5.5
Added ``channel`` parameter.
"""
if isinstance(target, string_type):
target = Color(target)
if isinstance(fill, string_type):
fill = Color(fill)
assertions.assert_color(target=target, fill=fill)
assertions.assert_real(fuzz=fuzz)
assertions.assert_bool(invert=invert)
with target:
with fill:
if channel is None:
r = library.MagickOpaquePaintImage(self.wand,
target.resource,
fill.resource,
fuzz,
invert)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickOpaquePaintImageChannel(
self.wand, channel_ch, target.resource,
fill.resource, fuzz, invert
)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickOpaquePaintImage(self.wand,
target.resource,
fill.resource,
fuzz,
invert)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def optimize_layers(self):
"""Attempts to crop each frame to the smallest image without altering
the animation. For best results, call
:meth:`Image.coalesce() <wand.image.BaseImage.coalesce>` before
manipulating any frames. For timing accuracy, any
:attr:`SingleImage.delay <wand.sequence.SingleImage.delay>` overwrites
must be applied after optimizing layers.
.. note::
This will only affect ``GIF`` image formats.
.. versionadded:: 0.5.0
"""
r = library.MagickOptimizeImageLayers(self.wand)
if r:
self.wand = r
self.reset_sequence()
return bool(r)
@manipulative
@trap_exception
def optimize_transparency(self):
"""Iterates over frames, and sets transparent values for each
pixel unchanged by previous frame.
.. note::
This will only affect ``GIF`` image formats.
.. versionadded:: 0.5.0
"""
if library.MagickOptimizeImageTransparency:
return library.MagickOptimizeImageTransparency(self.wand)
else: # pragma: no cover
raise AttributeError('`MagickOptimizeImageTransparency\' not '
'available on current version of MagickWand '
'library.')
@manipulative
@trap_exception
def ordered_dither(self, threshold_map='threshold', channel=None):
"""Executes a ordered-based dither operations based on predetermined
threshold maps.
+-----------+-------+-----------------------------+
| Map | Alias | Description |
+===========+=======+=============================+
| threshold | 1x1 | Threshold 1x1 (non-dither) |
+-----------+-------+-----------------------------+
| checks | 2x1 | Checkerboard 2x1 (dither) |
+-----------+-------+-----------------------------+
| o2x2 | 2x2 | Ordered 2x2 (dispersed) |
+-----------+-------+-----------------------------+
| o3x3 | 3x3 | Ordered 3x3 (dispersed) |
+-----------+-------+-----------------------------+
| o4x4 | 4x4 | Ordered 4x4 (dispersed) |
+-----------+-------+-----------------------------+
| o8x8 | 8x8 | Ordered 8x8 (dispersed) |
+-----------+-------+-----------------------------+
| h4x4a | 4x1 | Halftone 4x4 (angled) |
+-----------+-------+-----------------------------+
| h6x6a | 6x1 | Halftone 6x6 (angled) |
+-----------+-------+-----------------------------+
| h8x8a | 8x1 | Halftone 8x8 (angled) |
+-----------+-------+-----------------------------+
| h4x4o | | Halftone 4x4 (orthogonal) |
+-----------+-------+-----------------------------+
| h6x6o | | Halftone 6x6 (orthogonal) |
+-----------+-------+-----------------------------+
| h8x8o | | Halftone 8x8 (orthogonal) |
+-----------+-------+-----------------------------+
| h16x16o | | Halftone 16x16 (orthogonal) |
+-----------+-------+-----------------------------+
| c5x5b | c5x5 | Circles 5x5 (black) |
+-----------+-------+-----------------------------+
| c5x5w | | Circles 5x5 (white) |
+-----------+-------+-----------------------------+
| c6x6b | c6x6 | Circles 6x6 (black) |
+-----------+-------+-----------------------------+
| c6x6w | | Circles 6x6 (white) |
+-----------+-------+-----------------------------+
| c7x7b | c7x7 | Circles 7x7 (black) |
+-----------+-------+-----------------------------+
| c7x7w | | Circles 7x7 (white) |
+-----------+-------+-----------------------------+
:param threshold_map: Name of threshold dither to use, followed by
optional arguments.
:type threshold_map: :class:`basestring`
:param channel: Optional argument to apply dither to specific color
channel. See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.7
"""
assertions.assert_string(threshold_map=threshold_map)
bmap = binary(threshold_map)
if MAGICK_VERSION_NUMBER <= 0x700:
if channel is None:
r = library.MagickOrderedPosterizeImage(self.wand, bmap)
else:
channel_ch = self._channel_to_mask(channel)
r = library.MagickOrderedPosterizeImageChannel(self.wand,
channel_ch,
bmap)
else: # pragma: no cover
if channel is None:
r = library.MagickOrderedDitherImage(self.wand, bmap)
else:
channel_ch = self._channel_to_mask(channel)
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickOrderedDitherImage(self.wand, bmap)
library.MagickSetImageChannelMask(self.wand, mask)
return r
def parse_meta_geometry(self, geometry):
"""Helper method to translate geometry format, and calculate
meta-characters against image dimensions.
See "Image Geometry" definitions & examples for more info:
https://imagemagick.org/script/command-line-processing.php#geometry
:param geometry: user string following ImageMagick's geometry format.
:type geometry: :class:`basestring`
:returns: Calculated width, height, offset-x, & offset-y.
:rtype: :class:`tuple`
:raises ValueError: If given geometry can not be parsed.
.. versionadded:: 0.5.6
"""
assertions.assert_string(geometry=geometry)
x = ctypes.c_ssize_t(0)
y = ctypes.c_ssize_t(0)
width = ctypes.c_size_t(self.width)
height = ctypes.c_size_t(self.height)
r = libmagick.ParseMetaGeometry(binary(geometry),
ctypes.byref(x),
ctypes.byref(y),
ctypes.byref(width),
ctypes.byref(height))
if not bool(r):
raise ValueError('Unable to parse geometry')
return (width.value, height.value, x.value, y.value)
def percent_escape(self, string_format):
"""Convenience method that expands ImageMagick's `Percent Escape`_
characters into image attribute values.
.. _Percent Escape: https://imagemagick.org/script/escape.php
.. code::
with wand.image import Image
with Image(filename='tests/assets/sasha.jpg') as img:
print(img.percent_escape('%f %wx%h'))
#=> sasha.jpg 204x247
.. note::
Not all percent escaped values can be populated as I/O operations
are managed by Python, and not the CLI utility.
:param string_format: The prescient escaped string to be translated.
:type string_format: :class:`basestring`
:returns: String of expanded values.
:rtype: :class:`basestring`
.. versionadded:: 0.5.6
"""
local_overwrites = {
'%m': self.format,
'%[magick]': self.format
}
for k, v in local_overwrites.items():
string_format = string_format.replace(k, v)
self.options['format'] = string_format
return text(self.make_blob('INFO'))
@manipulative
@trap_exception
def polaroid(self, angle=0.0, caption=None, font=None, method='undefined'):
"""Creates a special effect simulating a Polaroid photo.
:see: Example of :ref:`polaroid`.
:param angle: applies a shadow effect along this angle.
:type angle: :class:`numbers.Real`
:param caption: Writes a message at the bottom of the photo's border.
:type caption: :class:`basestring`
:param font: Specify font style.
:type font: :class:`wand.font.Font`
:param method: Interpolation method. ImageMagick-7 only.
:type method: :class:`basestring`
.. versionadded:: 0.5.4
"""
assertions.assert_real(angle=angle)
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
method=method)
ctx_ptr = library.NewDrawingWand()
if caption:
assertions.assert_string(caption=caption)
caption = binary(caption)
library.MagickSetImageProperty(self.wand, b'Caption',
caption)
if isinstance(font, Font):
if font.path:
library.DrawSetFont(ctx_ptr, binary(font.path))
if font.size:
library.DrawSetFontSize(ctx_ptr, font.size)
if font.color:
with font.color:
library.DrawSetFillColor(ctx_ptr, font.color.resource)
library.DrawSetTextAntialias(ctx_ptr, font.antialias)
if font.stroke_color:
with font.stroke_color:
library.DrawSetStrokeColor(ctx_ptr,
font.stroke_color.resource)
if font.stroke_width:
library.DrawSetStrokeWidth(ctx_ptr, font.stroke_width)
elif font:
raise TypeError('font must be in instance of '
'wand.font.Font, not ' + repr(font))
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickPolaroidImage(self.wand, ctx_ptr, angle)
else: # pragma: no cover
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
r = library.MagickPolaroidImage(self.wand, ctx_ptr, caption, angle,
method_idx)
ctx_ptr = library.DestroyDrawingWand(ctx_ptr)
return r
@manipulative
@trap_exception
def polynomial(self, arguments):
"""Replace image with the sum of all images in a sequence by
calculating the pixel values a coefficient-weight value, and a
polynomial-exponent.
For example::
with Image(filename='rose:') as img:
img.polynomial(arguments=[0.5, 1.0])
The output image will be calculated as:
.. math::
output = 0.5 * image ^ {1.0}
This can work on multiple images in a sequence by calculating across
each frame in the image stack.
.. code::
with Image(filename='2frames.gif') as img:
img.polynomial(arguments=[0.5, 1.0, 0.25, 1.25])
Where the results would be calculated as:
.. math::
output = 0.5 * frame1 ^ {1.0} + 0.25 * frame2 ^ {1.25}
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param arguments: A list of real numbers where at least two numbers
(weight & exponent) are need for each image in the
sequence.
:type arguments: :class:`collections.abc.Sequence`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickPolynomialImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
if not isinstance(arguments, abc.Sequence):
raise TypeError('expected sequence of doubles, not ' +
repr(arguments))
argc = len(arguments)
argv = (ctypes.c_double * argc)(*arguments)
return library.MagickPolynomialImage(self.wand, (argc >> 1), argv)
@manipulative
@trap_exception
def posterize(self, levels=None, dither='no'):
"""Reduce color levels per channel.
:param levels: Number of levels per channel.
:type levels: :class:`numbers.Integral`
:param dither: Dither method to apply.
See :const:`DITHER_METHODS`.
:type dither: `basestring`
.. versionadded:: 0.5.0
"""
assertions.assert_integer(levels=levels)
assertions.string_in_list(DITHER_METHODS, 'wand.image.DITHER_METHODS',
dither=dither)
dither_idx = DITHER_METHODS.index(dither)
return library.MagickPosterizeImage(self.wand, levels, dither_idx)
@manipulative
@trap_exception
def quantize(self, number_colors, colorspace_type=None,
treedepth=0, dither=False, measure_error=False):
"""`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 target number of colors to reduce the image.
:type number_colors: :class:`numbers.Integral`
:param colorspace_type: Available value can be found
in the :const:`COLORSPACE_TYPES`. Defaults
:attr:`colorspace`.
:type colorspace_type: :class:`basestring`
:param treedepth: A value between ``0`` & ``8`` where ``0`` will
allow ImageMagick to calculate the optimal depth
with ``Log4(number_colors)``. Default value is ``0``.
:type treedepth: :class:`numbers.Integral`
:param dither: Perform dither operation between neighboring pixel
values. If using ImageMagick-6, this can be a value
of ``True``, or ``False``. With ImageMagick-7, use
a string from :const:`DITHER_METHODS`. Default
``False``.
:type dither: :class:`bool`, or :class:`basestring`
:param measure_error: Include total quantization error of all pixels
in an image & quantized value.
:type measure_error: :class:`bool`
.. versionadded:: 0.4.2
.. versionchanged:: 0.5.9
Fixed ImageMagick-7 ``dither`` argument, and added keyword defaults.
"""
assertions.assert_integer(number_colors=number_colors)
if colorspace_type is None:
colorspace_type = self.colorspace
assertions.string_in_list(COLORSPACE_TYPES,
'wand.image.COLORSPACE_TYPES',
colorspace_type=colorspace_type)
assertions.assert_integer(treedepth=treedepth)
if MAGICK_VERSION_NUMBER < 0x700:
assertions.assert_bool(dither=dither)
else: # pragma: no cover
if dither is False:
dither = 'no'
elif dither is True:
dither = 'riemersma'
assertions.string_in_list(DITHER_METHODS,
'wand.image.DITHER_METHODS',
dither=dither)
dither = DITHER_METHODS.index(dither)
assertions.assert_bool(measure_error=measure_error)
return library.MagickQuantizeImage(
self.wand, number_colors,
COLORSPACE_TYPES.index(colorspace_type),
treedepth, dither, measure_error
)
@manipulative
@trap_exception
def random_threshold(self, low=0.0, high=1.0, channel=None):
"""Performs a random dither to force a pixel into a binary black &
white state. Each color channel operarates independently from each
other.
:param low: bottom threshold. Any pixel value below the given value
will be rendered "0", or no value. Given threshold value
can be between ``0.0`` & ``1.0``, or ``0`` &
:attr:`quantum_range`.
:type low: :class:`numbers.Real`
:param high: top threshold. Any pixel value above the given value
will be rendered as max quantum value. Given threshold
value can be between ``0.0`` & ``1.0``, or ``0`` &
:attr:`quantum_range`.
:type high: :class:`numbers.Real`
:param channel: Optional argument to apply dither to specific color
channel. See :const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.7
"""
assertions.assert_real(low=low, high=high)
if 0 < low <= 1.0:
low *= self.quantum_range
if 0 < high <= 1.0:
high *= self.quantum_range
if channel is None:
r = library.MagickRandomThresholdImage(self.wand, low, high)
else:
ch_channel = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickRandomThresholdImageChannel(self.wand,
ch_channel,
low,
high)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
r = library.MagickRandomThresholdImage(self.wand, low, high)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
def range_channel(self, channel='default_channels'):
"""Calculate the minimum and maximum of quantum values in image.
.. code:: python
from wand.image import Image
with Image(filename='input.jpg') as img:
minima, maxima = img.range_channel()
:param channel: Select which color channel to evaluate. See
:const:`CHANNELS`. Default ``'default_channels'``.
:type channel: :class:`basestring`
:returns: Tuple of :attr:`minima` & :attr:`maxima`
values. Each value will be between 0.0 &
:attr:`quantum_range`.
:rtype: :class:`tuple`
.. versionadded:: 0.5.3
"""
ch_channel = self._channel_to_mask(channel)
min_color = ctypes.c_double(0.0)
max_color = ctypes.c_double(0.0)
if MAGICK_VERSION_NUMBER < 0x700:
library.MagickGetImageChannelRange(self.wand, ch_channel,
ctypes.byref(min_color),
ctypes.byref(max_color))
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
ch_channel)
library.MagickGetImageRange(self.wand,
ctypes.byref(min_color),
ctypes.byref(max_color))
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return min_color.value, max_color.value
@manipulative
@trap_exception
def range_threshold(self, low_black=0.0, low_white=None, high_white=None,
high_black=None):
"""Applies soft & hard thresholding.
For a soft thresholding, parameters should be monotonically increasing:
with Image(filename='text.png') as img:
img.range_threshold(0.2, 0.4, 0.6, 0.8)
For a hard thresholding, parameters should be the same:
with Image(filename='text.png') as img:
img.range_threshold(0.4, 0.4, 0.6, 0.6)
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:param low_black: Define the minimum threshold value.
:type low_black: :class:`numbers.Real`
:param low_white: Define the minimum threshold value.
:type low_white: :class:`numbers.Real`
:param high_white: Define the maximum threshold value.
:type high_white: :class:`numbers.Real`
:param high_black: Define the maximum threshold value.
:type high_black: :class:`numbers.Real`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickRangeThresholdImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
# Populate defaults to follow CLI behavior
if low_white is None:
low_white = low_black
if high_white is None:
high_white = low_white
if high_black is None:
high_black = high_white
assertions.assert_real(low_black=low_black, low_white=low_white,
high_white=high_white, high_black=high_black)
if 0 < low_black <= 1.0:
low_black *= self.quantum_range
if 0 < low_white <= 1.0:
low_white *= self.quantum_range
if 0 < high_white <= 1.0:
high_white *= self.quantum_range
if 0 < high_black <= 1.0:
high_black *= self.quantum_range
return library.MagickRangeThresholdImage(self.wand,
low_black, low_white,
high_white, high_black)
@trap_exception
def read_mask(self, clip_mask=None):
"""Sets the read mask where the gray values of the clip mask
are used to blend during composite operations. Call this method with
a ``None`` argument to clear any previously set masks.
This method is also useful for :meth:`compare` method for limiting
region of interest.
.. warning::
This method is only available with ImageMagick-7.
:param clip_mask: Image to reference as blend mask.
:type clip_mask: :class:`BaseImage`
.. versionadded:: 0.5.7
"""
r = False
ReadPixelMask = 0x000001
if library.MagickSetImageMask is None:
raise WandLibraryVersionError('Method requires ImageMagick-7.')
else: # pragma: no cover
if clip_mask is None:
r = library.MagickSetImageMask(self.wand, ReadPixelMask, None)
elif isinstance(clip_mask, BaseImage):
r = library.MagickSetImageMask(self.wand, ReadPixelMask,
clip_mask.wand)
return r
def region(self, width=None, height=None, x=None, y=None, gravity=None):
"""Extract an area of the image. This is the same as :meth:`crop`,
but returns a new instance of :class:`Image` without altering the
original source image.
.. code-block:: python
from wand.image import Image
with Image(filename='input.jpg') as img:
with img.region(width=100, height=100, x=0, y=0) as area:
pass
:param width: Area size on the X-axis. Default value is
the image's :attr:`page_width`.
:type width: :class:`numbers.Integral`
:param height: Area size on the Y-axis.Default value is
the image's :attr:`page_height`.
:type height: :class:`numbers.Integral`
:param x: X-axis offset. This number can be negative. Default value is
the image's :attr:`page_x`.
:type x: :class:`numbers.Integral`
:param y: Y-axis offset. This number can be negative. Default value is
the image's :attr:`page_y`.
:type y: :class:`numbers.Integral`
:param region: Helper attribute to set ``x`` & ``y`` offset. See
:const:`GRAVITY_TYPES`.
:type region: :class:`basestring`
:returns: New instance of Wand.
:rtype: :class:`Image`
.. versionadded:: 0.6.8
"""
ow, oh, ox, oy = self.page
if width is None:
width = ow
if height is None:
height = oh
assertions.assert_unsigned_integer(width=width, height=height)
if gravity is not None:
if x is not None or y is not None:
raise ValueError('x & y can not be used with gravity.')
y, x = self._gravity_to_offset(gravity, width, height)
if x is None:
x = ox
if y is None:
y = oy
assertions.assert_integer(x=x, y=y)
new_wand = library.MagickGetImageRegion(self.wand, width, height, x, y)
if not new_wand:
self.raise_exception()
return Image(BaseImage(new_wand))
@manipulative
@trap_exception
def remap(self, affinity=None, method='no'):
"""Rebuild image palette with closest color from given affinity image.
:see: Example of :ref:`remap`.
:param affinity: reference image.
:type affinity: :class:`BaseImage`
:param method: dither method. See :const:`DITHER_METHODS`.
Default is ``'no'`` dither.
:type method: :class:`basestring`
.. versionadded:: 0.5.3
"""
if not isinstance(affinity, BaseImage):
raise TypeError('Expecting affinity to be a BaseImage, not ' +
repr(affinity))
assertions.string_in_list(DITHER_METHODS, 'wand.image.DITHER_METHODS',
method=method)
method_idx = DITHER_METHODS.index(method)
return library.MagickRemapImage(self.wand, affinity.wand, method_idx)
@manipulative
@trap_exception
def resample(self, x_res=None, y_res=None, filter='undefined', blur=1):
"""Adjust the number of pixels in an image so that when displayed at
the given Resolution or Density the image will still look the same size
in real world terms.
.. note::
This method will automatically :meth:`coalesce` & resample all
frames in a GIF animation. For other image formats,
:meth:`resample` will only effect the current image in the stack.
Use :meth:`iterator_reset` & :meth:`iterator_next` to traverse
the image stack to resample all images in a multi-layer document.
.. code::
with Image(filename='input.tiff') as img:
img.iterator_reset()
while True:
img.resample(128, 128)
if not img.iterator_next():
break
:param x_res: the X resolution (density) in the scaled image. default
is the original resolution.
:type x_res: :class:`numbers.Real`
:param y_res: the Y resolution (density) in the scaled image. default
is the original resolution.
:type y_res: :class:`numbers.Real`
: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`
.. versionadded:: 0.4.5
"""
if x_res is None:
x_res, _ = self.resolution
if y_res is None:
_, y_res = self.resolution
assertions.assert_real(x_res=x_res, y_res=y_res, blur=blur)
if x_res < 1:
raise ValueError('x_res must be a Real number, not ' +
repr(x_res))
elif y_res < 1:
raise ValueError('y_res must be a Real number, not ' +
repr(y_res))
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)
self.reset_sequence()
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(n + 1):
library.MagickSetIteratorIndex(self.wand, i)
r = library.MagickResampleImage(self.wand, x_res, y_res,
filter, blur)
else:
r = library.MagickResampleImage(self.wand, x_res, y_res,
filter, blur)
return r
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)
def reset_sequence(self):
"""Abstract method prototype.
See :meth:`wand.image.Image.reset_sequence()`.
.. versionadded:: 0.6.0
"""
pass
@manipulative
@trap_exception
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
assertions.assert_counting_number(width=width, height=height)
assertions.assert_real(blur=blur)
if 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)
self.reset_sequence()
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(n + 1):
library.MagickSetIteratorIndex(self.wand, i)
r = 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)
return r
@manipulative
@trap_exception
def roll(self, x=0, y=0):
"""Shifts all pixels over by an X/Y offset.
:param x: Number of columns to roll over. Negative value will roll
pixels from right-to-left, and positive value will roll
pixels from left-to-right. Default value: ``0``.
:type x: :class:`numbers.Integral`
:param y: Number of rows to roll over. Negative value will roll
pixels from bottom-to-top, and positive value will roll
pixels from top-to-bottm. Default value: ``0``.
:type y: :class:`numbers.Integral`
.. versionadded:: 0.6.8
"""
assertions.assert_integer(x=x, y=y)
return library.MagickRollImage(self.wand, x, y)
@manipulative
@trap_exception
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.
:see: Example of :ref:`rotate`.
: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 isinstance(background, string_type):
background = Color(background)
assertions.assert_color(background=background)
assertions.assert_real(degree=degree)
with background:
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
self.reset_sequence()
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)
result = 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 reset_coords:
self.reset_coords()
return result
@manipulative
@trap_exception
def rotational_blur(self, angle=0.0, channel=None):
"""Blur an image in a radius around the center of an image.
.. warning:: Requires ImageMagick-6.8.8 or greater.
:see: Example of :ref:`rotational_blur`.
:param angle: Degrees of rotation to blur with.
:type angle: :class:`numbers.Real`
:param channel: Optional channel to apply the effect against. See
:const:`CHANNELS` for a list of possible values.
:type channel: :class:`basestring`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.4
"""
if not library.MagickRotationalBlurImage: # pragma: no cover
msg = ("Method `rotational_blur` not available on installed "
"version of ImageMagick library. ")
raise WandLibraryVersionError(msg)
assertions.assert_real(angle=angle)
if channel:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickRotationalBlurImageChannel(self.wand,
channel_ch,
angle)
else: # pragma: no cover
channel_mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickRotationalBlurImage(self.wand, angle)
library.MagickSetImageChannelMask(self.wand, channel_mask)
else:
r = library.MagickRotationalBlurImage(self.wand, angle)
return r
@manipulative
@trap_exception
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 trade-off.
: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
assertions.assert_counting_number(width=width, height=height)
if self.animation:
self.wand = library.MagickCoalesceImages(self.wand)
self.reset_sequence()
library.MagickSetLastIterator(self.wand)
n = library.MagickGetIteratorIndex(self.wand)
library.MagickResetIterator(self.wand)
for i in xrange(n + 1):
library.MagickSetIteratorIndex(self.wand, i)
r = 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)
return bool(r)
@manipulative
@trap_exception
def scale(self, columns=1, rows=1):
"""Increase image size by scaling each pixel value by given ``columns``
and ``rows``.
:param columns: The number of columns, in pixels, to scale the image
horizontally.
:type columns: :class:`numbers.Integral`
:param rows: The number of rows, in pixels, to scale the image
vertically.
:type rows: :class:`numbers.Integral`
.. versionadded:: 0.5.7
"""
assertions.assert_counting_number(columns=columns, rows=rows)
return library.MagickScaleImage(self.wand, columns, rows)
@manipulative
@trap_exception
def selective_blur(self, radius=0.0, sigma=0.0, threshold=0.0,
channel=None):
"""Blur an image within a given threshold.
For best effects, use a value between 10% and 50% of
:attr:`quantum_range`
.. code::
from wand.image import Image
with Image(filename='photo.jpg') as img:
# Apply 8x3 blur with a 10% threshold
img.selective_blur(8.0, 3.0, 0.1 * img.quantum_range)
:see: Example of :ref:`selective_blur`.
:param radius: Size of gaussian aperture.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of gaussian operator.
:type sigma: :class:`numbers.Real`
:param threshold: Only pixels within contrast threshold are effected.
Value should be between ``0.0`` and
:attr:`quantum_range`.
:type threshold: :class:`numbers.Real`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
assertions.assert_real(radius=radius, sigma=sigma, threshold=threshold)
if channel is None:
r = library.MagickSelectiveBlurImage(self.wand,
radius,
sigma,
threshold)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSelectiveBlurImageChannel(self.wand,
channel_ch,
radius,
sigma,
threshold)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickSelectiveBlurImage(self.wand,
radius,
sigma,
threshold)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def sepia_tone(self, threshold=0.8):
"""Creates a Sepia Tone special effect similar to a darkroom chemical
toning.
:see: Example of :ref:`sepia_tone`.
:param threshold: The extent of the toning. Value can be between ``0``
& :attr:`quantum_range`, or ``0`` & ``1.0``.
Default value is ``0.8`` or "80%".
:type threshold: :class:`numbers.Real`
.. versionadded:: 0.5.7
"""
assertions.assert_real(threshold=threshold)
if 0.0 < threshold <= 1.0:
threshold *= self.quantum_range
return library.MagickSepiaToneImage(self.wand, threshold)
@manipulative
@trap_exception
def shade(self, gray=False, azimuth=0.0, elevation=0.0):
"""Creates a 3D effect by simulating a light from an
elevated angle.
:see: Example of :ref:`shade`.
:param gray: Isolate the effect on pixel intensity.
Default is False.
:type gray: :class:`bool`
:param azimuth: Angle from x-axis.
:type azimuth: :class:`numbers.Real`
:param elevation: Amount of pixels from the z-axis.
:type elevation: :class:`numbers.Real`
.. versionadded:: 0.5.0
"""
assertions.assert_real(azimuth=azimuth, elevation=elevation)
return library.MagickShadeImage(self.wand, gray,
azimuth, elevation)
@manipulative
@trap_exception
def shadow(self, alpha=0.0, sigma=0.0, x=0, y=0):
"""Generates an image shadow.
:param alpha: Ratio of transparency.
:type alpha: :class:`numbers.Real`
:param sigma: Standard deviation of the gaussian filter.
:type sigma: :class:`numbers.Real`
:param x: x-offset.
:type x: :class:`numbers.Integral`
:param y: y-offset.
:type y: :class:`numbers.Integral`
.. versionadded:: 0.5.0
"""
assertions.assert_real(alpha=alpha, sigma=sigma)
assertions.assert_integer(x=x, y=y)
return library.MagickShadowImage(self.wand, alpha, sigma, x, y)
@manipulative
@trap_exception
def sharpen(self, radius=0.0, sigma=0.0, channel=None):
"""Applies a gaussian effect to enhance the sharpness of an
image.
.. note::
For best results, ensure ``radius`` is larger than
``sigma``.
Defaults values of zero will have ImageMagick attempt
to auto-select suitable values.
:see: Example of :ref:`sharpen`.
:param radius: size of gaussian aperture.
:type radius: :class:`numbers.Real`
:param sigma: Standard deviation of the gaussian filter.
:type sigma: :class:`numbers.Real`
:param channel: Optional color channel to target. See
:const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.0
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
assertions.assert_real(radius=radius, sigma=sigma)
if channel is None:
r = library.MagickSharpenImage(self.wand, radius, sigma)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSharpenImageChannel(self.wand,
channel_ch,
radius, sigma)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickSharpenImage(self.wand, radius, sigma)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def shave(self, columns=0, rows=0):
"""Remove pixels from the edges.
:param columns: amount to shave off both sides of the x-axis.
:type columns: :class:`numbers.Integral`
:param rows: amount to shave off both sides of the y-axis.
:type rows: :class:`numbers.Integral`
.. versionadded:: 0.5.0
"""
assertions.assert_integer(columns=columns, row=rows)
return library.MagickShaveImage(self.wand, columns, rows)
@manipulative
@trap_exception
def shear(self, background='WHITE', x=0.0, y=0.0):
"""Shears the image to create a parallelogram, and fill the space
created with a ``background`` color.
:param background: Color to fill the void created by shearing the
image.
:type background: :class:`wand.color.Color`
:param x: Slide the image along the X-axis.
:type x: :class:`numbers.Real`
:param y: Slide the image along the Y-axis.
:type y: :class:`numbers.Real`
.. versionadded:: 0.5.4
"""
if isinstance(background, string_type):
background = Color(background)
assertions.assert_color(background=background)
assertions.assert_real(x=x, y=y)
with background:
r = library.MagickShearImage(self.wand, background.resource, x, y)
return r
@manipulative
@trap_exception
def sigmoidal_contrast(self, sharpen=True, strength=0.0, midpoint=0.0,
channel=None):
"""Modifies the contrast of the image by applying non-linear sigmoidal
algorithm.
.. code:: python
with Image(filename='photo.jpg') as img:
img.sigmoidal_contrast(sharpen=True,
strength=3,
midpoint=0.65 * img.quantum_range)
:param sharpen: Increase the contrast when ``True`` (default), else
reduces contrast.
:type sharpen: :class:`bool`
:param strength: How much to adjust the contrast. Where a value of
``0.0`` has no effect, ``3.0`` is typical, and
``20.0`` is extreme.
:type strength: :class:`numbers.Real`
:param midpoint: Normalized value between `0.0` & :attr:`quantum_range`
:type midpoint: :class:`numbers.Real`
:param channel: Optional color channel to target. See
:const:`CHANNELS`.
:type channel: :class:`basestring`
.. versionadded:: 0.5.4
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
assertions.assert_bool(sharpen=sharpen)
assertions.assert_real(strength=strength, midpoint=midpoint)
if channel is None:
r = library.MagickSigmoidalContrastImage(self.wand,
sharpen,
strength,
midpoint)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSigmoidalContrastImageChannel(
self.wand, channel_ch, sharpen, strength, midpoint
)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickSigmoidalContrastImage(self.wand,
sharpen,
strength,
midpoint)
library.MagickSetImageChannelMask(self.wand, mask)
return r
def similarity(self, reference, threshold=0.0,
metric='undefined'):
"""Scan image for best matching ``reference`` image, and
return location & similarity.
Use parameter ``threshold`` to stop subimage scanning if the matching
similarity value is below the given value. This is the same as the CLI
``-similarity-threshold`` option.
This method will always return a location & the lowest computed
similarity value. Users are responsible for checking the similarity
value to determine if a matching location is valid. Traditionally, a
similarity value greater than `0.3183099` is considered dissimilar.
.. code:: python
from wand.image import Image
dissimilarity_threshold = 0.318
similarity_threshold = 0.05
with Image(filename='subject.jpg') as img:
with Image(filename='object.jpg') as reference:
location, diff = img.similarity(reference,
similarity_threshold)
if diff > dissimilarity_threshold:
print('Images too dissimilar to match')
elif diff <= similarity_threshold:
print('First match @ {left}x{top}'.format(**location))
else:
print('Best match @ {left}x{top}'.format(**location))
.. warning::
This operation can be slow to complete.
:param reference: Image to search for.
:type reference: :class:`wand.image.Image`
:param threshold: Stop scanning if reference similarity is
below given threshold. Value can be between ``0.0``
and :attr:`quantum_range`. Default is ``0.0``.
:type threshold: :class:`numbers.Real`
:param metric: specify which comparison algorithm to use. See
:const:`COMPARE_METRICS` for a list of values.
Only used by ImageMagick-7.
:type metric: :class:`basestring`
:returns: List of location & similarity value. Location being a
dictionary of ``width``, ``height``, ``left``, & ``top``.
The similarity value is the compare distance, so a value of
``0.0`` means an exact match.
:rtype: :class:`tuple` (:class:`dict`, :class:`numbers.Real`)
.. versionadded:: 0.5.4
has been added.
"""
assertions.assert_real(threshold=threshold)
if not isinstance(reference, BaseImage):
raise TypeError('reference must be in instance of '
'wand.image.Image, not ' + repr(reference))
rio = RectangleInfo(0, 0, 0, 0)
diff = ctypes.c_double(0.0)
if MAGICK_VERSION_NUMBER < 0x700:
artifact_value = binary(str(threshold)) # FIXME
library.MagickSetImageArtifact(self.wand,
b'compare:similarity-threshold',
artifact_value)
r = library.MagickSimilarityImage(self.wand,
reference.wand,
ctypes.byref(rio),
ctypes.byref(diff))
else: # pragma: no cover
assertions.string_in_list(COMPARE_METRICS,
'wand.image.COMPARE_METRICS',
metric=metric)
metric_idx = COMPARE_METRICS.index(metric)
r = library.MagickSimilarityImage(self.wand,
reference.wand,
metric_idx,
threshold,
ctypes.byref(rio),
ctypes.byref(diff))
if not r: # pragma: no cover
self.raise_exception()
else:
r = library.DestroyMagickWand(r)
location = dict(width=rio.width, height=rio.height,
top=rio.y, left=rio.x)
return (location, diff.value)
@manipulative
@trap_exception
def sketch(self, radius=0.0, sigma=0.0, angle=0.0):
"""Simulates a pencil sketch effect. For best results, ``radius``
value should be larger than ``sigma``.
:see: Example of :ref:`sketch`.
:param radius: size of Gaussian aperture.
:type radius: :class:`numbers.Real`
:param sigma: standard deviation of the Gaussian operator.
:type sigma: :class:`numbers.Real`
:param angle: direction of blur.
:type angle: :class:`numbers.Real`
.. versionadded:: 0.5.3
"""
assertions.assert_real(radius=radius, sigma=sigma, angle=angle)
return library.MagickSketchImage(self.wand, radius, sigma, angle)
@trap_exception
def smush(self, stacked=False, offset=0):
"""Appends all images together. Similar behavior to :meth:`concat`,
but with an optional offset between images.
:param stacked: If True, will join top-to-bottom. If False, join images
from left-to-right (default).
:type stacked: :class:`bool`
:param offset: Minimum space (in pixels) between each join.
:type offset: :class:`numbers.Integral`
.. versionadded:: 0.5.3
"""
assertions.assert_integer(offset=offset)
library.MagickResetIterator(self.wand)
result = library.MagickSmushImages(self.wand, bool(stacked), offset)
if result:
self.wand = result
self.reset_sequence()
return bool(result)
@manipulative
@trap_exception
def solarize(self, threshold=0.0, channel=None):
"""Simulates extreme overexposure.
:see: Example of :ref:`solarize`.
:param threshold: between ``0.0`` and :attr:`quantum_range`.
:type threshold: :class:`numbers.Real`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added ``channel`` argument.
"""
assertions.assert_real(threshold=threshold)
if channel is None:
r = library.MagickSolarizeImage(self.wand, threshold)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSolarizeImageChannel(self.wand,
channel_ch,
threshold)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickSolarizeImage(self.wand, threshold)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def sparse_color(self, method, colors, channel_mask=0x7):
"""Interpolates color values between points on an image.
The ``colors`` argument should be a dict mapping
:class:`~wand.color.Color` keys to coordinate tuples.
For example::
from wand.color import Color
from wand.image import Image
colors = {
Color('RED'): (10, 50),
Color('YELLOW'): (174, 32),
Color('ORANGE'): (74, 123)
}
with Image(filename='input.png') as img:
img.sparse_color('bilinear', colors)
The available interpolate methods are:
- ``'barycentric'``
- ``'bilinear'``
- ``'shepards'``
- ``'voronoi'``
- ``'inverse'``
- ``'manhattan'``
You can control which color channels are effected by building a custom
channel mask. For example::
from wand.image import Image, CHANNELS
with Image(filename='input.png') as img:
colors = {
img[50, 50]: (50, 50),
img[100, 50]: (100, 50),
img[50, 75]: (50, 75),
img[100, 100]: (100, 100)
}
# Only apply Voronoi to Red & Alpha channels
mask = CHANNELS['red'] | CHANNELS['alpha']
img.sparse_color('voronoi', colors, channel_mask=mask)
:param method: Interpolate method. See :const:`SPARSE_COLOR_METHODS`
:type method: :class:`basestring`
:param colors: A dictionary of :class:`~wand.color.Color` keys mapped
to an (x, y) coordinate tuple.
:type colors: :class:`abc.Mapping`
{ :class:`~wand.color.Color`: (int, int) }
:param channel_mask: Isolate specific color channels to apply
interpolation. Default to RGB channels.
:type channel_mask: :class:`numbers.Integral`
.. versionadded:: 0.5.3
"""
assertions.string_in_list(SPARSE_COLOR_METHODS,
'wand.image.SPARSE_COLOR_METHODS',
method=method)
if not isinstance(colors, abc.Mapping):
raise TypeError('Colors must be a dict, not' + repr(colors))
assertions.assert_unsigned_integer(channel_mask=channel_mask)
method_idx = SPARSE_COLOR_METHODS[method]
arguments = list()
for color, point in colors.items():
if isinstance(color, string_type):
color = Color(color)
x, y = point
arguments.append(x)
arguments.append(y)
with color as c:
if channel_mask & CHANNELS['red']:
arguments.append(c.red)
if channel_mask & CHANNELS['green']:
arguments.append(c.green)
if channel_mask & CHANNELS['blue']:
arguments.append(c.blue)
if channel_mask & CHANNELS['alpha']:
arguments.append(c.alpha)
argc = len(arguments)
args = (ctypes.c_double * argc)(*arguments)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSparseColorImage(self.wand,
channel_mask,
method_idx,
argc,
args)
else: # pragma: no cover
# Set active channel, and capture mask to restore.
channel_mask = library.MagickSetImageChannelMask(self.wand,
channel_mask)
r = library.MagickSparseColorImage(self.wand,
method_idx,
argc,
args)
# Restore original state of channels
library.MagickSetImageChannelMask(self.wand, channel_mask)
return r
@manipulative
@trap_exception
def splice(self, width=None, height=None, x=None, y=None, gravity=None):
"""Partitions image by splicing a ``width`` x ``height`` rectangle at
(``x``, ``y``) offset coordinate. The space inserted will be replaced
by the :attr:`background_color` value.
:param width: number of pixel columns.
:type width: :class:`numbers.Integral`
:param height: number of pixel rows.
:type height: :class:`numbers.Integral`
:param x: offset on the X-axis.
:type x: :class:`numbers.Integral`
:param y: offset on the Y-axis.
:type y: :class:`numbers.Integral`
.. versionadded:: 0.5.3
"""
ow, oh = self.size
if width is None:
width = ow
if height is None:
height = oh
assertions.assert_unsigned_integer(width=width, height=height)
if gravity is None:
if x is None:
x = 0
if y is None:
y = 0
else:
if x is not None or y is not None:
raise ValueError('x & y can not be used with gravity.')
y, x = self._gravity_to_offset(gravity, width, height)
assertions.assert_integer(x=x, y=y)
return library.MagickSpliceImage(self.wand, width, height, x, y)
@manipulative
@trap_exception
def spread(self, radius=0.0, method='undefined'):
"""Randomly displace pixels within a defined radius.
:see: Example of :ref:`spread`.
:param radius: Distance a pixel can be displaced from source. Default
value is ``0.0``, which will allow ImageMagick to auto
select a radius.
:type radius: :class:`numbers.Real`
:param method: Interpolation method. Only available with ImageMagick-7.
See :const:`PIXEL_INTERPOLATE_METHODS`.
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.7
Added default value to ``radius``.
"""
assertions.assert_real(radius=radius)
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
method=method)
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSpreadImage(self.wand, radius)
else: # pragma: no cover
r = library.MagickSpreadImage(self.wand, method_idx, radius)
return r
@manipulative
@trap_exception
def statistic(self, stat='undefined', width=None, height=None,
channel=None):
"""Replace each pixel with the statistic results from neighboring pixel
values. The ``width`` & ``height`` defines the size, or aperture, of
the neighboring pixels.
:see: Example of :ref:`statistic`.
:param stat: The type of statistic to calculate. See
:const:`STATISTIC_TYPES`.
:type stat: :class:`basestring`
:param width: The size of neighboring pixels on the X-axis.
:type width: :class:`numbers.Integral`
:param height: The size of neighboring pixels on the Y-axis.
:type height: :class:`numbers.Integral`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.5.3
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
"""
assertions.string_in_list(STATISTIC_TYPES,
'wand.image.STATISTIC_TYPES',
statistic=stat)
assertions.assert_integer(width=width, height=height)
stat_idx = STATISTIC_TYPES.index(stat)
if channel is None:
r = library.MagickStatisticImage(self.wand, stat_idx,
width, height)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickStatisticImageChannel(self.wand,
channel_ch,
stat_idx,
width,
height)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand,
channel_ch)
r = library.MagickStatisticImage(self.wand, stat_idx,
width, height)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def stegano(self, watermark, offset=0):
"""Hide a digital watermark of an image within the image.
.. code-block:: python
from wand.image import Image
# Embed watermark
with Image(filename='source.png') as img:
with Image(filename='gray_watermark.png') as watermark:
print('watermark size (for recovery)', watermark.size)
img.stegano(watermark)
img.save(filename='public.png')
# Recover watermark
with Image(width=w, height=h, pseudo='stegano:public.png') as img:
img.save(filename='recovered_watermark.png')
:param watermark: Image to hide within image.
:type watermark: :class:`wand.image.Image`
:param offset: Start embedding image after a number of pixels.
:type offset: :class:`numbers.Integral`
.. versionadded:: 0.5.4
"""
if not isinstance(watermark, BaseImage):
raise TypeError('Watermark image must be in instance of '
'wand.image.Image, not ' + repr(watermark))
assertions.assert_integer(offset=offset)
new_wand = library.MagickSteganoImage(self.wand, watermark.wand,
offset)
if new_wand:
self.wand = new_wand
self.reset_sequence()
return bool(new_wand)
@trap_exception
def strip(self):
"""Strips an image of all profiles and comments.
.. versionadded:: 0.2.0
"""
return library.MagickStripImage(self.wand)
@manipulative
@trap_exception
def swirl(self, degree=0.0, method="undefined"):
"""Swirls pixels around the center of the image. The larger the degree
the more pixels will be effected.
:see: Example of :ref:`swirl`.
:param degree: Defines the amount of pixels to be effected. Value
between ``-360.0`` and ``360.0``.
:type degree: :class:`numbers.Real`
:param method: Controls interpolation of the effected pixels. Only
available for ImageMagick-7. See
:const:`PIXEL_INTERPOLATE_METHODS`.
:type method: :class:`basestring`
.. versionadded:: 0.5.7
"""
assertions.assert_real(degree=degree)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickSwirlImage(self.wand, degree)
else: # pragma: no cover
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
method=method)
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
r = library.MagickSwirlImage(self.wand, degree, method_idx)
return r
@manipulative
@trap_exception
def texture(self, tile):
"""Repeat tile-image across the width & height of the image.
.. code:: python
from wand.image import Image
with Image(width=100, height=100) as canvas:
with Image(filename='tile.png') as tile:
canvas.texture(tile)
canvas.save(filename='output.png')
:param tile: image to repeat across canvas.
:type tile: :class:`Image <wand.image.BaseImage>`
.. versionadded:: 0.5.4
"""
if not isinstance(tile, BaseImage):
raise TypeError('Tile image must be an instance of '
'wand.image.Image, not ' + repr(tile))
r = library.MagickTextureImage(self.wand, tile.wand)
if r:
self.wand = r
return bool(r)
@manipulative
@trap_exception
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. A normalized float
between ``0.0`` and ``1.0``.
: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
"""
assertions.assert_real(threshold=threshold)
threshold *= self.quantum_range + 1
if channel is None:
r = library.MagickThresholdImage(self.wand, threshold)
else:
ch_const = self._channel_to_mask(channel)
r = library.MagickThresholdImageChannel(
self.wand, ch_const,
threshold
)
return r
@manipulative
@trap_exception
def thumbnail(self, width=None, height=None):
"""Changes the size of an image to the given dimensions and removes any
associated profiles. The goal is to produce small low cost thumbnail
images suited for display on the web.
: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.5.4
"""
if width is None:
width = self.width
if height is None:
height = self.height
assertions.assert_unsigned_integer(width=width, height=height)
return library.MagickThumbnailImage(self.wand, width, height)
@manipulative
@trap_exception
def tint(self, color=None, alpha=None):
"""Applies a color vector to each pixel in the image.
:see: Example of :ref:`tint`.
:param color: Color to calculate midtone.
:type color: :class:`~wand.color.Color`
:param alpha: Determine how to blend.
:type alpha: :class:`~wand.color.Color`
.. versionadded:: 0.5.3
"""
if isinstance(color, string_type):
color = Color(color)
if isinstance(alpha, string_type):
alpha = Color(alpha)
assertions.assert_color(color=color, alpha=alpha)
with color:
with alpha:
r = library.MagickTintImage(self.wand,
color.resource,
alpha.resource)
return r
@manipulative
@trap_exception
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.
**X**\\ ``:``\\ **Y**
Resize at a given aspect ratio. Common aspect ratios may
include ``4:3`` for video/tv, ``3:2`` for 35mm film, ``16:9`` for
HDTV, and ``2.39:1`` for cinema. Aspect ratio can be used with the
crop parameter, but is only available with ImageMagick version 7.0.8
or greater.
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 thin 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
.. versionchanged:: 0.5.0
Will call :meth:`crop()` followed by :meth:`resize()` in the event
that :c:func:`MagickTransformImage` is not available.
.. deprecated:: 0.6.0
Use :meth:`crop()` and :meth:`resize()` instead.
""" # 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.
assertions.assert_string(crop=crop, resize=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 not library.MagickTransformImage: # pragma: no cover
# Method removed from ImageMagick-7.
if crop:
x = ctypes.c_ssize_t(0)
y = ctypes.c_ssize_t(0)
width = ctypes.c_size_t(self.width)
height = ctypes.c_size_t(self.height)
if b':' in crop: # For "4:3" aspect cropping.
libmagick.ParseMetaGeometry(crop,
ctypes.byref(x),
ctypes.byref(y),
ctypes.byref(width),
ctypes.byref(height))
else:
libmagick.GetGeometry(crop,
ctypes.byref(x),
ctypes.byref(y),
ctypes.byref(width),
ctypes.byref(height))
self.crop(top=y.value,
left=x.value,
width=width.value,
height=height.value,
reset_coords=False)
if resize:
x = ctypes.c_ssize_t(0)
y = ctypes.c_ssize_t(0)
width = ctypes.c_size_t(self.width)
height = ctypes.c_size_t(self.height)
libmagick.ParseMetaGeometry(resize,
ctypes.byref(x),
ctypes.byref(y),
ctypes.byref(width),
ctypes.byref(height))
self.resize(width=width.value,
height=height.value)
# Both `BaseImage.crop` & `BaseImage.resize` will handle
# animation & error handling, so we can stop here.
return True
if self.animation:
new_wand = library.NewMagickWand()
src_wand = library.MagickCoalesceImages(self.wand)
length = library.MagickGetNumberImages(self.wand)
for i in xrange(length):
library.MagickSetIteratorIndex(src_wand, i)
tmp_wand = library.MagickTransformImage(src_wand,
crop,
resize)
library.MagickAddImage(new_wand, tmp_wand)
if bool(tmp_wand):
library.DestroyMagickWand(tmp_wand)
if bool(src_wand):
library.DestroyMagickWand(src_wand)
self.reset_sequence()
else:
new_wand = library.MagickTransformImage(self.wand, crop, resize)
if new_wand:
self.wand = new_wand
return bool(new_wand)
@manipulative
@trap_exception
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
"""
assertions.string_in_list(COLORSPACE_TYPES,
'wand.image.COLORSPACE_TYPES',
colorspace=colorspace_type)
return library.MagickTransformImageColorspace(
self.wand,
COLORSPACE_TYPES.index(colorspace_type)
)
@manipulative
@trap_exception
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.Real`
:param invert: Boolean to tell to paint the inverse selection.
:type invert: :class:`bool`
.. versionadded:: 0.3.0
.. versionchanged:: 0.6.3
Parameter ``fuzz`` type switched from Integral to Real.
"""
assertions.assert_real(alpha=alpha, fuzz=fuzz)
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
with color:
r = library.MagickTransparentPaintImage(self.wand, color.resource,
alpha, fuzz, invert)
return r
@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
if MAGICK_VERSION_NUMBER < 0x700:
image_type = 'truecolormatte'
else: # pragma: no cover
image_type = 'truecoloralpha'
library.MagickSetImageType(self.wand,
IMAGE_TYPES.index(image_type))
# Perform the black channel subtraction
self.evaluate(operator='subtract',
value=t.value,
channel='opacity')
self.raise_exception()
@manipulative
@trap_exception
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
"""
return library.MagickTransposeImage(self.wand)
@manipulative
@trap_exception
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
"""
return library.MagickTransverseImage(self.wand)
@manipulative
@trap_exception
def trim(self, color=None, fuzz=0.0, reset_coords=False,
percent_background=None, background_color=None):
"""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. Value can be between ``0.0``,
and :attr:`quantum_range`.
:type fuzz: :class:`numbers.Real`
:param reset_coords: Reset coordinates after trimming image. Default
``False``.
:type reset_coords: :class:`bool`
:param percent_background: Sets how aggressive the trim operation will
be. A value of `0.0` will trim to the
minimal bounding box of all matching color,
and `1.0` to the most outer edge.
:type percent_background: :class:`numbers.Real`
:param background_color: Local alias to :attr:`background_color`,
and has the same effect as defining ``color``
parameter -- but much faster.
.. versionadded:: 0.2.1
.. versionchanged:: 0.3.0
Optional ``color`` and ``fuzz`` parameters.
.. versionchanged:: 0.5.2
The ``color`` parameter may accept color-compliant strings.
.. versionchanged:: 0.6.0
Optional ``reset_coords`` parameter added.
.. versionchanged:: 0.6.4
Optional ``percent_background`` & ``background_color`` parameters
have been added.
"""
use_border = background_color is None
if use_border:
if color is None:
color = self[0, 0]
elif isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
with color:
self.border(color, 1, 1, compose='copy')
assertions.assert_real(fuzz=fuzz)
assertions.assert_bool(reset_coords=reset_coords)
if percent_background is not None:
assertions.assert_real(percent_background=percent_background)
percent_background = max(min(percent_background, 1.0), 0.0) * 100.0
str_pb = '{0:g}%'.format(percent_background)
library.MagickSetImageArtifact(self.wand,
binary('trim:percent-background'),
binary(str_pb))
if not use_border:
if isinstance(background_color, string_type):
background_color = Color(background_color)
assertions.assert_color(background_color=background_color)
bc_key = 'trim:background-color'
bc_val = background_color.string
library.MagickSetImageArtifact(self.wand,
binary(bc_key),
binary(bc_val))
r = library.MagickTrimImage(self.wand, fuzz)
if reset_coords:
self.reset_coords()
elif use_border:
# Re-calculate page coordinates as we added a 1x1 border before
# applying the trim.
adjusted_coords = list(self.page)
# Width & height are unsigned.
adjusted_coords[0] = max(adjusted_coords[0] - 2, 0)
adjusted_coords[1] = max(adjusted_coords[1] - 2, 0)
# X & Y are signed. It's common for page offsets to be negative.
adjusted_coords[2] -= 1
adjusted_coords[3] -= 1
self.page = adjusted_coords
return r
@manipulative
@trap_exception
def unique_colors(self):
"""Discards all duplicate pixels, and rebuilds the image
as a single row.
.. versionadded:: 0.5.0
"""
return library.MagickUniqueImageColors(self.wand)
@manipulative
@trap_exception
def unsharp_mask(self, radius=0.0, sigma=1.0, amount=1.0, threshold=0.0,
channel=None):
"""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.
:see: Example of :ref:`unsharp_mask`.
: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 difference amount.
:type threshold: :class:`numbers.Real`
:param channel: Optional color channel to target. See
:const:`CHANNELS`
:type channel: :class:`basestring`
.. versionadded:: 0.3.4
.. versionchanged:: 0.5.5
Added optional ``channel`` argument.
.. versionchanged:: 0.5.7
Added default values to match CLI behavior.
"""
assertions.assert_real(radius=radius, sigma=sigma,
amount=amount, threshold=threshold)
if channel is None:
r = library.MagickUnsharpMaskImage(self.wand, radius, sigma,
amount, threshold)
else:
channel_ch = self._channel_to_mask(channel)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickUnsharpMaskImageChannel(
self.wand, channel_ch, radius, sigma, amount, threshold
)
else: # pragma: no cover
mask = library.MagickSetImageChannelMask(self.wand, channel_ch)
r = library.MagickUnsharpMaskImage(self.wand, radius, sigma,
amount, threshold)
library.MagickSetImageChannelMask(self.wand, mask)
return r
@manipulative
@trap_exception
def vignette(self, radius=0.0, sigma=0.0, x=0, y=0):
"""Creates a soft vignette style effect on the image.
:see: Example of :ref:`vignette`.
:param radius: the radius of the Gaussian blur effect.
:type radius: :class:`numbers.Real`
:param sigma: the standard deviation of the Gaussian effect.
:type sigma: :class:`numbers.Real`
:param x: Number of pixels to offset inward from the top & bottom of
the image before drawing effect.
:type x: :class:`numbers.Integral`
:param y: Number of pixels to offset inward from the left & right of
the image before drawing effect.
:type y: :class:`numbers.Integral`
.. versionadded:: 0.5.2
"""
assertions.assert_real(radius=radius, sigma=sigma)
return library.MagickVignetteImage(self.wand, radius, sigma, x, y)
@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)
watermark_image.clamp()
self.composite(watermark_image, left=left, top=top)
self.raise_exception()
@manipulative
@trap_exception
def wave(self, amplitude=0.0, wave_length=0.0, method='undefined'):
"""Creates a ripple effect within the image.
:see: Example of :ref:`wave`.
:param amplitude: height of wave form.
:type amplitude: :class:`numbers.Real`
:param wave_length: width of wave form.
:type wave_length: :class:`numbers.Real`
:param method: pixel interpolation method. Only available with
ImageMagick-7. See :const:`PIXEL_INTERPOLATE_METHODS`
:type method: :class:`basestring`
.. versionadded:: 0.5.2
"""
assertions.assert_real(amplitude=amplitude, wave_length=wave_length)
assertions.string_in_list(PIXEL_INTERPOLATE_METHODS,
'wand.image.PIXEL_INTERPOLATE_METHODS',
method=method)
if MAGICK_VERSION_NUMBER < 0x700:
r = library.MagickWaveImage(self.wand, amplitude, wave_length)
else: # pragma: no cover
method_idx = PIXEL_INTERPOLATE_METHODS.index(method)
r = library.MagickWaveImage(self.wand, amplitude, wave_length,
method_idx)
return r
@manipulative
@trap_exception
def wavelet_denoise(self, threshold=0.0, softness=0.0):
"""Removes noise by applying a `wavelet transform`_.
.. _`wavelet transform`:
https://en.wikipedia.org/wiki/Wavelet_transform
.. warning::
This class method is only available with ImageMagick 7.0.8-41, or
greater.
:see: Example of :ref:`wavelet_denoise`.
:param threshold: Smoothing limit.
:type threshold: :class:`numbers.Real`
:param softness: Attenuate of the smoothing threshold.
:type softness: :class:`numbers.Real`
:raises WandLibraryVersionError: If system's version of ImageMagick
does not support this method.
.. versionadded:: 0.5.5
"""
if library.MagickWaveletDenoiseImage is None:
msg = 'Method requires ImageMagick version 7.0.8-41 or greater.'
raise WandLibraryVersionError(msg)
assertions.assert_real(threshold=threshold, softness=softness)
if 0.0 < threshold <= 1.0:
threshold *= self.quantum_range
if 0.0 < softness <= 1.0:
softness *= self.quantum_range
return library.MagickWaveletDenoiseImage(self.wand, threshold,
softness)
@manipulative
@trap_exception
def white_balance(self):
"""Uses LAB colorspace to apply a white balance to the image.
.. note::
Requires ImageMagick-7.0.10-37 or later.
.. versionadded:: 0.6.4
"""
msg = 'Requires ImageMagick-7.0.10-37, or later.'
if MAGICK_VERSION_NUMBER < 0x70A:
raise WandLibraryVersionError(msg)
elif library.MagickWhiteBalanceImage is None:
raise WandLibraryVersionError(msg)
return library.MagickWhiteBalanceImage(self.wand)
@manipulative
@trap_exception
def white_threshold(self, threshold):
"""Forces all pixels above a given color as white. Leaves pixels
below threshold unaltered.
:param threshold: Color to be referenced as a threshold.
:type threshold: :class:`Color`
.. versionadded:: 0.5.2
"""
if isinstance(threshold, string_type):
threshold = Color(threshold)
assertions.assert_color(threshold=threshold)
with threshold:
r = library.MagickWhiteThresholdImage(self.wand,
threshold.resource)
return r
@trap_exception
def write_mask(self, clip_mask=None):
"""Sets the write mask which prevents pixel-value updates to the image.
Call this method with a ``None`` argument to clear any previously set
masks.
.. warning::
This method is only available with ImageMagick-7.
:param clip_mask: Image to reference as blend mask.
:type clip_mask: :class:`BaseImage`
.. versionadded:: 0.5.7
"""
r = False
WritePixelMask = 0x000002
if library.MagickSetImageMask is None:
raise WandLibraryVersionError('Method requires ImageMagick-7.')
else: # pragma: no cover
if clip_mask is None:
w, h = self.size
with Image(width=w, height=h, pseudo='xc:none') as clear:
r = library.MagickSetImageMask(self.wand,
WritePixelMask,
clear.wand)
elif isinstance(clip_mask, BaseImage):
r = library.MagickSetImageMask(self.wand, WritePixelMask,
clip_mask.wand)
return r
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. Additional
:ref:`read_mods` are supported.
: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 image 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.abc.Sequence`,
:Class:`numbers.Integral`
:param colorspace: sets the stack's default colorspace value before
reading any images.
See :const:`COLORSPACE_TYPES`.
:type colorspace: :class:`basestring`
:param units: paired with ``resolution`` for defining an image's pixel
density. See :const:`UNIT_TYPES`.
:type units: :class:`basestring`
.. 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.
.. versionadded:: 0.5.0
The ``pseudo`` parameter.
.. versionchanged:: 0.5.4
Read constructor no longer sets "transparent" background by default.
Use the ``background`` parameter to specify canvas color when reading
in image.
.. versionchanged:: 0.5.7
Added the ``colorspace`` & ``units`` parameter.
.. versionchanged:: 0.6.3
Added ``sampling_factors`` parameter for working with YUV streams.
.. 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:`ArtifactTree`) A dict mapping to image artifacts.
#: Similar to :attr:`metadata`, but used to alter behavior of various
#: internal operations.
#:
#: .. versionadded:: 0.5.0
artifacts = 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
#: (:class:`Metadata`) The metadata mapping of the image. Read only.
#:
#: .. versionadded:: 0.3.0
metadata = None
#: (:class:`ProfileDict`) The mapping of image profiles.
#:
#: .. versionadded:: 0.5.1
profiles = None
def __init__(self, image=None, blob=None, file=None, filename=None,
pseudo=None, background=None, colorspace=None, depth=None,
extract=None, format=None, height=None, interlace=None,
resolution=None, sampling_factors=None, units=None,
width=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')
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):
self._preamble_read(
background=background, colorspace=colorspace, depth=depth,
extract=extract, format=format, height=height,
interlace=interlace, resolution=resolution,
sampling_factors=sampling_factors, width=width
)
if file is not None:
self.read(file=file)
elif blob is not None:
self.read(blob=blob)
elif filename is not None:
self.read(filename=filename)
# 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:
if pseudo is None:
self.blank(width, height, background)
else:
self.pseudo(width, height, pseudo)
if depth:
r = library.MagickSetImageDepth(self.wand, depth)
if not r:
raise self.raise_exception()
if units is not None:
self.units = units
self.metadata = Metadata(self)
self.artifacts = ArtifactTree(self)
from .sequence import Sequence
self.sequence = Sequence(self)
self.profiles = ProfileDict(self)
self.raise_exception()
def __repr__(self):
return super(Image, self).__repr__(
extra_format=' {self.format!r} ({self.width}x{self.height})'
)
def _preamble_read(self, background=None, colorspace=None, depth=None,
extract=None, format=None, height=None, interlace=None,
resolution=None, sampling_factors=None, units=None,
width=None):
"""Set-up MagickWand properties before reading an image file. The
properties are unique to the image decoder.
:param background: Defines the default background color.
:type background: :class:`Color`, :class:`basestring`
:param colorspace: Defines what colorspace the decoder should operate
in. See :const:`COLORSPACE_TYPES`.
:type colorspace: :class:`basestring`
:param depth: Bits per color sample.
:type depth: :class:`numbers.Integral`
:param extract: Only decode a sub-region of the image.
:type extract: :class:`basestring`
:param format: Defines the decoder image format.
:type format: :class:`basestring`
:param height: Defines how high a blank canvas should be. Only used if
``width`` is also defined.
:type height: :class:`numbers.Integral`
:param interlace: Defines the interlacing scheme for raw data streams.
See :const:`INTERLACE_TYPES`.
:type interlace: :class:`basestring`
:param resolution: Defines the pixel density of a scalable formats.
PDF & SVG as examples.
:type resolution: :class:`collections.abc.Sequence`,
:class:`numbers.Integral`
:param sampling_factors: Defines how a YUV might be upsampled.
:type sampling_factors: :class:`collections.abc.Sequence`,
:class:`basestring`
:param units: Unused.
:type units: :class:`numbers.Integral`
:param width: Defines how wide a blank canvas should be. Only used if
``height`` is also defined.
:type width: :class:`numbers.Intragal`
.. versionadded:: 0.6.3
"""
if background:
if isinstance(background, string_type):
background = Color(background)
assertions.assert_color(background=background)
with background:
library.MagickSetBackgroundColor(self.wand,
background.resource)
if colorspace is not None:
assertions.string_in_list(
COLORSPACE_TYPES,
'wand.image.COLORSPACE_TYPES',
colorspace=colorspace
)
colorspace_idx = COLORSPACE_TYPES.index(colorspace)
library.MagickSetColorspace(self.wand, colorspace_idx)
if depth is not None:
assertions.assert_counting_number(depth=depth)
library.MagickSetDepth(self.wand, depth)
if extract is not None:
assertions.assert_string(extract=extract)
library.MagickSetExtract(self.wand, binary(extract))
if format is not None:
assertions.assert_string(format=format)
library.MagickSetFormat(self.wand, binary(format))
library.MagickSetFilename(self.wand, b'buffer.' + binary(format))
if interlace is not None:
assertions.string_in_list(
INTERLACE_TYPES,
'wand.image.INTERLACE_TYPES',
interlace=interlace
)
c_interlace = INTERLACE_TYPES.index(interlace)
library.MagickSetInterlaceScheme(self.wand, c_interlace)
if resolution is not None:
if (isinstance(resolution, abc.Sequence) and
len(resolution) == 2):
library.MagickSetResolution(self.wand, *resolution)
elif isinstance(resolution, numbers.Real):
library.MagickSetResolution(self.wand, resolution, resolution)
else:
raise TypeError('resolution must be a (x, y) pair or an '
'real number of the same x/y')
if sampling_factors is not None:
self.sampling_factors = sampling_factors
if width is not None and height is not None:
assertions.assert_counting_number(width=width, height=height)
library.MagickSetSize(self.wand, width, height)
def _repr_png_(self):
with self.convert('png') as cloned:
return cloned.make_blob()
@classmethod
def from_array(cls, array, channel_map=None, storage=None):
"""Create an image instance from a :mod:`numpy` array, or any other
datatype that implements `__array_interface__`__ protocol.
.. code::
import numpy
from wand.image import Image
matrix = numpy.random.rand(100, 100, 3)
with Image.from_array(matrix) as img:
img.save(filename='noise.png')
Use the optional ``channel_map`` & ``storage`` arguments to specify
the order of color channels & data size. If ``channel_map`` is omitted,
this method will will guess ``"RGB"``, or ``"CMYK"`` based on
array shape. If ``storage`` is omitted, this method will reference the
array's ``typestr`` value, and raise a :class:`ValueError` if
storage-type can not be mapped.
Float values must be normalized between `0.0` and `1.0`, and signed
integers should be converted to unsigned values between `0` and
max value of type.
Instances of :class:`Image` can also be exported to numpy arrays::
with Image(filename='rose:') as img:
matrix = numpy.array(img)
__ https://docs.scipy.org/doc/numpy/reference/arrays.interface.html
:param array: Numpy array of pixel values.
:type array: :class:`numpy.array`
:param channel_map: Color channel layout.
:type channel_map: :class:`basestring`
:param storage: Datatype per pixel part.
:type storage: :class:`basestring`
:returns: New instance of an image.
:rtype: :class:`~wand.image.Image`
.. versionadded:: 0.5.3
.. versionchanged:: 0.6.0
Input ``array`` now expects the :attr:`shape` property to be defined
as ```( 'height', 'width', 'channels' )```.
"""
arr_itr = array.__array_interface__
typestr = arr_itr['typestr'] # Required by interface.
shape = arr_itr['shape'] # Required by interface.
if storage is None:
# Attempt to guess storage
storage_map = dict(u1='char', i1='char',
u2='short', i2='short',
u4='integer', i4='integer',
u8='long', i8='integer',
f4='float', f8='double')
for token in storage_map:
if token in typestr:
storage = storage_map[token]
break
if storage is None:
raise ValueError('Unable to determine storage type.')
if channel_map is None:
# Attempt to guess channel map
if len(shape) == 3:
if shape[2] < 5:
channel_map = 'RGBA'[0:shape[2]]
else:
channel_map = 'CMYKA'[0:shape[2]]
else:
channel_map = 'R'
strides = arr_itr.get('strides', None)
if hasattr(array, 'ctypes') and strides is None:
data_ptr = array.ctypes.data_as(ctypes.c_void_p)
elif hasattr(array, 'tobytes'):
data_ptr = array.tobytes()
elif hasattr(array, 'tostring'):
data_ptr = array.tostring()
else:
data_ptr, _ = arr_itr.get('data')
storage_idx = STORAGE_TYPES.index(storage)
height, width = shape[:2]
genesis()
wand = library.NewMagickWand()
instance = cls(BaseImage(wand))
r = library.MagickConstituteImage(instance.wand,
width,
height,
binary(channel_map),
storage_idx,
data_ptr)
if not r:
instance.raise_exception(cls)
return instance
@classmethod
def ping(cls, file=None, filename=None, blob=None, **kwargs):
"""Ping image header into Image() object, but without any pixel data.
This is useful for inspecting image meta-data without decoding the
whole image.
: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 vector formats (like PDF)
:type resolution: :class:`collections.abc.Sequence`,
:class:`numbers.Integral`
:param format: suggest image file format when reading from a ``blob``,
or ``file`` property.
:type format: :class:`basestring`
.. versionadded:: 0.5.6
"""
r = None
instance = cls()
instance._preamble_read(**kwargs)
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.MagickPingImageFile(instance.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, abc.Iterable):
raise TypeError('blob must be iterable, not ' +
repr(blob))
if not isinstance(blob, binary_type):
blob = b''.join(blob)
r = library.MagickPingImageBlob(instance.wand, blob, len(blob))
elif filename is not None:
filename = encode_filename(filename)
r = library.MagickPingImage(instance.wand, filename)
if not r:
instance.raise_exception()
msg = ('MagickPingImage returns false, but did raise ImageMagick '
'exception. This can occur when a delegate is missing, or '
'returns EXIT_SUCCESS without generating a raster.')
raise WandRuntimeError(msg)
else:
units = kwargs.get('units')
if units is not None:
instance.units = units
instance.metadata = Metadata(instance)
instance.artifacts = ArtifactTree(instance)
from .sequence import Sequence
instance.sequence = Sequence(instance)
instance.profiles = ProfileDict(instance)
return instance
@classmethod
def stereogram(cls, left, right):
"""Create a new stereogram image from two existing images.
:see: Example of :ref:`stereogram`.
:param left: Left-eye image.
:type left: :class:`wand.image.Image`
:param right: Right-eye image.
:type right: :class:`wand.image.Image`
.. versionadded:: 0.5.4
"""
if not isinstance(left, BaseImage):
raise TypeError('Left image must be in instance of '
'wand.image.Image, not ' + repr(left))
if not isinstance(right, BaseImage):
raise TypeError('Right image must be in instance of '
'wand.image.Image, not ' + repr(right))
wand = library.MagickStereoImage(left.wand, right.wand)
if not wand: # pragma: no cover
left.raise_exception()
return cls(BaseImage(wand))
@property
def animation(self):
is_gif = self.mimetype in ('image/gif', 'image/x-gif')
frames = library.MagickGetNumberImages(self.wand)
return is_gif and frames > 1
@property
def mimetype(self):
"""(:class:`basestring`) The MIME type of the image
e.g. ``'image/jpeg'``, ``'image/png'``.
.. versionadded:: 0.1.7
"""
mtype = None
rp = libmagick.MagickToMime(binary(self.format))
if rp:
mtype = text(ctypes.string_at(rp))
rp = libmagick.DestroyString(rp)
return mtype
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 image.
: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
"""
assertions.assert_counting_number(width=width, height=height)
if background is None:
background = Color('transparent')
elif isinstance(background, string_type):
background = Color(background)
assertions.assert_color(background=background)
with background:
r = library.MagickNewImage(self.wand, width, height,
background.resource)
if not r:
self.raise_exception()
return self
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 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 compare_layers(self, method):
"""Generates new images showing the delta pixels between
layers. Similar pixels are converted to transparent.
Useful for debugging complex animations. ::
with img.compare_layers('compareany') as delta:
delta.save(filename='framediff_%02d.png')
.. note::
May not work as expected if animations are already
optimized.
:param method: Can be ``'compareany'``,
``'compareclear'``, or ``'compareoverlay'``
:type method: :class:`basestring`
:returns: new image stack.
:rtype: :class:`Image`
.. versionadded:: 0.5.0
"""
if not isinstance(method, string_type):
raise TypeError('method must be a string from IMAGE_LAYER_METHOD, '
'not ' + repr(method))
if method not in ('compareany', 'compareclear', 'compareoverlay'):
raise ValueError('method can only be \'compareany\', '
'\'compareclear\', or \'compareoverlay\'')
r = None
m = IMAGE_LAYER_METHOD.index(method)
if MAGICK_VERSION_NUMBER >= 0x700: # pragma: no cover
r = library.MagickCompareImagesLayers(self.wand, m)
elif library.MagickCompareImageLayers:
r = library.MagickCompareImageLayers(self.wand, m)
elif library.MagickCompareImagesLayers: # pragma: no cover
r = library.MagickCompareImagesLayers(self.wand, m)
else:
raise AttributeError('MagickCompareImageLayers method '
'not available on system.')
if not r:
self.raise_exception()
return Image(image=BaseImage(r))
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
.. versionchanged:: 0.6.11
Call :c:func:`MagickSetFormat` method after
:c:func:`MagickSetImageFormat`. This will ensure image info, magick,
and filename properties are aligned.
"""
cloned = self.clone()
cloned.format = format
library.MagickSetFormat(cloned.wand,
binary(format.strip().upper()))
return cloned
def data_url(self):
"""Generate a base64 `data-url`_ string from the loaded image.
Useful for converting small graphics into ASCII strings for HTML/CSS
web development.
.. _data-url: https://en.wikipedia.org/wiki/Data_URI_scheme
:returns: a data-url formatted string.
:rtype: :class:`basestring`
.. versionadded:: 0.6.3
"""
from base64 import b64encode
mime_type = self.mimetype
base_bytes = b64encode(self.make_blob())
return "data:{0};base64,{1}".format(mime_type, text(base_bytes))
@trap_exception
def image_add(self, image):
"""Copies a given image on to the image stack. By default, the added
image will be append at the end of the stack, or immediately after
the current image iterator defined by :meth:`~BaseImage.iterator_set`.
Use :meth:`~BaseImage.iterator_reset` before calling this method to
insert the new image before existing images on the stack.
:param image: raster to add.
:type image: :class:`Image`
.. versionadded:: 0.6.7
"""
if not isinstance(image, Image):
raise TypeError('image must be instance of wand.image.Image')
return library.MagickAddImage(self.wand, image.wand)
def image_get(self):
"""Generate & return a clone of a single image at the current
image-stack index.
.. versionadded:: 0.6.7
"""
r = library.MagickGetImage(self.wand)
if not r:
self.raise_exception()
return None # noqa - Safety if exception isn't thrown.
return Image(BaseImage(r))
@trap_exception
def image_remove(self):
"""Remove an image from the image-stack at the current index.
.. versionadded:: 0.6.7
"""
return library.MagickRemoveImage(self.wand)
@trap_exception
def image_set(self, image):
"""Overwrite current image on the image-stack with given image.
:param image: Wand instance of images to write to stack.
:type image: :class:`wand.image.Image`
.. versionadded:: 0.6.7
"""
if not isinstance(image, Image):
raise TypeError('image must be an instance of wand.image.Image,',
' not ' + repr(image))
return library.MagickSetImage(self.wand, image.wand)
@trap_exception
def image_swap(self, i, j):
"""Swap two images on the image-stack.
:param i: image index to replace with ``j``
:type i: :class:`numbers.Integral`
:param j: image index to replace with ``i``
:type j: :class:`numbers.Integral`
.. versionadded:: 0.6.7
"""
assertions.assert_integer(i=i)
assertions.assert_integer(j=j)
op = self.iterator_get()
self.iterator_set(i)
with self.image_get() as a:
self.iterator_set(j)
with self.image_get() as b:
self.image_set(a)
self.iterator_set(i)
self.image_set(b)
self.iterator_set(op)
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 library.MagickGetNumberImages(self.wand) > 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)
blob_p = library.MagickRelinquishMemory(blob_p)
return blob
else: # pragma: no cover
self.raise_exception()
@trap_exception
def montage(self, font=None, tile=None, thumbnail=None, mode='unframe',
frame=None):
"""Generates a new image containing thumbnails if each previous image
read. ::
with Image() as img:
for file_path in ['first.png', 'second.png', 'third.png']:
with Image(filename=file_path) as item:
img.options['label'] = file_path
img.image_add(item)
style = Font('monospace', 24, 'green')
img.montage(font=style, tile='3x1', thumbnail='15x15')
img.save(filename='montage.png')
:param font: Define font style to use when labeling each thumbnail.
Thumbnail labeling will only be rendered if ``'label'``
value in :attr:`options` dict is defined.
:type font: :class:`~wand.font.Font`
:param tile: The number of thunbnails per rows & column on a page.
Example: ``"6x4"``.
:type tile: :class:`basestring`
:param thumbnail: Preferred image size. Montage will attempt to
generate a thumbnail to match the geometry. This
can also define the border size on each thumbnail.
Example: ``"120x120x+4+3>"``.
:type thumbnail: :class:`basestring`
:param mode: Which effect to render. Options include ``"frame"``,
``"unframe"``, and ``"concatenate"``. Default ``"frame"``.
:type mode: :class:`basestring`
:param frame: Define ornamental boarder around each thrumbnail.
The color of the frame is defined by the image's matte
color. Example: ``"15x15+3+3"``.
:type frame: :class:`basestring`
.. versionadded:: 0.6.8
"""
if font is not None:
if not isinstance(font, Font):
msg = 'font must be an instance of wand.font.Font'
raise TypeError(msg)
else:
font = Font('helvetica', 16, 'black')
if tile is not None:
assertions.assert_string(tile=tile)
tile = binary(tile)
if thumbnail is not None:
assertions.assert_string(thumbnail=thumbnail)
thumbnail = binary(thumbnail)
assertions.in_list(MONTAGE_MODES,
'wand.image.MONTAGE_MODES',
mode=mode)
mode_idx = MONTAGE_MODES.index(mode)
if frame is not None:
assertions.assert_string(frame=frame)
frame = binary(frame)
ctx_ptr = library.NewDrawingWand()
if font.path:
library.DrawSetFont(ctx_ptr, binary(font.path))
library.DrawSetFontFamily(ctx_ptr, binary(font.path))
if font.size:
library.DrawSetFontSize(ctx_ptr, font.size)
if font.color:
with font.color:
library.DrawSetFillColor(ctx_ptr, font.color.resource)
if font.stroke_color:
with font.stroke_color:
library.DrawSetStrokeColor(ctx_ptr, font.stroke_color.resource)
new_wand = library.MagickMontageImage(self.wand, ctx_ptr, tile,
thumbnail, mode_idx, frame)
ctx_ptr = library.DestroyDrawingWand(ctx_ptr)
ok = bool(new_wand)
if ok:
self.wand = new_wand
self.reset_sequence()
return ok
def pseudo(self, width, height, pseudo='xc:'):
"""Creates a new image from ImageMagick's internal protocol coders.
:param width: Total columns of the new image.
:type width: :class:`numbers.Integral`
:param height: Total rows of the new image.
:type height: :class:`numbers.Integral`
:param pseudo: The protocol & arguments for the pseudo image.
:type pseudo: :class:`basestring`
.. versionadded:: 0.5.0
"""
assertions.assert_counting_number(width=width, height=height)
assertions.assert_string(pseudo=pseudo)
r = library.MagickSetSize(self.wand, width, height)
if not r:
self.raise_exception()
r = library.MagickReadImage(self.wand, encode_filename(pseudo))
if not r:
self.raise_exception()
def read(self, file=None, filename=None, blob=None, background=None,
colorspace=None, depth=None, extract=None, format=None,
height=None, interlace=None, resolution=None,
sampling_factors=None, units=None, width=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.
Additional :ref:`read_mods` are supported.
:type filename: :class:`basestring`
:param background: set default background color.
:type background: :class:`Color`, :class:`basestring`
:param colorspace: set default colorspace.
See :const:`COLORSPACE_TYPES`.
:type colorspace: :class:`basestring`
:param depth: sets bits per color sample. Usually ``8``, or ``16``.
:type depth: :class:`numbers.Integral`
:param format: sets which image decoder to read with. Helpful when
reading ``blob`` data with ambiguous headers.
:type format: :class:`basestring`
:param height: used with ``width`` to define the canvas size. Useful
for reading image streams.
:type height: :class:`numbers.Integral`
:param interlace: Defines the interlacing scheme for raw data streams.
See :const:`INTERLACE_TYPES`.
:type interlace: :class:`basestring`
:param resolution: set a resolution value (DPI),
useful for vectorial formats (like PDF)
:type resolution: :class:`collections.abc.Sequence`,
:class:`numbers.Integral`
:param sampling_factors: set up/down stampling factors for YUV data
stream. Usually ``"4:2:2"``
:type sampling_factors: :class:`collections.abc.Sequence`,
:class:`basestring`
:param units: used with ``resolution``, can either be
``'pixelperinch'``, or ``'pixelpercentimeter'``.
:type units: :class:`basestring`
:param width: used with ``height`` to define the canvas size. Useful
for reading image streams.
:type width: :class:`numbers.Integral`
.. versionadded:: 0.3.0
.. versionchanged:: 0.5.7
Added ``units`` parameter.
.. versionchanged:: 0.6.3
Added, or documented, optional pre-read parameters:
``background``, ``colorspace``, ``depth``, ``format``, ``height``,
``interlace``, ``sampling_factors``, & ``width``.
"""
r = None
# Resolution must be set after image reading.
self._preamble_read(
background=background, colorspace=colorspace, depth=depth,
extract=extract, format=format, height=height, interlace=interlace,
resolution=resolution, sampling_factors=sampling_factors,
width=width
)
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, abc.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()
msg = ('MagickReadImage returns false, but did not raise '
'ImageMagick exception. This can occur when a delegate '
'is missing, or returns EXIT_SUCCESS without generating a '
'raster.')
raise WandRuntimeError(msg)
else:
if units is not None:
self.units = units
def reset_sequence(self):
"""Remove any previously allocated :class:`~wand.sequence.SingleImage`
instances in :attr:`sequence` attribute.
.. versionadded:: 0.6.0
"""
for instance in self.sequence.instances:
if hasattr(instance, 'destroy'):
instance.destroy()
self.sequence.instances = []
def save(self, file=None, filename=None, adjoin=True):
"""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`
:param adjoin: write all images to a single multi-image file. Only
available if file format supports frames, layers, & etc.
:type adjoin: :class:`bool`
.. versionadded:: 0.1.1
.. versionchanged:: 0.1.5
The ``file`` parameter was added.
.. versionchanged:: 6.0.0
The ``adjoin`` parameter was added.
"""
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 library.MagickGetNumberImages(self.wand) > 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):
if not hasattr(filename, '__fspath__'):
raise TypeError('filename must be a string, not ' +
repr(filename))
filename = encode_filename(filename)
if library.MagickGetNumberImages(self.wand) > 1:
r = library.MagickWriteImages(self.wand, filename, adjoin)
else:
r = library.MagickWriteImage(self.wand, filename)
if not r:
self.raise_exception()
class Iterator(Resource, abc.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.abc.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):
assertions.assert_unsigned_integer(seek=y)
if y > self.height:
raise ValueError('can not 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))
if x is None:
r_pixels = [None] * width.value
for x in xrange(width.value):
r_pixels[x] = Color.from_pixelwand(pixels[x])
return r_pixels
return Color.from_pixelwand(pixels[x]) if pixels else None
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 still 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, abc.MutableMapping):
"""Free-form mutable mapping of global internal settings.
.. versionadded:: 0.3.0
.. versionchanged:: 0.5.0
Remove key check to :const:`OPTIONS`. Image properties are specific to
vendor, and this library should not attempt to manage the 100+ options
in a whitelist.
"""
def __iter__(self):
return iter(OPTIONS)
def __len__(self):
return len(OPTIONS)
def __getitem__(self, key):
assertions.assert_string(key=key)
opt_str = b''
opt_p = library.MagickGetOption(self.image.wand, binary(key))
if opt_p:
opt_str = text(ctypes.string_at(opt_p))
opt_p = library.MagickRelinquishMemory(opt_p)
else:
raise KeyError(key)
return opt_str
def __setitem__(self, key, value):
assertions.assert_string(key=key, value=value)
image = self.image
library.MagickSetOption(image.wand, binary(key), binary(value))
def __delitem__(self, key):
self[key] = ''
class Metadata(ImageProperty, abc.MutableMapping):
"""Class that implements dict-like read-only access to image metadata
like EXIF or IPTC headers. Most WRITE encoders will ignore properties
assigned here.
: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`
"""
assertions.assert_string(key=k)
image = self.image
value = b''
vp = library.MagickGetImageProperty(image.wand, binary(k))
if vp:
value = text(ctypes.string_at(vp))
vp = library.MagickRelinquishMemory(vp)
else:
raise KeyError(k)
return value
def __setitem__(self, k, v):
"""
:param k: Metadata header name string.
:type k: :class:`basestring`
:param v: Value to assign.
:type v: :class:`basestring`
.. versionadded: 0.5.0
"""
assertions.assert_string(key=k, value=v)
image = self.image
r = library.MagickSetImageProperty(image.wand, binary(k), binary(v))
if not r:
image.raise_exception()
return v
def __delitem__(self, k):
"""
:param k: Metadata header name string.
:type k: :class:`basestring`
.. versionadded: 0.5.0
"""
assertions.assert_string(key=k)
image = self.image
r = library.MagickDeleteImageProperty(image.wand, binary(k))
if not r:
image.raise_exception()
def __iter__(self):
image = self.image
num = ctypes.c_size_t()
props_p = library.MagickGetImageProperties(image.wand, b'', num)
props = [text(ctypes.string_at(props_p[i])) for i in xrange(num.value)]
props_p = 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)
props_p = library.MagickRelinquishMemory(props_p)
return num.value
class ArtifactTree(ImageProperty, abc.MutableMapping):
"""Splay tree to map image artifacts. Values defined here
are intended to be used elseware, and will not be written
to the encoded image.
For example::
# Omit timestamp from PNG file headers.
with Image(filename='input.png') as img:
img.artifacts['png:exclude-chunks'] = 'tIME'
img.save(filename='output.png')
:param image: an image instance
:type image: :class:`Image`
.. note::
You don't have to use this by yourself.
Use :attr:`Image.artifacts` property instead.
.. versionadded:: 0.5.0
"""
def __init__(self, image):
if not isinstance(image, Image):
raise TypeError('expected a wand.image.Image instance, '
'not ' + repr(image))
super(ArtifactTree, 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`
.. versionadded: 0.5.0
"""
assertions.assert_string(key=k)
image = self.image
vs = b''
vp = library.MagickGetImageArtifact(image.wand, binary(k))
if vp:
vs = text(ctypes.string_at(vp))
vp = library.MagickRelinquishMemory(vp)
if len(vs) < 1:
vp = library.MagickGetImageProperty(image.wand, binary(k))
if vp:
vs = text(ctypes.string_at(vp))
vp = library.MagickRelinquishMemory(vp)
else:
vs = None
return vs
def __setitem__(self, k, v):
"""
:param k: Metadata header name string.
:type k: :class:`basestring`
:param v: Value to assign.
:type v: :class:`basestring`
.. versionadded: 0.5.0
"""
assertions.assert_string(key=k, value=v)
image = self.image
r = library.MagickSetImageArtifact(image.wand, binary(k), binary(v))
if not r: # pragma: no cover
image.raise_exception()
return v
def __delitem__(self, k):
"""
:param k: Metadata header name string.
:type k: :class:`basestring`
.. versionadded: 0.5.0
"""
assertions.assert_string(key=k)
image = self.image
r = library.MagickDeleteImageArtifact(image.wand, binary(k))
if not r: # pragma: no cover
image.raise_exception()
def __iter__(self):
image = self.image
num = ctypes.c_size_t(0)
art_p = library.MagickGetImageArtifacts(image.wand, b'', num)
props = [text(ctypes.string_at(art_p[i])) for i in xrange(num.value)]
art_p = library.MagickRelinquishMemory(art_p)
return iter(props)
def __len__(self):
image = self.image
num = ctypes.c_size_t(0)
art_p = library.MagickGetImageArtifacts(image.wand, b'', num)
art_p = library.MagickRelinquishMemory(art_p)
return num.value
class ProfileDict(ImageProperty, abc.MutableMapping):
"""The mapping table of embedded image profiles.
Use this to get, set, and delete whole profile payloads on an image. Each
payload is a raw binary string.
For example::
with Image(filename='photo.jpg') as img:
# Extract EXIF
with open('exif.bin', 'wb') as payload:
payload.write(img.profiles['exif'])
# Import ICC
with open('color_profile.icc', 'rb') as payload:
img.profiles['icc'] = payload.read()
# Remove XMP
del imp.profiles['xmp']
.. seealso::
`Embedded Image Profiles`__ for a list of supported profiles.
__ https://imagemagick.org/script/formats.php#embedded
.. versionadded:: 0.5.1
"""
def __init__(self, image):
if not isinstance(image, Image):
raise TypeError('expected a wand.image.Image instance, '
'not ' + repr(image))
super(ProfileDict, self).__init__(image)
def __delitem__(self, k):
assertions.assert_string(key=k)
num = ctypes.c_size_t(0)
profile_p = library.MagickRemoveImageProfile(self.image.wand,
binary(k), num)
profile_p = library.MagickRelinquishMemory(profile_p)
def __getitem__(self, k):
assertions.assert_string(key=k)
num = ctypes.c_size_t(0)
return_profile = None
profile_p = library.MagickGetImageProfile(self.image.wand,
binary(k), num)
if num.value > 0:
return_profile = ctypes.string_at(profile_p, num.value)
profile_p = library.MagickRelinquishMemory(profile_p)
return return_profile
def __iter__(self):
num = ctypes.c_size_t(0)
prop = library.MagickGetImageProfiles(self.image.wand, b'', num)
profiles = [text(ctypes.string_at(prop[i])) for i in xrange(num.value)]
prop = library.MagickRelinquishMemory(prop)
return iter(profiles)
def __len__(self):
num = ctypes.c_size_t(0)
profiles_p = library.MagickGetImageProfiles(self.image.wand, b'', num)
profiles_p = library.MagickRelinquishMemory(profiles_p)
return num.value
def __setitem__(self, k, v):
assertions.assert_string(key=k)
if not isinstance(v, binary_type):
raise TypeError('value must be a binary string, not ' + repr(v))
r = library.MagickSetImageProfile(self.image.wand,
binary(k), v, len(v))
if not r:
self.image.raise_exception()
class ChannelImageDict(ImageProperty, abc.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()
if library.MagickSeparateImageChannel:
succeeded = library.MagickSeparateImageChannel(img.wand, c)
else:
succeeded = library.MagickSeparateImage(img.wand, c)
if not succeeded:
try:
img.raise_exception()
except WandException:
img.close()
raise
return img
class ChannelDepthDict(ImageProperty, abc.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]
if library.MagickGetImageChannelDepth:
depth = library.MagickGetImageChannelDepth(self.image.wand, c)
else:
mask = 0
if c != 0:
mask = library.MagickSetImageChannelMask(self.image.wand, c)
depth = library.MagickGetImageDepth(self.image.wand)
if mask != 0:
library.MagickSetImageChannelMask(self.image.wand, mask)
return int(depth)
class HistogramDict(abc.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 __del__(self):
if self.pixels:
self.pixels = library.DestroyPixelWands(self.pixels,
self.size.value)
def __len__(self):
if self.counts is None:
return self.size.value
return len(self.counts)
def __iter__(self):
if self.counts is None:
self._build_counts()
return iter(self.counts)
def __getitem__(self, color):
if self.counts is None:
self._build_counts()
if isinstance(color, string_type):
color = Color(color)
assertions.assert_color(color=color)
return self.counts[color]
def _build_counts(self):
self.counts = {}
for i in xrange(self.size.value):
color_count = library.PixelGetColorCount(self.pixels[i])
color = Color.from_pixelwand(self.pixels[i])
self.counts[color] = color_count
class ConnectedComponentObject(object):
"""Generic Python wrapper to translate
:c:type:`CCObjectInfo` structure into a class describing objects found
within an image. This class is generated by
:meth:`Image.connected_components()
<wand.image.BaseImage.connected_components>` method.
.. versionadded:: 0.5.5
.. versionchanged:: 0.6.3
Added :attr:`merge` & :attr:`metric` for ImageMagick 7.0.10
.. versionchanged:: 0.6.8
Added :attr:`key` property for ImageMagick 7.1.0
"""
#: (:class:`numbers.Integral`) Serialized object identifier
#: starting at `0`.
_id = None
#: (:class:`numbers.Integral`) Width of objects minimum
#: bounding rectangle.
width = None
#: (:class:`numbers.Integral`) Height of objects minimum
#: bounding rectangle.
height = None
#: (:class:`numbers.Integral`) X offset of objects minimum
#: bounding rectangle.
left = None
#: (:class:`numbers.Integral`) Y offset of objects minimum
#: bounding rectangle.
top = None
#: (:class:`numbers.Real`) X offset of objects centroid.
center_x = None
#: (:class:`numbers.Real`) Y offset of objects centroid.
center_y = None
#: (:class:`numbers.Real`) Quantity of pixels that make-up
#: the objects shape.
area = None
#: (:class:`~wand.color.Color`) The average color of the
#: shape.
mean_color = None
#: (:class:`bool`) Object merge flag. Only available after
#: ImageMagick-7.0.10.
#: ..versionadded:: 0.6.3
merge = None
#: (:class:`list`) List of doubles used by metric. Only available after
#: ImageMagick-7.0.10.
#: ..versionadded:: 0.6.3
metric = None
def __init__(self, cc_object=None):
if isinstance(cc_object, CCObjectInfo):
self.clone_from_cc_object_info(cc_object)
if isinstance(cc_object, CCObjectInfo70A):
self.clone_from_cc_object_info(cc_object)
self.clone_from_extra_70A_info(cc_object)
if isinstance(cc_object, CCObjectInfo710):
self.clone_from_cc_object_info(cc_object)
self.clone_from_extra_70A_info(cc_object)
self.clone_from_extra_710_info(cc_object)
@property
def size(self):
"""(:class:`tuple` (:attr:`width`, :attr:`height`))
Minimum bounding rectangle."""
return self.width, self.height
@property
def offset(self):
"""(:class:`tuple` (:attr:`left`, :attr:`top`))
Position of objects minimum bounding rectangle."""
return self.left, self.top
@property
def centroid(self):
"""(:class:`tuple` (:attr:`center_x`, :attr:`center_y`))
Center of object."""
return self.center_x, self.center_y
def clone_from_cc_object_info(self, cc_object):
"""Copy data from :class:`~wand.cdefs.structures.CCObjectInfo`."""
self._id = cc_object._id
self.width = cc_object.bounding_box.width
self.height = cc_object.bounding_box.height
self.left = cc_object.bounding_box.x
self.top = cc_object.bounding_box.y
self.center_x = cc_object.centroid.x
self.center_y = cc_object.centroid.y
self.area = cc_object.area
pinfo_size = ctypes.sizeof(PixelInfo)
raw_buffer = ctypes.create_string_buffer(pinfo_size)
ctypes.memmove(raw_buffer,
ctypes.byref(cc_object.color),
pinfo_size)
self.mean_color = Color(raw=raw_buffer)
def clone_from_extra_70A_info(self, cc_object):
"""Copy the additional values from CCObjectInfo structure. This is the
:attr:`merge` & :attr:`metric` properties added in ImageMagick 7.0.10.
.. versionadded:: 0.6.3
"""
self.merge = cc_object.merge
self.metric = []
for i in range(cc_object.CCMaxMetrics):
self.metric.append(cc_object.metric[i])
def clone_from_extra_710_info(self, cc_object):
"""Copy additional value from CCObjectInfo structure for properties
added to ImageMagick 7.1.0.
.. versionadded:: 0.6.8
"""
self.key = cc_object.key
def __repr__(self):
fmt = ("{name}({_id}: {width}x{height}+{left}+{top} {center_x:.2f},"
"{center_y:.2f} {area:.0f} {mean_color})")
return fmt.format(name=self.__class__.__name__, **self.__dict__)
class ClosedImageError(DestroyedResourceError):
"""An error that rises when some code tries access to an already closed
image.
"""