view data/plugin/macro/ @ 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
children 448a086d840c
line wrap: on
line source
# -*- coding: iso-8859-1 -*-
    MoinMoin - MoinImage image rendering support class and a test macro using it.

    * 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

    import Image
except ImportError:
    Image = None

    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
        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 =
        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
                    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
                            time_str = exif_data['DateTimeOriginal']
                            tm = time.strptime(time_str, "%Y:%m:%d %H:%M:%S")
                            t = time.mktime(tm)
                                tz_str = exif_data['TimeZoneOffset']
                                tz_offset = int(tz_str) * 3600
                                tz_offset = 0
                            # mktime is inverse function of (our) localtime, adjust by time.timezone
                            t -= time.timezone + tz_offset
                            t = 0
                        exif_data['DateTimeOriginal'] = t
                except (IOError, AttributeError):
                    exif_data = {}
                self.__exif = exif_data
                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:
                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'
            raise ValueError("content_type %r not supported" % content_type)
        image = self.image
        if image is not None: # we have PIL
                # 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()
  , output_type)
            buf.flush() # XXX needed?
            data = buf.getvalue()
        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'
            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 =
    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) +
            '---' +