diff data/plugin/macro/Image.py @ 265:37b96db69d0e

MoinImage macro: renamed to Image
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 13 Sep 2008 23:56:05 +0200
parents data/plugin/macro/MoinImage.py@b7e7be4340b0
children 3e4eaae2f7df
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/plugin/macro/Image.py	Sat Sep 13 23:56:05 2008 +0200
@@ -0,0 +1,235 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - MoinImage image rendering support class and a test macro using it.
+
+    Features:
+    * rendering (and caching) of thumbnails/webnails/originals
+    * gives ready-to-use image urls
+    * auto-rotation based on EXIF information
+    * determines creation time from EXIF or file system
+    * easy access to some EXIF data (cached)
+
+    Requires PIL and ExifTags libs.
+
+    @copyright: 2008 MoinMoin:ThomasWaldmann,
+                2008 MoinMoin:ReimarBauer
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import os, time
+import StringIO
+
+from MoinMoin import caching, wikiutil
+from MoinMoin.action import AttachFile, cache
+
+try:
+    from PIL import Image as PILImage
+except ImportError:
+    PILImage = None
+try:
+    from PIL import IptcImagePlugin as PILIptc
+except ImportError:
+    PILIptc = None
+
+try:
+    import ExifTags
+except ImportError:
+    ExifTags = None
+
+class Image(object):
+    # predefined sizes (width, height) - use them if you like:
+    WEBNAIL_SIZE = (640, 480)
+    THUMBNAIL_SIZE = (128, 128)
+
+    # we don't process and cache all EXIF infos, but just these:
+    EXIF_CACHED = set(['DateTimeOriginal', 'TimeZoneOffset', 'Orientation', ])
+
+    def __init__(self, request,
+                 item_name, # PageName/AttachName for now, later this is just the item name
+                 description=u'', # we just store this, but we do not use it in Image
+        ):
+        self.request = request
+        self.pagename, self.attachname = item_name.rsplit('/', 1)
+        self.description = description
+
+        # cached property values:
+        self.__filename = None
+        self.__content_type = None
+        self.__cache_key = None
+        self.__image = None
+        self.__exif = None
+        self.__ctime = None
+
+    def _get_filename(self):
+        if self.__filename is None:
+            self.__filename = AttachFile.getFilename(self.request, self.pagename, self.attachname)
+        return self.__filename
+    _filename = property(_get_filename)
+
+    def _get_content_type(self):
+        if self.__content_type is None:
+            self.__content_type = wikiutil.MimeType(filename=self._filename).mime_type()
+        return self.__content_type
+    content_type = property(_get_content_type)
+
+    def _get_image(self):
+        if self.__image is None and PILImage is not None:
+            self.__image = PILImage.open(self._filename)
+        return self.__image
+    image = property(_get_image) # the original image (PIL Image object) or None
+
+    def _get_cache_key(self):
+        if self.__cache_key is None:
+            self.__cache_key = cache.key(self.request, itemname=self.pagename, attachname=self.attachname)
+        return self.__cache_key
+    _cache_key = property(_get_cache_key)
+
+    def _get_exif_data(self):
+        """ return exif data for this image, use a cache for them """
+        if self.__exif is None:
+            key = self._cache_key
+            exif_cache = caching.CacheEntry(self.request, 'exif', key, 'wiki',
+                                            use_pickle=True, do_locking=False)
+            if not exif_cache.exists():
+                # we don't want to cache all EXIF data, just a few interesting values
+                try:
+                    exif_data = {}
+                    if self.image is not None: # we have PIL
+                        for tag, value in self.image._getexif().items():
+                            tag_name = ExifTags.TAGS.get(tag)
+                            if tag_name in self.EXIF_CACHED:
+                                exif_data[tag_name] = value
+                        try:
+                            time_str = exif_data['DateTimeOriginal']
+                            tm = time.strptime(time_str, "%Y:%m:%d %H:%M:%S")
+                            t = time.mktime(tm)
+                            try:
+                                tz_str = exif_data['TimeZoneOffset']
+                                tz_offset = int(tz_str) * 3600
+                            except:
+                                tz_offset = 0
+                            # mktime is inverse function of (our) localtime, adjust by time.timezone
+                            t -= time.timezone + tz_offset
+                        except:
+                            t = 0
+                        exif_data['DateTimeOriginal'] = t
+                except (IOError, AttributeError):
+                    exif_data = {}
+                try:
+                    iptc_data = {}
+                    if self.image is not None and PILIptc: # we have PIL and the IPTC plugin
+                        iptc = PILIptc.getiptcinfo(self.image) or {}
+                        for name, key in [
+                            ('headline', (2, 105)),
+                            ('caption', (2, 120)),
+                            ('copyright', (2, 116)),
+                            ('keywords', (2, 25)), ]:
+                            try:
+                                iptc_data[name] = iptc[key]
+                            except KeyError:
+                                pass
+                except:
+                    iptc_data = {}
+                cache_data = (exif_data, iptc_data)
+                exif_cache.update(cache_data)
+                self.__exif, self.__iptc = cache_data
+            else:
+                self.__exif, self.__iptc = exif_cache.content()
+        return self.__exif, self.__iptc
+    exif = property(lambda self: self._get_exif_data()[0]) # dict of preprocessed EXIF data (string -> value)
+    iptc = property(lambda self: self._get_exif_data()[1]) # dict of preprocessed IPTC data (string -> value)
+
+    def _get_ctime(self):
+        """ return creation time of image (either from EXIF or file date) as UNIX timestamp """
+        if self.__ctime is None:
+            try:
+                ctime = self.exif['DateTimeOriginal']
+            except KeyError:
+                ctime = os.path.getctime(self._filename)
+            self.__ctime = ctime
+        return self.__ctime
+    ctime = property(_get_ctime)
+
+    def _transform(self, size=None, content_type=None, transpose_op=None):
+        """ resize to new size (optional), transpose according to exif infos,
+            return data as content_type (default: same ct as original image)
+        """
+        if content_type is None:
+            content_type = self.content_type
+        if content_type == 'image/jpeg':
+            output_type = 'JPEG'
+        elif content_type == 'image/png':
+            output_type = 'PNG'
+        elif content_type == 'image/gif':
+            output_type = 'GIF'
+        else:
+            raise ValueError("content_type %r not supported" % content_type)
+        image = self.image
+        if image is not None: # we have PIL
+            try:
+                # if we have EXIF data, we can transpose (e.g. rotate left),
+                # so the rendered image is correctly oriented:
+                transpose_op = transpose_op or self.exif['Orientation']
+            except KeyError:
+                transpose_op = 1 # no change
+
+            if size is not None:
+                image = image.copy() # create copy first as thumbnail works in-place
+                image.thumbnail(size, PILImage.ANTIALIAS)
+
+            transpose_func = {
+                1: lambda image: image,
+                2: lambda image: image.transpose(PILImage.FLIP_LEFT_RIGHT),
+                3: lambda image: image.transpose(PILImage.ROTATE_180),
+                4: lambda image: image.transpose(PILImage.FLIP_TOP_BOTTOM),
+                5: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_TOP_BOTTOM),
+                6: lambda image: image.transpose(PILImage.ROTATE_270),
+                7: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_LEFT_RIGHT),
+                8: lambda image: image.transpose(PILImage.ROTATE_90),
+            }
+            image = transpose_func[transpose_op](image)
+
+            buf = StringIO.StringIO()
+            image.save(buf, output_type)
+            buf.flush() # XXX needed?
+            data = buf.getvalue()
+            buf.close()
+        else: # XXX what to do without PIL?
+            data = ''
+        return content_type, data
+
+    def url(self, size=None):
+        """ return a cache url for a rendering of this image with specified size -
+            the code automatically makes sure that the cache contains that rendering.
+            If size is None, it gives a url for the full image size rendering.
+            Otherwise, size has to be a tuple (w, h) - if you like, you can use
+            these class level constant sizes:
+                WEBNAIL_SIZE - medium size, one of those likely to fit in a browser window
+                THUMBNAIL_SIZE - small size, for showing collections
+        """
+        request = self.request
+        content_type = self.content_type
+        if size is None:
+            size_str = 'orig'
+        else:
+            size_str = '%d_%d' % size
+        key = '%s_%s_%s' % (content_type.replace('/', '_'), size_str, self._cache_key)
+        if not cache.exists(request, key):
+            content_type, data = self._transform(size=size, content_type=content_type)
+            cache.put(request, key, data, content_type=content_type)
+        return cache.url(request, key)
+
+
+def macro_Image(macro, itemname=wikiutil.required_arg(unicode), width=9999, height=9999, alt=u''):
+    """ Embed an Image into a wiki page.
+
+        We use a very high default value for width and height, because PIL will calculate the
+        image dimensions to not be larger than (width, height) and also not be larger than
+        the original image. Thus, by not giving widht or height, you'll get the original image,
+        and if you specify width or height you will get an image of that width or that height.
+
+        <<Image(PageName/attachname,width=100,alt="sample image")>>
+    """
+    img = Image(macro.request, itemname, alt)
+    return macro.formatter.image(src=img.url((width, height)), alt=img.description)
+