summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2015-12-13 17:20:49 -0800
committerMike Crute <mcrute@gmail.com>2015-12-13 17:20:49 -0800
commitf0dd6b36a5d84e953ad86e64a5c0e377a8643569 (patch)
tree05654183489a676aa3f1ae6bc99101ee765e3d2c
parent4aaa43a0ad9ff683668c141f5f26a51612de2152 (diff)
downloadmoin_thumbnail-better-scaling.tar.bz2
moin_thumbnail-better-scaling.tar.xz
moin_thumbnail-better-scaling.zip
Parse exif for rotation infobetter-scaling
-rw-r--r--thumbnail/action/Thumbnail.py132
1 files 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 @@
1import os 1import os
2import hashlib 2import hashlib
3import exifread
4from collections import namedtuple
3from PIL import Image, ImageOps 5from PIL import Image, ImageOps
4from PIL.ImageFileIO import ImageFileIO 6from PIL.ImageFileIO import ImageFileIO
5 7
@@ -13,22 +15,117 @@ from MoinMoin.caching import CacheEntry
13logging = log.getLogger(__name__) 15logging = log.getLogger(__name__)
14 16
15 17
18class ExifWrapper(object):
19
20 ROTATED = (5, 6, 7, 8)
21
22 def __init__(self, exif_data):
23 self.exif_data = exif_data
24
25 @classmethod
26 def from_fp(cls, fp):
27 return cls(exifread.process_file(fp))
28
29 def _get_first_value(self, tag):
30 value = self.exif_data.get(tag)
31
32 if value and len(value.values) >= 1:
33 return value.values[0]
34
35 @property
36 def image_width(self):
37 return self._get_first_value('EXIF ExifImageWidth')
38
39 @property
40 def image_length(self):
41 return self._get_first_value('EXIF ExifImageLength')
42
43 @property
44 def is_rotated(self):
45 value = self._get_first_value('Image Orientation')
46 return value in self.ROTATED
47
48
49class ImageWrapper(object):
50
51 _IMG_WIDTH = 0
52 _IMG_HEIGHT = 1
53
54 def __init__(self, filename, img, exif):
55 self.filename = filename
56 self.exif = exif
57 self.img = img
58
59 @classmethod
60 def from_fp(cls, fp):
61 img = Image.open(fp)
62 img.load()
63 fp.seek(0)
64
65 return cls(fp.name, img, ExifWrapper.from_fp(fp))
66
67 @property
68 def _width(self):
69 return float(self.img.size[self._IMG_WIDTH])
70
71 @property
72 def _height(self):
73 return float(self.img.size[self._IMG_HEIGHT])
74
75 @property
76 def is_landscape(self):
77 return self.width > self.height
78
79 @property
80 def height(self):
81 if self.exif.is_rotated:
82 return self._width
83 else:
84 return self._height
85
86 @property
87 def width(self):
88 if self.exif.is_rotated:
89 return self._height
90 else:
91 return self._width
92
93 def to_jpeg_data(self):
94 return self.img.tostring('jpeg', self.img.mode)
95
96 def content_type(self):
97 mt = wikiutil.MimeType(filename=self.filename)
98 return mt.content_type()
99
100 def crop(self, *args, **kwargs):
101 self.img = self.img.crop(*args, **kwargs)
102
103 def thumbnail(self, *args, **kwargs):
104 self.img.thumbnail(*args, **kwargs)
105
106 def grayscale(self):
107 self.img = ImageOps.grayscale(self.img)
108
109
110def grayscale(img):
111 img.grayscale()
112
113
16def crop(img, width, height): 114def crop(img, width, height):
17 src_width, src_height = img.size 115 src_ratio = float(img.width) / float(img.height)
18 src_ratio = float(src_width) / float(src_height)
19 dst_width, dst_height = int(width), int(height) 116 dst_width, dst_height = int(width), int(height)
20 dst_ratio = float(dst_width) / float(dst_height) 117 dst_ratio = float(dst_width) / float(dst_height)
21 118
22 if dst_ratio < src_ratio: 119 if dst_ratio < src_ratio:
23 crop_height = src_height 120 crop_height = img.height
24 crop_width = crop_height * dst_ratio 121 crop_width = crop_height * dst_ratio
25 x_offset = float(src_width - crop_width) / 2 122 x_offset = float(img.width - crop_width) / 2
26 y_offset = 0 123 y_offset = 0
27 else: 124 else:
28 crop_width = src_width 125 crop_width = img.width
29 crop_height = crop_width / dst_ratio 126 crop_height = crop_width / dst_ratio
30 x_offset = 0 127 x_offset = 0
31 y_offset = float(src_height - crop_height) / 3 128 y_offset = float(img.height - crop_height) / 3
32 129
33 return img.crop(( 130 return img.crop((
34 x_offset, 131 x_offset,
@@ -40,13 +137,12 @@ def crop(img, width, height):
40 137
41def thumbnail(img, long_side): 138def thumbnail(img, long_side):
42 long_side = int(long_side) 139 long_side = int(long_side)
43 width, height = [float(d) for d in img.size]
44 140
45 if height > width: 141 if img.is_landscape:
46 width = (width / height) * long_side 142 width = (img.width / img.height) * long_side
47 height = long_side 143 height = long_side
48 else: 144 else:
49 height = (height / width) * long_side 145 height = (img.height / img.width) * long_side
50 width = long_side 146 width = long_side
51 147
52 img.thumbnail((width, height), Image.ANTIALIAS) 148 img.thumbnail((width, height), Image.ANTIALIAS)
@@ -55,12 +151,11 @@ def thumbnail(img, long_side):
55 151
56def thumbnail_constrain(img, size, dimension): 152def thumbnail_constrain(img, size, dimension):
57 size = int(size) 153 size = int(size)
58 width, height = [float(d) for d in img.size]
59 154
60 if dimension.lower() == "h": 155 if dimension.lower() == "h":
61 height, width = size, ((width * size) / height) 156 height, width = size, ((img.width * size) / img.height)
62 elif dimension.lower() == "w": 157 elif dimension.lower() == "w":
63 width, height = size, ((height * size) / width) 158 width, height = size, ((img.height * size) / img.width)
64 else: 159 else:
65 raise Exception("Must contrain valid dimension") 160 raise Exception("Must contrain valid dimension")
66 161
@@ -94,22 +189,24 @@ def execute(pagename, request):
94 cache = CacheEntry(request, page, 189 cache = CacheEntry(request, page,
95 get_cache_key(request, filename), scope="item") 190 get_cache_key(request, filename), scope="item")
96 191
192 """
97 if cache.exists() and (cache.mtime() >= os.path.getmtime(fpath)): 193 if cache.exists() and (cache.mtime() >= os.path.getmtime(fpath)):
98 logging.info("Using cache for %s", fpath) 194 logging.info("Using cache for %s", fpath)
99 cache.open(mode="r") 195 cache.open(mode="r")
100 request.write(cache.read()) 196 request.write(cache.read())
101 cache.close() 197 cache.close()
102 return 198 return
199 """
103 200
104 action_map = { 201 action_map = {
105 'ds': ImageOps.grayscale, 202 'ds': grayscale,
106 'cr': crop, 203 'cr': crop,
107 'th': thumbnail, 204 'th': thumbnail,
108 'tc': thumbnail_constrain, 205 'tc': thumbnail_constrain,
109 } 206 }
110 207
111 with open(fpath) as fp: 208 with open(fpath) as fp:
112 img = Image.open(fp) 209 img = ImageWrapper.from_fp(fp)
113 210
114 for action in request.values.getlist("do"): 211 for action in request.values.getlist("do"):
115 action = action.split(":") 212 action = action.split(":")
@@ -122,9 +219,8 @@ def execute(pagename, request):
122 request.write("Error: {}".format(e)) 219 request.write("Error: {}".format(e))
123 return 220 return
124 221
125 mt = wikiutil.MimeType(filename=filename) 222 request.headers['Content-Type'] = img.content_type()
126 request.headers['Content-Type'] = mt.content_type() 223 data = img.to_jpeg_data()
127 data = img.tostring('jpeg', img.mode)
128 224
129 try: 225 try:
130 cache.lock("w") 226 cache.lock("w")