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