Mercurial > libervia-backend
comparison sat/tools/image.py @ 3259:f300d78f08f3
core: image convertion + SVG support:
/!\ new optional dependency: CairoSVG (with installed `[SVG]` extra)
- new `convert` method in `tools.image` to save an image in an other format, with support
for SVG (when CairoSVG is available)
- new `imageConvert` method is available for frontends
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 19 Apr 2020 16:53:44 +0200 |
parents | 54934ee3f69c |
children | 1512cbd6c4ac |
comparison
equal
deleted
inserted
replaced
3258:7aa01e262e05 | 3259:f300d78f08f3 |
---|---|
21 import tempfile | 21 import tempfile |
22 import mimetypes | 22 import mimetypes |
23 from PIL import Image | 23 from PIL import Image |
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 _ | |
27 from sat.core import exceptions | |
28 from sat.core.log import getLogger | |
29 | |
30 log = getLogger(__name__) | |
31 | |
32 try: | |
33 import cairosvg | |
34 except Exception as e: | |
35 log.warning(_("SVG support not available, please install cairosvg: {e}").format( | |
36 e=e)) | |
37 cairosvg = None | |
26 | 38 |
27 | 39 |
28 def check(host, path, max_size=None): | 40 def check(host, path, max_size=None): |
29 """Analyze image and return a report | 41 """Analyze image and return a report |
30 | 42 |
55 report['too_large'] = False | 67 report['too_large'] = False |
56 | 68 |
57 return report | 69 return report |
58 | 70 |
59 | 71 |
60 def _resizeBlocking(image_path, new_size, dest=None): | 72 def _resizeBlocking(image_path, new_size, dest): |
61 im_path = Path(image_path) | 73 im_path = Path(image_path) |
62 im = Image.open(im_path) | 74 im = Image.open(im_path) |
63 resized = im.resize(new_size, Image.LANCZOS) | 75 resized = im.resize(new_size, Image.LANCZOS) |
64 | 76 |
65 if dest is None: | 77 if dest is None: |
78 | 90 |
79 @param image_path(str, Path): path of the original image | 91 @param image_path(str, Path): path of the original image |
80 @param new_size(tuple[int, int]): size to use for new image | 92 @param new_size(tuple[int, int]): size to use for new image |
81 @param dest(None, Path, file): where the resized image must be stored, can be: | 93 @param dest(None, Path, file): where the resized image must be stored, can be: |
82 - None: use a temporary file | 94 - None: use a temporary file |
95 file will be converted to PNG | |
83 - Path: path to the file to create/overwrite | 96 - Path: path to the file to create/overwrite |
84 - file: a file object which must be opened for writing in binary mode | 97 - file: a file object which must be opened for writing in binary mode |
85 @return (Path): path of the resized file. | 98 @return (Path): path of the resized file. |
86 The image at this path should be deleted after use | 99 The image at this path should be deleted after use |
87 """ | 100 """ |
88 return threads.deferToThread(_resizeBlocking, image_path, new_size, dest) | 101 return threads.deferToThread(_resizeBlocking, image_path, new_size, dest) |
102 | |
103 | |
104 def _convertBlocking(image_path, dest, extra): | |
105 media_type = mimetypes.guess_type(str(image_path), strict=False)[0] | |
106 | |
107 if dest is None: | |
108 dest = tempfile.NamedTemporaryFile(suffix=".png", delete=False) | |
109 filepath = Path(dest.name) | |
110 elif isinstance(dest, Path): | |
111 filepath = dest | |
112 else: | |
113 # we should have a file-like object | |
114 try: | |
115 name = dest.name | |
116 except AttributeError: | |
117 name = None | |
118 if name: | |
119 try: | |
120 filepath = Path(name) | |
121 except TypeError: | |
122 filepath = Path('noname.png') | |
123 else: | |
124 filepath = Path('noname.png') | |
125 | |
126 if media_type == "image/svg+xml": | |
127 if cairosvg is None: | |
128 raise exceptions.MissingModule( | |
129 f"Can't convert SVG image at {image_path} due to missing CairoSVG module") | |
130 width, height = extra.get('width'), extra.get('height') | |
131 cairosvg.svg2png( | |
132 url=str(image_path), write_to=dest, | |
133 output_width=width, output_height=height | |
134 ) | |
135 else: | |
136 suffix = filepath.suffix | |
137 if not suffix: | |
138 raise ValueError( | |
139 "A suffix is missing for destination, it is needed to determine file " | |
140 "format") | |
141 if not suffix in Image.EXTENSION: | |
142 Image.init() | |
143 try: | |
144 im_format = Image.EXTENSION[suffix] | |
145 except KeyError: | |
146 raise ValueError( | |
147 "Dest image format can't be determined, {suffix!r} suffix is unknown" | |
148 ) | |
149 im = Image.open(image_path) | |
150 im.save(dest, format=im_format) | |
151 | |
152 log.debug(f"image {image_path} has been converted to {filepath}") | |
153 return filepath | |
154 | |
155 | |
156 def convert(image_path, dest=None, extra=None): | |
157 """Convert an image to a new file, and return its path | |
158 | |
159 @param image_path(str, Path): path of the image to convert | |
160 @param dest(None, Path, file): where the converted image must be stored, can be: | |
161 - None: use a temporary file | |
162 - Path: path to the file to create/overwrite | |
163 - file: a file object which must be opened for writing in binary mode | |
164 @param extra(None, dict): conversion options | |
165 if image_path link to a SVG file, following options can be used: | |
166 - width: destination width | |
167 - height: destination height | |
168 @return (Path): path of the converted file. | |
169 a generic name is used if dest is an unnamed file like object | |
170 """ | |
171 image_path = Path(image_path) | |
172 if not image_path.is_file(): | |
173 raise ValueError(f"There is no file at {image_path}!") | |
174 if extra is None: | |
175 extra = {} | |
176 return threads.deferToThread(_convertBlocking, image_path, dest, extra) | |
89 | 177 |
90 | 178 |
91 def guess_type(source): | 179 def guess_type(source): |
92 """Guess image media type | 180 """Guess image media type |
93 | 181 |
107 img = Image.open(source) | 195 img = Image.open(source) |
108 try: | 196 try: |
109 return Image.MIME[img.format] | 197 return Image.MIME[img.format] |
110 except KeyError: | 198 except KeyError: |
111 return None | 199 return None |
112 | |
113 |