From 8973e81aa1fc58cf20cac8e520f5a9770f44c230 Mon Sep 17 00:00:00 2001 From: Cervinko Cera Date: Sat, 9 Apr 2016 00:36:30 +0200 Subject: [PATCH] added Wand 4.2 to lib --- lib/wand/__init__.py | 6 + lib/wand/__init__.pyc | Bin 0 -> 362 bytes lib/wand/api.py | 1399 ++++++++++++++++ lib/wand/api.pyc | Bin 0 -> 27988 bytes lib/wand/color.py | 307 ++++ lib/wand/color.pyc | Bin 0 -> 11975 bytes lib/wand/compat.py | 119 ++ lib/wand/compat.pyc | Bin 0 -> 3504 bytes lib/wand/display.py | 78 + lib/wand/drawing.py | 1988 ++++++++++++++++++++++ lib/wand/exceptions.py | 111 ++ lib/wand/exceptions.pyc | Bin 0 -> 4818 bytes lib/wand/font.py | 103 ++ lib/wand/font.pyc | Bin 0 -> 4185 bytes lib/wand/image.py | 3498 +++++++++++++++++++++++++++++++++++++++ lib/wand/image.pyc | Bin 0 -> 118771 bytes lib/wand/resource.py | 244 +++ lib/wand/resource.pyc | Bin 0 -> 7593 bytes lib/wand/sequence.py | 345 ++++ lib/wand/sequence.pyc | Bin 0 -> 12403 bytes lib/wand/version.py | 251 +++ lib/wand/version.pyc | Bin 0 -> 7237 bytes 22 files changed, 8449 insertions(+) create mode 100644 lib/wand/__init__.py create mode 100644 lib/wand/__init__.pyc create mode 100644 lib/wand/api.py create mode 100644 lib/wand/api.pyc create mode 100644 lib/wand/color.py create mode 100644 lib/wand/color.pyc create mode 100644 lib/wand/compat.py create mode 100644 lib/wand/compat.pyc create mode 100644 lib/wand/display.py create mode 100644 lib/wand/drawing.py create mode 100644 lib/wand/exceptions.py create mode 100644 lib/wand/exceptions.pyc create mode 100644 lib/wand/font.py create mode 100644 lib/wand/font.pyc create mode 100644 lib/wand/image.py create mode 100644 lib/wand/image.pyc create mode 100644 lib/wand/resource.py create mode 100644 lib/wand/resource.pyc create mode 100644 lib/wand/sequence.py create mode 100644 lib/wand/sequence.pyc create mode 100644 lib/wand/version.py create mode 100644 lib/wand/version.pyc diff --git a/lib/wand/__init__.py b/lib/wand/__init__.py new file mode 100644 index 00000000..ef794bde --- /dev/null +++ b/lib/wand/__init__.py @@ -0,0 +1,6 @@ +""":mod:`wand` --- Simple `MagickWand API`_ binding for Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _MagickWand API: http://www.imagemagick.org/script/magick-wand.php + +""" diff --git a/lib/wand/__init__.pyc b/lib/wand/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d85c954b22e0ed6f231081aef4d47f860b1473c7 GIT binary patch literal 362 zcmbu5&q@U$6vn*;brE_8m$T8+wjrXHEs6_waiNVfqc%$8AJmK7O)u9Y^#YxtAX<0e z`}z67(WB>|SF~Pl4~qQM2G@I3p$JXL3vxsD-PxLbAbKBlKeRFEfpN~+-ubi`Hav~9 zQ*Q^%*~9K}kZf{6yD+oYnAz@BZ4C7NUqz`9O#Z#6XSUYV1GLtJ^W$t#IERRNhN7}d z6}}U`jx3T*lsa)Vp2(Hl@UyHaDaprTL5YBH#CaZ@(DnF-Lr bdN4@of~!)BbgEnIzQu1kF6Qn6eL;~g1e0(0 literal 0 HcmV?d00001 diff --git a/lib/wand/api.py b/lib/wand/api.py new file mode 100644 index 00000000..1c48fadd --- /dev/null +++ b/lib/wand/api.py @@ -0,0 +1,1399 @@ +""":mod:`wand.api` --- Low-level interfaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionchanged:: 0.1.10 + Changed to throw :exc:`~exceptions.ImportError` instead of + :exc:`~exceptions.AttributeError` when the shared library fails to load. + +""" +import ctypes +import ctypes.util +import itertools +import os +import os.path +import platform +import sys +import traceback +if platform.system() == "Windows": + try: + import winreg + except ImportError: + import _winreg as winreg + +__all__ = ('MagickPixelPacket', 'PointInfo', 'AffineMatrix', 'c_magick_char_p', + 'library', 'libc', 'libmagick', 'load_library') + + +class c_magick_char_p(ctypes.c_char_p): + """This subclass prevents the automatic conversion behavior of + :class:`ctypes.c_char_p`, allowing memory to be properly freed in the + destructor. It must only be used for non-const character pointers + returned by ImageMagick functions. + + """ + + def __del__(self): + """Relinquishes memory allocated by ImageMagick. + We don't need to worry about checking for ``NULL`` because + :c:func:`MagickRelinquishMemory` does that for us. + Note alslo that :class:`ctypes.c_char_p` has no + :meth:`~object.__del__` method, so we don't need to + (and indeed can't) call the superclass destructor. + + """ + library.MagickRelinquishMemory(self) + + +def library_paths(): + """Iterates for library paths to try loading. The result paths are not + guaranteed that they exist. + + :returns: a pair of libwand and libmagick paths. they can be the same. + path can be ``None`` as well + :rtype: :class:`tuple` + + """ + libwand = None + libmagick = None + versions = '', '-6', '-Q16', '-Q8', '-6.Q16' + options = '', 'HDRI', 'HDRI-2' + system = platform.system() + magick_home = os.environ.get('MAGICK_HOME') + + if system == 'Windows': + # ImageMagick installers normally install coder and filter DLLs in + # subfolders, we need to add those folders to PATH, otherwise loading + # the DLL later will fail. + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\ImageMagick\Current") as reg_key: + libPath = winreg.QueryValueEx(reg_key, "LibPath") + coderPath = winreg.QueryValueEx(reg_key, "CoderModulesPath") + filterPath = winreg.QueryValueEx(reg_key, "FilterModulesPath") + magick_home = libPath[0] + os.environ['PATH'] += (';' + libPath[0] + ";" + + coderPath[0] + ";" + filterPath[0]) + except OSError: + # otherwise use MAGICK_HOME, and we assume the coder and + # filter DLLs are in the same directory + pass + + magick_path = lambda dir: os.path.join(magick_home, *dir) + combinations = itertools.product(versions, options) + for suffix in (version + option for version, option in combinations): + # On Windows, the API is split between two libs. On other platforms, + # it's all contained in one. + if magick_home: + if system == 'Windows': + libwand = 'CORE_RL_wand_{0}.dll'.format(suffix), + libmagick = 'CORE_RL_magick_{0}.dll'.format(suffix), + yield magick_path(libwand), magick_path(libmagick) + libwand = 'libMagickWand{0}.dll'.format(suffix), + libmagick = 'libMagickCore{0}.dll'.format(suffix), + yield magick_path(libwand), magick_path(libmagick) + elif system == 'Darwin': + libwand = 'lib', 'libMagickWand{0}.dylib'.format(suffix), + yield magick_path(libwand), magick_path(libwand) + else: + libwand = 'lib', 'libMagickWand{0}.so'.format(suffix), + yield magick_path(libwand), magick_path(libwand) + if system == 'Windows': + libwand = ctypes.util.find_library('CORE_RL_wand_' + suffix) + libmagick = ctypes.util.find_library('CORE_RL_magick_' + suffix) + yield libwand, libmagick + libwand = ctypes.util.find_library('libMagickWand' + suffix) + libmagick = ctypes.util.find_library('libMagickCore' + suffix) + yield libwand, libmagick + else: + libwand = ctypes.util.find_library('MagickWand' + suffix) + yield libwand, libwand + + +def load_library(): + """Loads the MagickWand library. + + :returns: the MagickWand library and the ImageMagick library + :rtype: :class:`ctypes.CDLL` + + """ + tried_paths = [] + for libwand_path, libmagick_path in library_paths(): + if libwand_path is None or libmagick_path is None: + continue + try: + tried_paths.append(libwand_path) + libwand = ctypes.CDLL(libwand_path) + if libwand_path == libmagick_path: + libmagick = libwand + else: + tried_paths.append(libmagick_path) + libmagick = ctypes.CDLL(libmagick_path) + except (IOError, OSError): + continue + return libwand, libmagick + raise IOError('cannot find library; tried paths: ' + repr(tried_paths)) + + +if not hasattr(ctypes, 'c_ssize_t'): + if ctypes.sizeof(ctypes.c_uint) == ctypes.sizeof(ctypes.c_void_p): + ctypes.c_ssize_t = ctypes.c_int + elif ctypes.sizeof(ctypes.c_ulong) == ctypes.sizeof(ctypes.c_void_p): + ctypes.c_ssize_t = ctypes.c_long + elif ctypes.sizeof(ctypes.c_ulonglong) == ctypes.sizeof(ctypes.c_void_p): + ctypes.c_ssize_t = ctypes.c_longlong + + +class MagickPixelPacket(ctypes.Structure): + + _fields_ = [('storage_class', ctypes.c_int), + ('colorspace', ctypes.c_int), + ('matte', ctypes.c_int), + ('fuzz', ctypes.c_double), + ('depth', ctypes.c_size_t), + ('red', ctypes.c_double), + ('green', ctypes.c_double), + ('blue', ctypes.c_double), + ('opacity', ctypes.c_double), + ('index', ctypes.c_double)] + + +class PointInfo(ctypes.Structure): + + _fields_ = [('x', ctypes.c_double), + ('y', ctypes.c_double)] + + +class AffineMatrix(ctypes.Structure): + _fields_ = [('sx', ctypes.c_double), + ('rx', ctypes.c_double), + ('ry', ctypes.c_double), + ('sy', ctypes.c_double), + ('tx', ctypes.c_double), + ('ty', ctypes.c_double)] + + +# Preserve the module itself even if it fails to import +sys.modules['wand._api'] = sys.modules['wand.api'] + +try: + libraries = load_library() +except (OSError, IOError): + msg = 'http://docs.wand-py.org/en/latest/guide/install.html' + if sys.platform.startswith('freebsd'): + msg = 'pkg_add -r' + elif sys.platform == 'win32': + msg += '#install-imagemagick-on-windows' + elif sys.platform == 'darwin': + mac_pkgmgrs = {'brew': 'brew install freetype imagemagick', + 'port': 'port install imagemagick'} + for pkgmgr in mac_pkgmgrs: + with os.popen('which ' + pkgmgr) as f: + if f.read().strip(): + msg = mac_pkgmgrs[pkgmgr] + break + else: + msg += '#install-imagemagick-on-mac' + else: + distname, _, __ = platform.linux_distribution() + distname = (distname or '').lower() + if distname in ('debian', 'ubuntu'): + msg = 'apt-get install libmagickwand-dev' + elif distname in ('fedora', 'centos', 'redhat'): + msg = 'yum install ImageMagick-devel' + raise ImportError('MagickWand shared library not found.\n' + 'You probably had not installed ImageMagick library.\n' + 'Try to install:\n ' + msg) + +#: (:class:`ctypes.CDLL`) The MagickWand library. +library = libraries[0] + +#: (:class:`ctypes.CDLL`) The ImageMagick library. It is the same with +#: :data:`library` on platforms other than Windows. +#: +#: .. versionadded:: 0.1.10 +libmagick = libraries[1] + +try: + library.MagickWandGenesis.argtypes = [] + library.MagickWandTerminus.argtypes = [] + + library.NewMagickWand.argtypes = [] + library.NewMagickWand.restype = ctypes.c_void_p + + library.MagickNewImage.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_int, ctypes.c_void_p] + + library.ClearMagickWand.argtypes = [ctypes.c_void_p] + + library.DestroyMagickWand.argtypes = [ctypes.c_void_p] + library.DestroyMagickWand.restype = ctypes.c_void_p + + library.CloneMagickWand.argtypes = [ctypes.c_void_p] + library.CloneMagickWand.restype = ctypes.c_void_p + + library.IsMagickWand.argtypes = [ctypes.c_void_p] + + library.MagickGetException.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_int)] + library.MagickGetException.restype = c_magick_char_p + + library.MagickClearException.argtypes = [ctypes.c_void_p] + + library.MagickSetFilename.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + library.MagickReadImageBlob.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_size_t] + + library.MagickReadImage.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + library.MagickReadImageFile.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.MagickGetImageFormat.argtypes = [ctypes.c_void_p] + library.MagickGetImageFormat.restype = c_magick_char_p + + library.MagickSetImageFormat.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + libmagick.MagickToMime.argtypes = [ctypes.c_char_p] + libmagick.MagickToMime.restype = c_magick_char_p + + library.MagickGetImageSignature.argtypes = [ctypes.c_void_p] + library.MagickGetImageSignature.restype = c_magick_char_p + + library.MagickGetImageProperty.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickGetImageProperty.restype = c_magick_char_p + + library.MagickGetImageProperties.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t) + ] + library.MagickGetImageProperties.restype = ctypes.POINTER(ctypes.c_char_p) + + library.MagickSetImageProperty.argtypes = [ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p] + + library.MagickDeleteImageProperty.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickGetImageBackgroundColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.MagickSetImageBackgroundColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.MagickSetImageMatte.argtypes = [ctypes.c_void_p, ctypes.c_int] + + library.MagickGetImageMatteColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.MagickSetImageMatteColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.MagickGetImageAlphaChannel.argtypes = [ctypes.c_void_p] + library.MagickGetImageAlphaChannel.restype = ctypes.c_size_t + + library.MagickSetImageAlphaChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickGetImageBlob.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t)] + library.MagickGetImageBlob.restype = ctypes.POINTER(ctypes.c_ubyte) + + library.MagickGetImagesBlob.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t)] + library.MagickGetImagesBlob.restype = ctypes.POINTER(ctypes.c_ubyte) + + library.MagickWriteImage.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + library.MagickWriteImageFile.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.MagickWriteImages.argtypes = [ctypes.c_void_p, ctypes.c_char_p, + ctypes.c_int] + + library.MagickWriteImagesFile.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.MagickGetImageResolution.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_double), + ctypes.POINTER(ctypes.c_double) + ] + + library.MagickSetImageResolution.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double] + + library.MagickSetResolution.argtypes = [ctypes.c_void_p, ctypes.c_double, + ctypes.c_double] + + library.MagickGetImageWidth.argtypes = [ctypes.c_void_p] + library.MagickGetImageWidth.restype = ctypes.c_size_t + + library.MagickGetImageHeight.argtypes = [ctypes.c_void_p] + library.MagickGetImageHeight.restype = ctypes.c_size_t + + library.MagickGetImageOrientation.argtypes = [ctypes.c_void_p] + library.MagickGetImageOrientation.restype = ctypes.c_int + + library.MagickSetImageOrientation.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickGetImageUnits.argtypes = [ctypes.c_void_p] + + library.MagickSetImageUnits.argtypes = [ctypes.c_void_p, ctypes.c_int] + + library.MagickGetImageVirtualPixelMethod.argtypes = [ctypes.c_void_p] + + library.MagickSetImageVirtualPixelMethod.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickGetImageColorspace.argtypes = [ctypes.c_void_p] + library.MagickGetImageColorspace.restype = ctypes.c_int + + library.MagickSetImageColorspace.argtypes = [ctypes.c_void_p, ctypes.c_int] + library.MagickTransformImageColorspace.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickGetImageCompression.argtypes = [ctypes.c_void_p] + library.MagickGetImageCompression.restype = ctypes.c_int + + library.MagickSetImageCompression.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickGetImageDepth.argtypes = [ctypes.c_void_p] + library.MagickGetImageDepth.restype = ctypes.c_size_t + + library.MagickSetImageDepth.argtypes = [ctypes.c_void_p] + + library.MagickGetImageChannelDepth.argtypes = [ctypes.c_void_p, + ctypes.c_int] + library.MagickGetImageChannelDepth.restype = ctypes.c_size_t + + library.MagickSeparateImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickCropImage.argtypes = [ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_size_t, ctypes.c_ssize_t, + ctypes.c_ssize_t] + + library.MagickFlipImage.argtypes = [ctypes.c_void_p] + + library.MagickFlopImage.argtypes = [ctypes.c_void_p] + + library.MagickFrameImage.argtypes = [ctypes.c_void_p, # wand + ctypes.c_void_p, # matte_color + ctypes.c_size_t, # width + ctypes.c_size_t, # height + ctypes.c_ssize_t, # inner_bevel + ctypes.c_ssize_t] # outer_bevel + + library.MagickFunctionImage.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # MagickFunction + ctypes.c_size_t, # number_arguments + ctypes.POINTER(ctypes.c_double), # arguments + ] + + library.MagickFunctionImageChannel.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # channel + ctypes.c_int, # MagickFunction + ctypes.c_size_t, # number_arguments + ctypes.POINTER(ctypes.c_double), # arguments + ] + + library.MagickFxImage.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # expression + library.MagickFxImage.restype = ctypes.c_void_p + + library.MagickFxImageChannel.argtypes = [ctypes.c_void_p, # wand + ctypes.c_int, # channel + ctypes.c_char_p] # expression + library.MagickFxImageChannel.restype = ctypes.c_void_p + + library.MagickResetImagePage.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + library.MagickSampleImage.argtypes = [ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_size_t] + + library.MagickResizeImage.argtypes = [ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_size_t, ctypes.c_int, + ctypes.c_double] + + library.MagickTransformImage.argtypes = [ctypes.c_void_p, ctypes.c_char_p, + ctypes.c_char_p] + library.MagickTransformImage.restype = ctypes.c_void_p + + library.MagickTransparentPaintImage.argtypes = [ + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_double, ctypes.c_double, + ctypes.c_int + ] + + library.MagickLiquidRescaleImage.argtypes = [ + ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t, + ctypes.c_double, ctypes.c_double + ] + + library.MagickRotateImage.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_double] + + library.MagickBorderImage.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_size_t, ctypes.c_size_t] + + library.MagickResetIterator.argtypes = [ctypes.c_void_p] + + library.MagickSetLastIterator.argtypes = [ctypes.c_void_p] + + library.MagickGetIteratorIndex.argtypes = [ctypes.c_void_p] + library.MagickGetIteratorIndex.restype = ctypes.c_ssize_t + + library.MagickCoalesceImages.argtypes = [ctypes.c_void_p] + library.MagickCoalesceImages.restype = ctypes.c_void_p + + library.MagickIdentifyImage.argtypes = [ctypes.c_void_p] + library.MagickIdentifyImage.restype = ctypes.c_char_p + + library.MagickRelinquishMemory.argtypes = [ctypes.c_void_p] + library.MagickRelinquishMemory.restype = ctypes.c_void_p + + library.NewPixelIterator.argtypes = [ctypes.c_void_p] + library.NewPixelIterator.restype = ctypes.c_void_p + + library.DestroyPixelIterator.argtypes = [ctypes.c_void_p] + library.DestroyPixelIterator.restype = ctypes.c_void_p + + library.ClonePixelIterator.argtypes = [ctypes.c_void_p] + library.ClonePixelIterator.restype = ctypes.c_void_p + + library.IsPixelIterator.argtypes = [ctypes.c_void_p] + + library.PixelGetIteratorException.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_int)] + library.PixelGetIteratorException.restype = c_magick_char_p + + library.PixelClearIteratorException.argtypes = [ctypes.c_void_p] + + library.PixelSetFirstIteratorRow.argtypes = [ctypes.c_void_p] + + library.PixelSetIteratorRow.argtypes = [ctypes.c_void_p, ctypes.c_ssize_t] + + library.PixelGetNextIteratorRow.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t) + ] + library.PixelGetNextIteratorRow.restype = ctypes.POINTER(ctypes.c_void_p) + + library.NewPixelWand.argtypes = [] + library.NewPixelWand.restype = ctypes.c_void_p + + library.DestroyPixelWand.argtypes = [ctypes.c_void_p] + library.DestroyPixelWand.restype = ctypes.c_void_p + + library.IsPixelWand.argtypes = [ctypes.c_void_p] + + library.PixelGetException.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_int)] + library.PixelGetException.restype = c_magick_char_p + + library.PixelClearException.argtypes = [ctypes.c_void_p] + + library.IsPixelWandSimilar.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_double] + + library.PixelGetMagickColor.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.PixelSetMagickColor.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.PixelSetColor.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + library.PixelGetColorAsString.argtypes = [ctypes.c_void_p] + library.PixelGetColorAsString.restype = c_magick_char_p + + library.PixelGetColorAsNormalizedString.argtypes = [ctypes.c_void_p] + library.PixelGetColorAsNormalizedString.restype = c_magick_char_p + + library.PixelGetRed.argtypes = [ctypes.c_void_p] + library.PixelGetRed.restype = ctypes.c_double + + library.PixelGetGreen.argtypes = [ctypes.c_void_p] + library.PixelGetGreen.restype = ctypes.c_double + + library.PixelGetBlue.argtypes = [ctypes.c_void_p] + library.PixelGetBlue.restype = ctypes.c_double + + library.PixelGetAlpha.argtypes = [ctypes.c_void_p] + library.PixelGetAlpha.restype = ctypes.c_double + + library.PixelGetRedQuantum.argtypes = [ctypes.c_void_p] + library.PixelGetRedQuantum.restype = ctypes.c_size_t + + library.PixelGetGreenQuantum.argtypes = [ctypes.c_void_p] + library.PixelGetGreenQuantum.restype = ctypes.c_size_t + + library.PixelGetBlueQuantum.argtypes = [ctypes.c_void_p] + library.PixelGetBlueQuantum.restype = ctypes.c_size_t + + library.PixelGetAlphaQuantum.argtypes = [ctypes.c_void_p] + library.PixelGetAlphaQuantum.restype = ctypes.c_size_t + + library.PixelGetColorCount.argtypes = [ctypes.c_void_p] + library.PixelGetColorCount.restype = ctypes.c_size_t + + library.MagickGetQuantumRange.argtypes = [ctypes.POINTER(ctypes.c_size_t)] + + library.MagickSetIteratorIndex.argtypes = [ctypes.c_void_p, + ctypes.c_ssize_t] + + library.MagickGetImageType.argtypes = [ctypes.c_void_p] + + library.MagickSetImageType.argtypes = [ctypes.c_void_p, ctypes.c_int] + + library.MagickEvaluateImage.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_double] + + library.MagickLevelImage.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.MagickLevelImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.MagickEvaluateImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, + ctypes.c_double] + + library.MagickContrastStretchImage.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # black + ctypes.c_double] # white + + library.MagickContrastStretchImageChannel.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # channel + ctypes.c_double, # black + ctypes.c_double, # white + ] + + library.MagickGammaImage.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.MagickGammaImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_double] + + library.MagickLinearStretchImage.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # black + ctypes.c_double] # white + + library.MagickCompositeImage.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_int, ctypes.c_ssize_t, + ctypes.c_ssize_t] + + library.MagickCompositeImageChannel.argtypes = [ + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_ssize_t, ctypes.c_ssize_t + ] + + library.MagickGetImageCompressionQuality.argtypes = [ctypes.c_void_p] + library.MagickGetImageCompressionQuality.restype = ctypes.c_ssize_t + + library.MagickSetImageCompressionQuality.argtypes = [ctypes.c_void_p, + ctypes.c_ssize_t] + + library.MagickStripImage.argtypes = [ctypes.c_void_p] + + library.MagickTrimImage.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.MagickGaussianBlurImage.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double] + + library.MagickUnsharpMaskImage.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.MagickGetNumberImages.argtypes = [ctypes.c_void_p] + library.MagickGetNumberImages.restype = ctypes.c_size_t + + library.MagickGetIteratorIndex.argtypes = [ctypes.c_void_p] + library.MagickGetIteratorIndex.restype = ctypes.c_size_t + + library.MagickSetIteratorIndex.argtypes = [ctypes.c_void_p, + ctypes.c_ssize_t] + + library.MagickSetFirstIterator.argtypes = [ctypes.c_void_p] + + library.MagickSetLastIterator.argtypes = [ctypes.c_void_p] + + library.MagickAddImage.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + library.MagickRemoveImage.argtypes = [ctypes.c_void_p] + + libmagick.GetNextImageInList.argtypes = [ctypes.c_void_p] + libmagick.GetNextImageInList.restype = ctypes.c_void_p + + library.MagickGetImageDelay.argtypes = [ctypes.c_void_p] + library.MagickGetImageDelay.restype = ctypes.c_ssize_t + + library.MagickSetImageDelay.argtypes = [ctypes.c_void_p, ctypes.c_ssize_t] + + library.NewMagickWandFromImage.argtypes = [ctypes.c_void_p] + library.NewMagickWandFromImage.restype = ctypes.c_void_p + + library.GetImageFromMagickWand.argtypes = [ctypes.c_void_p] + library.GetImageFromMagickWand.restype = ctypes.c_void_p + + libmagick.CloneImages.argtypes = [ctypes.c_void_p, ctypes.c_char_p, + ctypes.c_void_p] + libmagick.CloneImages.restype = ctypes.c_void_p + + libmagick.AcquireExceptionInfo.argtypes = [] + libmagick.AcquireExceptionInfo.restype = ctypes.c_void_p + + libmagick.DestroyExceptionInfo.argtypes = [ctypes.c_void_p] + libmagick.DestroyExceptionInfo.restype = ctypes.c_void_p + + libmagick.DestroyImage.argtypes = [ctypes.c_void_p] + libmagick.DestroyImage.restype = ctypes.c_void_p + + library.MagickGetSize.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint)] + library.MagickGetSize.restype = ctypes.c_int + + library.MagickSetSize.argtypes = [ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_uint] + library.MagickSetSize.restype = ctypes.c_int + + library.MagickSetDepth.argtypes = [ctypes.c_void_p, + ctypes.c_uint] + library.MagickSetDepth.restype = ctypes.c_int + + library.MagickSetFormat.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickSetFormat.restype = ctypes.c_int + + library.MagickGetFont.argtypes = [ctypes.c_void_p] + library.MagickGetFont.restype = ctypes.c_char_p + + library.MagickSetFont.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickSetFont.restype = ctypes.c_int + + library.MagickGetPointsize.argtypes = [ctypes.c_void_p] + library.MagickGetPointsize.restype = ctypes.c_double + + library.MagickSetPointsize.argtypes = [ctypes.c_void_p, + ctypes.c_double] + library.MagickSetPointsize.restype = ctypes.c_int + + library.MagickGetGravity.argtypes = [ctypes.c_void_p] + library.MagickGetGravity.restype = ctypes.c_int + + library.MagickSetGravity.argtypes = [ctypes.c_void_p, + ctypes.c_int] + library.MagickSetGravity.restype = ctypes.c_int + + library.MagickSetLastIterator.argtypes = [ctypes.c_void_p] + + library.MagickGetBackgroundColor.argtypes = [ctypes.c_void_p] + library.MagickGetBackgroundColor.restype = ctypes.c_void_p + + library.MagickSetBackgroundColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + library.MagickSetBackgroundColor.restype = ctypes.c_int + + library.MagickGetOption.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickGetOption.restype = ctypes.c_char_p + + library.MagickSetOption.argtypes = [ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p] + library.MagickSetOption.restype = ctypes.c_int + + library.MagickDeleteOption.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickDeleteOption.restype = ctypes.c_int + + library.MagickGetAntialias.argtypes = [ctypes.c_void_p] + library.MagickGetAntialias.restype = ctypes.c_int + + library.MagickSetAntialias.argtypes = [ctypes.c_void_p, + ctypes.c_int] + library.MagickSetAntialias.restype = ctypes.c_int + + library.MagickGetImageHistogram.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t) + ] + library.MagickGetImageHistogram.restype = ctypes.POINTER(ctypes.c_void_p) + + # These functions are const so it's okay for them to be c_char_p + libmagick.GetMagickVersion.argtypes = [ctypes.POINTER(ctypes.c_size_t)] + libmagick.GetMagickVersion.restype = ctypes.c_char_p + + libmagick.GetMagickReleaseDate.argtypes = [] + libmagick.GetMagickReleaseDate.restype = ctypes.c_char_p + + libmagick.GetMagickQuantumDepth.argtypes = [ + ctypes.POINTER(ctypes.c_size_t) + ] + libmagick.GetMagickQuantumDepth.restype = ctypes.c_char_p + + library.NewDrawingWand.restype = ctypes.c_void_p + + library.CloneDrawingWand.argtypes = [ctypes.c_void_p] + library.CloneDrawingWand.restype = ctypes.c_void_p + + library.DestroyDrawingWand.argtypes = [ctypes.c_void_p] + library.DestroyDrawingWand.restype = ctypes.c_void_p + + library.IsDrawingWand.argtypes = [ctypes.c_void_p] + library.IsDrawingWand.restype = ctypes.c_int + + library.DrawGetException.argtypes = [ctypes.c_void_p, + ctypes.POINTER(ctypes.c_int)] + library.DrawGetException.restype = ctypes.c_char_p + + library.DrawClearException.argtypes = [ctypes.c_void_p] + library.DrawClearException.restype = ctypes.c_int + + library.DrawAffine.argtypes = [ + ctypes.c_void_p, # Drawing wand + ctypes.POINTER(AffineMatrix), # AffineMatrix + ] + + library.DrawComment.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_char_p, # comment + ] + + library.DrawComposite.argtypes = [ + ctypes.c_void_p, # DrawingWand wand + ctypes.c_int, # CompositeOperator + ctypes.c_double, # x + ctypes.c_double, # y + ctypes.c_double, # width + ctypes.c_double, # height + ctypes.c_void_p, # MagickWand wand + ] + library.DrawComposite.restype = ctypes.c_uint + + library.DrawSetBorderColor.argtypes = [ctypes.c_void_p, # wand + ctypes.c_void_p] # PixelWand color + + library.DrawSetClipPath.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # clip_mask + library.DrawSetClipPath.restype = ctypes.c_int + + library.DrawSetClipRule.argtypes = [ctypes.c_void_p, # wand + ctypes.c_uint] # FillRule + + library.DrawSetClipUnits.argtypes = [ctypes.c_void_p, # wand + ctypes.c_uint] # ClipPathUnits + + library.DrawSetFont.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + + library.DrawSetFontFamily.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # font_family + library.DrawSetFontFamily.restype = ctypes.c_uint + + library.DrawSetFontResolution.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + library.DrawSetFontResolution.restype = ctypes.c_uint + + library.DrawSetFontSize.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.DrawSetFontStretch.argtypes = [ctypes.c_void_p, # wand + ctypes.c_int] # font_stretch + + library.DrawSetFontStyle.argtypes = [ctypes.c_void_p, # wand + ctypes.c_int] # style + + library.DrawSetFontWeight.argtypes = [ctypes.c_void_p, # wand + ctypes.c_size_t] # font_weight + + library.DrawSetFillColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawSetFillOpacity.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.DrawSetFillPatternURL.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # fill_url + library.DrawSetFillPatternURL.restype = ctypes.c_uint + + library.DrawSetFillRule.argtypes = [ctypes.c_void_p, + ctypes.c_uint] + + library.DrawSetOpacity.argtypes = [ctypes.c_void_p, ctypes.c_double] + + library.DrawSetStrokeAntialias.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # stroke_antialias + ] + + library.DrawSetStrokeColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawSetStrokeDashArray.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_size_t, # number_elements + ctypes.POINTER(ctypes.c_double), + ] + + library.DrawSetStrokeDashOffset.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # dash_offset + ] + + library.DrawSetStrokeLineCap.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # linecap + ] + + library.DrawSetStrokeLineJoin.argtypes = [ctypes.c_void_p, # wand + ctypes.c_int] # linejoin + + library.DrawSetStrokeMiterLimit.argtypes = [ctypes.c_void_p, # wand + ctypes.c_size_t] # miterlimit + + library.DrawSetStrokeOpacity.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # stroke_opacity + + library.DrawSetStrokePatternURL.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # fill_url + library.DrawSetStrokePatternURL.restype = ctypes.c_uint + + library.DrawSetStrokeWidth.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.DrawSetTextAlignment.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.DrawSetTextAntialias.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.DrawSetTextDecoration.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + try: + library.DrawSetTextDirection.argtypes = [ctypes.c_void_p, + ctypes.c_int] + except AttributeError: + library.DrawSetTextDirection = None + + library.DrawSetTextEncoding.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + + try: + library.DrawSetTextInterlineSpacing.argtypes = [ctypes.c_void_p, + ctypes.c_double] + except AttributeError: + library.DrawSetTextInterlineSpacing = None + + library.DrawSetTextInterwordSpacing.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.DrawSetTextKerning.argtypes = [ctypes.c_void_p, + ctypes.c_double] + + library.DrawSetTextUnderColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawSetVectorGraphics.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + + library.DrawSetVectorGraphics.restype = ctypes.c_int + + library.DrawResetVectorGraphics.argtypes = [ctypes.c_void_p] + + library.DrawSetViewbox.argtypes = [ctypes.c_void_p, # wand + ctypes.c_ssize_t, # x1 + ctypes.c_ssize_t, # y1 + ctypes.c_ssize_t, # x2 + ctypes.c_ssize_t] # y2 + + library.DrawGetBorderColor.argtypes = [ctypes.c_void_p, # wand + ctypes.c_void_p] # PixelWand color + + library.DrawGetClipPath.argtypes = [ctypes.c_void_p] + library.DrawGetClipPath.restype = c_magick_char_p + + library.DrawGetClipRule.argtypes = [ctypes.c_void_p] + library.DrawGetClipRule.restype = ctypes.c_uint + + library.DrawGetClipUnits.argtypes = [ctypes.c_void_p] + library.DrawGetClipUnits.restype = ctypes.c_uint + + library.DrawGetFillColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawGetFillOpacity.argtypes = [ctypes.c_void_p] + library.DrawGetFillOpacity.restype = ctypes.c_double + + library.DrawGetFillRule.argtypes = [ctypes.c_void_p] + library.DrawGetFillRule.restype = ctypes.c_uint + + library.DrawGetOpacity.argtypes = [ctypes.c_void_p] + library.DrawGetOpacity.restype = ctypes.c_double + + library.DrawGetStrokeAntialias.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeAntialias.restype = ctypes.c_int + + library.DrawGetStrokeColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawGetStrokeDashArray.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t), + ] + library.DrawGetStrokeDashArray.restype = ctypes.POINTER(ctypes.c_double) + + library.DrawGetStrokeDashOffset.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeDashOffset.restype = ctypes.c_double + + library.DrawGetStrokeLineCap.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeLineCap.restype = ctypes.c_int + + library.DrawGetStrokeLineJoin.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeLineJoin.restype = ctypes.c_int + + library.DrawGetStrokeMiterLimit.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeMiterLimit.restype = ctypes.c_size_t + + library.DrawGetStrokeOpacity.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeOpacity.restype = ctypes.c_double + + library.DrawGetStrokeWidth.argtypes = [ctypes.c_void_p] + library.DrawGetStrokeWidth.restype = ctypes.c_double + + library.DrawGetFont.argtypes = [ctypes.c_void_p] + library.DrawGetFont.restype = c_magick_char_p + + library.DrawGetFontFamily.argtypes = [ctypes.c_void_p] + library.DrawGetFontFamily.restype = c_magick_char_p + + library.DrawGetFontResolution.argtypes = [ + ctypes.c_void_p, # wand + ctypes.POINTER(ctypes.c_double), # x + ctypes.POINTER(ctypes.c_double), # y + ] + library.DrawGetFontResolution.restype = ctypes.c_uint + + library.DrawGetFontSize.argtypes = [ctypes.c_void_p] + library.DrawGetFontSize.restype = ctypes.c_double + + library.DrawGetFontStyle.argtypes = [ctypes.c_void_p] + library.DrawGetFontStyle.restype = ctypes.c_int + + library.DrawGetFontWeight.argtypes = [ctypes.c_void_p] + library.DrawGetFontWeight.restype = ctypes.c_size_t + + library.DrawGetFontStretch.argtypes = [ctypes.c_void_p] + library.DrawGetFontStretch.restype = ctypes.c_int + + library.DrawGetTextAlignment.argtypes = [ctypes.c_void_p] + library.DrawGetTextAlignment.restype = ctypes.c_int + + library.DrawGetTextAntialias.argtypes = [ctypes.c_void_p] + library.DrawGetTextAntialias.restype = ctypes.c_int + + library.DrawGetTextDecoration.argtypes = [ctypes.c_void_p] + library.DrawGetTextDecoration.restype = ctypes.c_int + + try: + library.DrawGetTextDirection.argtypes = [ctypes.c_void_p] + library.DrawGetTextDirection.restype = ctypes.c_int + except AttributeError: + library.DrawGetTextDirection = None + + library.DrawGetTextEncoding.argtypes = [ctypes.c_void_p] + library.DrawGetTextEncoding.restype = c_magick_char_p + + try: + library.DrawGetTextInterlineSpacing.argtypes = [ctypes.c_void_p] + library.DrawGetTextInterlineSpacing.restype = ctypes.c_double + except AttributeError: + library.DrawGetTextInterlineSpacing = None + + library.DrawGetTextInterwordSpacing.argtypes = [ctypes.c_void_p] + library.DrawGetTextInterwordSpacing.restype = ctypes.c_double + + library.DrawGetTextKerning.argtypes = [ctypes.c_void_p] + library.DrawGetTextKerning.restype = ctypes.c_double + + library.DrawGetTextUnderColor.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + library.DrawGetVectorGraphics.argtypes = [ctypes.c_void_p] + library.DrawGetVectorGraphics.restype = c_magick_char_p + + library.DrawSetGravity.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.DrawGetGravity.argtypes = [ctypes.c_void_p] + library.DrawGetGravity.restype = ctypes.c_int + + library.MagickAnnotateImage.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double, + ctypes.c_char_p] + library.MagickAnnotateImage.restype = ctypes.c_int + + library.MagickDistortImage.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_int, # method + ctypes.c_size_t, # number_arguments + ctypes.POINTER(ctypes.c_double), # arguments + ctypes.c_int, # bestfit + ] + library.MagickDistortImage.restype = ctypes.c_int + + library.ClearDrawingWand.argtypes = [ctypes.c_void_p] + + library.MagickDrawImage.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + library.MagickDrawImage.restype = ctypes.c_int + + library.DrawAnnotation.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.POINTER(ctypes.c_ubyte)] + + library.DrawArc.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # sx + ctypes.c_double, # sy + ctypes.c_double, # ex + ctypes.c_double, # ey + ctypes.c_double, # sd + ctypes.c_double] # ed + + library.DrawBezier.argtypes = [ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(PointInfo)] + + library.DrawCircle.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # ox + ctypes.c_double, # oy + ctypes.c_double, # px + ctypes.c_double] # py + + library.DrawColor.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double, # y + ctypes.c_uint] # PaintMethod + + library.DrawEllipse.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # ox + ctypes.c_double, # oy + ctypes.c_double, # rx + ctypes.c_double, # ry + ctypes.c_double, # start + ctypes.c_double] # end + + library.DrawLine.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.DrawMatte.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double, # y + ctypes.c_uint] # PaintMethod + + library.DrawPathClose.argtypes = [ctypes.c_void_p] # wand + + library.DrawPathCurveToAbsolute.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x1 + ctypes.c_double, # y1 + ctypes.c_double, # x2 + ctypes.c_double, # y2 + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathCurveToRelative.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x1 + ctypes.c_double, # y1 + ctypes.c_double, # x2 + ctypes.c_double, # y2 + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathCurveToQuadraticBezierAbsolute.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x1 + ctypes.c_double, # y1 + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathCurveToQuadraticBezierRelative.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x1 + ctypes.c_double, # y1 + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathCurveToQuadraticBezierSmoothAbsolute.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathCurveToQuadraticBezierSmoothRelative.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathCurveToSmoothAbsolute.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x2 + ctypes.c_double, # y2 + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathCurveToSmoothRelative.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x2 + ctypes.c_double, # y2 + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathEllipticArcAbsolute.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # rx + ctypes.c_double, # ry + ctypes.c_double, # rotation + ctypes.c_uint, # arc_flag + ctypes.c_uint, # sweep_flag + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathEllipticArcRelative.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # rx + ctypes.c_double, # ry + ctypes.c_double, # rotation + ctypes.c_uint, # arc_flag + ctypes.c_uint, # sweep_flag + ctypes.c_double, # x + ctypes.c_double, # y + ] + + library.DrawPathFinish.argtypes = [ctypes.c_void_p] # wand + + library.DrawPathLineToAbsolute.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathLineToRelative.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathLineToHorizontalAbsolute.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x + ] + + library.DrawPathLineToHorizontalRelative.argtypes = [ + ctypes.c_void_p, # wand + ctypes.c_double, # x + ] + + library.DrawPathLineToVerticalAbsolute.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # y + + library.DrawPathLineToVerticalRelative.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # y + + library.DrawPathMoveToAbsolute.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathMoveToRelative.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPathStart.argtypes = [ctypes.c_void_p] # wand + + library.DrawPoint.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawPolygon.argtypes = [ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(PointInfo)] + + library.DrawPolyline.argtypes = [ctypes.c_void_p, + ctypes.c_ulong, + ctypes.POINTER(PointInfo)] + + library.DrawRotate.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # degree + + library.DrawRectangle.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.DrawRoundRectangle.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x1 + ctypes.c_double, # y1 + ctypes.c_double, # x2 + ctypes.c_double, # y2 + ctypes.c_double, # rx + ctypes.c_double] # ry + + library.DrawScale.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + library.DrawSkewX.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # degree + + library.DrawSkewY.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double] # degree + + library.DrawTranslate.argtypes = [ctypes.c_void_p, # wand + ctypes.c_double, # x + ctypes.c_double] # y + + # -- Drawing stack management -- + library.PushDrawingWand.argtypes = [ctypes.c_void_p] + library.PushDrawingWand.restype = ctypes.c_uint + library.DrawPushClipPath.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p] # clip_mask_id + library.DrawPushDefs.argtypes = [ctypes.c_void_p] + library.DrawPushPattern.argtypes = [ctypes.c_void_p, # wand + ctypes.c_char_p, # clip_mask_id + ctypes.c_double, # x + ctypes.c_double, # y + ctypes.c_double, # width + ctypes.c_double] # height + library.DrawPushClipPath.restype = ctypes.c_uint + library.PopDrawingWand.argtypes = [ctypes.c_void_p] + library.PopDrawingWand.restype = ctypes.c_uint + library.DrawPopClipPath.argtypes = [ctypes.c_void_p] + library.DrawPopDefs.argtypes = [ctypes.c_void_p] + library.DrawPopPattern.argtypes = [ctypes.c_void_p] + + library.MagickNegateImage.argtypes = [ctypes.c_void_p, ctypes.c_int] + + library.MagickNegateImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int] + + library.MagickNormalizeImage.argtypes = [ctypes.c_void_p] + + library.MagickNormalizeImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + library.MagickEqualizeImage.argtypes = [ctypes.c_void_p] + + library.MagickQueryConfigureOption.argtypes = [ctypes.c_char_p] + library.MagickQueryConfigureOption.restype = c_magick_char_p + + library.MagickQueryConfigureOptions.argtypes = [ + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t), + ] + library.MagickQueryConfigureOptions.restype = \ + ctypes.POINTER(c_magick_char_p) + + library.MagickQueryFontMetrics.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_char_p] + library.MagickQueryFontMetrics.restype = ctypes.POINTER(ctypes.c_double) + + library.MagickQueryFonts.argtypes = [ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t)] + library.MagickQueryFonts.restype = ctypes.POINTER(c_magick_char_p) + + library.MagickQueryFormats.argtypes = [ctypes.c_char_p, + ctypes.POINTER(ctypes.c_size_t)] + library.MagickQueryFormats.restype = ctypes.POINTER(c_magick_char_p) + + library.MagickQueryMultilineFontMetrics.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_char_p] + library.MagickQueryMultilineFontMetrics.restype = ctypes.POINTER( + ctypes.c_double + ) + + library.MagickThresholdImage.argtypes = [ctypes.c_void_p, ctypes.c_double] + + library.MagickThresholdImageChannel.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_double] + + library.MagickModulateImage.argtypes = [ctypes.c_void_p, + ctypes.c_double, + ctypes.c_double, + ctypes.c_double] + + library.MagickAppendImages.argtypes = [ctypes.c_void_p, + ctypes.c_int] + library.MagickAppendImages.restype = ctypes.c_void_p + + library.MagickTransposeImage.argtypes = [ctypes.c_void_p] + library.MagickTransverseImage.argtypes = [ctypes.c_void_p] + + library.MagickQuantizeImage.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_bool, + ctypes.c_bool] + +except AttributeError: + raise ImportError('MagickWand shared library not found or incompatible\n' + 'Original exception was raised in:\n' + + traceback.format_exc()) + +try: + library.MagickAutoOrientImage.argtypes = [ctypes.c_void_p] +except AttributeError: + # MagickAutoOrientImage was added in 6.8.9+, we have a fallback function + # so we pass silently if we cant import it + pass + + +#: (:class:`ctypes.CDLL`) The C standard library. +libc = None + +if platform.system() == 'Windows': + msvcrt = ctypes.util.find_msvcrt() + # workaround -- the newest visual studio DLL is named differently: + if not msvcrt and "1900" in platform.python_compiler(): + msvcrt = "vcruntime140.dll" + if msvcrt: + libc = ctypes.CDLL(msvcrt) +else: + if platform.system() == 'Darwin': + libc = ctypes.cdll.LoadLibrary('libc.dylib') + elif platform.system() == 'FreeBSD': + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c')) + else: + libc = ctypes.cdll.LoadLibrary('libc.so.6') + libc.fdopen.argtypes = [ctypes.c_int, ctypes.c_char_p] + libc.fdopen.restype = ctypes.c_void_p + libc.fflush.argtypes = [ctypes.c_void_p] diff --git a/lib/wand/api.pyc b/lib/wand/api.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e9d955a2e0f18e6259e5326ca657f0bb20e3d0f GIT binary patch literal 27988 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  + #{red:02X}{green:02X}{blue:02X} + """ + return html.format(red=self.red_int8, + green=self.green_int8, + blue=self.blue_int8) + + +def scale_quantum_to_int8(quantum): + """Straightforward port of :c:func:`ScaleQuantumToChar()` inline + function. + + :param quantum: quantum value + :type quantum: :class:`numbers.Integral` + :returns: 8bit integer of the given ``quantum`` value + :rtype: :class:`numbers.Integral` + + .. versionadded:: 0.3.0 + + """ + if quantum <= 0: + return 0 + table = {8: 1, 16: 257.0, 32: 16843009.0, 64: 72340172838076673.0} + v = quantum / table[QUANTUM_DEPTH] + if v >= 255: + return 255 + return int(v + 0.5) diff --git a/lib/wand/color.pyc b/lib/wand/color.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9212e3e7f72fdfeb1a2fda52ef77b116a3967a3 GIT binary patch literal 11975 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 literal 0 HcmV?d00001 diff --git a/lib/wand/compat.py b/lib/wand/compat.py new file mode 100644 index 00000000..3545b7ba --- /dev/null +++ b/lib/wand/compat.py @@ -0,0 +1,119 @@ +""":mod:`wand.compat` --- Compatibility layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provides several subtle things to support +multiple Python versions (2.6, 2.7, 3.2--3.5) and VM implementations +(CPython, PyPy). + +""" +import contextlib +import io +import sys +import types + +__all__ = ('PY3', 'binary', 'binary_type', 'encode_filename', 'file_types', + 'nested', 'string_type', 'text', 'text_type', 'xrange') + + +#: (:class:`bool`) Whether it is Python 3.x or not. +PY3 = sys.version_info >= (3,) + +#: (:class:`type`) Type for representing binary data. :class:`str` in Python 2 +#: and :class:`bytes` in Python 3. +binary_type = bytes if PY3 else str + +#: (:class:`type`) Type for text data. :class:`basestring` in Python 2 +#: and :class:`str` in Python 3. +string_type = str if PY3 else basestring # noqa + +#: (:class:`type`) Type for representing Unicode textual data. +#: :class:`unicode` in Python 2 and :class:`str` in Python 3. +text_type = str if PY3 else unicode # noqa + + +def binary(string, var=None): + """Makes ``string`` to :class:`str` in Python 2. + Makes ``string`` to :class:`bytes` in Python 3. + + :param string: a string to cast it to :data:`binary_type` + :type string: :class:`bytes`, :class:`str`, :class:`unicode` + :param var: an optional variable name to be used for error message + :type var: :class:`str` + + """ + if isinstance(string, text_type): + return string.encode() + elif isinstance(string, binary_type): + return string + if var: + raise TypeError('{0} must be a string, not {1!r}'.format(var, string)) + raise TypeError('expected a string, not ' + repr(string)) + + +if PY3: + def text(string): + if isinstance(string, bytes): + return string.decode('utf-8') + return string +else: + def text(string): + """Makes ``string`` to :class:`str` in Python 3. + Does nothing in Python 2. + + :param string: a string to cast it to :data:`text_type` + :type string: :class:`bytes`, :class:`str`, :class:`unicode` + + """ + return string + + +#: The :func:`xrange()` function. Alias for :func:`range()` in Python 3. +xrange = range if PY3 else xrange # noqa + + +#: (:class:`type`, :class:`tuple`) Types for file objects that have +#: ``fileno()``. +file_types = io.RawIOBase if PY3 else (io.RawIOBase, types.FileType) + + +def encode_filename(filename): + """If ``filename`` is a :data:`text_type`, encode it to + :data:`binary_type` according to filesystem's default encoding. + + """ + if isinstance(filename, text_type): + return filename.encode(sys.getfilesystemencoding()) + return filename + + +try: + nested = contextlib.nested +except AttributeError: + # http://hg.python.org/cpython/file/v2.7.6/Lib/contextlib.py#l88 + @contextlib.contextmanager + def nested(*managers): + exits = [] + vars = [] + exc = (None, None, None) + try: + for mgr in managers: + exit = mgr.__exit__ + enter = mgr.__enter__ + vars.append(enter()) + exits.append(exit) + yield vars + except: + exc = sys.exc_info() + finally: + while exits: + exit = exits.pop() + try: + if exit(*exc): + exc = (None, None, None) + except: + exc = sys.exc_info() + if exc != (None, None, None): + # PEP 3109 + e = exc[0](exc[1]) + e.__traceback__ = e[2] + raise e diff --git a/lib/wand/compat.pyc b/lib/wand/compat.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2bb0504750ed9c555ae2e4a8643ac0964cab5fa GIT binary patch literal 3504 zcmcImUvCpf5TEnePGS-QN%)geiG@n7sS<}K0xI{9+J;u55&<~~Q%lHR-))jh>^pP2 zhPa7&C_MH%^r_#V?|ts4=wm-XrM~o`{mt$t*^&rv*`4OE`C&(H>pKTF|hD|?OxQ=~lFK2MGFRFB`Iyqc$- z5~XK7{CMRZ3TK%28LT)@u}2SJ)q431U(z4ws@){4ww}m1tOQB7CrwK%E-s2yJC52> zCo%)kkpq>Mp1#hnRJyknX@TAQ9VL2c@;C~W7Fs=4sq6^dZyV6fRupe)VGY_#@WT4ON9k=cfGd1lAWT7p9NheaM=`d+QjTCM(>*%p7D6As zXQldEJdoRR2QF!~+yPoG_Cq!3NUf_(Yl$cxxna3d68ODJqCGH5zbLbUduO3)Po}af zoR+GP*@(3TQX3H&t2mTKR$I<1_SRZXn!R!$JTlKITR39w;jAA=?7(aTr~a`_p*t2y zk1+xWf)L3zkj5@%3))KbwF<>Xk_wfk=(|d5xv5^T!|pONg73&3QD4NJ?p+;ZN7N#$xWqE$Cr)85FXezJbxwR?<~V!B!e+lk~Qh`1SXVN|yrJ ziQ1`Je4^S*7%g#Zu&dHkdV?BpLzmI?Npw`2_DbHgm-mX^1ux(=Za&&7nilAe!1~<` z)_^S#xz13w_O9sySo>yU@#8W}*Z3+7)2yfMIYY%xEwh3eztXpKAinwQXqvB#aG0sX zgRm}p5WZY%EK~IF=w9^{jy)bc1bzqnyP4p{0TLYdjy|sgme+bN9BW>9EUcpXCJ*Mo z3U|5x$6bHIH^>;J@s|PfKFCJ^b2$SHP`sVzXm|h{>xrk}zqNrFHj33PL?A7ssJd`+ zcs~omy&@NG+>?GeW3VV?F;b9gjFg25n1L!=WFB|hyJGgo-XJUlP3U%W>bsSbww zUOk>A6d;I6NKSAXcIVQg9wH5X&Dkpbm3x#}yjY+C(jAoe^dn-}&lDD{1X2Cb-tm1v zd4N_2b6BJupVGfr5CLVLKRKF*CBre=AEVF*ZH$_B;?m*+Sg>w`?OKFOpomcvF&b^y zSzB9s>B~1~gX>xnE9FAh#qp! z6o}wWWuV$J*l9M)c7N`=GDfmVwGDf$L0h#5-P(#{FZ=Corgni@94{Myx&i9S=-xt* zXG}jt(^q-FToK{D==tO$HcsGw$~%qT_vUgF-Xy-0pkwAu=H@UebL`d(M3h< zW2KhX%5N!y!DY%%SyOX%f2yNZ>NUMCLlW<2uy;S)uPs+HO#4YntA!8FwCWA-ncC_q`4@pF(&Lj zSxMqz#92^nBCWAa)`zw@tl6V*SCda}-T4apSQLGOIK(->#%HdvzM8d?Q}CAtn%cz>% literal 0 HcmV?d00001 diff --git a/lib/wand/display.py b/lib/wand/display.py new file mode 100644 index 00000000..8fd62295 --- /dev/null +++ b/lib/wand/display.py @@ -0,0 +1,78 @@ +""":mod:`wand.display` --- Displaying images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`display()` functions shows you the image. It is useful for +debugging. + +If you are in Mac, the image will be opened by your default image application +(:program:`Preview.app` usually). + +If you are in Windows, the image will be opened by :program:`imdisplay.exe`, +or your default image application (:program:`Windows Photo Viewer` usually) +if :program:`imdisplay.exe` is unavailable. + +You can use it from CLI also. Execute :mod:`wand.display` module through +:option:`python -m` option: + +.. sourcecode:: console + + $ python -m wand.display wandtests/assets/mona-lisa.jpg + +.. versionadded:: 0.1.9 + +""" +import ctypes +import os +import platform +import sys +import tempfile + +from .image import Image +from .api import library +from .exceptions import BlobError, DelegateError + +__all__ = 'display', + + +def display(image, server_name=':0'): + """Displays the passed ``image``. + + :param image: an image to display + :type image: :class:`~wand.image.Image` + :param server_name: X11 server name to use. it is ignored and not used + for Mac. default is ``':0'`` + :type server_name: :class:`str` + + """ + if not isinstance(image, Image): + raise TypeError('image must be a wand.image.Image instance, not ' + + repr(image)) + system = platform.system() + if system == 'Windows': + try: + image.save(filename='win:.') + except DelegateError: + pass + else: + return + if system in ('Windows', 'Darwin'): + ext = '.' + image.format.lower() + path = tempfile.mktemp(suffix=ext) + image.save(filename=path) + os.system(('start ' if system == 'Windows' else 'open ') + path) + else: + library.MagickDisplayImage.argtypes = [ctypes.c_void_p, + ctypes.c_char_p] + library.MagickDisplayImage(image.wand, str(server_name).encode()) + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print>>sys.stderr, 'usage: python -m wand.display FILE' + raise SystemExit + path = sys.argv[1] + try: + with Image(filename=path) as image: + display(image) + except BlobError: + print>>sys.stderr, 'cannot read the file', path diff --git a/lib/wand/drawing.py b/lib/wand/drawing.py new file mode 100644 index 00000000..e34245cd --- /dev/null +++ b/lib/wand/drawing.py @@ -0,0 +1,1988 @@ +""":mod:`wand.drawing` --- Drawings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module provides some vector drawing functions. + +.. versionadded:: 0.3.0 + +""" +import collections +import ctypes +import numbers + +from .api import library, MagickPixelPacket, PointInfo, AffineMatrix +from .color import Color +from .compat import binary, string_type, text, text_type, xrange +from .image import Image, COMPOSITE_OPERATORS +from .resource import Resource +from .exceptions import WandLibraryVersionError + +__all__ = ('CLIP_PATH_UNITS', 'FILL_RULE_TYPES', 'FONT_METRICS_ATTRIBUTES', + 'GRAVITY_TYPES', 'LINE_CAP_TYPES', 'LINE_JOIN_TYPES', + 'PAINT_METHOD_TYPES', 'STRETCH_TYPES', 'STYLE_TYPES', + 'TEXT_ALIGN_TYPES', 'TEXT_DECORATION_TYPES', + 'TEXT_DIRECTION_TYPES', 'Drawing', 'FontMetrics') + + +#: (:class:`collections.Sequence`) The list of clip path units +#: +#: - ``'undefined_path_units'`` +#: - ``'user_space'`` +#: - ``'user_space_on_use'`` +#: - ``'object_bounding_box'`` +CLIP_PATH_UNITS = ('undefined_path_units', 'user_space', 'user_space_on_use', + 'object_bounding_box') + +#: (:class:`collections.Sequence`) The list of text align types. +#: +#: - ``'undefined'`` +#: - ``'left'`` +#: - ``'center'`` +#: - ``'right'`` +TEXT_ALIGN_TYPES = 'undefined', 'left', 'center', 'right' + +#: (:class:`collections.Sequence`) The list of text decoration types. +#: +#: - ``'undefined'`` +#: - ``'no'`` +#: - ``'underline'`` +#: - ``'overline'`` +#: - ``'line_through'`` +TEXT_DECORATION_TYPES = ('undefined', 'no', 'underline', 'overline', + 'line_through') + +#: (:class:`collections.Sequence`) The list of text direction types. +#: +#: - ``'undefined'`` +#: - ``'right_to_left'`` +#: - ``'left_to_right'`` +TEXT_DIRECTION_TYPES = ('undefined', 'right_to_left', 'left_to_right') + +#: (:class:`collections.Sequence`) The list of text gravity types. +#: +#: - ``'forget'`` +#: - ``'north_west'`` +#: - ``'north'`` +#: - ``'north_east'`` +#: - ``'west'`` +#: - ``'center'`` +#: - ``'east'`` +#: - ``'south_west'`` +#: - ``'south'`` +#: - ``'south_east'`` +#: - ``'static'`` +GRAVITY_TYPES = ('forget', 'north_west', 'north', 'north_east', 'west', + 'center', 'east', 'south_west', 'south', 'south_east', + 'static') + +#: (:class:`collections.Sequence`) The list of fill-rule types. +#: +#: - ``'undefined'`` +#: - ``'evenodd'`` +#: - ``'nonzero'`` +FILL_RULE_TYPES = ('undefined', 'evenodd', 'nonzero') + +#: (:class:`collections.Sequence`) The attribute names of font metrics. +FONT_METRICS_ATTRIBUTES = ('character_width', 'character_height', 'ascender', + 'descender', 'text_width', 'text_height', + 'maximum_horizontal_advance', 'x1', 'y1', 'x2', + 'y2', 'x', 'y') + +#: The tuple subtype which consists of font metrics data. +FontMetrics = collections.namedtuple('FontMetrics', FONT_METRICS_ATTRIBUTES) + +#: (:class:`collections.Sequence`) The list of stretch types for fonts +#: +#: - ``'undefined;`` +#: - ``'normal'`` +#: - ``'ultra_condensed'`` +#: - ``'extra_condensed'`` +#: - ``'condensed'`` +#: - ``'semi_condensed'`` +#: - ``'semi_expanded'`` +#: - ``'expanded'`` +#: - ``'extra_expanded'`` +#: - ``'ultra_expanded'`` +#: - ``'any'`` +STRETCH_TYPES = ('undefined', 'normal', 'ultra_condensed', 'extra_condensed', + 'condensed', 'semi_condensed', 'semi_expanded', 'expanded', + 'extra_expanded', 'ultra_expanded', 'any') + +#: (:class:`collections.Sequence`) The list of style types for fonts +#: +#: - ``'undefined;`` +#: - ``'normal'`` +#: - ``'italic'`` +#: - ``'oblique'`` +#: - ``'any'`` +STYLE_TYPES = ('undefined', 'normal', 'italic', 'oblique', 'any') + +#: (:class:`collections.Sequence`) The list of LineCap types +#: +#: - ``'undefined;`` +#: - ``'butt'`` +#: - ``'round'`` +#: - ``'square'`` +LINE_CAP_TYPES = ('undefined', 'butt', 'round', 'square') + +#: (:class:`collections.Sequence`) The list of LineJoin types +#: +#: - ``'undefined'`` +#: - ``'miter'`` +#: - ``'round'`` +#: - ``'bevel'`` +LINE_JOIN_TYPES = ('undefined', 'miter', 'round', 'bevel') + + +#: (:class:`collections.Sequence`) The list of paint method types. +#: +#: - ``'undefined'`` +#: - ``'point'`` +#: - ``'replace'`` +#: - ``'floodfill'`` +#: - ``'filltoborder'`` +#: - ``'reset'`` +PAINT_METHOD_TYPES = ('undefined', 'point', 'replace', + 'floodfill', 'filltoborder', 'reset') + + +class Drawing(Resource): + """Drawing object. It maintains several vector drawing instructions + and can get drawn into zero or more :class:`~wand.image.Image` objects + by calling it. + + For example, the following code draws a diagonal line to the ``image``:: + + with Drawing() as draw: + draw.line((0, 0), image.size) + draw(image) + + :param drawing: an optional drawing object to clone. + use :meth:`clone()` method rather than this parameter + :type drawing: :class:`Drawing` + + .. versionadded:: 0.3.0 + + """ + + c_is_resource = library.IsDrawingWand + c_destroy_resource = library.DestroyDrawingWand + c_get_exception = library.DrawGetException + c_clear_exception = library.DrawClearException + + def __init__(self, drawing=None): + with self.allocate(): + if not drawing: + wand = library.NewDrawingWand() + elif not isinstance(drawing, type(self)): + raise TypeError('drawing must be a wand.drawing.Drawing ' + 'instance, not ' + repr(drawing)) + else: + wand = library.CloneDrawingWand(drawing.resource) + self.resource = wand + + def clone(self): + """Copies a drawing object. + + :returns: a duplication + :rtype: :class:`Drawing` + + """ + return type(self)(drawing=self) + + @property + def border_color(self): + """(:class:`~wand.color.Color`) the current border color. It also can + be set. This attribute controls the behavior of + :meth:`~wand.drawing.Drawing.color()` during ``'filltoborder'`` + operation. + + .. versionadded:: 0.4.0 + """ + pixelwand = library.NewPixelWand() + library.DrawGetBorderColor(self.resource, pixelwand) + size = ctypes.sizeof(MagickPixelPacket) + buffer = ctypes.create_string_buffer(size) + library.PixelGetMagickColor(pixelwand, buffer) + return Color(raw=buffer) + + @border_color.setter + def border_color(self, border_color): + if not isinstance(border_color, Color): + raise ValueError('expected wand.color.Color, not ' + + repr(border_color)) + with border_color: + library.DrawSetBorderColor(self.resource, border_color.resource) + + @property + def clip_path(self): + """(:class:`basestring`) The current clip path. It also can be set. + + .. versionadded:: 0.4.0 + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + clip_path_p = library.DrawGetClipPath(self.resource) + return text(clip_path_p.value) + + @clip_path.setter + def clip_path(self, path): + if not isinstance(path, string_type): + raise TypeError('expected a string, not ' + repr(path)) + okay = library.DrawSetClipPath(self.resource, binary(path)) + if okay == 0: + raise ValueError('Clip path not understood') + + @property + def clip_rule(self): + """(:class:`basestring`) The current clip rule. It also can be set. + It's a string value from :const:`FILL_RULE_TYPES` list. + + .. versionadded:: 0.4.0 + """ + clip_rule = library.DrawGetClipRule(self.resource) + return FILL_RULE_TYPES[clip_rule] + + @clip_rule.setter + def clip_rule(self, clip_rule): + if not isinstance(clip_rule, string_type): + raise TypeError('expected a string, not ' + repr(clip_rule)) + elif clip_rule not in FILL_RULE_TYPES: + raise ValueError('expected a string from FILE_RULE_TYPES, not' + + repr(clip_rule)) + library.DrawSetClipRule(self.resource, + FILL_RULE_TYPES.index(clip_rule)) + + @property + def clip_units(self): + """(:class:`basestring`) The current clip units. It also can be set. + It's a string value from :const:`CLIP_PATH_UNITS` list. + + .. versionadded:: 0.4.0 + """ + clip_unit = library.DrawGetClipUnits(self.resource) + return CLIP_PATH_UNITS[clip_unit] + + @clip_units.setter + def clip_units(self, clip_unit): + if not isinstance(clip_unit, string_type): + raise TypeError('expected a string, not ' + repr(clip_unit)) + elif clip_unit not in CLIP_PATH_UNITS: + raise ValueError('expected a string from CLIP_PATH_UNITS, not' + + repr(clip_unit)) + library.DrawSetClipUnits(self.resource, + CLIP_PATH_UNITS.index(clip_unit)) + + @property + def font(self): + """(:class:`basestring`) The current font name. It also can be set. + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + font_p = library.DrawGetFont(self.resource) + return text(font_p.value) + + @font.setter + def font(self, font): + if not isinstance(font, string_type): + raise TypeError('expected a string, not ' + repr(font)) + library.DrawSetFont(self.resource, binary(font)) + + @property + def font_family(self): + """(:class:`basestring`) The current font family. It also can be set. + + .. versionadded:: 0.4.0 + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + font_family_p = library.DrawGetFontFamily(self.resource) + return text(font_family_p.value) + + @font_family.setter + def font_family(self, family): + if not isinstance(family, string_type): + raise TypeError('expected a string, not ' + repr(family)) + library.DrawSetFontFamily(self.resource, binary(family)) + + @property + def font_resolution(self): + """(:class:`~collections.Sequence`) The current font resolution. It also + can be set. + + .. versionadded:: 0.4.0 + """ + x, y = ctypes.c_double(0.0), ctypes.c_double(0.0) + library.DrawGetFontResolution(self.resource, + ctypes.byref(x), + ctypes.byref(y)) + return x.value, y.value + + @font_resolution.setter + def font_resolution(self, resolution): + if not isinstance(resolution, collections.Sequence): + raise TypeError('expected sequence, not ' + repr(resolution)) + if len(resolution) != 2: + raise ValueError('expected sequence of 2 floats') + library.DrawSetFontResolution(self.resource, *resolution) + + @property + def font_size(self): + """(:class:`numbers.Real`) The font size. It also can be set.""" + return library.DrawGetFontSize(self.resource) + + @font_size.setter + 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)) + library.DrawSetFontSize(self.resource, size) + + @property + def font_stretch(self): + """(:class:`basestring`) The current font family. It also can be set. + + .. versionadded:: 0.4.0 + """ + stretch_index = library.DrawGetFontStretch(self.resource) + return text(STRETCH_TYPES[stretch_index]) + + @font_stretch.setter + def font_stretch(self, stretch): + if not isinstance(stretch, string_type): + raise TypeError('expected a string, not ' + repr(stretch)) + elif stretch not in STRETCH_TYPES: + raise ValueError('expected a string from STRETCH_TYPES, not' + + repr(stretch)) + library.DrawSetFontStretch(self.resource, + STRETCH_TYPES.index(stretch)) + + @property + def font_style(self): + """(:class:`basestring`) The current font style. It also can be set. + + .. versionadded:: 0.4.0 + """ + style_index = library.DrawGetFontStyle(self.resource) + return text(STYLE_TYPES[style_index]) + + @font_style.setter + def font_style(self, style): + if not isinstance(style, string_type): + raise TypeError('expected a string, not ' + repr(style)) + elif style not in STYLE_TYPES: + raise ValueError('expected a string from STYLE_TYPES, not' + + repr(style)) + library.DrawSetFontStyle(self.resource, + STYLE_TYPES.index(style)) + + @property + def font_weight(self): + """(:class:`~numbers.Integral`) The current font weight. + It also can be set. + + .. versionadded:: 0.4.0 + """ + return library.DrawGetFontWeight(self.resource) + + @font_weight.setter + def font_weight(self, weight): + if not isinstance(weight, numbers.Integral): + raise TypeError('expected a integral, not ' + repr(weight)) + library.DrawSetFontWeight(self.resource, weight) + + @property + def fill_color(self): + """(:class:`~wand.color.Color`) The current color to fill. + It also can be set. + + """ + pixel = library.NewPixelWand() + library.DrawGetFillColor(self.resource, pixel) + size = ctypes.sizeof(MagickPixelPacket) + buffer = ctypes.create_string_buffer(size) + library.PixelGetMagickColor(pixel, buffer) + return Color(raw=buffer) + + @fill_color.setter + def fill_color(self, color): + if not isinstance(color, Color): + raise TypeError('color must be a wand.color.Color object, not ' + + repr(color)) + with color: + library.DrawSetFillColor(self.resource, color.resource) + + @property + def fill_opacity(self): + """(:class:`~numbers.Real`) The current fill opacity. + It also can be set. + + .. versionadded:: 0.4.0 + """ + return library.DrawGetFillOpacity(self.resource) + + @fill_opacity.setter + def fill_opacity(self, opacity): + if not isinstance(opacity, numbers.Real): + raise TypeError('opacity must be a double, not ' + + repr(opacity)) + library.DrawSetFillOpacity(self.resource, opacity) + + @property + def fill_rule(self): + """(:class:`basestring`) The current fill rule. It can also be set. + It's a string value from :const:`FILL_RULE_TYPES` list. + + .. versionadded:: 0.4.0 + """ + fill_rule_index = library.DrawGetFillRule(self.resource) + if fill_rule_index not in FILL_RULE_TYPES: + self.raise_exception() + return text(FILL_RULE_TYPES[fill_rule_index]) + + @fill_rule.setter + def fill_rule(self, fill_rule): + if not isinstance(fill_rule, string_type): + raise TypeError('expected a string, not ' + repr(fill_rule)) + elif fill_rule not in FILL_RULE_TYPES: + raise ValueError('expected a string from FILE_RULE_TYPES, not' + + repr(fill_rule)) + library.DrawSetFillRule(self.resource, + FILL_RULE_TYPES.index(fill_rule)) + + @property + def opacity(self): + """(:class:`~numbers.Real`) returns the opacity used when drawing with + the fill or stroke color or texture. Fully opaque is 1.0. This method + only affects vector graphics, and is experimental. To set the opacity + of a drawing, use + :attr:`Drawing.fill_opacity` & :attr:`Drawing.stroke_opacity` + + .. versionadded:: 0.4.0 + """ + return library.DrawGetOpacity(self.resource) + + @opacity.setter + def opacity(self, opaque): + library.DrawSetOpacity(self.resource, ctypes.c_double(opaque)) + + @property + def stroke_antialias(self): + """(:class:`bool`) Controls whether stroked outlines are antialiased. + Stroked outlines are antialiased by default. When antialiasing is + disabled stroked pixels are thresholded to determine if the stroke + color or underlying canvas color should be used. + + It also can be set. + + .. versionadded:: 0.4.0 + + """ + stroke_antialias = library.DrawGetStrokeAntialias(self.resource) + return bool(stroke_antialias) + + @stroke_antialias.setter + def stroke_antialias(self, stroke_antialias): + library.DrawSetStrokeAntialias(self.resource, bool(stroke_antialias)) + + @property + def stroke_color(self): + """(:class:`~wand.color.Color`) The current color of stroke. + It also can be set. + + .. versionadded:: 0.3.3 + + """ + pixel = library.NewPixelWand() + library.DrawGetStrokeColor(self.resource, pixel) + size = ctypes.sizeof(MagickPixelPacket) + buffer = ctypes.create_string_buffer(size) + library.PixelGetMagickColor(pixel, buffer) + return Color(raw=buffer) + + @stroke_color.setter + def stroke_color(self, color): + if not isinstance(color, Color): + raise TypeError('color must be a wand.color.Color object, not ' + + repr(color)) + with color: + library.DrawSetStrokeColor(self.resource, color.resource) + + @property + def stroke_dash_array(self): + """(:class:`~collections.Sequence`) - (:class:`numbers.Real`) An array + representing the pattern of dashes & gaps used to stroke paths. + It also can be set. + + .. versionadded:: 0.4.0 + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + number_elements = ctypes.c_size_t(0) + dash_array_p = library.DrawGetStrokeDashArray( + self.resource, ctypes.byref(number_elements) + ) + dash_array = [] + if dash_array_p is not None: + dash_array = [float(dash_array_p[i]) + for i in xrange(number_elements.value)] + library.MagickRelinquishMemory(dash_array_p) + return dash_array + + @stroke_dash_array.setter + def stroke_dash_array(self, dash_array): + dash_array_l = len(dash_array) + dash_array_p = (ctypes.c_double * dash_array_l)(*dash_array) + library.DrawSetStrokeDashArray(self.resource, + dash_array_l, + dash_array_p) + + @property + def stroke_dash_offset(self): + """(:class:`numbers.Real`) The stroke dash offset. It also can be set. + + .. versionadded:: 0.4.0 + """ + return library.DrawGetStrokeDashOffset(self.resource) + + @stroke_dash_offset.setter + def stroke_dash_offset(self, offset): + library.DrawSetStrokeDashOffset(self.resource, float(offset)) + + @property + def stroke_line_cap(self): + """(:class:`basestring`) The stroke line cap. It also can be set. + + .. versionadded:: 0.4.0 + """ + line_cap_index = library.DrawGetStrokeLineCap(self.resource) + if line_cap_index not in LINE_CAP_TYPES: + self.raise_exception() + return text(LINE_CAP_TYPES[line_cap_index]) + + @stroke_line_cap.setter + def stroke_line_cap(self, line_cap): + if not isinstance(line_cap, string_type): + raise TypeError('expected a string, not ' + repr(line_cap)) + elif line_cap not in LINE_CAP_TYPES: + raise ValueError('expected a string from LINE_CAP_TYPES, not' + + repr(line_cap)) + library.DrawSetStrokeLineCap(self.resource, + LINE_CAP_TYPES.index(line_cap)) + + @property + def stroke_line_join(self): + """(:class:`basestring`) The stroke line join. It also can be set. + + .. versionadded:: 0.4.0 + """ + line_join_index = library.DrawGetStrokeLineJoin(self.resource) + if line_join_index not in LINE_JOIN_TYPES: + self.raise_exception() + return text(LINE_JOIN_TYPES[line_join_index]) + + @stroke_line_join.setter + def stroke_line_join(self, line_join): + if not isinstance(line_join, string_type): + raise TypeError('expected a string, not ' + repr(line_join)) + elif line_join not in LINE_JOIN_TYPES: + raise ValueError('expected a string from LINE_JOIN_TYPES, not' + + repr(line_join)) + library.DrawSetStrokeLineJoin(self.resource, + LINE_JOIN_TYPES.index(line_join)) + + @property + def stroke_miter_limit(self): + """(:class:`~numbers.Integral`) The current miter limit. + It also can be set. + + .. versionadded:: 0.4.0 + """ + return library.DrawGetStrokeMiterLimit(self.resource) + + @stroke_miter_limit.setter + def stroke_miter_limit(self, miter_limit): + if not isinstance(miter_limit, numbers.Integral): + raise TypeError('opacity must be a integer, not ' + + repr(miter_limit)) + library.DrawSetStrokeMiterLimit(self.resource, miter_limit) + + @property + def stroke_opacity(self): + """(:class:`~numbers.Real`) The current stroke opacity. + It also can be set. + + .. versionadded:: 0.4.0 + """ + return library.DrawGetStrokeOpacity(self.resource) + + @stroke_opacity.setter + def stroke_opacity(self, opacity): + if not isinstance(opacity, numbers.Real): + raise TypeError('opacity must be a double, not ' + + repr(opacity)) + library.DrawSetStrokeOpacity(self.resource, opacity) + + @property + def stroke_width(self): + """(:class:`numbers.Real`) The stroke width. It also can be set. + + .. versionadded:: 0.3.3 + + """ + return library.DrawGetStrokeWidth(self.resource) + + @stroke_width.setter + def stroke_width(self, width): + if not isinstance(width, numbers.Real): + raise TypeError('expected a numbers.Real, but got ' + repr(width)) + elif width < 0.0: + raise ValueError('cannot be less then 0.0, but got ' + repr(width)) + library.DrawSetStrokeWidth(self.resource, width) + + @property + def text_alignment(self): + """(:class:`basestring`) The current text alignment setting. + It's a string value from :const:`TEXT_ALIGN_TYPES` list. + It also can be set. + + """ + text_alignment_index = library.DrawGetTextAlignment(self.resource) + if not text_alignment_index: + self.raise_exception() + return text(TEXT_ALIGN_TYPES[text_alignment_index]) + + @text_alignment.setter + def text_alignment(self, align): + if not isinstance(align, string_type): + raise TypeError('expected a string, not ' + repr(align)) + elif align not in TEXT_ALIGN_TYPES: + raise ValueError('expected a string from TEXT_ALIGN_TYPES, not ' + + repr(align)) + library.DrawSetTextAlignment(self.resource, + TEXT_ALIGN_TYPES.index(align)) + + @property + def text_antialias(self): + """(:class:`bool`) The boolean value which represents whether + antialiasing is used for text rendering. It also can be set to + ``True`` or ``False`` to switch the setting. + + """ + result = library.DrawGetTextAntialias(self.resource) + return bool(result) + + @text_antialias.setter + def text_antialias(self, value): + library.DrawSetTextAntialias(self.resource, bool(value)) + + @property + def text_decoration(self): + """(:class:`basestring`) The text decoration setting, a string + from :const:`TEXT_DECORATION_TYPES` list. It also can be set. + + """ + text_decoration_index = library.DrawGetTextDecoration(self.resource) + if not text_decoration_index: + self.raise_exception() + return text(TEXT_DECORATION_TYPES[text_decoration_index]) + + @text_decoration.setter + def text_decoration(self, decoration): + if not isinstance(decoration, string_type): + raise TypeError('expected a string, not ' + repr(decoration)) + elif decoration not in TEXT_DECORATION_TYPES: + raise ValueError('expected a string from TEXT_DECORATION_TYPES, ' + 'not ' + repr(decoration)) + library.DrawSetTextDecoration(self.resource, + TEXT_DECORATION_TYPES.index(decoration)) + + @property + def text_direction(self): + """(:class:`basestring`) The text direction setting. a string + from :const:`TEXT_DIRECTION_TYPES` list. It also can be set.""" + if library.DrawGetTextDirection is None: + raise WandLibraryVersionError( + 'the installed version of ImageMagick does not support ' + 'this feature' + ) + text_direction_index = library.DrawGetTextDirection(self.resource) + if not text_direction_index: + self.raise_exception() + return text(TEXT_DIRECTION_TYPES[text_direction_index]) + + @text_direction.setter + def text_direction(self, direction): + if library.DrawGetTextDirection is None: + raise WandLibraryVersionError( + 'The installed version of ImageMagick does not support ' + 'this feature' + ) + if not isinstance(direction, string_type): + raise TypeError('expected a string, not ' + repr(direction)) + elif direction not in TEXT_DIRECTION_TYPES: + raise ValueError('expected a string from TEXT_DIRECTION_TYPES, ' + 'not ' + repr(direction)) + library.DrawSetTextDirection(self.resource, + TEXT_DIRECTION_TYPES.index(direction)) + + @property + def text_encoding(self): + """(:class:`basestring`) The internally used text encoding setting. + Although it also can be set, but it's not encouraged. + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + text_encoding_p = library.DrawGetTextEncoding(self.resource) + return text(text_encoding_p.value) + + @text_encoding.setter + def text_encoding(self, encoding): + if encoding is not None and not isinstance(encoding, string_type): + raise TypeError('expected a string, not ' + repr(encoding)) + elif encoding is None: + # encoding specify an empty string to set text encoding + # to system's default. + encoding = b'' + else: + encoding = binary(encoding) + library.DrawSetTextEncoding(self.resource, encoding) + + @property + def text_interline_spacing(self): + """(:class:`numbers.Real`) The setting of the text line spacing. + It also can be set. + + """ + if library.DrawGetTextInterlineSpacing is None: + raise WandLibraryVersionError('The installed version of ' + 'ImageMagick does not support ' + 'this feature') + return library.DrawGetTextInterlineSpacing(self.resource) + + @text_interline_spacing.setter + def text_interline_spacing(self, spacing): + if library.DrawSetTextInterlineSpacing is None: + raise WandLibraryVersionError('The installed version of ' + 'ImageMagick does not support ' + 'this feature') + if not isinstance(spacing, numbers.Real): + raise TypeError('expected a numbers.Real, but got ' + + repr(spacing)) + library.DrawSetTextInterlineSpacing(self.resource, spacing) + + @property + def text_interword_spacing(self): + """(:class:`numbers.Real`) The setting of the word spacing. + It also can be set. + + """ + return library.DrawGetTextInterwordSpacing(self.resource) + + @text_interword_spacing.setter + def text_interword_spacing(self, spacing): + if not isinstance(spacing, numbers.Real): + raise TypeError('expeted a numbers.Real, but got ' + repr(spacing)) + library.DrawSetTextInterwordSpacing(self.resource, spacing) + + @property + def text_kerning(self): + """(:class:`numbers.Real`) The setting of the text kerning. + It also can be set. + + """ + return library.DrawGetTextKerning(self.resource) + + @text_kerning.setter + def text_kerning(self, kerning): + if not isinstance(kerning, numbers.Real): + raise TypeError('expected a numbers.Real, but got ' + + repr(kerning)) + library.DrawSetTextKerning(self.resource, kerning) + + @property + def text_under_color(self): + """(:class:`~wand.color.Color`) The color of a background rectangle + to place under text annotations. It also can be set. + + """ + pixel = library.NewPixelWand() + library.DrawGetTextUnderColor(self.resource, pixel) + size = ctypes.sizeof(MagickPixelPacket) + buffer = ctypes.create_string_buffer(size) + library.PixelGetMagickColor(pixel, buffer) + return Color(raw=buffer) + + @text_under_color.setter + def text_under_color(self, color): + if not isinstance(color, Color): + raise TypeError('expected a wand.color.Color object, not ' + + repr(color)) + with color: + library.DrawSetTextUnderColor(self.resource, color.resource) + + @property + def vector_graphics(self): + """(:class:`basestring`) The XML text of the Vector Graphics. + It also can be set. The drawing-wand XML is experimental, + and subject to change. + + Setting this property to None will reset all vector graphic properties + to the default state. + + .. versionadded:: 0.4.0 + + .. versionchanged: 0.4.1 + Safely release allocated memory with + :c:func:`MagickRelinquishMemory` instead of :c:func:`libc.free`. + + """ + vector_graphics_p = library.DrawGetVectorGraphics(self.resource) + return '' + text(vector_graphics_p.value) + '' + + @vector_graphics.setter + def vector_graphics(self, vector_graphics): + if vector_graphics is not None and not isinstance(vector_graphics, + string_type): + raise TypeError('expected a string, not ' + repr(vector_graphics)) + elif vector_graphics is None: + # Reset all vector graphic properties on drawing wand. + library.DrawResetVectorGraphics(self.resource) + else: + vector_graphics = binary(vector_graphics) + okay = library.DrawSetVectorGraphics(self.resource, + vector_graphics) + if okay == 0: + raise ValueError("Vector graphic not understood.") + + @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.DrawGetGravity(self.resource) + if not gravity_index: + self.raise_exception() + return text(GRAVITY_TYPES[gravity_index]) + + @gravity.setter + def gravity(self, value): + if not isinstance(value, string_type): + raise TypeError('expected a string, not ' + repr(value)) + elif value not in GRAVITY_TYPES: + raise ValueError('expected a string from GRAVITY_TYPES, not ' + + repr(value)) + library.DrawSetGravity(self.resource, GRAVITY_TYPES.index(value)) + + def clear(self): + library.ClearDrawingWand(self.resource) + + def draw(self, image): + """Renders the current drawing into the ``image``. You can simply + call :class:`Drawing` instance rather than calling this method. + That means the following code which calls :class:`Drawing` object + itself:: + + drawing(image) + + is equivalent to the following code which calls :meth:`draw()` method:: + + drawing.draw(image) + + :param image: the image to be drawn + :type image: :class:`~wand.image.Image` + + """ + if not isinstance(image, Image): + raise TypeError('image must be a wand.image.Image instance, not ' + + repr(image)) + res = library.MagickDrawImage(image.wand, self.resource) + if not res: + self.raise_exception() + + def affine(self, matrix): + """Adjusts the current affine transformation matrix with the specified + affine transformation matrix. Note that the current affine transform is + adjusted rather than replaced. + + .. sourcecode:: text + + | sx rx 0 | + | x', y', 1 | = | x, y, 1 | * | ry sy 0 | + | tx ty 1 | + + :param matrix: a list of :class:`~numbers.Real` to define affine + matrix ``[sx, rx, ry, sy, tx, ty]`` + :type matrix: :class:`collections.Sequence` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(matrix, collections.Sequence) or len(matrix) != 6: + raise ValueError('matrix must be a list of size Real numbers') + for idx, val in enumerate(matrix): + if not isinstance(val, numbers.Real): + raise TypeError('expecting numbers.Real in position #' + + repr(idx)) + amx = AffineMatrix(sx=matrix[0], rx=matrix[1], + ry=matrix[2], sy=matrix[3], + tx=matrix[4], ty=matrix[5]) + library.DrawAffine(self.resource, amx) + + def arc(self, start, end, degree): + """Draws a arc using the current :attr:`stroke_color`, + :attr:`stroke_width`, and :attr:`fill_color`. + + :param start: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents starting x and y of the arc + :type start: :class:`~collections.Sequence` + :param end: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents ending x and y of the arc + :type end: :class:`~collections.Sequence` + :param degree: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents starting degree, and ending degree + :type degree: :class:`~collections.Sequence` + + .. versionadded:: 0.4.0 + + """ + start_x, start_y = start + end_x, end_y = end + degree_start, degree_end = degree + library.DrawArc(self.resource, + float(start_x), float(start_y), + float(end_x), float(end_y), + float(degree_start), float(degree_end)) + + def circle(self, origin, perimeter): + """Draws a circle from ``origin`` to ``perimeter`` + + :param origin: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents origin x and y of circle + :type origin: :class:`collections.Sequence` + :param perimeter: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents perimeter x and y of circle + :type perimeter: :class:`collections.Sequence` + + .. versionadded:: 0.4.0 + + """ + origin_x, origin_y = origin + perimeter_x, perimeter_y = perimeter + library.DrawCircle(self.resource, + float(origin_x), float(origin_y), # origin + float(perimeter_x), float(perimeter_y)) # perimeter + + def color(self, x=None, y=None, paint_method='undefined'): + """Draws a color on the image using current fill color, starting + at specified position & method. + + Available methods in :class:`wand.drawing.PAINT_METHOD_TYPES`: + + - ``'undefined'`` + - ``'point'`` + - ``'replace'`` + - ``'floodfill'`` + - ``'filltoborder'`` + - ``'reset'`` + + .. versionadded:: 0.4.0 + + """ + if x is None or y is None: + raise TypeError('Both x & y coordinates need to be defined') + if not isinstance(paint_method, string_type): + raise TypeError('expected a string, not ' + repr(paint_method)) + elif paint_method not in PAINT_METHOD_TYPES: + raise ValueError('expected a string from PAINT_METHOD_TYPES, not ' + + repr(paint_method)) + library.DrawColor(self.resource, float(x), float(y), + PAINT_METHOD_TYPES.index(paint_method)) + + def comment(self, message=None): + """Adds a comment to the vector stream. + + :param message: the comment to set. + :type message: :class:`basestring` + + .. versionadded:: 0.4.0 + """ + if message is not None and not isinstance(message, string_type): + raise TypeError('expected a string, not ' + repr(message)) + elif message is None: + message = b'' + else: + message = binary(message) + library.DrawComment(self.resource, message) + + def composite(self, operator, left, top, width, height, image): + """Composites an image onto the current image, using the specified + composition operator, specified position, and at the specified size. + + :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 type: :const:`COMPOSITE_OPERATORS` + :param left: the column offset of the composited drawing source + :type left: :class:`numbers.Real` + :param top: the row offset of the composited drawing source + :type top: :class:`numbers.Real` + :param width: the total columns to include in the composited source + :type width: :class:`numbers.Real` + :param height: the total rows to include in the composited source + :type height: :class:`numbers.Real` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(operator, string_type): + raise TypeError('operator must be a string, not ' + + repr(operator)) + elif not isinstance(left, numbers.Real): + raise TypeError('left must be an integer, not ' + repr(left)) + elif not isinstance(top, numbers.Real): + raise TypeError('top must be an integer, not ' + repr(left)) + elif not isinstance(width, numbers.Real): + raise TypeError('width must be an integer, not ' + repr(left)) + elif not isinstance(height, numbers.Real): + raise TypeError('height must be an integer, not ' + repr(left)) + try: + op = COMPOSITE_OPERATORS.index(operator) + except IndexError: + raise IndexError(repr(operator) + ' is an invalid composite ' + 'operator type; see wand.image.COMPOSITE_' + 'OPERATORS dictionary') + okay = library.DrawComposite(self.resource, op, left, top, width, + height, image.wand) + if okay == 0: + self.raise_exception() + + def ellipse(self, origin, radius, rotation=(0, 360)): + """Draws a ellipse at ``origin`` with independent x & y ``radius``. + Ellipse can be partial by setting start & end ``rotation``. + + :param origin: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents origin x and y of circle + :type origin: :class:`collections.Sequence` + :param radius: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents radius x and radius y of circle + :type radius: :class:`collections.Sequence` + :param rotation: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents start and end of ellipse. + Default (0,360) + :type rotation: :class:`collections.Sequence` + + .. versionadded:: 0.4.0 + + """ + origin_x, origin_y = origin + radius_x, radius_y = radius + rotation_start, rotation_end = rotation + library.DrawEllipse(self.resource, + float(origin_x), float(origin_y), # origin + float(radius_x), float(radius_y), # radius + float(rotation_start), float(rotation_end)) + + def line(self, start, end): + """Draws a line ``start`` to ``end``. + + :param start: (:class:`~numbers.Integral`, :class:`numbers.Integral`) + pair which represents starting x and y of the line + :type start: :class:`collections.Sequence` + :param end: (:class:`~numbers.Integral`, :class:`numbers.Integral`) + pair which represents ending x and y of the line + :type end: :class:`collections.Sequence` + + """ + start_x, start_y = start + end_x, end_y = end + library.DrawLine(self.resource, + int(start_x), int(start_y), + int(end_x), int(end_y)) + + def matte(self, x=None, y=None, paint_method='undefined'): + """Paints on the image's opacity channel in order to set effected pixels + to transparent. + + To influence the opacity of pixels. The available methods are: + + - ``'undefined'`` + - ``'point'`` + - ``'replace'`` + - ``'floodfill'`` + - ``'filltoborder'`` + - ``'reset'`` + + .. versionadded:: 0.4.0 + + """ + if x is None or y is None: + raise TypeError('Both x & y coordinates need to be defined') + if not isinstance(paint_method, string_type): + raise TypeError('expected a string, not ' + repr(paint_method)) + elif paint_method not in PAINT_METHOD_TYPES: + raise ValueError('expected a string from PAINT_METHOD_TYPES, not ' + + repr(paint_method)) + library.DrawMatte(self.resource, float(x), float(y), + PAINT_METHOD_TYPES.index(paint_method)) + + def path_close(self): + """Adds a path element to the current path which closes + the current subpath by drawing a straight line from the current point + to the current subpath's most recent starting point. + + .. versionadded:: 0.4.0 + + """ + library.DrawPathClose(self.resource) + return self + + def path_curve(self, to=None, controls=None, smooth=False, relative=False): + """Draws a cubic Bezier curve from the current point to given ``to`` + (x,y) coordinate using ``controls`` points at the beginning and + the end of the curve. + If ``smooth`` is set to True, only one ``controls`` is expected + and the previous control is used, else two pair of coordinates are + expected to define the control points. The ``to`` coordinate then + becomes the new current point. + + :param to: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents coordinates to draw to + :type to: :class:`collections.Sequence` + :param controls: (:class:`~numbers.Real`, :class:`numbers.Real`) + coordinate to used to influence curve + :type controls: :class:`collections.Sequence` + :param smooth: :class:`bool` assume last defined control coordinate + :type smooth: :class:`bool` + :param relative: treat given coordinates as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if to is None: + raise TypeError('to is missing') + if controls is None: + raise TypeError('controls is missing') + x, y = to + if smooth: + x2, y2 = controls + else: + (x1, y1), (x2, y2) = controls + + if smooth: + if relative: + library.DrawPathCurveToSmoothRelative(self.resource, + x2, y2, x, y) + else: + library.DrawPathCurveToSmoothAbsolute(self.resource, + x2, y2, x, y) + else: + if relative: + library.DrawPathCurveToRelative(self.resource, + x1, y1, x2, y2, x, y) + else: + library.DrawPathCurveToAbsolute(self.resource, + x1, y1, x2, y2, x, y) + return self + + def path_curve_to_quadratic_bezier(self, to=None, control=None, + smooth=False, relative=False): + """Draws a quadratic Bezier curve from the current point to given + ``to`` coordinate. The control point is assumed to be the reflection of + the control point on the previous command if ``smooth`` is True, else a + pair of ``control`` coordinates must be given. Each coordinates can be + relative, or absolute, to the current point by setting the ``relative`` + flag. The ``to`` coordinate then becomes the new current point, and the + ``control`` coordinate will be assumed when called again + when ``smooth`` is set to true. + + :param to: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents coordinates to draw to + :type to: :class:`collections.Sequence` + :param control: (:class:`~numbers.Real`, :class:`numbers.Real`) + coordinate to used to influence curve + :type control: :class:`collections.Sequence` + :param smooth: assume last defined control coordinate + :type smooth: :class:`bool` + :param relative: treat given coordinates as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if to is None: + raise TypeError('to is missing') + x, y = to + + if smooth: + if relative: + library.DrawPathCurveToQuadraticBezierSmoothRelative( + self.resource, float(x), float(y) + ) + else: + library.DrawPathCurveToQuadraticBezierSmoothAbsolute( + self.resource, float(x), float(y) + ) + else: + if control is None: + raise TypeError('control is missing') + x1, y1 = control + if relative: + library.DrawPathCurveToQuadraticBezierRelative(self.resource, + float(x1), + float(y1), + float(x), + float(y)) + else: + library.DrawPathCurveToQuadraticBezierAbsolute(self.resource, + float(x1), + float(y1), + float(x), + float(y)) + return self + + def path_elliptic_arc(self, to=None, radius=None, rotation=0.0, + large_arc=False, clockwise=False, relative=False): + """Draws an elliptical arc from the current point to given ``to`` + coordinates. The ``to`` coordinates can be relative, or absolute, + to the current point by setting the ``relative`` flag. + The size and orientation of the ellipse are defined by + two radii (rx, ry) in ``radius`` and an ``rotation`` parameters, + which indicates how the ellipse as a whole is + rotated relative to the current coordinate system. The center of the + ellipse is calculated automagically to satisfy the constraints imposed + by the other parameters. ``large_arc`` and ``clockwise`` contribute to + the automatic calculations and help determine how the arc is drawn. + If ``large_arc`` is True then draw the larger of the available arcs. + If ``clockwise`` is true, then draw the arc matching a clock-wise + rotation. + + :param to: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents coordinates to draw to + :type to: :class:`collections.Sequence` + :param radius: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents the radii of the ellipse to draw + :type radius: :class:`collections.Sequence` + :param rotate: degree to rotate ellipse on x-axis + :type rotate: :class:`~numbers.Real` + :param large_arc: draw largest available arc + :type large_arc: :class:`bool` + :param clockwise: draw arc path clockwise from start to target + :type clockwise: :class:`bool` + :param relative: treat given coordinates as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if to is None: + raise TypeError('to is missing') + if radius is None: + raise TypeError('radius is missing') + x, y = to + rx, ry = radius + if relative: + library.DrawPathEllipticArcRelative(self.resource, + float(rx), float(ry), + float(rotation), + bool(large_arc), + bool(clockwise), + float(x), float(y)) + else: + library.DrawPathEllipticArcAbsolute(self.resource, + float(rx), float(ry), + float(rotation), + bool(large_arc), + bool(clockwise), + float(x), float(y)) + return self + + def path_finish(self): + """Terminates the current path. + + .. versionadded:: 0.4.0 + + """ + library.DrawPathFinish(self.resource) + return self + + def path_line(self, to=None, relative=False): + """Draws a line path from the current point to the given ``to`` + coordinate. The ``to`` coordinates can be relative, or absolute, to the + current point by setting the ``relative`` flag. The coordinate then + becomes the new current point. + + :param to: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents coordinates to draw to. + :type to: :class:`collections.Sequence` + :param relative: :class:`bool` + treat given coordinates as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if to is None: + raise TypeError('to is missing') + x, y = to + if relative: + library.DrawPathLineToRelative(self.resource, float(x), float(y)) + else: + library.DrawPathLineToAbsolute(self.resource, float(x), float(y)) + return self + + def path_horizontal_line(self, x=None, relative=False): + """Draws a horizontal line path from the current point to the target + point. Given ``x`` parameter can be relative, or absolute, to the + current point by setting the ``relative`` flag. The target point then + becomes the new current point. + + :param x: :class:`~numbers.Real` + x-axis point to draw to. + :type x: :class:`~numbers.Real` + :param relative: :class:`bool` + treat given point as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if x is None: + raise TypeError('x is missing') + if relative: + library.DrawPathLineToHorizontalRelative(self.resource, float(x)) + else: + library.DrawPathLineToHorizontalAbsolute(self.resource, float(x)) + return self + + def path_vertical_line(self, y=None, relative=False): + """Draws a vertical line path from the current point to the target + point. Given ``y`` parameter can be relative, or absolute, to the + current point by setting the ``relative`` flag. The target point then + becomes the new current point. + + :param y: :class:`~numbers.Real` + y-axis point to draw to. + :type y: :class:`~numbers.Real` + :param relative: :class:`bool` + treat given point as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if y is None: + raise TypeError('y is missing') + if relative: + library.DrawPathLineToVerticalRelative(self.resource, float(y)) + else: + library.DrawPathLineToVerticalAbsolute(self.resource, float(y)) + return self + + def path_move(self, to=None, relative=False): + """Starts a new sub-path at the given coordinates. Given ``to`` + parameter can be relative, or absolute, by setting the ``relative`` + flag. + + :param to: (:class:`~numbers.Real`, :class:`numbers.Real`) + pair which represents coordinates to draw to. + :type to: :class:`collections.Sequence` + :param relative: :class:`bool` + treat given coordinates as relative to current point + :type relative: :class:`bool` + + .. versionadded:: 0.4.0 + + """ + if to is None: + raise TypeError('to is missing') + x, y = to + if relative: + library.DrawPathMoveToRelative(self.resource, float(x), float(y)) + else: + library.DrawPathMoveToAbsolute(self.resource, float(x), float(y)) + return self + + def path_start(self): + """Declares the start of a path drawing list which is terminated by a + matching :meth:`path_finish()` command. All other `path_*` commands + must be enclosed between a :meth:`path_start()` and a + :meth:`path_finish()` command. This is because path drawing commands + are subordinate commands and they do not function by themselves. + + .. versionadded:: 0.4.0 + + """ + library.DrawPathStart(self.resource) + return self + + def point(self, x, y): + """Draws a point at given ``x`` and ``y`` + + :param x: :class:`~numbers.Real` x of point + :type x: :class:`~numbers.Real` + :param y: :class:`~numbers.Real` y of point + :type y: :class:`~numbers.Real` + + .. versionadded:: 0.4.0 + + """ + library.DrawPoint(self.resource, + float(x), + float(y)) + + def pop(self): + """Pop destroys the current drawing wand and returns to the previously + pushed drawing wand. Multiple drawing wands may exist. It is an error + to attempt to pop more drawing wands than have been pushed, and it is + proper form to pop all drawing wands which have been pushed. + + :returns: success of pop operation + :rtype: `bool` + + .. versionadded:: 0.4.0 + + """ + return bool(library.PopDrawingWand(self.resource)) + + def pop_clip_path(self): + """Terminates a clip path definition. + + .. versionadded:: 0.4.0 + + """ + library.DrawPopClipPath(self.resource) + + def pop_defs(self): + """Terminates a definition list. + + .. versionadded:: 0.4.0 + + """ + library.DrawPopDefs(self.resource) + + def pop_pattern(self): + """Terminates a pattern definition. + + .. versionadded:: 0.4.0 + + """ + library.DrawPopPattern(self.resource) + + def push(self): + """Push clones the current drawing wand to create a new drawing wand. + The original drawing wand(s) may be returned to by invoking + :class:`Drawing.pop`. The drawing wands are stored on a drawing wand + stack. For every Pop there must have already been an equivalent Push. + + :returns: success of push operation + :rtype: `bool` + + .. versionadded:: 0.4.0 + + """ + return bool(library.PushDrawingWand(self.resource)) + + def push_clip_path(self, clip_mask_id): + """Starts a clip path definition which is comprised of any number of + drawing commands and terminated by a :class:`Drawing.pop_clip_path` + command. + + :param clip_mask_id: string identifier to associate with the clip path. + :type clip_mask_id: :class:`basestring` + + .. versionadded:: 0.4.0 + + """ + library.DrawPushClipPath(self.resource, binary(clip_mask_id)) + + def push_defs(self): + """Indicates that commands up to a terminating :class:`Drawing.pop_defs` + command create named elements (e.g. clip-paths, textures, etc.) which + may safely be processed earlier for the sake of efficiency. + + .. versionadded:: 0.4.0 + + """ + library.DrawPushDefs(self.resource) + + def push_pattern(self, pattern_id, left, top, width, height): + """Indicates that subsequent commands up to a + :class:`Drawing.pop_pattern` command comprise the definition of a named + pattern. The pattern space is assigned top left corner coordinates, a + width and height, and becomes its own drawing space. Anything which can + be drawn may be used in a pattern definition. + Named patterns may be used as stroke or brush definitions. + + :param pattern_id: a unique identifier for the pattern. + :type pattern_id: :class:`basestring` + :param left: x ordinate of top left corner. + :type left: :class:`numbers.Real` + :param top: y ordinate of top left corner. + :type top: :class:`numbers.Real` + :param width: width of pattern space. + :type width: :class:`numbers.Real` + :param height: height of pattern space. + :type height: :class:`numbers.Real` + :returns: success of push operation + :rtype: `bool` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(pattern_id, string_type): + raise TypeError('pattern_id must be a string, not ' + + repr(pattern_id)) + elif not isinstance(left, numbers.Real): + raise TypeError('left must be numbers.Real, not ' + repr(left)) + elif not isinstance(top, numbers.Real): + raise TypeError('top must be numbers.Real, not ' + repr(top)) + elif not isinstance(width, numbers.Real): + raise TypeError('width must be numbers.Real, not ' + repr(width)) + elif not isinstance(height, numbers.Real): + raise TypeError('height must be numbers.Real, not ' + repr(height)) + okay = library.DrawPushPattern(self.resource, binary(pattern_id), + left, top, + width, height) + return bool(okay) + + def rectangle(self, left=None, top=None, right=None, bottom=None, + width=None, height=None, radius=None, xradius=None, + yradius=None): + """Draws a rectangle using the current :attr:`stoke_color`, + :attr:`stroke_width`, and :attr:`fill_color`. + + .. sourcecode:: text + + +--------------------------------------------------+ + | ^ ^ | + | | | | + | top | | + | | | | + | v | | + | <-- left --> +-------------------+ bottom | + | | ^ | | | + | | <-- width --|---> | | | + | | height | | | + | | | | | | + | | v | | | + | +-------------------+ v | + | <--------------- right ----------> | + +--------------------------------------------------+ + + :param left: x-offset of the rectangle to draw + :type left: :class:`numbers.Real` + :param top: y-offset of the rectangle to draw + :type top: :class:`numbers.Real` + :param right: second x-offset of the rectangle to draw. + this parameter and ``width`` parameter are exclusive + each other + :type right: :class:`numbers.Real` + :param bottom: second y-offset of the rectangle to draw. + this parameter and ``height`` parameter are exclusive + each other + :type bottom: :class:`numbers.Real` + :param width: the :attr:`width` of the rectangle to draw. + this parameter and ``right`` parameter are exclusive + each other + :type width: :class:`numbers.Real` + :param height: the :attr:`height` of the rectangle to draw. + this parameter and ``bottom`` parameter are exclusive + each other + :type height: :class:`numbers.Real` + :param radius: the corner rounding. this is a short-cut for setting + both :attr:`xradius`, and :attr:`yradius` + :type radius: :class:`numbers.Real` + :param xradius: the :attr:`xradius` corner in horizontal direction. + :type xradius: :class:`numbers.Real` + :param yradius: the :attr:`yradius` corner in vertical direction. + :type yradius: :class:`numbers.Real` + + .. versionadded:: 0.3.6 + + .. versionchanged:: 0.4.0 + Radius keywords added to create rounded rectangle. + + """ + if left is None: + raise TypeError('left is missing') + elif top is None: + raise TypeError('top is missing') + elif right is None and width is None: + raise TypeError('right/width is missing') + elif bottom is None and height is None: + raise TypeError('bottom/height is missing') + elif 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') + elif not isinstance(left, numbers.Real): + raise TypeError('left must be numbers.Real, not ' + repr(left)) + elif not isinstance(top, numbers.Real): + raise TypeError('top must be numbers.Real, not ' + repr(top)) + elif not (right is None or isinstance(right, numbers.Real)): + raise TypeError('right must be numbers.Real, not ' + repr(right)) + elif not (bottom is None or isinstance(bottom, numbers.Real)): + raise TypeError('bottom must be numbers.Real, not ' + repr(bottom)) + elif not (width is None or isinstance(width, numbers.Real)): + raise TypeError('width must be numbers.Real, not ' + repr(width)) + elif not (height is None or isinstance(height, numbers.Real)): + raise TypeError('height must be numbers.Real, not ' + repr(height)) + if right is None: + if width < 0: + raise ValueError('width must be positive, not ' + repr(width)) + right = left + width + elif right < left: + raise ValueError('right must be more than left ({0!r}), ' + 'not {1!r})'.format(left, right)) + if bottom is None: + if height < 0: + raise ValueError('height must be positive, not ' + + repr(height)) + bottom = top + height + elif bottom < top: + raise ValueError('bottom must be more than top ({0!r}), ' + 'not {1!r})'.format(top, bottom)) + if radius is not None: + xradius = yradius = radius + if xradius is not None or yradius is not None: + if xradius is None: + xradius = 0.0 + if yradius is None: + yradius = 0.0 + if not isinstance(xradius, numbers.Real): + raise TypeError('xradius must be numbers.Real, not ' + + repr(xradius)) + if not isinstance(yradius, numbers.Real): + raise TypeError('yradius must be numbers.Real, not ' + + repr(xradius)) + library.DrawRoundRectangle(self.resource, left, top, right, bottom, + xradius, yradius) + else: + library.DrawRectangle(self.resource, left, top, right, bottom) + self.raise_exception() + + def rotate(self, degree): + """Applies the specified rotation to the current coordinate space. + + :param degree: degree to rotate + :type degree: :class:`~numbers.Real` + + .. versionadded:: 0.4.0 + + """ + library.DrawRotate(self.resource, float(degree)) + + def polygon(self, points=None): + """Draws a polygon using the current :attr:`stoke_color`, + :attr:`stroke_width`, and :attr:`fill_color`, using the specified + array of coordinates. + + Example polygon on ``image`` :: + + with Drawing() as draw: + points = [(40,10), (20,50), (90,10), (70,40)] + draw.polygon(points) + draw.draw(image) + + :param points: list of x,y tuples + :type points: :class:`list` + + .. versionadded:: 0.4.0 + + """ + + (points_l, points_p) = _list_to_point_info(points) + library.DrawPolygon(self.resource, points_l, + ctypes.cast(points_p, ctypes.POINTER(PointInfo))) + + def polyline(self, points=None): + """Draws a polyline using the current :attr:`stoke_color`, + :attr:`stroke_width`, and :attr:`fill_color`, using the specified + array of coordinates. + + Identical to :class:`~wand.drawing.Drawing.polygon`, but without closed + stroke line. + + :param points: list of x,y tuples + :type points: :class:`list` + + .. versionadded:: 0.4.0 + + """ + + (points_l, points_p) = _list_to_point_info(points) + library.DrawPolyline(self.resource, points_l, + ctypes.cast(points_p, ctypes.POINTER(PointInfo))) + + def bezier(self, points=None): + """Draws a bezier curve through a set of points on the image, using + the specified array of coordinates. + + At least four points should be given to complete a bezier path. + The first & forth point being the start & end point, and the second + & third point controlling the direction & curve. + + Example bezier on ``image`` :: + + with Drawing() as draw: + points = [(40,10), # Start point + (20,50), # First control + (90,10), # Second control + (70,40)] # End point + draw.stroke_color = Color('#000') + draw.fill_color = Color('#fff') + draw.bezier(points) + draw.draw(image) + + :param points: list of x,y tuples + :type points: :class:`list` + + .. versionadded:: 0.4.0 + + """ + + (points_l, points_p) = _list_to_point_info(points) + library.DrawBezier(self.resource, points_l, + ctypes.cast(points_p, ctypes.POINTER(PointInfo))) + + def text(self, x, y, body): + """Writes a text ``body`` into (``x``, ``y``). + + :param x: the left offset where to start writing a text + :type x: :class:`numbers.Integral` + :param y: the baseline where to start writing text + :type y: :class:`numbers.Integral` + :param body: the body string to write + :type body: :class:`basestring` + + """ + if not isinstance(x, numbers.Integral) or x < 0: + exc = ValueError if x < 0 else TypeError + raise exc('x must be a natural number, not ' + repr(x)) + elif not isinstance(y, numbers.Integral) or y < 0: + exc = ValueError if y < 0 else TypeError + raise exc('y must be a natural number, not ' + repr(y)) + elif not isinstance(body, string_type): + raise TypeError('body must be a string, not ' + repr(body)) + elif not body: + raise ValueError('body string cannot be empty') + if isinstance(body, text_type): + # According to ImageMagick C API docs, we can use only UTF-8 + # at this time, so we do hardcoding here. + # http://imagemagick.org/api/drawing-wand.php#DrawSetTextEncoding + if not self.text_encoding: + self.text_encoding = 'UTF-8' + body = body.encode(self.text_encoding) + body_p = ctypes.create_string_buffer(body) + library.DrawAnnotation( + self.resource, x, y, + ctypes.cast(body_p, ctypes.POINTER(ctypes.c_ubyte)) + ) + + def scale(self, x=None, y=None): + """ + Adjusts the scaling factor to apply in the horizontal and vertical + directions to the current coordinate space. + + :param x: Horizontal scale factor + :type x: :class:`~numbers.Real` + :param y: Vertical scale factor + :type y: :class:`~numbers.Real` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(x, numbers.Real): + raise TypeError('expecting numbers.Real, not ' + repr(x)) + if not isinstance(y, numbers.Real): + raise TypeError('expecting numbers.Real, not ' + repr(y)) + library.DrawScale(self.resource, x, y) + + def set_fill_pattern_url(self, url): + """Sets the URL to use as a fill pattern for filling objects. Only local + URLs ("#identifier") are supported at this time. These local URLs are + normally created by defining a named fill pattern with + Drawing.push_pattern & Drawing.pop_pattern. + + :param url: URL to use to obtain fill pattern. + :type url: :class:`basestring` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(url, string_type): + raise TypeError('expecting basestring, not ' + repr(url)) + if url[0] != '#': + raise ValueError('value not a relative URL, ' + 'expecting "#identifier"') + okay = library.DrawSetFillPatternURL(self.resource, binary(url)) + if okay == 0: + # ThrowDrawException(DrawError,"URLNotFound",fill_url) + self.raise_exception() + + def set_stroke_pattern_url(self, url): + """Sets the pattern used for stroking object outlines. Only local + URLs ("#identifier") are supported at this time. These local URLs are + normally created by defining a named stroke pattern with + Drawing.push_pattern & Drawing.pop_pattern. + + :param url: URL to use to obtain stroke pattern. + :type url: :class:`basestring` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(url, string_type): + raise TypeError('expecting basestring, not ' + repr(url)) + if url[0] != '#': + raise ValueError('value not a relative URL, ' + 'expecting "#identifier"') + okay = library.DrawSetStrokePatternURL(self.resource, binary(url)) + if okay == 0: + # ThrowDrawException(DrawError,"URLNotFound",fill_url) + self.raise_exception() + + def skew(self, x=None, y=None): + """Skews the current coordinate system in the horizontal direction if + ``x`` is given, and vertical direction if ``y`` is given. + + :param x: Skew horizontal direction + :type x: :class:`~numbers.Real` + :param y: Skew vertical direction + :type y: :class:`~numbers.Real` + + .. versionadded:: 0.4.0 + + """ + if x is not None: + library.DrawSkewX(self.resource, float(x)) + if y is not None: + library.DrawSkewY(self.resource, float(y)) + + def translate(self, x=None, y=None): + """Applies a translation to the current coordinate system which moves + the coordinate system origin to the specified coordinate. + + :param x: Skew horizontal direction + :type x: :class:`~numbers.Real` + :param y: Skew vertical direction + :type y: :class:`~numbers.Real` + + .. versionadded:: 0.4.0 + """ + if x is None or y is None: + raise TypeError('Both x & y coordinates need to be defined') + library.DrawTranslate(self.resource, float(x), float(y)) + + def get_font_metrics(self, image, text, multiline=False): + """Queries font metrics from the given ``text``. + + :param image: the image to be drawn + :type image: :class:`~wand.image.Image` + :param text: the text string for get font metrics. + :type text: :class:`basestring` + :param multiline: text is multiline or not + :type multiline: `boolean` + + """ + if not isinstance(image, Image): + raise TypeError('image must be a wand.image.Image instance, not ' + + repr(image)) + if not isinstance(text, string_type): + raise TypeError('text must be a string, not ' + repr(text)) + if multiline: + font_metrics_f = library.MagickQueryMultilineFontMetrics + else: + font_metrics_f = library.MagickQueryFontMetrics + if isinstance(text, text_type): + if self.text_encoding: + text = text.encode(self.text_encoding) + else: + text = binary(text) + result = font_metrics_f(image.wand, self.resource, text) + args = (result[i] for i in xrange(13)) + return FontMetrics(*args) + + def viewbox(self, left, top, right, bottom): + """Viewbox sets the overall canvas size to be recorded with the drawing + vector data. Usually this will be specified using the same size as the + canvas image. When the vector data is saved to SVG or MVG formats, the + viewbox is use to specify the size of the canvas image that a viewer + will render the vector data on. + + :param left: the left most point of the viewbox. + :type left: :class:`~numbers.Integral` + :param top: the top most point of the viewbox. + :type top: :class:`~numbers.Integral` + :param right: the right most point of the viewbox. + :type right: :class:`~numbers.Integral` + :param bottom: the bottom most point of the viewbox. + :type bottom: :class:`~numbers.Integral` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(left, numbers.Integral): + raise TypeError('left must be an integer, not ' + repr(left)) + if not isinstance(top, numbers.Integral): + raise TypeError('top must be an integer, not ' + repr(top)) + if not isinstance(right, numbers.Integral): + raise TypeError('right must be an integer, not ' + repr(right)) + if not isinstance(bottom, numbers.Integral): + raise TypeError('bottom must be an integer, not ' + repr(bottom)) + library.DrawSetViewbox(self.resource, left, top, right, bottom) + + def __call__(self, image): + return self.draw(image) + + +def _list_to_point_info(points): + """ + Helper method to convert a list of tuples to ``const * PointInfo`` + + :param points: a list of tuples + :type points: `list` + :returns: tuple of point length and c_double array + :rtype: `tuple` + :raises: `TypeError` + + .. versionadded:: 0.4.0 + + """ + if not isinstance(points, list): + raise TypeError('points must be a list, not ' + repr(points)) + point_length = len(points) + tuple_size = 2 + point_info_size = point_length * tuple_size + # Allocate sequence of memory + point_info = (ctypes.c_double * point_info_size)() + for double_index in xrange(0, point_info_size): + tuple_index = double_index // tuple_size + tuple_offset = double_index % tuple_size + point_info[double_index] = ctypes.c_double( + points[tuple_index][tuple_offset] + ) + return (point_length, point_info) diff --git a/lib/wand/exceptions.py b/lib/wand/exceptions.py new file mode 100644 index 00000000..ff241460 --- /dev/null +++ b/lib/wand/exceptions.py @@ -0,0 +1,111 @@ +""":mod:`wand.exceptions` --- Errors and warnings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module maps MagickWand API's errors and warnings to Python's native +exceptions and warnings. You can catch all MagickWand errors using Python's +natural way to catch errors. + +.. seealso:: + + `ImageMagick Exceptions `_ + +.. versionadded:: 0.1.1 + +""" + + +class WandException(Exception): + """All Wand-related exceptions are derived from this class.""" + + +class WandWarning(WandException, Warning): + """Base class for Wand-related warnings.""" + + +class WandError(WandException): + """Base class for Wand-related errors.""" + + +class WandFatalError(WandException): + """Base class for Wand-related fatal errors.""" + + +class WandLibraryVersionError(WandException): + """Base class for Wand-related ImageMagick version errors. + + .. versionadded:: 0.3.2 + + """ + + +#: (:class:`list`) A list of error/warning domains, these descriptions and +#: codes. The form of elements is like: (domain name, description, codes). +DOMAIN_MAP = [ + ('ResourceLimit', + 'A program resource is exhausted e.g. not enough memory.', + (MemoryError,), + [300, 400, 700]), + ('Type', 'A font is unavailable; a substitution may have occurred.', (), + [305, 405, 705]), + ('Option', 'A command-line option was malformed.', (), [310, 410, 710]), + ('Delegate', 'An ImageMagick delegate failed to complete.', (), + [315, 415, 715]), + ('MissingDelegate', + 'The image type can not be read or written because the appropriate; ' + 'delegate is missing.', + (ImportError,), + [320, 420, 720]), + ('CorruptImage', 'The image file may be corrupt.', + (ValueError,), [325, 425, 725]), + ('FileOpen', 'The image file could not be opened for reading or writing.', + (IOError,), [330, 430, 730]), + ('Blob', 'A binary large object could not be allocated, read, or written.', + (IOError,), [335, 435, 735]), + ('Stream', 'There was a problem reading or writing from a stream.', + (IOError,), [340, 440, 740]), + ('Cache', 'Pixels could not be read or written to the pixel cache.', + (), [345, 445, 745]), + ('Coder', 'There was a problem with an image coder.', (), [350, 450, 750]), + ('Module', 'There was a problem with an image module.', (), + [355, 455, 755]), + ('Draw', 'A drawing operation failed.', (), [360, 460, 760]), + ('Image', 'The operation could not complete due to an incompatible image.', + (), [365, 465, 765]), + ('Wand', 'There was a problem specific to the MagickWand API.', (), + [370, 470, 770]), + ('Random', 'There is a problem generating a true or pseudo-random number.', + (), [375, 475, 775]), + ('XServer', 'An X resource is unavailable.', (), [380, 480, 780]), + ('Monitor', 'There was a problem activating the progress monitor.', (), + [385, 485, 785]), + ('Registry', 'There was a problem getting or setting the registry.', (), + [390, 490, 790]), + ('Configure', 'There was a problem getting a configuration file.', (), + [395, 495, 795]), + ('Policy', + 'A policy denies access to a delegate, coder, filter, path, or resource.', + (), [399, 499, 799]) +] + + +#: (:class:`list`) The list of (base_class, suffix) pairs (for each code). +#: It would be zipped with :const:`DOMAIN_MAP` pairs' last element. +CODE_MAP = [ + (WandWarning, 'Warning'), + (WandError, 'Error'), + (WandFatalError, 'FatalError') +] + + +#: (:class:`dict`) The dictionary of (code, exc_type). +TYPE_MAP = {} + + +for domain, description, bases, codes in DOMAIN_MAP: + for code, (base, suffix) in zip(codes, CODE_MAP): + name = domain + suffix + locals()[name] = TYPE_MAP[code] = type(name, (base,) + bases, { + '__doc__': description, + 'wand_error_code': code + }) +del name, base, suffix diff --git a/lib/wand/exceptions.pyc b/lib/wand/exceptions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..355af6df0d248eef62877745a6f469fa24fac110 GIT binary patch literal 4818 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 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 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 + with Image() as textboard: + library.MagickSetSize(textboard.wand, width, height) + textboard.font = font or self.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() + + @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 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): + cls = type(self) + if getattr(self, 'c_resource', None) is None: + return '<{0}.{1}: (closed)>'.format(cls.__module__, cls.__name__) + return '<{0}.{1}: {2} ({3}x{4})>'.format( + cls.__module__, cls.__name__, + self.signature[:7], self.width, self.height + ) + + +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): + cls = type(self) + if getattr(self, 'c_resource', None) is None: + return '<{0}.{1}: (closed)>'.format(cls.__module__, cls.__name__) + return '<{0}.{1}: {2} {3!r} ({4}x{5})>'.format( + cls.__module__, cls.__name__, + self.signature[:7], self.format, self.width, 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. + + """ diff --git a/lib/wand/image.pyc b/lib/wand/image.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aad9b3cb41973f7357534467b12e6327e337ec44 GIT binary patch literal 118771 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 z z5cDMxa1%hQsB?Ii_1)wb_QKAcF-Q{g>KLSXvP}|htd4`(E`IRNpKbWj;~7yq2FVEj zO^h*kv+;xzP@QU{81s)eW02%#yK&R-eYBp{ghhI7^DGSr_U{I8M0k>%n5u0$BjpLp|0nG)(+;s|ME5Cba_74ZFd`sHQJ?r992%8bb22^GR`| z@JL_|VUS%F&Zs0Y*_bC48iCoX7*Ge{!(%p!v|2^C0~V1!cL>>7 z@1^N#zXv2;@r2|!A@7<@tq{I7Y6GMs6_uW0{dR6(_KtY-1~O>yIapPk!o2T1&ha{q z{Nith-Rj!g4O+AjqmrN4h~8IxMSDYXKRE)`xyVLvZ1CfmIGZl3PEiosC$L3LuQsW& z6%Y{>V(am50FdQB;$STnmS)w+=_4T(flj?9GJ1!=-=f)d0)PGOGVz>Y6eg8fH2WAA zAabkE6v_$VbLonYFWrI43&$2zUJp4ZL_?X3_*F?=$_dfbM^uK;rR}1H(oGopUv?k> zFy4M?Bdx?XCU{a#2B#H)-bwb>!*<5{Hqlfh1MCjNp2eXcUzupmci46+xTyYxn^a%x zI7-xAD=SDpbc_&n;>0B0fW)(9xT~J8{(aR^^pP{aNwsp{{Xz3mHkDmr?X^llTH=7@ zJp1bqe534BXQObG(2fY(l&p7-VDF;~mxiAw^#MV)Ts#VD0aan^yIr;~TdXo{OFu05 z!!b1bWFefROW>z{vXnG=mNRPvC#^Zh`6%>**(If+E_@as?F*G6%9gynt`Wuyr=Hel zX&MVQYyOa{`EB}iniK6!JKdtNU}JL5M=^FN21oIwKW()(C=ZiVF}@TBPc_@hcd(ip zR%%m4yv|B(FWoJ_%Mog|oqy?`_4V4qfzYN1$7`zISyVoo7H-W5wlUog(WEca?Xp4vE8GCs+UE6uv5A*smGReHlc$*>AgC0yyZzX zR-jj@@Qc#0^huo|D-`xhDgUO<^mPr(i?q|_Kh!}Qw(=`Fh@UCHs>5&V@HHJC=1@s)ztaI(Fh;O$9LW{v}I^$=iOWPZogsc4OHdYvVF^z zTZeXRy?e`>hpydvGc~xb+q!S-_Mu&aDF3#OQb+6csJw65vU6zD4E^~(u``eG*Iwc< zM-3;#E;Br5cgSD5$|svu1WQ-0QN1gjP!Q;k3+3LF# z$D+0qg{-y}zjULFnrgGXpqP`kQTuOb+He>_X9X99>cVEkl%&rH?}XjI+ozV#o}N`& zF(N@D0th=9=^BibXBQ-`oxbpZZ;=4Y`N@({Ha&w2nTp`9okF*M@>CWhqWjd_45cb+ z_b9nHcc-L4Uw*Nq6!{At^2d+n(h$^8?R4+ERc`ucm_8Bf^&PCLN$ad@APb3Z@@fS%9_JoE% zlusp!QUwvYIYW=%5Q1-NUA-09ITZ)Fcf4kU|VZ zrY2Jk&}G9x45qFvmL}X64?CS~`(L;(L>KEtc%-v0)ZV0Xwac=er!Rh1eI4hM9K|VD z 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 Sl-F)`Ry!9#*E(yLfAeoAd2{Ii literal 0 HcmV?d00001 diff --git a/lib/wand/sequence.py b/lib/wand/sequence.py new file mode 100644 index 00000000..f80fe0a8 --- /dev/null +++ b/lib/wand/sequence.py @@ -0,0 +1,345 @@ +""":mod:`wand.sequence` --- Sequences +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 0.3.0 + +""" +import collections +import contextlib +import ctypes +import numbers + +from .api import libmagick, library +from .compat import binary, xrange +from .image import BaseImage, ImageProperty +from .version import MAGICK_VERSION_INFO + +__all__ = 'Sequence', 'SingleImage' + + +class Sequence(ImageProperty, collections.MutableSequence): + """The list-like object that contains every :class:`SingleImage` + in the :class:`~wand.image.Image` container. It implements + :class:`collections.Sequence` prototocol. + + .. versionadded:: 0.3.0 + + """ + + def __init__(self, image): + super(Sequence, self).__init__(image) + self.instances = [] + + def __del__(self): + for instance in self.instances: + if instance is not None: + instance.c_resource = None + + @property + def current_index(self): + """(:class:`numbers.Integral`) The current index of + its internal iterator. + + .. note:: + + It's only for internal use. + + """ + return library.MagickGetIteratorIndex(self.image.wand) + + @current_index.setter + def current_index(self, index): + library.MagickSetIteratorIndex(self.image.wand, index) + + @contextlib.contextmanager + def index_context(self, index): + """Scoped setter of :attr:`current_index`. Should be + used for :keyword:`with` statement e.g.:: + + with image.sequence.index_context(3): + print(image.size) + + .. note:: + + It's only for internal use. + + """ + index = self.validate_position(index) + tmp_idx = self.current_index + self.current_index = index + yield index + self.current_index = tmp_idx + + def __len__(self): + return library.MagickGetNumberImages(self.image.wand) + + def validate_position(self, index): + if not isinstance(index, numbers.Integral): + raise TypeError('index must be integer, not ' + repr(index)) + length = len(self) + if index >= length or index < -length: + raise IndexError( + 'out of index: {0} (total: {1})'.format(index, length) + ) + if index < 0: + index += length + return index + + def validate_slice(self, slice_, as_range=False): + if not (slice_.step is None or slice_.step == 1): + raise ValueError('slicing with step is unsupported') + length = len(self) + if slice_.start is None: + start = 0 + elif slice_.start < 0: + start = length + slice_.start + else: + start = slice_.start + start = min(length, start) + if slice_.stop is None: + stop = 0 + elif slice_.stop < 0: + stop = length + slice_.stop + else: + stop = slice_.stop + stop = min(length, stop or length) + return xrange(start, stop) if as_range else slice(start, stop, None) + + def __getitem__(self, index): + if isinstance(index, slice): + slice_ = self.validate_slice(index) + return [self[i] for i in xrange(slice_.start, slice_.stop)] + index = self.validate_position(index) + instances = self.instances + instances_length = len(instances) + if index < instances_length: + instance = instances[index] + if (instance is not None and + getattr(instance, 'c_resource', None) is not None): + return instance + else: + number_to_extend = index - instances_length + 1 + instances.extend(None for _ in xrange(number_to_extend)) + wand = self.image.wand + tmp_idx = library.MagickGetIteratorIndex(wand) + library.MagickSetIteratorIndex(wand, index) + image = library.GetImageFromMagickWand(wand) + exc = libmagick.AcquireExceptionInfo() + single_image = libmagick.CloneImages(image, binary(str(index)), exc) + libmagick.DestroyExceptionInfo(exc) + single_wand = library.NewMagickWandFromImage(single_image) + single_image = libmagick.DestroyImage(single_image) + library.MagickSetIteratorIndex(wand, tmp_idx) + instance = SingleImage(single_wand, self.image, image) + self.instances[index] = instance + return instance + + def __setitem__(self, index, image): + if isinstance(index, slice): + tmp_idx = self.current_index + slice_ = self.validate_slice(index) + del self[slice_] + self.extend(image, offset=slice_.start) + self.current_index = tmp_idx + else: + if not isinstance(image, BaseImage): + raise TypeError('image must be an instance of wand.image.' + 'BaseImage, not ' + repr(image)) + with self.index_context(index) as index: + library.MagickRemoveImage(self.image.wand) + library.MagickAddImage(self.image.wand, image.wand) + + def __delitem__(self, index): + if isinstance(index, slice): + range_ = self.validate_slice(index, as_range=True) + for i in reversed(range_): + del self[i] + else: + with self.index_context(index) as index: + library.MagickRemoveImage(self.image.wand) + if index < len(self.instances): + del self.instances[index] + + def insert(self, index, image): + try: + index = self.validate_position(index) + except IndexError: + index = len(self) + if not isinstance(image, BaseImage): + raise TypeError('image must be an instance of wand.image.' + 'BaseImage, not ' + repr(image)) + if not self: + library.MagickAddImage(self.image.wand, image.wand) + elif index == 0: + tmp_idx = self.current_index + self_wand = self.image.wand + wand = image.sequence[0].wand + try: + # Prepending image into the list using MagickSetFirstIterator() + # and MagickAddImage() had not worked properly, but was fixed + # since 6.7.6-0 (rev7106). + if MAGICK_VERSION_INFO >= (6, 7, 6, 0): + library.MagickSetFirstIterator(self_wand) + library.MagickAddImage(self_wand, wand) + else: + self.current_index = 0 + library.MagickAddImage(self_wand, + self.image.sequence[0].wand) + self.current_index = 0 + library.MagickAddImage(self_wand, wand) + self.current_index = 0 + library.MagickRemoveImage(self_wand) + finally: + self.current_index = tmp_idx + else: + with self.index_context(index - 1): + library.MagickAddImage(self.image.wand, image.sequence[0].wand) + self.instances.insert(index, None) + + def append(self, image): + if not isinstance(image, BaseImage): + raise TypeError('image must be an instance of wand.image.' + 'BaseImage, not ' + repr(image)) + wand = self.image.wand + tmp_idx = self.current_index + try: + library.MagickSetLastIterator(wand) + library.MagickAddImage(wand, image.sequence[0].wand) + finally: + self.current_index = tmp_idx + self.instances.append(None) + + def extend(self, images, offset=None): + tmp_idx = self.current_index + wand = self.image.wand + length = 0 + try: + if offset is None: + library.MagickSetLastIterator(self.image.wand) + else: + if offset == 0: + images = iter(images) + self.insert(0, next(images)) + offset += 1 + self.current_index = offset - 1 + if isinstance(images, type(self)): + library.MagickAddImage(wand, images.image.wand) + length = len(images) + else: + delta = 1 if MAGICK_VERSION_INFO >= (6, 7, 6, 0) else 2 + for image in images: + if not isinstance(image, BaseImage): + raise TypeError( + 'images must consist of only instances of ' + 'wand.image.BaseImage, not ' + repr(image) + ) + else: + library.MagickAddImage(wand, image.sequence[0].wand) + self.instances = [] + if offset is None: + library.MagickSetLastIterator(self.image.wand) + else: + self.current_index += delta + length += 1 + finally: + self.current_index = tmp_idx + null_list = [None] * length + if offset is None: + self.instances[offset:] = null_list + else: + self.instances[offset:offset] = null_list + + def _repr_png_(self): + library.MagickResetIterator(self.image.wand) + repr_wand = library.MagickAppendImages(self.image.wand, 1) + length = ctypes.c_size_t() + blob_p = library.MagickGetImagesBlob(repr_wand, + ctypes.byref(length)) + if blob_p and length.value: + blob = ctypes.string_at(blob_p, length.value) + library.MagickRelinquishMemory(blob_p) + return blob + else: + return None + + +class SingleImage(BaseImage): + """Each single image in :class:`~wand.image.Image` container. + For example, it can be a frame of GIF animation. + + Note that all changes on single images are invisible to their + containers until they are :meth:`~wand.image.BaseImage.close`\ d + (:meth:`~wand.resource.Resource.destroy`\ ed). + + .. versionadded:: 0.3.0 + + """ + + #: (:class:`wand.image.Image`) The container image. + container = None + + def __init__(self, wand, container, c_original_resource): + super(SingleImage, self).__init__(wand) + self.container = container + self.c_original_resource = c_original_resource + self._delay = None + + @property + def sequence(self): + return self, + + @property + def index(self): + """(:class:`numbers.Integral`) The index of the single image in + the :attr:`container` image. + + """ + wand = self.container.wand + library.MagickResetIterator(wand) + image = library.GetImageFromMagickWand(wand) + i = 0 + while self.c_original_resource != image and image: + image = libmagick.GetNextImageInList(image) + i += 1 + assert image + assert self.c_original_resource == image + return i + + @property + def delay(self): + """(:class:`numbers.Integral`) The delay to pause before display + the next image (in the :attr:`~wand.image.BaseImage.sequence` of + its :attr:`container`). It's hundredths of a second. + + """ + if self._delay is None: + container = self.container + with container.sequence.index_context(self.index): + self._delay = library.MagickGetImageDelay(container.wand) + return self._delay + + @delay.setter + def delay(self, delay): + if not isinstance(delay, numbers.Integral): + raise TypeError('delay must be an integer, not ' + repr(delay)) + elif delay < 0: + raise ValueError('delay cannot be less than zero') + self._delay = delay + + def destroy(self): + if self.dirty: + self.container.sequence[self.index] = self + if self._delay is not None: + container = self.container + with container.sequence.index_context(self.index): + library.MagickSetImageDelay(container.wand, self._delay) + super(SingleImage, self).destroy() + + def __repr__(self): + cls = type(self) + if getattr(self, 'c_resource', None) is None: + return '<{0}.{1}: (closed)>'.format(cls.__module__, cls.__name__) + return '<{0}.{1}: {2} ({3}x{4})>'.format( + cls.__module__, cls.__name__, + self.signature[:7], self.width, self.height + ) diff --git a/lib/wand/sequence.pyc b/lib/wand/sequence.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7efb6002867cb536000b0a4501561b1c7576b02c GIT binary patch literal 12403 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 literal 0 HcmV?d00001 diff --git a/lib/wand/version.py b/lib/wand/version.py new file mode 100644 index 00000000..1ad91d74 --- /dev/null +++ b/lib/wand/version.py @@ -0,0 +1,251 @@ +""":mod:`wand.version` --- Version data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can find the current version in the command line interface: + +.. sourcecode:: console + + $ python -m wand.version + 0.0.0 + $ python -m wand.version --verbose + Wand 0.0.0 + ImageMagick 6.7.7-6 2012-06-03 Q16 http://www.imagemagick.org + $ python -m wand.version --config | grep CC | cut -d : -f 2 + gcc -std=gnu99 -std=gnu99 + $ python -m wand.version --fonts | grep Helvetica + Helvetica + Helvetica-Bold + Helvetica-Light + Helvetica-Narrow + Helvetica-Oblique + $ python -m wand.version --formats | grep CMYK + CMYK + CMYKA + +.. versionadded:: 0.2.0 + The command line interface. + +.. versionadded:: 0.2.2 + The ``--verbose``/``-v`` option which also prints ImageMagick library + version for CLI. + +.. versionadded:: 0.4.1 + The ``--fonts``, ``--formats``, & ``--config`` option allows printing + additional information about ImageMagick library. + +""" +from __future__ import print_function + +import ctypes +import datetime +import re +import sys + +try: + from .api import libmagick, library +except ImportError: + libmagick = None +from .compat import binary, string_type, text + + +__all__ = ('VERSION', 'VERSION_INFO', 'MAGICK_VERSION', + 'MAGICK_VERSION_INFO', 'MAGICK_VERSION_NUMBER', + 'MAGICK_RELEASE_DATE', 'MAGICK_RELEASE_DATE_STRING', + 'QUANTUM_DEPTH', 'configure_options', 'fonts', 'formats') + +#: (:class:`tuple`) The version tuple e.g. ``(0, 1, 2)``. +#: +#: .. versionchanged:: 0.1.9 +#: Becomes :class:`tuple`. (It was string before.) +VERSION_INFO = (0, 4, 2) + +#: (:class:`basestring`) The version string e.g. ``'0.1.2'``. +#: +#: .. versionchanged:: 0.1.9 +#: Becomes string. (It was :class:`tuple` before.) +VERSION = '{0}.{1}.{2}'.format(*VERSION_INFO) + +if libmagick: + c_magick_version = ctypes.c_size_t() + #: (:class:`basestring`) The version string of the linked ImageMagick + #: library. The exactly same string to the result of + #: :c:func:`GetMagickVersion` function. + #: + #: Example:: + #: + #: 'ImageMagick 6.7.7-6 2012-06-03 Q16 http://www.imagemagick.org' + #: + #: .. versionadded:: 0.2.1 + MAGICK_VERSION = text( + libmagick.GetMagickVersion(ctypes.byref(c_magick_version)) + ) + + #: (:class:`numbers.Integral`) The version number of the linked + #: ImageMagick library. + #: + #: .. versionadded:: 0.2.1 + MAGICK_VERSION_NUMBER = c_magick_version.value + + _match = re.match(r'^ImageMagick\s+(\d+)\.(\d+)\.(\d+)(?:-(\d+))?', + MAGICK_VERSION) + #: (:class:`tuple`) The version tuple e.g. ``(6, 7, 7, 6)`` of + #: :const:`MAGICK_VERSION`. + #: + #: .. versionadded:: 0.2.1 + MAGICK_VERSION_INFO = tuple(int(v or 0) for v in _match.groups()) + + #: (:class:`datetime.date`) The release date of the linked ImageMagick + #: library. The same to the result of :c:func:`GetMagickReleaseDate` + #: function. + #: + #: .. versionadded:: 0.2.1 + MAGICK_RELEASE_DATE_STRING = text(libmagick.GetMagickReleaseDate()) + + #: (:class:`basestring`) The date string e.g. ``'2012-06-03'`` of + #: :const:`MAGICK_RELEASE_DATE_STRING`. This value is the exactly same + #: string to the result of :c:func:`GetMagickReleaseDate` function. + #: + #: .. versionadded:: 0.2.1 + MAGICK_RELEASE_DATE = datetime.date( + *map(int, MAGICK_RELEASE_DATE_STRING.split('-'))) + + c_quantum_depth = ctypes.c_size_t() + libmagick.GetMagickQuantumDepth(ctypes.byref(c_quantum_depth)) + #: (:class:`numbers.Integral`) The quantum depth configuration of + #: the linked ImageMagick library. One of 8, 16, 32, or 64. + #: + #: .. versionadded:: 0.3.0 + QUANTUM_DEPTH = c_quantum_depth.value + + del c_magick_version, _match, c_quantum_depth + + +def configure_options(pattern='*'): + """ + Queries ImageMagick library for configurations options given at + compile-time. + + Example: Find where the ImageMagick documents are installed:: + + >>> from wand.version import configure_options + >>> configure_options('DOC*') + {'DOCUMENTATION_PATH': '/usr/local/share/doc/ImageMagick-6'} + + :param pattern: A term to filter queries against. Supports wildcard '*' + characters. Default patterns '*' for all options. + :type pattern: :class:`basestring` + :returns: Directory of configuration options matching given pattern + :rtype: :class:`collections.defaultdict` + """ + if not isinstance(pattern, string_type): + raise TypeError('pattern must be a string, not ' + repr(pattern)) + pattern_p = ctypes.create_string_buffer(binary(pattern)) + config_count = ctypes.c_size_t(0) + configs = {} + configs_p = library.MagickQueryConfigureOptions(pattern_p, + ctypes.byref(config_count)) + cursor = 0 + while cursor < config_count.value: + config = configs_p[cursor].value + value = library.MagickQueryConfigureOption(config) + configs[text(config)] = text(value.value) + cursor += 1 + return configs + + +def fonts(pattern='*'): + """ + Queries ImageMagick library for available fonts. + + Available fonts can be configured by defining `types.xml`, + `type-ghostscript.xml`, or `type-windows.xml`. + Use :func:`wand.version.configure_options` to locate system search path, + and `resources `_ + article for defining xml file. + + Example: List all bold Helvetica fonts:: + + >>> from wand.version import fonts + >>> fonts('*Helvetica*Bold*') + ['Helvetica-Bold', 'Helvetica-Bold-Oblique', 'Helvetica-BoldOblique', + 'Helvetica-Narrow-Bold', 'Helvetica-Narrow-BoldOblique'] + + + :param pattern: A term to filter queries against. Supports wildcard '*' + characters. Default patterns '*' for all options. + :type pattern: :class:`basestring` + :returns: Sequence of matching fonts + :rtype: :class:`collections.Sequence` + """ + if not isinstance(pattern, string_type): + raise TypeError('pattern must be a string, not ' + repr(pattern)) + pattern_p = ctypes.create_string_buffer(binary(pattern)) + number_fonts = ctypes.c_size_t(0) + fonts = [] + fonts_p = library.MagickQueryFonts(pattern_p, + ctypes.byref(number_fonts)) + cursor = 0 + while cursor < number_fonts.value: + font = fonts_p[cursor].value + fonts.append(text(font)) + cursor += 1 + return fonts + + +def formats(pattern='*'): + """ + Queries ImageMagick library for supported formats. + + Example: List supported PNG formats:: + + >>> from wand.version import formats + >>> formats('PNG*') + ['PNG', 'PNG00', 'PNG8', 'PNG24', 'PNG32', 'PNG48', 'PNG64'] + + + :param pattern: A term to filter formats against. Supports wildcards '*' + characters. Default pattern '*' for all formats. + :type pattern: :class:`basestring` + :returns: Sequence of matching formats + :rtype: :class:`collections.Sequence` + """ + if not isinstance(pattern, string_type): + raise TypeError('pattern must be a string, not ' + repr(pattern)) + pattern_p = ctypes.create_string_buffer(binary(pattern)) + number_formats = ctypes.c_size_t(0) + formats = [] + formats_p = library.MagickQueryFormats(pattern_p, + ctypes.byref(number_formats)) + cursor = 0 + while cursor < number_formats.value: + value = formats_p[cursor].value + formats.append(text(value)) + cursor += 1 + return formats + +if __doc__ is not None: + __doc__ = __doc__.replace('0.0.0', VERSION) + +del libmagick + + +if __name__ == '__main__': + options = frozenset(sys.argv[1:]) + if '-v' in options or '--verbose' in options: + print('Wand', VERSION) + try: + print(MAGICK_VERSION) + except NameError: + pass + elif '--fonts' in options: + for font in fonts(): + print(font) + elif '--formats' in options: + for supported_format in formats(): + print(supported_format) + elif '--config' in options: + config_options = configure_options() + for key in config_options: + print('{:24s}: {}'.format(key, config_options[key])) + else: + print(VERSION) diff --git a/lib/wand/version.pyc b/lib/wand/version.pyc new file mode 100644 index 0000000000000000000000000000000000000000..976253556d3e4488e90e4c48554559f472190523 GIT binary patch literal 7237 zcmds6O?w-+5uPP2$zo*7A5r|71a6a%{vavKj-2puoGOyy=!q@IE7?xyxXaZpM6R^l zT`fR;#8MAQj_JLJ{()Y5$q(qEmmYfV^W@Oq&>zsw43-pSONrC^&?l4$0s~+$m>Ik? zfb;)4QqbdvA9;{{UcukrfCcKGIRGD^j->^D4nhmsIjHBrqIMp_Jk;~thxJ2HKh&)c zKz*QFAB6fKY@G!kdj}vKg7#skABOfY)Q7=(6rF-YvH+_ENVmYUEM$wlkp-XAgh2>L zpnU}DN1%Na>PMkH3iVN3{~f3w1HTCM%er?G-th@@}AY}~X zC=pj)10tRRIR@WY*}wW}YOb7t`dN?zuyPK39CjMM!EK%g`8xQg;9CHB9#-BUZZl?P z{mm{@K~yBxFMvD*E58EyYmje&yZ|e2gO7}Gr5sE44li$HDzZSOu0X|18cP;gmW%Ls z3?83{$3=)x_h-OA3*X`zm$-h8>zBEHp6gdY;;=ld4nz7^3+pRo@LvaclJ^0`nLEG6l9_8 z*8Y(@B(JgUsyVbBwm)C{A?KqOm{Z!wZTcre(V9da_n1R4PKnoz#3O28wEsaf>b(DcU-c(=FUOHqUDlt-a9!%ab7|84jq*ov z=y3tp6OA4%Hd=w#5^ktsk)-HuO7xpN z43<(i-6B!DZi+h*(|6_$SoM14Dh=+g%4TRZuCNH14ygGK6O04w%HoD$yrGO7LB#HZ zdI(5PH^j4G7?9NNQjDImkEAG8@NfK-EIh^fC>FdcXSox3#AiOBjuHkk(8E~Z293Y! zA=9GmmhkX{9>iiPh_J0G0xFH_X*zl?weuH-kF_Rn002R7H9R@-5LFKXJOHvo4>7( zVE6q8Q}c@t?m9EGpDli(k0Zq>ypu}DSYPP@q_UBCE187?Btk%&3`kLmw*Pc|r}FeF zekXQR8QZ_@Q~sCga_LL|^2IMJ{XeA-Yh`X<{Lmw%6Dv}zN2$aGtZV~3IR!fwXslb1 zj8SrdNc_-R<1Au<&BVgY8++1p^ig9z;#V2h#Cn}D8@N#KVXfMV+p_Al(m?4rX;tyy zBppQ3s^^kN%kqX?s$#85SXs?HyOL}va*3Nw8OhBgz2#s-(JF($tCYFC#G5&cWKz&H zZRT%Sp*>*9;a!zL=P2LNc_QMLPT}`!ivlnD&dLBem+3${DFBH>lu=PL2e{>(JZQ{O zhDkBhTYzou#o)Drc@UNZuxZ2g0MS=ofmOgz58H=`at7NwC^OC~s4v(;Fp+q}V32vRvvbf)oX(0Cn20>`h^(vPRn96`fiDs}a z@uV_@5_Ba&D9bu%OFW~*c6QTkC!wr~k0~NimeY7|+!H}NiBsJZt>^H(a(7x+@Lq4JICF3MQt@JM)KelnxH~()IJJmp z-1%&3@snar6ssMTR>K(Gsj6BiS``JY_6c5|EbbT)Yl)k>ZIQSd{V%GCDS^Ljp<_%0 zLaYjm9jslq>C*lxVxg0e>`H6|q3^k=FN&9nJ&8#6?#IKWT@RV4ikOkhZYR{;g%nL< z>!8BACaEx&8bMfhTus!x&{e9|SaOv#7}qevQmH#>q-tU&NTsLabW6m`J&m%lyNm_K zHm`;7m>E6WZN`Vz+g-^QS}CTUN&2_J4%ZsDO_RXx+0=j3jMSqI)Mn_X z)9pDb_)0q3CIryuurO&5orbMxPF)a4?{O40IJ&fz%4J5>bvNut1JRre?Kj6J=q=Go z487T=S@f*#Bpg$WmpWeDiA=yT1jR$MIu509g(;=N4Re(xv`L?gi(LFq;AoS&pW;`Y z#{!OzT4$^=Yt$OFhOHA=p0JK!>x^~O^2j`dV*qwh04Bd4%2;HSn;^9Nw%CK%~FO@F>=-I}4=tjO++7fsgf`|@L zgMGBJ*$x|5cu^*lo2^)B<)uNQO`kyUHC-DBp_n=`*_`qRN{Sj~J$r93mEMxw6re#^ zL{O%sP+LmNwouYdF)2hhY?<}xO3+B9xqc{d^Wc=fYUHc-23L|+a;xF+Hry1GaMnz^ zyII^NLE&>z;SR=b%u7SDgqitn)^61L!U2Gdz>5=Ta;zY;l!}*n_kc^3`uBtW<6^Im zuLZ>`;u%UgOZuPh>2>th?4N7y5Bo6f>*#WLQuxILkOhfB4#An?M=w(BTBepB93i?> z|J(3jzlO-eLBT#nwbN9bp@I@(os+%DCz#iHv*$-UM;B zpn}p_2J5KPUXrQ9o3lrWN`tuRB=*}xAW$U2%YtQE<&XGPv?DmZA1t5zWrFhHi)N_G zq*MsA*$wB}6qLbcchqO|x4R>M2GZ~izjy8p)-bJ9M23421_=Zn_%lA9Ro=^fC$49e zYZF=Jdbexxdhvx}A={}qIIrX!u_tdgsifZ@Ni6r|l|60Jq@SHvcF*=Ru&}Gf{~usD zfhTJZFtCuj&|uSH-WL=oY3hN3nMEkW$y=tr3>aQ&u10J81-}Lq_RZA@6ci<){Md)L zMF7WX<0Zs#_|{uq=j(9q>4<@gUI!IH8a-(tI%Y$%ohV{bE@w$8W#&(76W7&FO+4Kx z@goXN%^pM4vkxYUbhY<&MD;y?^uMjB10PKKXY@aeup%<%{q*d?L_POlqV|0-VTGBU z8(=pFoNf)~VnXQ7uO=mWh+y=09K0Pm4&PK9e2Kzqm2?~%yGH}i2Mv$VgBX`6{_BntavCIu!_xGagyLygUB zx-4)A)Z{ZBdH_A&I@zNL6K_jxod2PD`8Iw}wq~ET$%V%OU zjXR05>5+vmU_HLJ3?*JaXHW+=c?G)~YlODnb`zV7!Q5052HGYaXbOCuv(|AV-Dx|% zOms_=U-a1CH(fK-W0QgT3d3R9=0ixjz?)%+5A2!$M{XN!V@_upe$Hy!D8^q6OZqOPjrZ8q4MLBi2dlG;+uzfE=-gkn1t)c&{8r+J)SI0BNK^n*aa+ literal 0 HcmV?d00001