comparison sat/tools/image.py @ 3332:1512cbd6c4ac

tools (image): fix_orientation on resize + `fix_orientation` method: a new argument (`True` by default) fix orientation (i.e. rotate according to EXIF data) of image when resizing it. A new `fix_orientation` method (unused for now) let do it independently
author Goffi <goffi@goffi.org>
date Thu, 13 Aug 2020 23:46:18 +0200
parents f300d78f08f3
children 9498f32ba6f7
comparison
equal deleted inserted replaced
3331:b1e9f17fbb5a 3332:1512cbd6c4ac
18 18
19 """Methods to manipulate images""" 19 """Methods to manipulate images"""
20 20
21 import tempfile 21 import tempfile
22 import mimetypes 22 import mimetypes
23 from PIL import Image 23 from PIL import Image, ImageOps
24 from pathlib import Path 24 from pathlib import Path
25 from twisted.internet import threads 25 from twisted.internet import threads
26 from sat.core.i18n import _ 26 from sat.core.i18n import _
27 from sat.core import exceptions 27 from sat.core import exceptions
28 from sat.core.log import getLogger 28 from sat.core.log import getLogger
67 report['too_large'] = False 67 report['too_large'] = False
68 68
69 return report 69 return report
70 70
71 71
72 def _resizeBlocking(image_path, new_size, dest): 72 def _resize_blocking(image_path, new_size, dest, fix_orientation):
73 im_path = Path(image_path) 73 im_path = Path(image_path)
74 im = Image.open(im_path) 74 im = Image.open(im_path)
75 resized = im.resize(new_size, Image.LANCZOS) 75 resized = im.resize(new_size, Image.LANCZOS)
76 if fix_orientation:
77 resized = ImageOps.fix_orientation(resized)
76 78
77 if dest is None: 79 if dest is None:
78 dest = tempfile.NamedTemporaryFile(suffix=im_path.suffix, delete=False) 80 dest = tempfile.NamedTemporaryFile(suffix=im_path.suffix, delete=False)
79 elif isinstance(dest, Path): 81 elif isinstance(dest, Path):
80 dest = dest.open('wb') 82 dest = dest.open('wb')
83 resized.save(f, format=im.format) 85 resized.save(f, format=im.format)
84 86
85 return Path(f.name) 87 return Path(f.name)
86 88
87 89
88 def resize(image_path, new_size, dest=None): 90 def resize(image_path, new_size, dest=None, fix_orientation=True):
89 """Resize an image to a new file, and return its path 91 """Resize an image to a new file, and return its path
90 92
91 @param image_path(str, Path): path of the original image 93 @param image_path(str, Path): path of the original image
92 @param new_size(tuple[int, int]): size to use for new image 94 @param new_size(tuple[int, int]): size to use for new image
93 @param dest(None, Path, file): where the resized image must be stored, can be: 95 @param dest(None, Path, file): where the resized image must be stored, can be:
94 - None: use a temporary file 96 - None: use a temporary file
95 file will be converted to PNG 97 file will be converted to PNG
96 - Path: path to the file to create/overwrite 98 - Path: path to the file to create/overwrite
97 - file: a file object which must be opened for writing in binary mode 99 - file: a file object which must be opened for writing in binary mode
100 @param fix_orientation: if True, use EXIF data to set orientation
98 @return (Path): path of the resized file. 101 @return (Path): path of the resized file.
99 The image at this path should be deleted after use 102 The image at this path should be deleted after use
100 """ 103 """
101 return threads.deferToThread(_resizeBlocking, image_path, new_size, dest) 104 return threads.deferToThread(
102 105 _resize_blocking, image_path, new_size, dest, fix_orientation)
103 106
104 def _convertBlocking(image_path, dest, extra): 107
108 def _convert_blocking(image_path, dest, extra):
105 media_type = mimetypes.guess_type(str(image_path), strict=False)[0] 109 media_type = mimetypes.guess_type(str(image_path), strict=False)[0]
106 110
107 if dest is None: 111 if dest is None:
108 dest = tempfile.NamedTemporaryFile(suffix=".png", delete=False) 112 dest = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
109 filepath = Path(dest.name) 113 filepath = Path(dest.name)
171 image_path = Path(image_path) 175 image_path = Path(image_path)
172 if not image_path.is_file(): 176 if not image_path.is_file():
173 raise ValueError(f"There is no file at {image_path}!") 177 raise ValueError(f"There is no file at {image_path}!")
174 if extra is None: 178 if extra is None:
175 extra = {} 179 extra = {}
176 return threads.deferToThread(_convertBlocking, image_path, dest, extra) 180 return threads.deferToThread(_convert_blocking, image_path, dest, extra)
181
182
183 def __fix_orientation_blocking(image_path):
184 im = Image.open(image_path)
185 im_format = im.format
186 exif = im.getexif()
187 orientation = exif.get(0x0112)
188 if orientation is None or orientation<2:
189 # nothing to do
190 return False
191 im = ImageOps.exif_transpose(im)
192 im.save(image_path, im_format)
193 log.debug(f"image {image_path} orientation has been fixed")
194 return True
195
196
197 def fix_orientation(image_path: Path) -> bool:
198 """Apply orientation found in EXIF data if any
199
200 @param image_path: image location, image will be modified in place
201 @return True if image has been modified
202 """
203 return threads.deferToThread(__fix_orientation_blocking, image_path)
177 204
178 205
179 def guess_type(source): 206 def guess_type(source):
180 """Guess image media type 207 """Guess image media type
181 208