diff options
author | Mike Crute <mcrute@gmail.com> | 2015-12-13 17:20:49 -0800 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2015-12-13 17:20:49 -0800 |
commit | f0dd6b36a5d84e953ad86e64a5c0e377a8643569 (patch) | |
tree | 05654183489a676aa3f1ae6bc99101ee765e3d2c | |
parent | 4aaa43a0ad9ff683668c141f5f26a51612de2152 (diff) | |
download | moin_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.py | 132 |
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 @@ | |||
1 | import os | 1 | import os |
2 | import hashlib | 2 | import hashlib |
3 | import exifread | ||
4 | from collections import namedtuple | ||
3 | from PIL import Image, ImageOps | 5 | from PIL import Image, ImageOps |
4 | from PIL.ImageFileIO import ImageFileIO | 6 | from PIL.ImageFileIO import ImageFileIO |
5 | 7 | ||
@@ -13,22 +15,117 @@ from MoinMoin.caching import CacheEntry | |||
13 | logging = log.getLogger(__name__) | 15 | logging = log.getLogger(__name__) |
14 | 16 | ||
15 | 17 | ||
18 | class 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 | |||
49 | class 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 | |||
110 | def grayscale(img): | ||
111 | img.grayscale() | ||
112 | |||
113 | |||
16 | def crop(img, width, height): | 114 | def 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 | ||
41 | def thumbnail(img, long_side): | 138 | def 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 | ||
56 | def thumbnail_constrain(img, size, dimension): | 152 | def 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") |