changeset 262:c605998e1123

added MoinImage class and test macro
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 13 Sep 2008 19:28:31 +0200
parents 6ec6242fae3a
children 448a086d840c
files data/plugin/macro/MoinImage.py
diffstat 1 files changed, 233 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/plugin/macro/MoinImage.py	Sat Sep 13 19:28:31 2008 +0200
@@ -0,0 +1,233 @@
+# -*- 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
+    * 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.
+"""
+
+# default width, height
+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', ])
+
+import os, time
+import StringIO
+
+from MoinMoin import caching, wikiutil
+from MoinMoin.action import AttachFile, cache
+
+try:
+    import Image
+except ImportError:
+    Image = None
+
+try:
+    import ExifTags
+except ImportError:
+    ExifTags = None
+
+class MoinImage(object):
+    def __init__(self, request,
+                 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 MoinImage
+                 webnail_size=WEBNAIL_SIZE,
+                 thumbnail_size=THUMBNAIL_SIZE,
+        ):
+        self.request = request
+        self.pagename, self.attachname = name.rsplit('/', 1)
+        self.description = description
+        self.webnail_size = webnail_size
+        self.thumbnail_size = thumbnail_size
+
+        # 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 Image is not None:
+            self.__image = Image.open(self._filename)
+        return self.__image
+    image = property(_get_image)
+
+    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 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 = {}
+                exif_cache.update(exif_data)
+                self.__exif = exif_data
+            else:
+                self.__exif = exif_cache.content()
+        return self.__exif
+    exif = property(_get_exif_data)
+
+    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, Image.ANTIALIAS)
+
+            transpose_func = {
+                1: lambda image: image,
+                2: lambda image: image.transpose(Image.FLIP_LEFT_RIGHT),
+                3: lambda image: image.transpose(Image.ROTATE_180),
+                4: lambda image: image.transpose(Image.FLIP_TOP_BOTTOM),
+                5: lambda image: image.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM),
+                6: lambda image: image.transpose(Image.ROTATE_270),
+                7: lambda image: image.transpose(Image.ROTATE_90).transpose(Image.FLIP_LEFT_RIGHT),
+                8: lambda image: image.transpose(Image.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 _cache_url(self, size=None):
+        """ return a cache url for a rendering of this image with specified size,
+            make sure the cache contains that rendering.
+        """
+        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(self.request, key)
+
+    def webnail_url(self):
+        """ return url of webnail of this image """
+        return self._cache_url(self.webnail_size)
+
+    def thumbnail_url(self):
+        """ return url of thumbnail of this image """
+        return self._cache_url(self.thumbnail_size)
+
+    def fullsize_url(self):
+        """ return url of non-resized, but maybe transposed original image """
+        return self._cache_url() # keep size as is
+
+
+def macro_MoinImage(macro, itemname=wikiutil.required_arg(unicode), desc=u''):
+    """ Embed an Image into a wiki page -
+        currently more for testing MoinImage class than for practical use:
+
+        <<MoinImage(PageName/attachname,sample image)>>
+    """
+    request = macro.request
+    fmt = macro.formatter
+    thispagename = fmt.page.page_name
+    pagename, fname = AttachFile.absoluteName(itemname, thispagename)
+    img = MoinImage(request, itemname, desc)
+
+    src = img.fullsize_url()
+    alt = fmt.text(img.description)
+    return (fmt.image(src=src, alt=alt) +
+            fmt.text(time.asctime(time.gmtime(img.ctime))) +
+            ' ' +
+            fmt.text(img.description) +
+            '---' +
+            fmt.text(repr(img.exif)))
+