view data/plugin/macro/Image.py @ 625:f4e63b74b969

FormSubmit: adapt to werkzeug MultiDict
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Mon, 25 Mar 2013 17:47:30 +0100
parents 588965ee2c63
children 127da830be6c
line wrap: on
line source
# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - Image rendering support class and a simple 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-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

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
                 headline=None,
                 caption=None,
        ):
        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 = 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_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:
            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 in ('image/jpeg', 'image/pjpeg'):
            output_type = 'JPEG'
        elif content_type in ('image/png', 'image/x-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),
            }
            try:
                image = transpose_func[transpose_op](image)
            except KeyError:
                pass

            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'', description=u'', clear=("none", "both", "left", "right")):
    """ 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.

        Optional you can set by the clear parameter how following text should be handled.

        <<Image(PageName/attachname,width=100,alt="sample image")>>
    """
    _ = macro.request.getText
    current_pagename = macro.formatter.page.page_name
    page_name, filename = AttachFile.absoluteName(itemname, current_pagename)
    if not AttachFile.exists(macro.request, page_name, filename):
        return _("Attachment '%(filename)s' does not exist!") % {"filename": itemname}                                                                     
    itemname = page_name + '/' + filename
    img = Image(macro.request, itemname, caption=alt)

    div_width = ""
    br_clear = ""
    if clear != "none":
        br_clear = '<br style="clear:%s;">' % clear
    if width != 9999:
        div_width = '<div style="width:%spx">' % width

    module = macro.formatter.__module__
    if module[module.rfind('.') + 1:] != "text_html":
        return macro.formatter.image(src=img.url((width, height)), alt=img.caption)

    return """<div class="thumbnail">%s<div class="decription">%s%s
              <div class="show-datetime">%s</div></div></div>%s""" % (
                                     macro.formatter.image(src=img.url((width, height)), alt=img.caption),
                                     div_width,
                                     macro.formatter.text(description),
                                     macro.request.user.getFormattedDateTime(img.ctime),
                                     br_clear
                                     )