From f0dd6b36a5d84e953ad86e64a5c0e377a8643569 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sun, 13 Dec 2015 17:20:49 -0800 Subject: Parse exif for rotation info --- thumbnail/action/Thumbnail.py | 132 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/thumbnail/action/Thumbnail.py b/thumbnail/action/Thumbnail.py index 3ff1f42..b3118e0 100644 --- a/thumbnail/action/Thumbnail.py +++ b/thumbnail/action/Thumbnail.py @@ -1,5 +1,7 @@ import os import hashlib +import exifread +from collections import namedtuple from PIL import Image, ImageOps from PIL.ImageFileIO import ImageFileIO @@ -13,22 +15,117 @@ 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_width, src_height = img.size - src_ratio = float(src_width) / float(src_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 = src_height + crop_height = img.height crop_width = crop_height * dst_ratio - x_offset = float(src_width - crop_width) / 2 + x_offset = float(img.width - crop_width) / 2 y_offset = 0 else: - crop_width = src_width + crop_width = img.width crop_height = crop_width / dst_ratio x_offset = 0 - y_offset = float(src_height - crop_height) / 3 + y_offset = float(img.height - crop_height) / 3 return img.crop(( x_offset, @@ -40,13 +137,12 @@ def crop(img, width, height): def thumbnail(img, long_side): long_side = int(long_side) - width, height = [float(d) for d in img.size] - if height > width: - width = (width / height) * long_side + if img.is_landscape: + width = (img.width / img.height) * long_side height = long_side else: - height = (height / width) * long_side + height = (img.height / img.width) * long_side width = long_side img.thumbnail((width, height), Image.ANTIALIAS) @@ -55,12 +151,11 @@ def thumbnail(img, long_side): def thumbnail_constrain(img, size, dimension): size = int(size) - width, height = [float(d) for d in img.size] if dimension.lower() == "h": - height, width = size, ((width * size) / height) + height, width = size, ((img.width * size) / img.height) elif dimension.lower() == "w": - width, height = size, ((height * size) / width) + width, height = size, ((img.height * size) / img.width) else: raise Exception("Must contrain valid dimension") @@ -94,22 +189,24 @@ def execute(pagename, request): 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': ImageOps.grayscale, + 'ds': grayscale, 'cr': crop, 'th': thumbnail, 'tc': thumbnail_constrain, } with open(fpath) as fp: - img = Image.open(fp) + img = ImageWrapper.from_fp(fp) for action in request.values.getlist("do"): action = action.split(":") @@ -122,9 +219,8 @@ def execute(pagename, request): request.write("Error: {}".format(e)) return - mt = wikiutil.MimeType(filename=filename) - request.headers['Content-Type'] = mt.content_type() - data = img.tostring('jpeg', img.mode) + request.headers['Content-Type'] = img.content_type() + data = img.to_jpeg_data() try: cache.lock("w") -- cgit v1.2.3