Mercurial > libervia-backend
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 |