view data/plugin/macro/ @ 501:16aa5b518631

macro.Image: removed wrong used /div
author Reimar Bauer <rb.proj AT googlemail DOT com>
date Wed, 17 Feb 2010 15:19:45 +0100
parents de15d700d553
children f9845325cfcf
line wrap: on
line source
# -*- coding: iso-8859-1 -*-
    MoinMoin - Image rendering support class and a simple 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
    * easy access to some EXIF data (cached)

    Requires PIL and ExifTags libs.

    @copyright: 2008 MoinMoin:ThomasWaldmann,
                2008-2010 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

    from PIL import Image as PILImage
except ImportError:
    PILImage = None
    from PIL import IptcImagePlugin as PILIptc
except ImportError:
    PILIptc = None

    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
        self.request = request
        self.pagename, self.attachname = item_name.rsplit('/', 1)

        # cached property values:
        self.__filename = None
        self.__content_type = None
        self.__cache_key = None
        self.__image = None
        self.__exif = None
        self.__ctime = None
        self.__headline = headline
        self.__caption = caption

    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 =
        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
                    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
                            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 = {}
                    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)), ]:
                                iptc_data[name] = iptc[key]
                            except KeyError:
                    iptc_data = {}
                cache_data = (exif_data, iptc_data)
                self.__exif, self.__iptc = cache_data
                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_headline(self):
        if self.__headline is None:
            self.__headline = self.iptc.get('headline', u'')
        return self.__headline
    headline = property(_get_headline)

    def _get_caption(self):
        if self.__caption is None:
            self.__caption = self.iptc.get('caption', u'')
        return self.__caption
    caption = property(_get_caption)

    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, 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)
            except KeyError:

            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 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'
            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'', description=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 width 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")>>
    _ = macro.request.getText
    if not AttachFile.exists(macro.request,, itemname):
        return _("Attachment '%(filename)s' does not exist!") % {"filename": itemname}
    if '/' not in itemname:
        itemname = + '/' + itemname
    img = Image(macro.request, itemname, caption=alt)

    div_width = ""
    if width != 9999:
        div_width = '<div style="width:%spx">' % width
    return """<div class="thumbnail">%s<div class="decription">%s%s
              <div class="show-datetime">%s</div></div></div>""" % (
                                     macro.formatter.image(src=img.url((width, height)), alt=img.caption),