comparison sat/plugins/plugin_xep_0264.py @ 3330:7b47f48d31f3

plugin XEP-0264: fix orientation of thumbnails: Rotation of thumbnailis is now modified according to EXIF orientation data. Original image is not modified as user may want to keep it, and orientation transformation can modify encoding parameters. Update `pillow` minimum version to the first one with `exif_transpose` implementation.
author Goffi <goffi@goffi.org>
date Thu, 13 Aug 2020 23:46:18 +0200
parents 15612c0fb421
children 4f1fcee83d36
comparison
equal deleted inserted replaced
3329:15612c0fb421 3330:7b47f48d31f3
32 32
33 from sat.core import exceptions 33 from sat.core import exceptions
34 import hashlib 34 import hashlib
35 35
36 try: 36 try:
37 from PIL import Image 37 from PIL import Image, ImageOps
38 except: 38 except:
39 raise exceptions.MissingModule( 39 raise exceptions.MissingModule(
40 "Missing module pillow, please download/install it from https://python-pillow.github.io" 40 "Missing module pillow, please download/install it from https://python-pillow.github.io"
41 ) 41 )
42 42
136 @param size(tuple(int)): requested size of thumbnail 136 @param size(tuple(int)): requested size of thumbnail
137 @return (unicode): unique id for this image/size 137 @return (unicode): unique id for this image/size
138 """ 138 """
139 return hashlib.sha256(repr((image_uid, size)).encode()).hexdigest() 139 return hashlib.sha256(repr((image_uid, size)).encode()).hexdigest()
140 140
141 def _blockingGenThumb(self, source_path, size=None, max_age=None, image_uid=None): 141 def _blockingGenThumb(
142 self, source_path, size=None, max_age=None, image_uid=None,
143 fix_orientation=True):
142 """Generate a thumbnail for image 144 """Generate a thumbnail for image
143 145
144 This is a blocking method and must be executed in a thread 146 This is a blocking method and must be executed in a thread
145 params are the same as for [generateThumbnail] 147 params are the same as for [generateThumbnail]
146 """ 148 """
150 img = Image.open(source_path) 152 img = Image.open(source_path)
151 except IOError: 153 except IOError:
152 return Failure(exceptions.DataError("Can't open image")) 154 return Failure(exceptions.DataError("Can't open image"))
153 155
154 img.thumbnail(size) 156 img.thumbnail(size)
157 if fix_orientation:
158 img = ImageOps.exif_transpose(img)
159
155 uid = self.getThumbId(image_uid or source_path, size) 160 uid = self.getThumbId(image_uid or source_path, size)
156 161
157 with self.host.common_cache.cacheData( 162 with self.host.common_cache.cacheData(
158 PLUGIN_INFO[C.PI_IMPORT_NAME], uid, MIME_TYPE, max_age 163 PLUGIN_INFO[C.PI_IMPORT_NAME], uid, MIME_TYPE, max_age
159 ) as f: 164 ) as f:
160 img.save(f, SAVE_FORMAT) 165 img.save(f, SAVE_FORMAT)
166 log.debug(f"fixed orientation for {f.name}")
161 167
162 return img.size, uid 168 return img.size, uid
163 169
164 def generateThumbnail(self, source_path, size=None, max_age=None, image_uid=None): 170 def generateThumbnail(
171 self, source_path, size=None, max_age=None, image_uid=None, fix_orientation=True):
165 """Generate a thumbnail of image 172 """Generate a thumbnail of image
166 173
167 @param source_path(unicode): absolute path to source image 174 @param source_path(unicode): absolute path to source image
168 @param size(int, None): max size of the thumbnail 175 @param size(int, None): max size of the thumbnail
169 can be one of self.SIZE_* 176 can be one of self.SIZE_*
170 None to use default value (i.e. self.SIZE_SMALL) 177 None to use default value (i.e. self.SIZE_SMALL)
171 @param max_age(int, None): same as for [memory.cache.Cache.cacheData]) 178 @param max_age(int, None): same as for [memory.cache.Cache.cacheData])
172 @param image_uid(unicode, None): unique ID to identify the image 179 @param image_uid(unicode, None): unique ID to identify the image
173 use hash whenever possible 180 use hash whenever possible
174 if None, source_path will be used 181 if None, source_path will be used
182 @param fix_orientation(bool): if True, fix orientation using EXIF data
175 @return D(tuple[tuple[int,int], unicode]): tuple with: 183 @return D(tuple[tuple[int,int], unicode]): tuple with:
176 - size of the thumbnail 184 - size of the thumbnail
177 - unique Id of the thumbnail 185 - unique Id of the thumbnail
178 """ 186 """
179
180 d = threads.deferToThread( 187 d = threads.deferToThread(
181 self._blockingGenThumb, source_path, size, max_age, image_uid=image_uid 188 self._blockingGenThumb, source_path, size, max_age, image_uid=image_uid,
189 fix_orientation=fix_orientation
182 ) 190 )
183 d.addErrback( 191 d.addErrback(
184 lambda failure_: log.error("thumbnail generation error: {}".format(failure_)) 192 lambda failure_: log.error("thumbnail generation error: {}".format(failure_))
185 ) 193 )
186 return d 194 return d