view data/plugin/macro/MoinImage.py @ 264:b7e7be4340b0

MoinImage: add IPTC support
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 13 Sep 2008 22:26:31 +0200
parents 448a086d840c
children
line wrap: on
line source
# -*- 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:
    import Image
except ImportError:
    Image = None
try:
    import IptcImagePlugin
except ImportError:
    IptcImagePlugin = None

try:
    import ExifTags
except ImportError:
    ExifTags = None

class MoinImage(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 MoinImage
        ):
        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 Image is not None:
            self.__image = Image.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 IptcImagePlugin: # we have PIL and the IPTC plugin
                        iptc = IptcImagePlugin.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, 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 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_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)
    return (fmt.image(src=img.url(img.THUMBNAIL_SIZE), alt=img.description) +
            fmt.text(time.asctime(time.gmtime(img.ctime))) +
            ' ' +
            fmt.text(img.description) +
            '---' +
            fmt.text(repr(img.iptc)))