diff --git a/wand/api.py b/wand/api.py index bbe083ed..a3c87545 100644 --- a/wand/api.py +++ b/wand/api.py @@ -129,6 +129,19 @@ class MagickPixelPacket(ctypes.Structure): library.MagickGetImageSignature.argtypes = [ctypes.c_void_p] library.MagickGetImageSignature.restype = ctypes.c_char_p + library.MagickGetImageProperty.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + library.MagickGetImageProperty.restype = ctypes.c_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] diff --git a/wand/image.py b/wand/image.py index 830fe70b..eed73711 100644 --- a/wand/image.py +++ b/wand/image.py @@ -368,6 +368,9 @@ class Image(Resource): """ + #: (:class:`Metadata`) The metadata mapping of the image. Read only. + metadata = None + c_is_resource = library.IsMagickWand c_destroy_resource = library.DestroyMagickWand c_get_exception = library.MagickGetException @@ -460,6 +463,7 @@ def __init__(self, image=None, blob=None, file=None, filename=None, read = True if not read: raise TypeError('invalid argument(s)') + self.metadata = Metadata(self) self.raise_exception() @property @@ -1422,9 +1426,50 @@ def clone(self): return type(self)(iterator=self) +class Metadata(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` + """ + def __init__(self, image): + if not isinstance(image, Image): + raise TypeError('expected a wand.image.Image instance, ' + 'not ' + repr(image)) + self.image = image + + def __getitem__(self, k): + """ + :param k: Metadata header name string. + :type k: :class:`basestring` + :returns: a header value string + :rtype: :class:`str` + """ + if not isinstance(k, basestring): + raise TypeError('k must be a string, not ' + repr(format)) + + v = library.MagickGetImageProperty(self.image.wand, k) + if v is None: + raise KeyError + return v + + def __iter__(self): + num = ctypes.c_size_t() + props_p = library.MagickGetImageProperties(self.image.wand, '', num) + props = [props_p[i] for i in xrange(num.value)] + library.MagickRelinquishMemory(props_p) + return iter(props) + + def __len__(self): + num = ctypes.c_size_t() + props_p = library.MagickGetImageProperties(self.image.wand, '', num) + library.MagickRelinquishMemory(props_p) + return num.value + + class ClosedImageError(DestroyedResourceError): """An error that rises when some code tries access to an already closed image. """ - diff --git a/wandtests/image.py b/wandtests/image.py index 25dd0fac..5db5d318 100644 --- a/wandtests/image.py +++ b/wandtests/image.py @@ -773,3 +773,14 @@ def reset_coords(): msg = 'img = {0!r}, control = {1!r}'.format( img.signature, sig) assert img.signature == sig, msg + + +@tests.test +def metadata(): + """Test metadata api""" + with Image(filename=asset('beach.jpg')) as img: + assert len(img.metadata) == 52 + assert 'exif:ApertureValue' in img.metadata + assert 'exif:UnknownValue' not in img.metadata + assert img.metadata['exif:ApertureValue'] == '192/32' + assert img.metadata.get('exif:UnknownValue', "IDK") == "IDK"