import os import hashlib import exifread from collections import namedtuple from PIL import Image, ImageOps from PIL.ImageFileIO import ImageFileIO from MoinMoin import log from MoinMoin import wikiutil from MoinMoin.Page import Page from MoinMoin.action import AttachFile from MoinMoin.caching import CacheEntry logging = log.getLogger(__name__) class ExifWrapper(object): ROTATED = (5, 6, 7, 8) def __init__(self, exif_data): self.exif_data = exif_data @classmethod def from_fp(cls, fp): return cls(exifread.process_file(fp)) def _get_first_value(self, tag): value = self.exif_data.get(tag) if value and len(value.values) >= 1: return value.values[0] @property def image_width(self): return self._get_first_value('EXIF ExifImageWidth') @property def image_length(self): return self._get_first_value('EXIF ExifImageLength') @property def is_rotated(self): value = self._get_first_value('Image Orientation') return value in self.ROTATED class ImageWrapper(object): _IMG_WIDTH = 0 _IMG_HEIGHT = 1 def __init__(self, filename, img, exif): self.filename = filename self.exif = exif self.img = img @classmethod def from_fp(cls, fp): img = Image.open(fp) img.load() fp.seek(0) return cls(fp.name, img, ExifWrapper.from_fp(fp)) @property def _width(self): return float(self.img.size[self._IMG_WIDTH]) @property def _height(self): return float(self.img.size[self._IMG_HEIGHT]) @property def is_landscape(self): return self.width > self.height @property def height(self): if self.exif.is_rotated: return self._width else: return self._height @property def width(self): if self.exif.is_rotated: return self._height else: return self._width def to_jpeg_data(self): return self.img.tostring('jpeg', self.img.mode) def content_type(self): mt = wikiutil.MimeType(filename=self.filename) return mt.content_type() def crop(self, *args, **kwargs): self.img = self.img.crop(*args, **kwargs) def thumbnail(self, *args, **kwargs): self.img.thumbnail(*args, **kwargs) def grayscale(self): self.img = ImageOps.grayscale(self.img) def grayscale(img): img.grayscale() def crop(img, width, height): src_ratio = float(img.width) / float(img.height) dst_width, dst_height = int(width), int(height) dst_ratio = float(dst_width) / float(dst_height) if dst_ratio < src_ratio: crop_height = img.height crop_width = crop_height * dst_ratio x_offset = float(img.width - crop_width) / 2 y_offset = 0 else: crop_width = img.width crop_height = crop_width / dst_ratio x_offset = 0 y_offset = float(img.height - crop_height) / 3 return img.crop(( x_offset, y_offset, x_offset + int(crop_width), y_offset + int(crop_height) )) def thumbnail(img, long_side): long_side = int(long_side) if img.is_landscape: width = (img.width / img.height) * long_side height = long_side else: height = (img.height / img.width) * long_side width = long_side img.thumbnail((width, height), Image.ANTIALIAS) return img def thumbnail_constrain(img, size, dimension): size = int(size) if dimension.lower() == "h": height, width = size, ((img.width * size) / img.height) elif dimension.lower() == "w": width, height = size, ((img.height * size) / img.width) else: raise Exception("Must contrain valid dimension") img.thumbnail((int(width), int(height)), Image.ANTIALIAS) return img def get_cache_key(request, filename): ops = hashlib.md5(":".join(request.values.getlist("do"))) key = filename.split(".") key.insert(0, "thumbnail") key.insert(-1, ops.hexdigest()[:8]) return ".".join(key) def execute(pagename, request): _ = request.getText if not request.user.may.read(pagename): return _('You are not allowed to view attachments of this page.') page = Page(request, pagename) pagename, filename, fpath = AttachFile._access_file(pagename, request) if not filename: request.status_code = 404 return cache = CacheEntry(request, page, get_cache_key(request, filename), scope="item") """ if cache.exists() and (cache.mtime() >= os.path.getmtime(fpath)): logging.info("Using cache for %s", fpath) cache.open(mode="r") request.write(cache.read()) cache.close() return """ action_map = { 'ds': grayscale, 'cr': crop, 'th': thumbnail, 'tc': thumbnail_constrain, } with open(fpath) as fp: img = ImageWrapper.from_fp(fp) for action in request.values.getlist("do"): action = action.split(":") args = action[1].split(",") if len(action) > 1 else [] try: img = action_map[action[0]](img, *args) except Exception, e: request.status_code = 400 request.write("Error: {}".format(e)) return request.headers['Content-Type'] = img.content_type() data = img.to_jpeg_data() try: cache.lock("w") cache.open(mode="w") cache.write(data) cache.close() except Exception, e: request.status_code = 500 request.write("Error: {}".format(e)) return finally: cache.unlock() request.write(data)