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