diff --git a/docs/changes.rst b/docs/changes.rst index 57a6ad08..82a06e7c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,6 +26,22 @@ To be released. - Added :meth:`Image.coalesce() ` method. - Added :meth:`Image.append() ` method. [:issue:`177`] - Implemented :meth:`__array_interface__` for NumPy [:issue:`65`] +- Migrated the following methods & attributes from :class:`Image ` + to :class:`BaseImage ` for a more uniformed code-base. + + - :attr:`Image.compression ` + - :attr:`Image.format ` + - :meth:`Image.auto_orient() ` + - :meth:`Image.border() ` + - :meth:`Image.contrast_stretch() ` + - :meth:`Image.gamma() ` + - :meth:`Image.level() ` + - :meth:`Image.linear_stretch() ` + - :meth:`Image.normalize() ` + - :meth:`Image.strip() ` + - :meth:`Image.transpose() ` + - :meth:`Image.transverse() ` + - :meth:`Image.trim() ` 0.4 series ~~~~~~~~~~ @@ -130,9 +146,9 @@ Released on August 3, 2015. - Added :attr:`Image.matte_color ` property. - Added :attr:`Image.virtual_pixel ` property. - Added :meth:`Image.distort() ` method. -- Added :meth:`Image.contrast_stretch() ` method. -- Added :meth:`Image.gamma() ` method. -- Added :meth:`Image.linear_stretch() ` method. +- Added :meth:`Image.contrast_stretch() ` method. +- Added :meth:`Image.gamma() ` method. +- Added :meth:`Image.linear_stretch() ` method. - Additional support for :attr:`Image.alpha_channel `. - Additional query functions have been added to :mod:`wand.version` API. [:issue:`120`] @@ -288,7 +304,7 @@ Released on March 23, 2014. - Added :meth:`Drawing.rectangle() ` method. :ref:`Now you can draw rectangles. ` [:issue:`159`] -- Added :attr:`Image.compression ` property. +- Added :attr:`Image.compression ` property. [:issue:`171`] - Added :func:`contextlib.nested()` function to :mod:`wand.compat` module. - Fixed :exc:`UnicodeEncodeError` when :meth:`Drawing.text() @@ -303,7 +319,7 @@ Version 0.3.5 Released on September 13, 2013. -- Fix segmentation fault on :meth:`Image.save() ` method. +- Fix segmentation fault on :meth:`Image.save() ` method. [:issue:`150`] @@ -399,8 +415,8 @@ Released on June 17, 2013. - Added :meth:`Image.caption() ` method. [:issue:`74` by Cha, Hojeong] - Added optional ``color`` parameter to :meth:`Image.trim() - ` method. -- Added :meth:`Image.border() ` method. + ` method. +- Added :meth:`Image.border() ` method. [:commit:`2496d37f75d75e9425f95dde07033217dc8afefc` by Jae-Myoung Yu] - Added ``resolution`` parameter to :meth:`Image.read() ` method and the constructor of :class:`~wand.image.Image`. @@ -445,7 +461,7 @@ Released on June 17, 2013. - :attr:`~wand.color.Color.blue_int8` - :attr:`~wand.color.Color.alpha_int8` -- Added :meth:`Image.normalize() ` method. +- Added :meth:`Image.normalize() ` method. [:issue:`95` by Michael Curry] - Added :meth:`Image.transparent_color() ` method. @@ -465,7 +481,7 @@ Released on June 17, 2013. - :class:`~wand.image.Image` has :attr:`~wand.image.BaseImage.histogram` dictionary. - Added optional ``fuzz`` parameter to :meth:`Image.trim() - ` method. [:issue:`113` by Evaldo Junior] + ` method. [:issue:`113` by Evaldo Junior] __ http://en.wikipedia.org/wiki/Seam_carving @@ -494,15 +510,15 @@ Version 0.2.3 Released on January 25, 2013. - Fixed a bug that :meth:`Image.transparentize() - ` method (and :meth:`Image.watermark() - ` method which internally uses it) didn't + ` method (and :meth:`Image.watermark() + ` method which internally uses it) didn't work. - Fixed segmentation fault occurred when :attr:`Color.red `, :attr:`Color.green `, - or :attr:`Color.blue ` is accessed. + or :attr:`Color.blue ` is accessed. - Added :attr:`Color.alpha ` property. - Fixed a bug that format converting using :attr:`Image.format - ` property or :meth:`Image.convert() + ` property or :meth:`Image.convert() ` method doesn't correctly work to save blob. @@ -517,16 +533,16 @@ Released on September 24, 2012. - Now :class:`~wand.image.Image` can be instantiated without any opening. Instead, it can take ``width``/``height`` and ``background``. [:issue:`53` by Michael Elovskikh] -- Added :meth:`Image.transform() ` method +- Added :meth:`Image.transform() ` method which is a convenience method accepting geometry strings to perform cropping and resizing. [:issue:`50` by Mitch Lindgren] -- Added :attr:`Image.units ` property. +- Added :attr:`Image.units ` property. [:issue:`45` by Piotr Florczyk] -- Now :meth:`Image.resize() ` method raises +- Now :meth:`Image.resize() ` method raises a proper error when it fails for any reason. [:issue:`41` by Piotr Florczyk] -- Added :attr:`Image.type ` property. +- Added :attr:`Image.type ` property. [:issue:`33` by Yauhen Yakimovich, :issue:`42` by Piotr Florczyk] __ http://olivier-freebsd-ports.googlecode.com/hg-history/efb852a5572/graphics/py-wand/files/patch-wand_api.py @@ -537,19 +553,19 @@ Version 0.2.1 Released on August 19, 2012. Beta version. -- Added :meth:`Image.trim() ` method. +- Added :meth:`Image.trim() ` method. [:issue:`26` by Jökull Sólberg Auðunsson] -- Added :attr:`Image.depth ` property. +- Added :attr:`Image.depth ` property. [:issue:`31` by Piotr Florczyk] - Now :class:`~wand.image.Image` can take an optional ``format`` hint. [:issue:`32` by Michael Elovskikh] -- Added :attr:`Image.alpha_channel ` +- Added :attr:`Image.alpha_channel ` property. [:issue:`35` by Piotr Florczyk] -- The default value of :meth:`Image.resize() `'s +- The default value of :meth:`Image.resize() `'s ``filter`` option has changed from ``'triangle'`` to ``'undefined'``. [:issue:`37` by Piotr Florczyk] @@ -569,20 +585,20 @@ Version 0.2.0 Released on June 20, 2012. Alpha version. -- Added :meth:`Image.transparentize() ` method. +- Added :meth:`Image.transparentize() ` method. [:issue:`19` by Jeremy Axmacher] -- Added :meth:`Image.composite() ` method. +- Added :meth:`Image.composite() ` method. [:issue:`19` by Jeremy Axmacher] -- Added :meth:`Image.watermark() ` method. +- Added :meth:`Image.watermark() ` method. [:issue:`19` by Jeremy Axmacher] -- Added :attr:`Image.quantum_range ` property. +- Added :attr:`Image.quantum_range ` property. [:issue:`19` by Jeremy Axmacher] -- Added :meth:`Image.reset_coords() ` method +- Added :meth:`Image.reset_coords() ` method and ``reset_coords`` option to :meth:`Image.rotate() - ` method. [:issue:`20` by Juan Pablo Scaletti] -- Added :meth:`Image.strip() ` method. + ` method. [:issue:`20` by Juan Pablo Scaletti] +- Added :meth:`Image.strip() ` method. [:issue:`23` by Dmitry Vukolov] -- Added :attr:`Image.compression_quality ` +- Added :attr:`Image.compression_quality ` property. [:issue:`23` by Dmitry Vukolov] - Now the current version can be found from the command line interface: ``python -m wand.version``. @@ -614,11 +630,11 @@ Released on December 23, 2011. Still alpha version. - Now :const:`wand.version.VERSION_INFO` becomes :class:`tuple` and :const:`wand.version.VERSION` becomes a string. -- Added :attr:`Image.background_color ` +- Added :attr:`Image.background_color ` property. - Added ``==`` operator for :class:`~wand.image.Image` type. - Added :func:`hash()` support of :class:`~wand.image.Image` type. -- Added :attr:`Image.signature ` property. +- Added :attr:`Image.signature ` property. - Added :mod:`wand.display` module. - Changed the theme of Sphinx documentation. - Changed the start example of the documentation. @@ -630,16 +646,16 @@ Released on December 2, 2011. Still alpha version. - Wrote some guide documentations: :doc:`guide/read`, :doc:`guide/write` and :doc:`guide/resizecrop`. -- Added :meth:`Image.rotate() ` method for in-place +- Added :meth:`Image.rotate() ` method for in-place rotation. -- Made :meth:`Image.crop() ` to raise proper +- Made :meth:`Image.crop() ` to raise proper :exc:`ValueError` instead of :exc:`IndexError` for invalid width/height arguments. -- Changed the type of :meth:`Image.resize() ` +- Changed the type of :meth:`Image.resize() ` method's ``blur`` parameter from :class:`numbers.Rational` to :class:`numbers.Real`. - Fixed a bug of raising :exc:`~exceptions.ValueError` when invalid ``filter`` - has passed to :meth:`Image.resize() ` method. + has passed to :meth:`Image.resize() ` method. Version 0.1.7 ------------- @@ -647,7 +663,7 @@ Version 0.1.7 Released on November 10, 2011. Still alpha version. - Added :attr:`Image.mimetype ` property. -- Added :meth:`Image.crop() ` method for in-place +- Added :meth:`Image.crop() ` method for in-place crop. Version 0.1.6 @@ -657,7 +673,7 @@ Released on October 31, 2011. Still alpha version. - Removed a side effect of :class:`Image.make_blob() ` method that changes the image format silently. -- Added :attr:`Image.format ` property. +- Added :attr:`Image.format ` property. - Added :meth:`Image.convert() ` method. - Fixed a bug about Python 2.6 compatibility. - Use the internal representation of :c:type:`PixelWand` instead of @@ -711,9 +727,9 @@ Released on October 4, 2011. Still alpha version. - Now it handles errors and warnings properly and in natural way of Python. - Added :meth:`Image.make_blob() ` method. - Added ``blob`` parameter into :class:`~wand.image.Image` constructor. -- Added :meth:`Image.resize() ` method. +- Added :meth:`Image.resize() ` method. - Added :meth:`Image.save() ` method. -- Added :meth:`Image.clone() ` method. +- Added :meth:`Image.clone() ` method. - Drawed `the pretty logo picture <_static/wand.png>`_ (thanks to `Hyojin Choi `_). diff --git a/wand/image.py b/wand/image.py index bb3e87d8..f7ea5429 100644 --- a/wand/image.py +++ b/wand/image.py @@ -1049,6 +1049,30 @@ def colorspace(self, colorspace_type): if not r: self.raise_exception() + @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) + ) + @property def compression_quality(self): """(:class:`numbers.Integral`) Compression quality of this image. @@ -1200,6 +1224,48 @@ def font_size(self, size): elif library.MagickSetPointsize(self.wand, size) is False: raise ValueError('unexpected error is occur') + @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 gravity(self): """(:class:`basestring`) The text placement gravity used when @@ -1599,6 +1665,59 @@ def append(self, stacked=False): self.raise_exception() self.wand = r + @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() + @manipulative def blur(self, radius, sigma): """Blurs the image. We convolve the image with a gaussian operator @@ -1626,6 +1745,40 @@ def blur(self, radius, sigma): if not r: self.raise_exception() + def border(self, color, width, height, compose="copy"): + """Surrounds the image with a border. + + :param bordercolor: the border color pixel wand + :type image: :class:`~wand.color.Color` + :param width: the border width + :type width: :class:`numbers.Integral` + :param height: the border height + :type height: :class:`numbers.Integral` + :param compose: Use composite operator when applying frame. Only used + if called with ImageMagick 7+. + :type compose: :class:`basestring` + + .. versionadded:: 0.3.0 + .. versionchanged:: 0.5.0 + Added ``compose`` paramater, and ImageMagick 7 support. + """ + if not isinstance(color, Color): + raise TypeError('color must be a wand.color.Color object, not ' + + repr(color)) + with color: + if MAGICK_VERSION_NUMBER < 0x700: + result = library.MagickBorderImage(self.wand, color.resource, + width, height) + else: + if compose not in COMPOSITE_OPERATORS: + raise TypeError(repr(compose) + ' is an invalid type. ' + + 'See wand.image.COMPOSITE_OPERATORS.') + compose_idx = COMPOSITE_OPERATORS.index(compose) + result = library.MagickBorderImage(self.wand, color.resource, + width, height, compose_idx) + if not result: + self.raise_exception() + @manipulative def caption(self, text, left=0, top=0, width=None, height=None, font=None, gravity=None): @@ -1808,6 +1961,65 @@ def composite_channel(self, channel, image, operator, left=0, top=0): library.MagickSetImageChannelMask(self.wand, ch_mask) 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: + ch_const = CHANNELS[channel] + if library.MagickContrastStretchImageChannel: + library.MagickContrastStretchImageChannel(self.wand, + ch_const, + black_point, + white_point) + else: + # Set active channel, and capture mask to restore. + channel_mask = library.MagickSetImageChannelMask(self.wand, + ch_const) + library.MagickContrastStretchImage(self.wand, + black_point, + white_point) + # Restore original state of channels + library.MagickSetImageChannelMask(self.wand, channel_mask) + 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 crop(self, left=0, top=0, right=None, bottom=None, width=None, height=None, reset_coords=True, @@ -2253,6 +2465,45 @@ def fx(self, expression, channel=None): return Image(image=BaseImage(new_wand)) 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: + ch_const = CHANNELS[channel] + if library.MagickGammaImageChannel: + library.MagickGammaImageChannel(self.wand, + ch_const, + adjustment_value) + else: + # Set active channel, and capture mask to restore. + channel_mask = library.MagickSetImageChannelMask(self.wand, + ch_const) + library.MagickGammaImage(self.wand, adjustment_value) + # Restore original state of channels + library.MagickSetImageChannelMask(self.wand, channel_mask) + 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 gaussian_blur(self, radius, sigma): """Blurs the image. We convolve the image with a gaussian operator @@ -2280,11 +2531,100 @@ def gaussian_blur(self, radius, sigma): if not r: self.raise_exception() - @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. - + 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` + + .. note:: + Images may not be affected if the ``white`` value is equal, or + less then, the ``black`` value. + + .. 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 MAGICK_HDRI: + bp -= 0.5 # TODO: Document why HDRI requires 0.5 adjustments. + wp -= 0.5 + if channel: + try: + ch_const = CHANNELS[channel] + except KeyError: + raise ValueError(repr(channel) + ' is an invalid channel type' + '; see wand.image.CHANNELS dictionary') + if library.MagickLevelImageChannel: + library.MagickLevelImageChannel(self.wand, + ch_const, + bp, + gamma, + wp) + else: + # Set active channel, and capture mask to restore. + channel_mask = library.MagickSetImageChannelMask(self.wand, + ch_const) + library.MagickLevelImage(self.wand, bp, gamma, wp) + # Restore original state of channels + library.MagickSetImageChannelMask(self.wand, channel_mask) + else: + library.MagickLevelImage(self.wand, bp, gamma, wp) + 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) + + @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 @@ -2434,6 +2774,46 @@ def negate(self, grayscale=False, channel=None): if not r: self.raise_exception() + @manipulative + 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') + if library.MagickNormalizeImageChannel: + r = library.MagickNormalizeImageChannel(self.wand, ch_const) + else: + with Image(image=self) as mask: + # Set active channel, and capture mask to restore. + channel_mask = library.MagickSetImageChannelMask(mask.wand, + ch_const) + r = library.MagickNormalizeImage(mask.wand) + # Restore original state of channels. + library.MagickSetImageChannelMask(mask.wand, + channel_mask) + # Copy adjusted mask over original value. + copy_mask = COMPOSITE_OPERATORS.index('copy_' + channel) + library.MagickCompositeImage(self.wand, + mask.wand, + copy_mask, + False, + 0, + 0) + else: + r = library.MagickNormalizeImage(self.wand) + if not r: + self.raise_exception() + @manipulative def optimize_layers(self): """Attempts to crop each frame to the smallest image without altering @@ -2748,6 +3128,16 @@ def rotate(self, degree, background=None, reset_coords=True): if reset_coords: self.reset_coords() + 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() + @manipulative def sample(self, width=None, height=None): """Resizes the image by sampling the pixels. It's basically quicker @@ -3086,6 +3476,52 @@ def transparentize(self, transparency): channel='opacity') 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 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, compose="copy") + result = library.MagickTrimImage(self.wand, fuzz) + if not result: + self.raise_exception() + @manipulative def unsharp_mask(self, radius, sigma, amount, threshold): """Sharpens the image using unsharp mask filter. We convolve the image @@ -3171,13 +3607,13 @@ class Image(BaseImage): :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 + 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 + :param height: the height of new blank image or an image loaded from raw data. :type height: :class:`numbers.Integral` :param depth: the depth used when loading raw data. @@ -3356,72 +3792,6 @@ 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) - ) - - @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 @@ -3436,59 +3806,6 @@ def mimetype(self): mimetype = rp.value return text(mimetype) - @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 blank(self, width, height, background=None): """Creates blank image. @@ -3523,40 +3840,6 @@ def blank(self, width, height, background=None): self.raise_exception() return self - def border(self, color, width, height, compose="copy"): - """Surrounds the image with a border. - - :param bordercolor: the border color pixel wand - :type image: :class:`~wand.color.Color` - :param width: the border width - :type width: :class:`numbers.Integral` - :param height: the border height - :type height: :class:`numbers.Integral` - :param compose: Use composite operator when applying frame. Only used - if called with ImageMagick 7+. - :type compose: :class:`basestring` - - .. versionadded:: 0.3.0 - .. versionchanged:: 0.5.0 - Added ``compose`` paramater, and ImageMagick 7 support. - """ - if not isinstance(color, Color): - raise TypeError('color must be a wand.color.Color object, not ' + - repr(color)) - with color: - if MAGICK_VERSION_NUMBER < 0x700: - result = library.MagickBorderImage(self.wand, color.resource, - width, height) - else: - if compose not in COMPOSITE_OPERATORS: - raise TypeError(repr(compose) + ' is an invalid type. ' + - 'See wand.image.COMPOSITE_OPERATORS.') - compose_idx = COMPOSITE_OPERATORS.index(compose) - result = library.MagickBorderImage(self.wand, color.resource, - width, height, compose_idx) - if not result: - self.raise_exception() - def clear(self): """Clears resources associated with the image, leaving the image blank, and ready to be used with new image. @@ -3618,65 +3901,6 @@ def compare_layers(self, method): self.raise_exception() return Image(image=BaseImage(r)) - @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: - ch_const = CHANNELS[channel] - if library.MagickContrastStretchImageChannel: - library.MagickContrastStretchImageChannel(self.wand, - ch_const, - black_point, - white_point) - else: - # Set active channel, and capture mask to restore. - channel_mask = library.MagickSetImageChannelMask(self.wand, - ch_const) - library.MagickContrastStretchImage(self.wand, - black_point, - white_point) - # Restore original state of channels - library.MagickSetImageChannelMask(self.wand, channel_mask) - 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() - def convert(self, format): """Converts the image format with the original image maintained. It returns a converted image instance which is new. :: @@ -3708,134 +3932,6 @@ def destroy(self): self.sequence.pop() super(Image, self).destroy() - @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: - ch_const = CHANNELS[channel] - if library.MagickGammaImageChannel: - library.MagickGammaImageChannel(self.wand, - ch_const, - adjustment_value) - else: - # Set active channel, and capture mask to restore. - channel_mask = library.MagickSetImageChannelMask(self.wand, - ch_const) - library.MagickGammaImage(self.wand, adjustment_value) - # Restore original state of channels - library.MagickSetImageChannelMask(self.wand, channel_mask) - 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() - - 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` - - .. note:: - Images may not be affected if the ``white`` value is equal, or - less then, the ``black`` value. - - .. 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 MAGICK_HDRI: - bp -= 0.5 # TODO: Document why HDRI requires 0.5 adjustments. - wp -= 0.5 - if channel: - try: - ch_const = CHANNELS[channel] - except KeyError: - raise ValueError(repr(channel) + ' is an invalid channel type' - '; see wand.image.CHANNELS dictionary') - if library.MagickLevelImageChannel: - library.MagickLevelImageChannel(self.wand, - ch_const, - bp, - gamma, - wp) - else: - # Set active channel, and capture mask to restore. - channel_mask = library.MagickSetImageChannelMask(self.wand, - ch_const) - library.MagickLevelImage(self.wand, bp, gamma, wp) - # Restore original state of channels - library.MagickSetImageChannelMask(self.wand, channel_mask) - else: - library.MagickLevelImage(self.wand, bp, gamma, wp) - 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 make_blob(self, format=None): """Makes the binary string of the image. @@ -3874,46 +3970,6 @@ def make_blob(self, format=None): return blob self.raise_exception() - @manipulative - 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') - if library.MagickNormalizeImageChannel: - r = library.MagickNormalizeImageChannel(self.wand, ch_const) - else: - with Image(image=self) as mask: - # Set active channel, and capture mask to restore. - channel_mask = library.MagickSetImageChannelMask(mask.wand, - ch_const) - r = library.MagickNormalizeImage(mask.wand) - # Restore original state of channels. - library.MagickSetImageChannelMask(mask.wand, - channel_mask) - # Copy adjusted mask over original value. - copy_mask = COMPOSITE_OPERATORS.index('copy_' + channel) - library.MagickCompositeImage(self.wand, - mask.wand, - copy_mask, - False, - 0, - 0) - else: - r = library.MagickNormalizeImage(self.wand) - if not r: - self.raise_exception() - def read(self, file=None, filename=None, blob=None, resolution=None): """Read new image into Image() object. @@ -4018,61 +4074,6 @@ def save(self, file=None, filename=None): if not r: 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() - - @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() - - 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, compose="copy") - result = library.MagickTrimImage(self.wand, fuzz) - if not result: - self.raise_exception() - class Iterator(Resource, collections.Iterator): """Row iterator for :class:`Image`. It shouldn't be instantiated