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 00000000..d85c954b
Binary files /dev/null and b/lib/wand/__init__.pyc differ
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 00000000..8e9d955a
Binary files /dev/null and b/lib/wand/api.pyc differ
diff --git a/lib/wand/color.py b/lib/wand/color.py
new file mode 100644
index 00000000..f8891b1e
--- /dev/null
+++ b/lib/wand/color.py
@@ -0,0 +1,307 @@
+""":mod:`wand.color` --- Colors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 0.1.2
+
+"""
+import ctypes
+
+from .api import MagickPixelPacket, library
+from .compat import binary, text
+from .resource import Resource
+from .version import QUANTUM_DEPTH
+
+__all__ = 'Color', 'scale_quantum_to_int8'
+
+
+class Color(Resource):
+ """Color value.
+
+ Unlike any other objects in Wand, its resource management can be
+ implicit when it used outside of :keyword:`with` block. In these case,
+ its resource are allocated for every operation which requires a resource
+ and destroyed immediately. Of course it is inefficient when the
+ operations are much, so to avoid it, you should use color objects
+ inside of :keyword:`with` block explicitly e.g.::
+
+ red_count = 0
+ with Color('#f00') as red:
+ with Image(filename='image.png') as img:
+ for row in img:
+ for col in row:
+ if col == red:
+ red_count += 1
+
+ :param string: a color namel string e.g. ``'rgb(255, 255, 255)'``,
+ ``'#fff'``, ``'white'``. see `ImageMagick Color Names`_
+ doc also
+ :type string: :class:`basestring`
+
+ .. versionchanged:: 0.3.0
+ :class:`Color` objects become hashable.
+
+ .. seealso::
+
+ `ImageMagick Color Names`_
+ The color can then be given as a color name (there is a limited
+ but large set of these; see below) or it can be given as a set
+ of numbers (in decimal or hexadecimal), each corresponding to
+ a channel in an RGB or RGBA color model. HSL, HSLA, HSB, HSBA,
+ CMYK, or CMYKA color models may also be specified. These topics
+ are briefly described in the sections below.
+
+ .. _ImageMagick Color Names: http://www.imagemagick.org/script/color.php
+
+ .. describe:: == (other)
+
+ Equality operator.
+
+ :param other: a color another one
+ :type color: :class:`Color`
+ :returns: ``True`` only if two images equal.
+ :rtype: :class:`bool`
+
+ """
+
+ c_is_resource = library.IsPixelWand
+ c_destroy_resource = library.DestroyPixelWand
+ c_get_exception = library.PixelGetException
+ c_clear_exception = library.PixelClearException
+
+ __slots__ = 'raw', 'c_resource', 'allocated'
+
+ def __init__(self, string=None, raw=None):
+ if (string is None and raw is None or
+ string is not None and raw is not None):
+ raise TypeError('expected one argument')
+
+ self.allocated = 0
+ if raw is None:
+ self.raw = ctypes.create_string_buffer(
+ ctypes.sizeof(MagickPixelPacket)
+ )
+ with self:
+ library.PixelSetColor(self.resource, binary(string))
+ library.PixelGetMagickColor(self.resource, self.raw)
+ else:
+ self.raw = raw
+
+ def __getinitargs__(self):
+ return self.string, None
+
+ def __enter__(self):
+ if not self.allocated:
+ with self.allocate():
+ self.resource = library.NewPixelWand()
+ library.PixelSetMagickColor(self.resource, self.raw)
+ self.allocated += 1
+ return Resource.__enter__(self)
+
+ def __exit__(self, type, value, traceback):
+ self.allocated -= 1
+ if not self.allocated:
+ Resource.__exit__(self, type, value, traceback)
+
+ @property
+ def string(self):
+ """(:class:`basestring`) The string representation of the color."""
+ with self:
+ color_string = library.PixelGetColorAsString(self.resource)
+ return text(color_string.value)
+
+ @property
+ def normalized_string(self):
+ """(:class:`basestring`) The normalized string representation of
+ the color. The same color is always represented to the same
+ string.
+
+ .. versionadded:: 0.3.0
+
+ """
+ with self:
+ string = library.PixelGetColorAsNormalizedString(self.resource)
+ return text(string.value)
+
+ @staticmethod
+ def c_equals(a, b):
+ """Raw level version of equality test function for two pixels.
+
+ :param a: a pointer to PixelWand to compare
+ :type a: :class:`ctypes.c_void_p`
+ :param b: a pointer to PixelWand to compare
+ :type b: :class:`ctypes.c_void_p`
+ :returns: ``True`` only if two pixels equal
+ :rtype: :class:`bool`
+
+ .. note::
+
+ It's only for internal use. Don't use it directly.
+ Use ``==`` operator of :class:`Color` instead.
+
+ """
+ alpha = library.PixelGetAlpha
+ return bool(library.IsPixelWandSimilar(a, b, 0) and
+ alpha(a) == alpha(b))
+
+ def __eq__(self, other):
+ if not isinstance(other, Color):
+ return False
+ with self as this:
+ with other:
+ return self.c_equals(this.resource, other.resource)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __hash__(self):
+ if self.alpha:
+ return hash(self.normalized_string)
+ return hash(None)
+
+ @property
+ def red(self):
+ """(:class:`numbers.Real`) Red, from 0.0 to 1.0."""
+ with self:
+ return library.PixelGetRed(self.resource)
+
+ @property
+ def green(self):
+ """(:class:`numbers.Real`) Green, from 0.0 to 1.0."""
+ with self:
+ return library.PixelGetGreen(self.resource)
+
+ @property
+ def blue(self):
+ """(:class:`numbers.Real`) Blue, from 0.0 to 1.0."""
+ with self:
+ return library.PixelGetBlue(self.resource)
+
+ @property
+ def alpha(self):
+ """(:class:`numbers.Real`) Alpha value, from 0.0 to 1.0."""
+ with self:
+ return library.PixelGetAlpha(self.resource)
+
+ @property
+ def red_quantum(self):
+ """(:class:`numbers.Integral`) Red.
+ Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
+
+ .. versionadded:: 0.3.0
+
+ """
+ with self:
+ return library.PixelGetRedQuantum(self.resource)
+
+ @property
+ def green_quantum(self):
+ """(:class:`numbers.Integral`) Green.
+ Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
+
+ .. versionadded:: 0.3.0
+
+ """
+ with self:
+ return library.PixelGetGreenQuantum(self.resource)
+
+ @property
+ def blue_quantum(self):
+ """(:class:`numbers.Integral`) Blue.
+ Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
+
+ .. versionadded:: 0.3.0
+
+ """
+ with self:
+ return library.PixelGetBlueQuantum(self.resource)
+
+ @property
+ def alpha_quantum(self):
+ """(:class:`numbers.Integral`) Alpha value.
+ Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
+
+ .. versionadded:: 0.3.0
+
+ """
+ with self:
+ return library.PixelGetAlphaQuantum(self.resource)
+
+ @property
+ def red_int8(self):
+ """(:class:`numbers.Integral`) Red as 8bit integer which is a common
+ style. From 0 to 255.
+
+ .. versionadded:: 0.3.0
+
+ """
+ return scale_quantum_to_int8(self.red_quantum)
+
+ @property
+ def green_int8(self):
+ """(:class:`numbers.Integral`) Green as 8bit integer which is
+ a common style. From 0 to 255.
+
+ .. versionadded:: 0.3.0
+
+ """
+ return scale_quantum_to_int8(self.green_quantum)
+
+ @property
+ def blue_int8(self):
+ """(:class:`numbers.Integral`) Blue as 8bit integer which is
+ a common style. From 0 to 255.
+
+ .. versionadded:: 0.3.0
+
+ """
+ return scale_quantum_to_int8(self.blue_quantum)
+
+ @property
+ def alpha_int8(self):
+ """(:class:`numbers.Integral`) Alpha value as 8bit integer which is
+ a common style. From 0 to 255.
+
+ .. versionadded:: 0.3.0
+
+ """
+ return scale_quantum_to_int8(self.alpha_quantum)
+
+ def __str__(self):
+ return self.string
+
+ def __repr__(self):
+ c = type(self)
+ return '{0}.{1}({2!r})'.format(c.__module__, c.__name__, self.string)
+
+ def _repr_html_(self):
+ html = """
+
+ #{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 00000000..a9212e3e
Binary files /dev/null and b/lib/wand/color.pyc differ
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 00000000..e2bb0504
Binary files /dev/null and b/lib/wand/compat.pyc differ
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 00000000..355af6df
Binary files /dev/null and b/lib/wand/exceptions.pyc differ
diff --git a/lib/wand/font.py b/lib/wand/font.py
new file mode 100644
index 00000000..d6a5f030
--- /dev/null
+++ b/lib/wand/font.py
@@ -0,0 +1,103 @@
+""":mod:`wand.font` --- Fonts
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 0.3.0
+
+:class:`Font` is an object which takes the :attr:`~Font.path` of font file,
+:attr:`~Font.size`, :attr:`~Font.color`, and whether to use
+:attr:`~Font.antialias`\ ing. If you want to use font by its name rather
+than the file path, use TTFQuery_ package. The font path resolution by its
+name is a very complicated problem to achieve.
+
+.. seealso::
+
+ TTFQuery_ --- Find and Extract Information from TTF Files
+ TTFQuery builds on the `FontTools-TTX`_ package to allow the Python
+ programmer to accomplish a number of tasks:
+
+ - query the system to find installed fonts
+
+ - retrieve metadata about any TTF font file
+
+ - this includes the glyph outlines (shape) of individual code-points,
+ which allows for rendering the glyphs in 3D (such as is done in
+ OpenGLContext)
+
+ - lookup/find fonts by:
+
+ - abstract family type
+ - proper font name
+
+ - build simple metadata registries for run-time font matching
+
+.. _TTFQuery: http://ttfquery.sourceforge.net/
+.. _FontTools-TTX: http://sourceforge.net/projects/fonttools/
+
+"""
+import numbers
+
+from .color import Color
+from .compat import string_type, text
+
+__all__ = 'Font',
+
+
+class Font(tuple):
+ """Font struct which is a subtype of :class:`tuple`.
+
+ :param path: the path of the font file
+ :type path: :class:`str`, :class:`basestring`
+ :param size: the size of typeface. 0 by default which means *autosized*
+ :type size: :class:`numbers.Real`
+ :param color: the color of typeface. black by default
+ :type color: :class:`~wand.color.Color`
+ :param antialias: whether to use antialiasing. :const:`True` by default
+ :type antialias: :class:`bool`
+
+ .. versionchanged:: 0.3.9
+ The ``size`` parameter becomes optional. Its default value is
+ 0, which means *autosized*.
+
+ """
+
+ def __new__(cls, path, size=0, color=None, antialias=True):
+ if not isinstance(path, string_type):
+ raise TypeError('path must be a string, not ' + repr(path))
+ if not isinstance(size, numbers.Real):
+ raise TypeError('size must be a real number, not ' + repr(size))
+ if color is None:
+ color = Color('black')
+ elif not isinstance(color, Color):
+ raise TypeError('color must be an instance of wand.color.Color, '
+ 'not ' + repr(color))
+ path = text(path)
+ return tuple.__new__(cls, (path, size, color, bool(antialias)))
+
+ @property
+ def path(self):
+ """(:class:`basestring`) The path of font file."""
+ return self[0]
+
+ @property
+ def size(self):
+ """(:class:`numbers.Real`) The font size in pixels."""
+ return self[1]
+
+ @property
+ def color(self):
+ """(:class:`wand.color.Color`) The font color."""
+ return self[2]
+
+ @property
+ def antialias(self):
+ """(:class:`bool`) Whether to apply antialiasing (``True``)
+ or not (``False``).
+
+ """
+ return self[3]
+
+ def __repr__(self):
+ return '{0.__module__}.{0.__name__}({1})'.format(
+ type(self),
+ tuple.__repr__(self)
+ )
diff --git a/lib/wand/font.pyc b/lib/wand/font.pyc
new file mode 100644
index 00000000..eb155d0c
Binary files /dev/null and b/lib/wand/font.pyc differ
diff --git a/lib/wand/image.py b/lib/wand/image.py
new file mode 100644
index 00000000..9ae63f67
--- /dev/null
+++ b/lib/wand/image.py
@@ -0,0 +1,3498 @@
+""":mod:`wand.image` --- Image objects
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Opens and manipulates images. Image objects can be used in :keyword:`with`
+statement, and these resources will be automatically managed (even if any
+error happened)::
+
+ with Image(filename='pikachu.png') as i:
+ print('width =', i.width)
+ print('height =', i.height)
+
+"""
+import collections
+import ctypes
+import functools
+import numbers
+import weakref
+
+from . import compat
+from .api import MagickPixelPacket, libc, libmagick, library
+from .color import Color
+from .compat import (binary, binary_type, encode_filename, file_types,
+ string_type, text, xrange)
+from .exceptions import MissingDelegateError, WandException
+from .resource import DestroyedResourceError, Resource
+from .font import Font
+
+
+__all__ = ('ALPHA_CHANNEL_TYPES', 'CHANNELS', 'COLORSPACE_TYPES',
+ 'COMPOSITE_OPERATORS', 'COMPRESSION_TYPES',
+ 'EVALUATE_OPS', 'FILTER_TYPES',
+ 'GRAVITY_TYPES', 'IMAGE_TYPES', 'ORIENTATION_TYPES', 'UNIT_TYPES',
+ 'FUNCTION_TYPES',
+ 'BaseImage', 'ChannelDepthDict', 'ChannelImageDict',
+ 'ClosedImageError', 'HistogramDict', 'Image', 'ImageProperty',
+ 'Iterator', 'Metadata', 'OptionDict', 'manipulative')
+
+
+#: (:class:`tuple`) The list of filter types.
+#:
+#: - ``'undefined'``
+#: - ``'point'``
+#: - ``'box'``
+#: - ``'triangle'``
+#: - ``'hermite'``
+#: - ``'hanning'``
+#: - ``'hamming'``
+#: - ``'blackman'``
+#: - ``'gaussian'``
+#: - ``'quadratic'``
+#: - ``'cubic'``
+#: - ``'catrom'``
+#: - ``'mitchell'``
+#: - ``'jinc'``
+#: - ``'sinc'``
+#: - ``'sincfast'``
+#: - ``'kaiser'``
+#: - ``'welsh'``
+#: - ``'parzen'``
+#: - ``'bohman'``
+#: - ``'bartlett'``
+#: - ``'lagrange'``
+#: - ``'lanczos'``
+#: - ``'lanczossharp'``
+#: - ``'lanczos2'``
+#: - ``'lanczos2sharp'``
+#: - ``'robidoux'``
+#: - ``'robidouxsharp'``
+#: - ``'cosine'``
+#: - ``'spline'``
+#: - ``'sentinel'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Resize Filters`__
+#: Demonstrates the results of resampling images using the various
+#: resize filters and blur settings available in ImageMagick.
+#:
+#: __ http://www.imagemagick.org/Usage/resize/
+FILTER_TYPES = ('undefined', 'point', 'box', 'triangle', 'hermite', 'hanning',
+ 'hamming', 'blackman', 'gaussian', 'quadratic', 'cubic',
+ 'catrom', 'mitchell', 'jinc', 'sinc', 'sincfast', 'kaiser',
+ 'welsh', 'parzen', 'bohman', 'bartlett', 'lagrange', 'lanczos',
+ 'lanczossharp', 'lanczos2', 'lanczos2sharp', 'robidoux',
+ 'robidouxsharp', 'cosine', 'spline', 'sentinel')
+
+#: (:class:`tuple`) The list of composition operators
+#:
+#: - ``'undefined'``
+#: - ``'no'``
+#: - ``'add'``
+#: - ``'atop'``
+#: - ``'blend'``
+#: - ``'bumpmap'``
+#: - ``'change_mask'``
+#: - ``'clear'``
+#: - ``'color_burn'``
+#: - ``'color_dodge'``
+#: - ``'colorize'``
+#: - ``'copy_black'``
+#: - ``'copy_blue'``
+#: - ``'copy'``
+#: - ``'copy_cyan'``
+#: - ``'copy_green'``
+#: - ``'copy_magenta'``
+#: - ``'copy_opacity'``
+#: - ``'copy_red'``
+#: - ``'copy_yellow'``
+#: - ``'darken'``
+#: - ``'dst_atop'``
+#: - ``'dst'``
+#: - ``'dst_in'``
+#: - ``'dst_out'``
+#: - ``'dst_over'``
+#: - ``'difference'``
+#: - ``'displace'``
+#: - ``'dissolve'``
+#: - ``'exclusion'``
+#: - ``'hard_light'``
+#: - ``'hue'``
+#: - ``'in'``
+#: - ``'lighten'``
+#: - ``'linear_light'``
+#: - ``'luminize'``
+#: - ``'minus'``
+#: - ``'modulate'``
+#: - ``'multiply'``
+#: - ``'out'``
+#: - ``'over'``
+#: - ``'overlay'``
+#: - ``'plus'``
+#: - ``'replace'``
+#: - ``'saturate'``
+#: - ``'screen'``
+#: - ``'soft_light'``
+#: - ``'src_atop'``
+#: - ``'src'``
+#: - ``'src_in'``
+#: - ``'src_out'``
+#: - ``'src_over'``
+#: - ``'subtract'``
+#: - ``'threshold'``
+#: - ``'xor'``
+#: - ``'divide'``
+#:
+#: .. versionchanged:: 0.3.0
+#: Renamed from :const:`COMPOSITE_OPS` to :const:`COMPOSITE_OPERATORS`.
+#:
+#: .. seealso::
+#:
+#: `Compositing Images`__ ImageMagick v6 Examples
+#: Image composition is the technique of combining images that have,
+#: or do not have, transparency or an alpha channel.
+#: This is usually performed using the IM :program:`composite` command.
+#: It may also be performed as either part of a larger sequence of
+#: operations or internally by other image operators.
+#:
+#: `ImageMagick Composition Operators`__
+#: Demonstrates the results of applying the various composition
+#: composition operators.
+#:
+#: __ http://www.imagemagick.org/Usage/compose/
+#: __ http://www.rubblewebs.co.uk/imagemagick/operators/compose.php
+COMPOSITE_OPERATORS = (
+ 'undefined', 'no', 'add', 'atop', 'blend', 'bumpmap', 'change_mask',
+ 'clear', 'color_burn', 'color_dodge', 'colorize', 'copy_black',
+ 'copy_blue', 'copy', 'copy_cyan', 'copy_green', 'copy_magenta',
+ 'copy_opacity', 'copy_red', 'copy_yellow', 'darken', 'dst_atop', 'dst',
+ 'dst_in', 'dst_out', 'dst_over', 'difference', 'displace', 'dissolve',
+ 'exclusion', 'hard_light', 'hue', 'in', 'lighten', 'linear_light',
+ 'luminize', 'minus', 'modulate', 'multiply', 'out', 'over', 'overlay',
+ 'plus', 'replace', 'saturate', 'screen', 'soft_light', 'src_atop', 'src',
+ 'src_in', 'src_out', 'src_over', 'subtract', 'threshold', 'xor', 'divide'
+)
+
+#: (:class:`dict`) The dictionary of channel types.
+#:
+#: - ``'undefined'``
+#: - ``'red'``
+#: - ``'gray'``
+#: - ``'cyan'``
+#: - ``'green'``
+#: - ``'magenta'``
+#: - ``'blue'``
+#: - ``'yellow'``
+#: - ``'alpha'``
+#: - ``'opacity'``
+#: - ``'black'``
+#: - ``'index'``
+#: - ``'composite_channels'``
+#: - ``'all_channels'``
+#: - ``'true_alpha'``
+#: - ``'rgb_channels'``
+#: - ``'gray_channels'``
+#: - ``'sync_channels'``
+#: - ``'default_channels'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Color Channels`__
+#: Lists the various channel types with descriptions of each
+#:
+#: __ http://www.imagemagick.org/Magick++/Enumerations.html#ChannelType
+CHANNELS = dict(undefined=0, red=1, gray=1, cyan=1, green=2, magenta=2,
+ blue=4, yellow=4, alpha=8, opacity=8, black=32, index=32,
+ composite_channels=47, all_channels=134217727, true_alpha=64,
+ rgb_channels=128, gray_channels=128, sync_channels=256,
+ default_channels=134217719)
+
+#: (:class:`tuple`) The list of evaluation operators
+#:
+#: - ``'undefined'``
+#: - ``'add'``
+#: - ``'and'``
+#: - ``'divide'``
+#: - ``'leftshift'``
+#: - ``'max'``
+#: - ``'min'``
+#: - ``'multiply'``
+#: - ``'or'``
+#: - ``'rightshift'``
+#: - ``'set'``
+#: - ``'subtract'``
+#: - ``'xor'``
+#: - ``'pow'``
+#: - ``'log'``
+#: - ``'threshold'``
+#: - ``'thresholdblack'``
+#: - ``'thresholdwhite'``
+#: - ``'gaussiannoise'``
+#: - ``'impulsenoise'``
+#: - ``'laplaciannoise'``
+#: - ``'multiplicativenoise'``
+#: - ``'poissonnoise'``
+#: - ``'uniformnoise'``
+#: - ``'cosine'``
+#: - ``'sine'``
+#: - ``'addmodulus'``
+#: - ``'mean'``
+#: - ``'abs'``
+#: - ``'exponential'``
+#: - ``'median'``
+#: - ``'sum'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Image Evaluation Operators`__
+#: Describes the MagickEvaluateImageChannel method and lists the
+#: various evaluations operators
+#:
+#: __ http://www.magickwand.org/MagickEvaluateImage.html
+EVALUATE_OPS = ('undefined', 'add', 'and', 'divide', 'leftshift', 'max',
+ 'min', 'multiply', 'or', 'rightshift', 'set', 'subtract',
+ 'xor', 'pow', 'log', 'threshold', 'thresholdblack',
+ 'thresholdwhite', 'gaussiannoise', 'impulsenoise',
+ 'laplaciannoise', 'multiplicativenoise', 'poissonnoise',
+ 'uniformnoise', 'cosine', 'sine', 'addmodulus', 'mean',
+ 'abs', 'exponential', 'median', 'sum', 'rootmeansquare')
+
+#: (:class:`tuple`) The list of colorspaces.
+#:
+#: - ``'undefined'``
+#: - ``'rgb'``
+#: - ``'gray'``
+#: - ``'transparent'``
+#: - ``'ohta'``
+#: - ``'lab'``
+#: - ``'xyz'``
+#: - ``'ycbcr'``
+#: - ``'ycc'``
+#: - ``'yiq'``
+#: - ``'ypbpr'``
+#: - ``'yuv'``
+#: - ``'cmyk'``
+#: - ``'srgb'``
+#: - ``'hsb'``
+#: - ``'hsl'``
+#: - ``'hwb'``
+#: - ``'rec601luma'``
+#: - ``'rec601ycbcr'``
+#: - ``'rec709luma'``
+#: - ``'rec709ycbcr'``
+#: - ``'log'``
+#: - ``'cmy'``
+#: - ``'luv'``
+#: - ``'hcl'``
+#: - ``'lch'``
+#: - ``'lms'``
+#: - ``'lchab'``
+#: - ``'lchuv'``
+#: - ``'scrgb'``
+#: - ``'hsi'``
+#: - ``'hsv'``
+#: - ``'hclp'``
+#: - ``'ydbdr'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Color Management`__
+#: Describes the ImageMagick color management operations
+#:
+#: __ http://www.imagemagick.org/script/color-management.php
+#:
+#: .. versionadded:: 0.3.4
+COLORSPACE_TYPES = ('undefined', 'rgb', 'gray', 'transparent', 'ohta', 'lab',
+ 'xyz', 'ycbcr', 'ycc', 'yiq', 'ypbpr', 'yuv', 'cmyk',
+ 'srgb', 'hsb', 'hsl', 'hwb', 'rec601luma', 'rec601ycbcr',
+ 'rec709luma', 'rec709ycbcr', 'log', 'cmy', 'luv', 'hcl',
+ 'lch', 'lms', 'lchab', 'lchuv', 'scrgb', 'hsi', 'hsv',
+ 'hclp', 'ydbdr')
+
+#: (:class:`tuple`) The list of alpha channel types
+#:
+#: - ``'undefined'``
+#: - ``'activate'``
+#: - ``'background'``
+#: - ``'copy'``
+#: - ``'deactivate'``
+#: - ``'extract'``
+#: - ``'opaque'``
+#: - ``'reset'``
+#: - ``'set'``
+#: - ``'shape'``
+#: - ``'transparent'``
+#: - ``'flatten'``
+#: - ``'remove'``
+#:
+#: .. seealso::
+#: `ImageMagick Image Channel`__
+#: Describes the SetImageAlphaChannel method which can be used
+#: to modify alpha channel. Also describes AlphaChannelType
+#:
+#: __ http://www.imagemagick.org/api/channel.php#SetImageAlphaChannel
+ALPHA_CHANNEL_TYPES = ('undefined', 'activate', 'background', 'copy',
+ 'deactivate', 'extract', 'opaque', 'reset', 'set',
+ 'shape', 'transparent', 'flatten', 'remove')
+
+#: (:class:`tuple`) The list of image types
+#:
+#: - ``'undefined'``
+#: - ``'bilevel'``
+#: - ``'grayscale'``
+#: - ``'grayscalematte'``
+#: - ``'palette'``
+#: - ``'palettematte'``
+#: - ``'truecolor'``
+#: - ``'truecolormatte'``
+#: - ``'colorseparation'``
+#: - ``'colorseparationmatte'``
+#: - ``'optimize'``
+#: - ``'palettebilevelmatte'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Image Types`__
+#: Describes the MagickSetImageType method which can be used
+#: to set the type of an image
+#:
+#: __ http://www.imagemagick.org/api/magick-image.php#MagickSetImageType
+IMAGE_TYPES = ('undefined', 'bilevel', 'grayscale', 'grayscalematte',
+ 'palette', 'palettematte', 'truecolor', 'truecolormatte',
+ 'colorseparation', 'colorseparationmatte', 'optimize',
+ 'palettebilevelmatte')
+
+#: (:class:`tuple`) The list of resolution unit types.
+#:
+#: - ``'undefined'``
+#: - ``'pixelsperinch'``
+#: - ``'pixelspercentimeter'``
+#:
+#: .. seealso::
+#:
+#: `ImageMagick Image Units`__
+#: Describes the MagickSetImageUnits method which can be used
+#: to set image units of resolution
+#:
+#: __ http://www.imagemagick.org/api/magick-image.php#MagickSetImageUnits
+UNIT_TYPES = 'undefined', 'pixelsperinch', 'pixelspercentimeter'
+
+#: (:class:`tuple`) The list of :attr:`~BaseImage.gravity` types.
+#:
+#: .. versionadded:: 0.3.0
+GRAVITY_TYPES = ('forget', 'north_west', 'north', 'north_east', 'west',
+ 'center', 'east', 'south_west', 'south', 'south_east',
+ 'static')
+
+#: (:class:`tuple`) The list of :attr:`~BaseImage.orientation` types.
+#:
+#: .. versionadded:: 0.3.0
+ORIENTATION_TYPES = ('undefined', 'top_left', 'top_right', 'bottom_right',
+ 'bottom_left', 'left_top', 'right_top', 'right_bottom',
+ 'left_bottom')
+
+#: (:class:`collections.Set`) The set of available :attr:`~BaseImage.options`.
+#:
+#: .. versionadded:: 0.3.0
+#:
+#: .. versionchanged:: 0.3.4
+#: Added ``'jpeg:sampling-factor'`` option.
+#:
+#: .. versionchanged:: 0.3.9
+#: Added ``'pdf:use-cropbox'`` option.
+OPTIONS = frozenset(['fill', 'jpeg:sampling-factor', 'pdf:use-cropbox'])
+
+#: (:class:`tuple`) The list of :attr:`Image.compression` types.
+#:
+#: .. versionadded:: 0.3.6
+COMPRESSION_TYPES = (
+ 'undefined', 'b44a', 'b44', 'bzip', 'dxt1', 'dxt3', 'dxt5', 'fax',
+ 'group4',
+ 'jbig1', # ISO/IEC std 11544 / ITU-T rec T.82
+ 'jbig2', # ISO/IEC std 14492 / ITU-T rec T.88
+ 'jpeg2000', # ISO/IEC std 15444-1
+ 'jpeg', 'losslessjpeg',
+ 'lzma', # Lempel-Ziv-Markov chain algorithm
+ 'lzw', 'no', 'piz', 'pxr24', 'rle', 'zip', 'zips'
+)
+
+#: (:class:`tuple`) The list of :attr:`Image.function` types.
+#:
+#: - ``'undefined'``
+#: - ``'polynomial'``
+#: - ``'sinusoid'``
+#: - ``'arcsin'``
+#: - ``'arctan'``
+FUNCTION_TYPES = ('undefined', 'polynomial', 'sinusoid', 'arcsin', 'arctan')
+
+
+#: (:class:`tuple`) The list of :method:`Image.distort` methods.
+#:
+#: - ``'undefined'``
+#: - ``'affine'``
+#: - ``'affine_projection'``
+#: - ``'scale_rotate_translate'``
+#: - ``'perspective'``
+#: - ``'perspective_projection'``
+#: - ``'bilinear_forward'``
+#: - ``'bilinear_reverse'``
+#: - ``'polynomial'``
+#: - ``'arc'``
+#: - ``'polar'``
+#: - ``'depolar'``
+#: - ``'cylinder_2_plane'``
+#: - ``'plane_2_cylinder'``
+#: - ``'barrel'``
+#: - ``'barrel_inverse'``
+#: - ``'shepards'``
+#: - ``'resize'``
+#: - ``'sentinel'``
+#:
+#: .. versionadded:: 0.4.1
+DISTORTION_METHODS = (
+ 'undefined', 'affine', 'affine_projection', 'scale_rotate_translate',
+ 'perspective', 'perspective_projection', 'bilinear_forward',
+ 'bilinear_reverse', 'polynomial', 'arc', 'polar', 'depolar',
+ 'cylinder_2_plane', 'plane_2_cylinder', 'barrel', 'barrel_inverse',
+ 'shepards', 'resize', 'sentinel'
+)
+
+#: (:class:`tuple`) The list of :attr:`~BaseImage.virtual_pixel` types.
+#: - ``'undefined'``
+#: - ``'background'``
+#: - ``'constant'``
+#: - ``'dither'``
+#: - ``'edge'``
+#: - ``'mirror'``
+#: - ``'random'``
+#: - ``'tile'``
+#: - ``'transparent'``
+#: - ``'mask'``
+#: - ``'black'``
+#: - ``'gray'``
+#: - ``'white'``
+#: - ``'horizontal_tile'``
+#: - ``'vertical_tile'``
+#: - ``'horizontal_tile_edge'``
+#: - ``'vertical_tile_edge'``
+#: - ``'checker_tile'``
+#:
+#: .. versionadded:: 0.4.1
+VIRTUAL_PIXEL_METHOD = ('undefined', 'background', 'constant', 'dither',
+ 'edge', 'mirror', 'random', 'tile', 'transparent',
+ 'mask', 'black', 'gray', 'white', 'horizontal_tile',
+ 'vertical_tile', 'horizontal_tile_edge',
+ 'vertical_tile_edge', 'checker_tile')
+
+
+def manipulative(function):
+ """Mark the operation manipulating itself instead of returning new one."""
+ @functools.wraps(function)
+ def wrapped(self, *args, **kwargs):
+ result = function(self, *args, **kwargs)
+ self.dirty = True
+ return result
+ return wrapped
+
+
+class BaseImage(Resource):
+ """The abstract base of :class:`Image` (container) and
+ :class:`~wand.sequence.SingleImage`. That means the most of
+ operations, defined in this abstract classs, are possible for
+ both :class:`Image` and :class:`~wand.sequence.SingleImage`.
+
+ .. versionadded:: 0.3.0
+
+ """
+
+ #: (:class:`OptionDict`) The mapping of internal option settings.
+ #:
+ #: .. versionadded:: 0.3.0
+ #:
+ #: .. versionchanged:: 0.3.4
+ #: Added ``'jpeg:sampling-factor'`` option.
+ #:
+ #: .. versionchanged:: 0.3.9
+ #: Added ``'pdf:use-cropbox'`` option.
+ options = None
+
+ #: (:class:`collections.Sequence`) The list of
+ #: :class:`~wand.sequence.SingleImage`\ s that the image contains.
+ #:
+ #: .. versionadded:: 0.3.0
+ sequence = None
+
+ #: (:class:`bool`) Whether the image is changed or not.
+ dirty = None
+
+ c_is_resource = library.IsMagickWand
+ c_destroy_resource = library.DestroyMagickWand
+ c_get_exception = library.MagickGetException
+ c_clear_exception = library.MagickClearException
+
+ __slots__ = '_wand',
+
+ def __init__(self, wand):
+ self.wand = wand
+ self.channel_images = ChannelImageDict(self)
+ self.channel_depths = ChannelDepthDict(self)
+ self.options = OptionDict(self)
+ self.dirty = False
+
+ @property
+ def wand(self):
+ """Internal pointer to the MagickWand instance. It may raise
+ :exc:`ClosedImageError` when the instance has destroyed already.
+
+ """
+ try:
+ return self.resource
+ except DestroyedResourceError:
+ raise ClosedImageError(repr(self) + ' is closed already')
+
+ @wand.setter
+ def wand(self, wand):
+ try:
+ self.resource = wand
+ except TypeError:
+ raise TypeError(repr(wand) + ' is not a MagickWand instance')
+
+ @wand.deleter
+ def wand(self):
+ del self.resource
+
+ def clone(self):
+ """Clones the image. It is equivalent to call :class:`Image` with
+ ``image`` parameter. ::
+
+ with img.clone() as cloned:
+ # manipulate the cloned image
+ pass
+
+ :returns: the cloned new image
+ :rtype: :class:`Image`
+
+ .. versionadded:: 0.1.1
+
+ """
+ return Image(image=self)
+
+ def __len__(self):
+ return self.height
+
+ def __iter__(self):
+ return Iterator(image=self)
+
+ def __getitem__(self, idx):
+ if (not isinstance(idx, string_type) and
+ isinstance(idx, collections.Iterable)):
+ idx = tuple(idx)
+ d = len(idx)
+ if not (1 <= d <= 2):
+ raise ValueError('index cannot be {0}-dimensional'.format(d))
+ elif d == 2:
+ x, y = idx
+ x_slice = isinstance(x, slice)
+ y_slice = isinstance(y, slice)
+ if x_slice and not y_slice:
+ y = slice(y, y + 1)
+ elif not x_slice and y_slice:
+ x = slice(x, x + 1)
+ elif not (x_slice or y_slice):
+ if not (isinstance(x, numbers.Integral) and
+ isinstance(y, numbers.Integral)):
+ raise TypeError('x and y must be integral, not ' +
+ repr((x, y)))
+ if x < 0:
+ x += self.width
+ if y < 0:
+ y += self.height
+ if x >= self.width:
+ raise IndexError('x must be less than width')
+ elif y >= self.height:
+ raise IndexError('y must be less than height')
+ elif x < 0:
+ raise IndexError('x cannot be less than 0')
+ elif y < 0:
+ raise IndexError('y cannot be less than 0')
+ with iter(self) as iterator:
+ iterator.seek(y)
+ return iterator.next(x)
+ if not (x.step is None and y.step is None):
+ raise ValueError('slicing with step is unsupported')
+ elif (x.start is None and x.stop is None and
+ y.start is None and y.stop is None):
+ return self.clone()
+ cloned = self.clone()
+ try:
+ cloned.crop(x.start, y.start, x.stop, y.stop)
+ except ValueError as e:
+ raise IndexError(str(e))
+ return cloned
+ else:
+ return self[idx[0]]
+ elif isinstance(idx, numbers.Integral):
+ if idx < 0:
+ idx += self.height
+ elif idx >= self.height:
+ raise IndexError('index must be less than height, but got ' +
+ repr(idx))
+ elif idx < 0:
+ raise IndexError('index cannot be less than zero, but got ' +
+ repr(idx))
+ with iter(self) as iterator:
+ iterator.seek(idx)
+ return iterator.next()
+ elif isinstance(idx, slice):
+ return self[:, idx]
+ raise TypeError('unsupported index type: ' + repr(idx))
+
+ def __eq__(self, other):
+ if isinstance(other, type(self)):
+ return self.signature == other.signature
+ return False
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __hash__(self):
+ return hash(self.signature)
+
+ @property
+ def animation(self):
+ """(:class:`bool`) Whether the image is animation or not.
+ It doesn't only mean that the image has two or more images (frames),
+ but all frames are even the same size. It's about image format,
+ not content. It's :const:`False` even if :mimetype:`image/ico`
+ consits of two or more images of the same size.
+
+ For example, it's :const:`False` for :mimetype:`image/jpeg`,
+ :mimetype:`image/gif`, :mimetype:`image/ico`.
+
+ If :mimetype:`image/gif` has two or more frames, it's :const:`True`.
+ If :mimetype:`image/gif` has only one frame, it's :const:`False`.
+
+ .. versionadded:: 0.3.0
+
+ .. versionchanged:: 0.3.8
+ Became to accept :mimetype:`image/x-gif` as well.
+
+ """
+ return False
+
+ @property
+ def gravity(self):
+ """(:class:`basestring`) The text placement gravity used when
+ annotating with text. It's a string from :const:`GRAVITY_TYPES`
+ list. It also can be set.
+
+ """
+ gravity_index = library.MagickGetGravity(self.wand)
+ if not gravity_index:
+ self.raise_exception()
+ return GRAVITY_TYPES[gravity_index]
+
+ @gravity.setter
+ @manipulative
+ def gravity(self, value):
+ if not isinstance(value, string_type):
+ raise TypeError('expected a string, not ' + repr(value))
+ if value not in GRAVITY_TYPES:
+ raise ValueError('expected a string from GRAVITY_TYPES, not '
+ + repr(value))
+ library.MagickSetGravity(self.wand, GRAVITY_TYPES.index(value))
+
+ @property
+ def font_path(self):
+ """(:class:`basestring`) The path of the current font.
+ It also can be set.
+
+ """
+ return text(library.MagickGetFont(self.wand))
+
+ @font_path.setter
+ @manipulative
+ def font_path(self, font):
+ font = binary(font)
+ if library.MagickSetFont(self.wand, font) is False:
+ raise ValueError('font is invalid')
+
+ @property
+ def font_size(self):
+ """(:class:`numbers.Real`) The font size. It also can be set."""
+ return library.MagickGetPointsize(self.wand)
+
+ @font_size.setter
+ @manipulative
+ def font_size(self, size):
+ if not isinstance(size, numbers.Real):
+ raise TypeError('expected a numbers.Real, but got ' + repr(size))
+ elif size < 0.0:
+ raise ValueError('cannot be less then 0.0, but got ' + repr(size))
+ elif library.MagickSetPointsize(self.wand, size) is False:
+ raise ValueError('unexpected error is occur')
+
+ @property
+ def font_antialias(self):
+ return bool(library.MagickGetAntialias(self.wand))
+
+ @font_antialias.setter
+ @manipulative
+ def font_antialias(self, antialias):
+ if not isinstance(antialias, bool):
+ raise TypeError('font_antialias must be a bool, not ' +
+ repr(antialias))
+ library.MagickSetAntialias(self.wand, antialias)
+
+ @property
+ def font(self):
+ """(:class:`wand.font.Font`) The current font options."""
+ return Font(
+ path=text(self.font_path),
+ size=self.font_size,
+ color=self.font_color,
+ antialias=self.font_antialias
+ )
+
+ @font.setter
+ @manipulative
+ def font(self, font):
+ if not isinstance(font, Font):
+ raise TypeError('font must be a wand.font.Font, not ' + repr(font))
+ self.font_path = font.path
+ self.font_size = font.size
+ self.font_color = font.color
+ self.font_antialias = font.antialias
+
+ @property
+ def 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 00000000..aad9b3cb
Binary files /dev/null and b/lib/wand/image.pyc differ
diff --git a/lib/wand/resource.py b/lib/wand/resource.py
new file mode 100644
index 00000000..609a3894
--- /dev/null
+++ b/lib/wand/resource.py
@@ -0,0 +1,244 @@
+""":mod:`wand.resource` --- Global resource management
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There is the global resource to manage in MagickWand API. This module
+implements automatic global resource management through reference counting.
+
+"""
+import contextlib
+import ctypes
+import warnings
+
+from .api import library
+from .compat import string_type
+from .exceptions import TYPE_MAP, WandException
+
+
+__all__ = ('genesis', 'terminus', 'increment_refcount', 'decrement_refcount',
+ 'Resource', 'DestroyedResourceError')
+
+
+def genesis():
+ """Instantiates the MagickWand API.
+
+ .. warning::
+
+ Don't call this function directly. Use :func:`increment_refcount()` and
+ :func:`decrement_refcount()` functions instead.
+
+ """
+ library.MagickWandGenesis()
+
+
+def terminus():
+ """Cleans up the MagickWand API.
+
+ .. warning::
+
+ Don't call this function directly. Use :func:`increment_refcount()` and
+ :func:`decrement_refcount()` functions instead.
+
+ """
+ library.MagickWandTerminus()
+
+
+#: (:class:`numbers.Integral`) The internal integer value that maintains
+#: the number of referenced objects.
+#:
+#: .. warning::
+#:
+#: Don't touch this global variable. Use :func:`increment_refcount()` and
+#: :func:`decrement_refcount()` functions instead.
+#:
+reference_count = 0
+
+
+def increment_refcount():
+ """Increments the :data:`reference_count` and instantiates the MagickWand
+ API if it is the first use.
+
+ """
+ global reference_count
+ if reference_count:
+ reference_count += 1
+ else:
+ genesis()
+ reference_count = 1
+
+
+def decrement_refcount():
+ """Decrements the :data:`reference_count` and cleans up the MagickWand
+ API if it will be no more used.
+
+ """
+ global reference_count
+ if not reference_count:
+ raise RuntimeError('wand.resource.reference_count is already zero')
+ reference_count -= 1
+ if not reference_count:
+ terminus()
+
+
+class Resource(object):
+ """Abstract base class for MagickWand object that requires resource
+ management. Its all subclasses manage the resource semiautomatically
+ and support :keyword:`with` statement as well::
+
+ with Resource() as resource:
+ # use the resource...
+ pass
+
+ It doesn't implement constructor by itself, so subclasses should
+ implement it. Every constructor should assign the pointer of its
+ resource data into :attr:`resource` attribute inside of :keyword:`with`
+ :meth:`allocate()` context. For example::
+
+ class Pizza(Resource):
+ '''My pizza yummy.'''
+
+ def __init__(self):
+ with self.allocate():
+ self.resource = library.NewPizza()
+
+ .. versionadded:: 0.1.2
+
+ """
+
+ #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` predicate function
+ #: that returns whether the given pointer (that contains a resource data
+ #: usuaully) is a valid resource.
+ #:
+ #: .. note::
+ #:
+ #: It is an abstract attribute that has to be implemented
+ #: in the subclass.
+ c_is_resource = NotImplemented
+
+ #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that destroys
+ #: the :attr:`resource`.
+ #:
+ #: .. note::
+ #:
+ #: It is an abstract attribute that has to be implemented
+ #: in the subclass.
+ c_destroy_resource = NotImplemented
+
+ #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that gets
+ #: an exception from the :attr:`resource`.
+ #:
+ #: .. note::
+ #:
+ #: It is an abstract attribute that has to be implemented
+ #: in the subclass.
+ c_get_exception = NotImplemented
+
+ #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that clears
+ #: an exception of the :attr:`resource`.
+ #:
+ #: .. note::
+ #:
+ #: It is an abstract attribute that has to be implemented
+ #: in the subclass.
+ c_clear_exception = NotImplemented
+
+ @property
+ def resource(self):
+ """Internal pointer to the resource instance. It may raise
+ :exc:`DestroyedResourceError` when the resource has destroyed already.
+
+ """
+ if getattr(self, 'c_resource', None) is None:
+ raise DestroyedResourceError(repr(self) + ' is destroyed already')
+ return self.c_resource
+
+ @resource.setter
+ def resource(self, resource):
+ # Delete the existing resource if there is one
+ if getattr(self, 'c_resource', None):
+ self.destroy()
+
+ if self.c_is_resource(resource):
+ self.c_resource = resource
+ else:
+ raise TypeError(repr(resource) + ' is an invalid resource')
+ increment_refcount()
+
+ @resource.deleter
+ def resource(self):
+ self.c_destroy_resource(self.resource)
+ self.c_resource = None
+
+ @contextlib.contextmanager
+ def allocate(self):
+ """Allocates the memory for the resource explicitly. Its subclasses
+ should assign the created resource into :attr:`resource` attribute
+ inside of this context. For example::
+
+ with resource.allocate():
+ resource.resource = library.NewResource()
+
+ """
+ increment_refcount()
+ try:
+ yield self
+ except:
+ decrement_refcount()
+ raise
+
+ def destroy(self):
+ """Cleans up the resource explicitly. If you use the resource in
+ :keyword:`with` statement, it was called implicitly so have not to
+ call it.
+
+ """
+ del self.resource
+ decrement_refcount()
+
+ def get_exception(self):
+ """Gets a current exception instance.
+
+ :returns: a current exception. it can be ``None`` as well if any
+ errors aren't occurred
+ :rtype: :class:`wand.exceptions.WandException`
+
+ """
+ severity = ctypes.c_int()
+ desc = self.c_get_exception(self.resource, ctypes.byref(severity))
+ if severity.value == 0:
+ return
+ self.c_clear_exception(self.wand)
+ exc_cls = TYPE_MAP[severity.value]
+ message = desc.value
+ if not isinstance(message, string_type):
+ message = message.decode(errors='replace')
+ return exc_cls(message)
+
+ def raise_exception(self, stacklevel=1):
+ """Raises an exception or warning if it has occurred."""
+ e = self.get_exception()
+ if isinstance(e, Warning):
+ warnings.warn(e, stacklevel=stacklevel + 1)
+ elif isinstance(e, Exception):
+ raise e
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.destroy()
+
+ def __del__(self):
+ try:
+ self.destroy()
+ except DestroyedResourceError:
+ pass
+
+
+class DestroyedResourceError(WandException, ReferenceError, AttributeError):
+ """An error that rises when some code tries access to an already
+ destroyed resource.
+
+ .. versionchanged:: 0.3.0
+ It becomes a subtype of :exc:`wand.exceptions.WandException`.
+
+ """
diff --git a/lib/wand/resource.pyc b/lib/wand/resource.pyc
new file mode 100644
index 00000000..8f0a1ed3
Binary files /dev/null and b/lib/wand/resource.pyc differ
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 00000000..7efb6002
Binary files /dev/null and b/lib/wand/sequence.pyc differ
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 00000000..97625355
Binary files /dev/null and b/lib/wand/version.pyc differ