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