diff 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
line wrap: on
line diff
--- a/sat/tools/image.py	Sun Apr 19 16:40:34 2020 +0200
+++ b/sat/tools/image.py	Sun Apr 19 16:53:44 2020 +0200
@@ -23,6 +23,18 @@
 from PIL import Image
 from pathlib import Path
 from twisted.internet import threads
+from sat.core.i18n import _
+from sat.core import exceptions
+from sat.core.log import getLogger
+
+log = getLogger(__name__)
+
+try:
+    import cairosvg
+except Exception as e:
+    log.warning(_("SVG support not available, please install cairosvg: {e}").format(
+        e=e))
+    cairosvg = None
 
 
 def check(host, path, max_size=None):
@@ -57,7 +69,7 @@
     return report
 
 
-def _resizeBlocking(image_path, new_size, dest=None):
+def _resizeBlocking(image_path, new_size, dest):
     im_path = Path(image_path)
     im = Image.open(im_path)
     resized = im.resize(new_size, Image.LANCZOS)
@@ -80,6 +92,7 @@
     @param new_size(tuple[int, int]): size to use for new image
     @param dest(None, Path, file): where the resized image must be stored, can be:
         - None: use a temporary file
+            file will be converted to PNG
         - Path: path to the file to create/overwrite
         - file: a file object which must be opened for writing in binary mode
     @return (Path): path of the resized file.
@@ -88,6 +101,81 @@
     return threads.deferToThread(_resizeBlocking, image_path, new_size, dest)
 
 
+def _convertBlocking(image_path, dest, extra):
+    media_type = mimetypes.guess_type(str(image_path), strict=False)[0]
+
+    if dest is None:
+        dest = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
+        filepath = Path(dest.name)
+    elif isinstance(dest, Path):
+        filepath = dest
+    else:
+        # we should have a file-like object
+        try:
+            name = dest.name
+        except AttributeError:
+            name = None
+        if name:
+            try:
+                filepath = Path(name)
+            except TypeError:
+                filepath = Path('noname.png')
+        else:
+            filepath = Path('noname.png')
+
+    if media_type == "image/svg+xml":
+        if cairosvg is None:
+            raise exceptions.MissingModule(
+                f"Can't convert SVG image at {image_path} due to missing CairoSVG module")
+        width, height = extra.get('width'), extra.get('height')
+        cairosvg.svg2png(
+            url=str(image_path), write_to=dest,
+            output_width=width, output_height=height
+        )
+    else:
+        suffix = filepath.suffix
+        if not suffix:
+            raise ValueError(
+                "A suffix is missing for destination, it is needed to determine file "
+                "format")
+        if not suffix in Image.EXTENSION:
+            Image.init()
+        try:
+            im_format = Image.EXTENSION[suffix]
+        except KeyError:
+            raise ValueError(
+                "Dest image format can't be determined, {suffix!r} suffix is unknown"
+            )
+        im = Image.open(image_path)
+        im.save(dest, format=im_format)
+
+    log.debug(f"image {image_path} has been converted to {filepath}")
+    return filepath
+
+
+def convert(image_path, dest=None, extra=None):
+    """Convert an image to a new file, and return its path
+
+    @param image_path(str, Path): path of the image to convert
+    @param dest(None, Path, file): where the converted image must be stored, can be:
+        - None: use a temporary file
+        - Path: path to the file to create/overwrite
+        - file: a file object which must be opened for writing in binary mode
+    @param extra(None, dict): conversion options
+        if image_path link to a SVG file, following options can be used:
+                - width: destination width
+                - height: destination height
+    @return (Path): path of the converted file.
+        a generic name is used if dest is an unnamed file like object
+    """
+    image_path = Path(image_path)
+    if not image_path.is_file():
+        raise ValueError(f"There is no file at {image_path}!")
+    if extra is None:
+        extra = {}
+    return threads.deferToThread(_convertBlocking, image_path, dest, extra)
+
+
 def guess_type(source):
     """Guess image media type
 
@@ -109,5 +197,3 @@
         return Image.MIME[img.format]
     except KeyError:
         return None
-
-