tw@262: # -*- coding: iso-8859-1 -*- tw@262: """ tw@267: MoinMoin - Image rendering support class and a simple macro using it. tw@262: tw@262: Features: tw@262: * rendering (and caching) of thumbnails/webnails/originals tw@262: * gives ready-to-use image urls tw@262: * auto-rotation based on EXIF information tw@262: * determines creation time from EXIF or file system tw@263: * easy access to some EXIF data (cached) tw@262: tw@262: Requires PIL and ExifTags libs. tw@262: tw@262: @copyright: 2008 MoinMoin:ThomasWaldmann, rb@496: 2008-2010 MoinMoin:ReimarBauer tw@262: @license: GNU GPL, see COPYING for details. tw@262: """ tw@262: tw@262: import os, time tw@262: import StringIO tw@262: tw@262: from MoinMoin import caching, wikiutil tw@262: from MoinMoin.action import AttachFile, cache tw@262: tw@262: try: tw@265: from PIL import Image as PILImage tw@262: except ImportError: tw@265: PILImage = None tw@264: try: tw@265: from PIL import IptcImagePlugin as PILIptc tw@264: except ImportError: tw@265: PILIptc = None tw@262: tw@262: try: tw@262: import ExifTags tw@262: except ImportError: tw@262: ExifTags = None tw@262: tw@265: class Image(object): tw@263: # predefined sizes (width, height) - use them if you like: tw@263: WEBNAIL_SIZE = (640, 480) tw@263: THUMBNAIL_SIZE = (128, 128) tw@263: tw@263: # we don't process and cache all EXIF infos, but just these: tw@263: EXIF_CACHED = set(['DateTimeOriginal', 'TimeZoneOffset', 'Orientation', ]) tw@263: tw@262: def __init__(self, request, tw@263: item_name, # PageName/AttachName for now, later this is just the item name tw@267: headline=None, tw@267: caption=None, tw@262: ): tw@262: self.request = request tw@263: self.pagename, self.attachname = item_name.rsplit('/', 1) tw@262: tw@262: # cached property values: tw@262: self.__filename = None tw@262: self.__content_type = None tw@262: self.__cache_key = None tw@262: self.__image = None tw@262: self.__exif = None tw@262: self.__ctime = None tw@267: self.__headline = headline tw@267: self.__caption = caption tw@262: tw@262: def _get_filename(self): tw@262: if self.__filename is None: tw@262: self.__filename = AttachFile.getFilename(self.request, self.pagename, self.attachname) tw@262: return self.__filename tw@262: _filename = property(_get_filename) tw@262: tw@262: def _get_content_type(self): tw@262: if self.__content_type is None: tw@262: self.__content_type = wikiutil.MimeType(filename=self._filename).mime_type() tw@262: return self.__content_type tw@262: content_type = property(_get_content_type) tw@262: tw@262: def _get_image(self): tw@265: if self.__image is None and PILImage is not None: tw@265: self.__image = PILImage.open(self._filename) tw@262: return self.__image tw@263: image = property(_get_image) # the original image (PIL Image object) or None tw@262: tw@262: def _get_cache_key(self): tw@262: if self.__cache_key is None: tw@262: self.__cache_key = cache.key(self.request, itemname=self.pagename, attachname=self.attachname) tw@262: return self.__cache_key tw@262: _cache_key = property(_get_cache_key) tw@262: tw@262: def _get_exif_data(self): tw@262: """ return exif data for this image, use a cache for them """ tw@262: if self.__exif is None: tw@262: key = self._cache_key tw@262: exif_cache = caching.CacheEntry(self.request, 'exif', key, 'wiki', tw@262: use_pickle=True, do_locking=False) tw@262: if not exif_cache.exists(): tw@262: # we don't want to cache all EXIF data, just a few interesting values tw@262: try: tw@262: exif_data = {} tw@262: if self.image is not None: # we have PIL tw@262: for tag, value in self.image._getexif().items(): tw@262: tag_name = ExifTags.TAGS.get(tag) tw@263: if tag_name in self.EXIF_CACHED: tw@262: exif_data[tag_name] = value tw@262: try: tw@262: time_str = exif_data['DateTimeOriginal'] tw@262: tm = time.strptime(time_str, "%Y:%m:%d %H:%M:%S") tw@262: t = time.mktime(tm) tw@262: try: tw@262: tz_str = exif_data['TimeZoneOffset'] tw@262: tz_offset = int(tz_str) * 3600 tw@262: except: tw@262: tz_offset = 0 tw@262: # mktime is inverse function of (our) localtime, adjust by time.timezone tw@262: t -= time.timezone + tz_offset tw@262: except: tw@262: t = 0 tw@262: exif_data['DateTimeOriginal'] = t tw@262: except (IOError, AttributeError): tw@262: exif_data = {} tw@264: try: tw@264: iptc_data = {} tw@265: if self.image is not None and PILIptc: # we have PIL and the IPTC plugin tw@265: iptc = PILIptc.getiptcinfo(self.image) or {} tw@264: for name, key in [ tw@264: ('headline', (2, 105)), tw@264: ('caption', (2, 120)), tw@264: ('copyright', (2, 116)), tw@264: ('keywords', (2, 25)), ]: tw@264: try: tw@264: iptc_data[name] = iptc[key] tw@264: except KeyError: tw@264: pass tw@264: except: tw@264: iptc_data = {} tw@264: cache_data = (exif_data, iptc_data) tw@264: exif_cache.update(cache_data) tw@264: self.__exif, self.__iptc = cache_data tw@262: else: tw@264: self.__exif, self.__iptc = exif_cache.content() tw@264: return self.__exif, self.__iptc tw@264: exif = property(lambda self: self._get_exif_data()[0]) # dict of preprocessed EXIF data (string -> value) tw@264: iptc = property(lambda self: self._get_exif_data()[1]) # dict of preprocessed IPTC data (string -> value) tw@262: tw@267: def _get_headline(self): tw@267: if self.__headline is None: tw@267: self.__headline = self.iptc.get('headline', u'') tw@267: return self.__headline tw@267: headline = property(_get_headline) tw@267: tw@267: def _get_caption(self): tw@267: if self.__caption is None: tw@267: self.__caption = self.iptc.get('caption', u'') tw@267: return self.__caption tw@267: caption = property(_get_caption) tw@267: tw@262: def _get_ctime(self): tw@262: """ return creation time of image (either from EXIF or file date) as UNIX timestamp """ tw@262: if self.__ctime is None: tw@262: try: tw@262: ctime = self.exif['DateTimeOriginal'] tw@262: except KeyError: tw@262: ctime = os.path.getctime(self._filename) tw@262: self.__ctime = ctime tw@262: return self.__ctime tw@262: ctime = property(_get_ctime) tw@262: tw@262: def _transform(self, size=None, content_type=None, transpose_op=None): tw@262: """ resize to new size (optional), transpose according to exif infos, tw@262: return data as content_type (default: same ct as original image) tw@262: """ tw@262: if content_type is None: tw@262: content_type = self.content_type tw@262: if content_type == 'image/jpeg': tw@262: output_type = 'JPEG' tw@262: elif content_type == 'image/png': tw@262: output_type = 'PNG' tw@262: elif content_type == 'image/gif': tw@262: output_type = 'GIF' tw@262: else: tw@262: raise ValueError("content_type %r not supported" % content_type) tw@262: image = self.image tw@262: if image is not None: # we have PIL tw@262: try: tw@262: # if we have EXIF data, we can transpose (e.g. rotate left), tw@262: # so the rendered image is correctly oriented: tw@262: transpose_op = transpose_op or self.exif['Orientation'] tw@262: except KeyError: tw@262: transpose_op = 1 # no change tw@262: tw@262: if size is not None: tw@262: image = image.copy() # create copy first as thumbnail works in-place tw@265: image.thumbnail(size, PILImage.ANTIALIAS) tw@262: tw@262: transpose_func = { tw@262: 1: lambda image: image, tw@265: 2: lambda image: image.transpose(PILImage.FLIP_LEFT_RIGHT), tw@265: 3: lambda image: image.transpose(PILImage.ROTATE_180), tw@265: 4: lambda image: image.transpose(PILImage.FLIP_TOP_BOTTOM), tw@265: 5: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_TOP_BOTTOM), tw@265: 6: lambda image: image.transpose(PILImage.ROTATE_270), tw@265: 7: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_LEFT_RIGHT), tw@265: 8: lambda image: image.transpose(PILImage.ROTATE_90), tw@262: } rb@500: try: rb@500: image = transpose_func[transpose_op](image) rb@500: except KeyError: rb@500: pass tw@262: tw@262: buf = StringIO.StringIO() tw@262: image.save(buf, output_type) tw@262: buf.flush() # XXX needed? tw@262: data = buf.getvalue() tw@262: buf.close() tw@262: else: # XXX what to do without PIL? tw@262: data = '' tw@262: return content_type, data tw@262: tw@263: def url(self, size=None): tw@263: """ return a cache url for a rendering of this image with specified size - tw@263: the code automatically makes sure that the cache contains that rendering. tw@263: If size is None, it gives a url for the full image size rendering. tw@263: Otherwise, size has to be a tuple (w, h) - if you like, you can use tw@263: these class level constant sizes: tw@263: WEBNAIL_SIZE - medium size, one of those likely to fit in a browser window tw@263: THUMBNAIL_SIZE - small size, for showing collections tw@262: """ tw@262: request = self.request tw@262: content_type = self.content_type tw@262: if size is None: tw@262: size_str = 'orig' tw@262: else: tw@262: size_str = '%d_%d' % size tw@262: key = '%s_%s_%s' % (content_type.replace('/', '_'), size_str, self._cache_key) tw@262: if not cache.exists(request, key): tw@262: content_type, data = self._transform(size=size, content_type=content_type) tw@262: cache.put(request, key, data, content_type=content_type) tw@263: return cache.url(request, key) tw@262: tw@262: rb@305: def macro_Image(macro, itemname=wikiutil.required_arg(unicode), width=9999, height=9999, alt=u'', description=u''): tw@265: """ Embed an Image into a wiki page. tw@262: tw@265: We use a very high default value for width and height, because PIL will calculate the tw@265: image dimensions to not be larger than (width, height) and also not be larger than tw@266: the original image. Thus, by not giving width or height, you'll get the original image, tw@265: and if you specify width or height you will get an image of that width or that height. tw@265: tw@265: <> tw@262: """ rb@496: _ = macro.request.getText rb@496: if not AttachFile.exists(macro.request, macro.formatter.page.page_name, itemname): rb@496: return _("Attachment '%(filename)s' does not exist!") % {"filename": itemname} tw@266: if '/' not in itemname: tw@266: itemname = macro.formatter.page.page_name + '/' + itemname rb@299: img = Image(macro.request, itemname, caption=alt) rb@342: rb@338: div_width = "" rb@338: if width != 9999: rb@338: div_width = '
' % width rb@338: return """
%s
%s%s rb@312:
%s
""" % ( rb@308: macro.formatter.image(src=img.url((width, height)), alt=img.caption), rb@338: div_width, rb@308: macro.formatter.text(description), rb@308: macro.request.user.getFormattedDateTime(img.ctime), rb@312: ) rb@299: