diff --git a/TODO.md b/TODO.md index 9b65148d..1440e1b9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,4 @@ - Media - - SVG uploads - Uploading progress bar (+local echo) - Text bubbles theming - Directly create cache files for our uploads before actually uploading diff --git a/requirements.txt b/requirements.txt index fcd6632a..69b3a1a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ hsluv == 0.0.2 Pillow >= 5.4.1, < 6 pymediainfo >= 4.1, < 5 +cairosvg >= 2.4.2, < 3 aiofiles >= 0.4.0, < 0.5 appdirs >= 1.4.3, < 2 filetype >= 1.0.5, < 2 diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py index 90b2251e..d37b5bc9 100644 --- a/src/python/matrix_client.py +++ b/src/python/matrix_client.py @@ -18,6 +18,7 @@ from typing import ( ) from uuid import uuid4 +import cairosvg from PIL import Image as PILImage from pymediainfo import MediaInfo @@ -252,18 +253,22 @@ class MatrixClient(nio.AsyncClient): content["url"] = url if kind == "image": + is_svg = mime == "image/svg+xml" + event_type = \ nio.RoomEncryptedImage if encrypt else nio.RoomMessageImage content["msgtype"] = "m.image" - content["info"]["w"], content["info"]["h"] = \ + content["info"]["w"], content["info"]["h"] = ( + utils.svg_dimensions(str(path)) if is_svg else PILImage.open(path).size + ) try: thumb_url, thumb_info, thumb_crypt_dict = \ await self.upload_thumbnail( - path, upload_item, encrypt=encrypt, + path, upload_item, is_svg=is_svg, encrypt=encrypt, ) except (UneededThumbnail, UnthumbnailableError): pass @@ -313,7 +318,7 @@ class MatrixClient(nio.AsyncClient): content["filename"] = path.name upload_item.status = UploadStatus.Success - del self.models[Upload, room_id] + del self.models[Upload, room_id][upload_item.uuid] uuid = str(uuid4()) @@ -451,19 +456,31 @@ class MatrixClient(nio.AsyncClient): self, path: Union[Path, str], item: Optional[Upload] = None, + is_svg: bool = False, encrypt: bool = False, ) -> Tuple[str, Dict[str, Any], CryptDict]: png_modes = ("1", "L", "P", "RGBA") try: - thumb = PILImage.open(path) + if is_svg: + svg_width, svg_height = utils.svg_dimensions(str(path)) + + thumb = PILImage.open(io.BytesIO( + cairosvg.svg2png( + url = str(path), + parent_width = svg_width, + parent_height = svg_height, + ), + )) + else: + thumb = PILImage.open(path) small = thumb.width <= 800 and thumb.height <= 600 is_jpg_png = thumb.format in ("JPEG", "PNG") jpgable_png = thumb.format == "PNG" and thumb.mode not in png_modes - if small and is_jpg_png and not jpgable_png: + if small and is_jpg_png and not jpgable_png and not is_svg: raise UneededThumbnail() if item: diff --git a/src/python/utils.py b/src/python/utils.py index 4cbfb3c5..1b34b3da 100644 --- a/src/python/utils.py +++ b/src/python/utils.py @@ -3,7 +3,7 @@ import html import xml.etree.cElementTree as xml_etree # FIXME: bandit warning from enum import Enum from enum import auto as autostr -from typing import IO, Optional +from typing import IO, Tuple, Union import filetype @@ -26,7 +26,7 @@ def dict_update_recursive(dict1, dict2): dict1[k] = dict2[k] -def is_svg(file: IO) -> bool: +def is_svg(file: Union[IO, bytes, str]) -> bool: try: _, element = next(xml_etree.iterparse(file, ("start",))) return element.tag == "{http://www.w3.org/2000/svg}svg" @@ -34,6 +34,22 @@ def is_svg(file: IO) -> bool: return False +def svg_dimensions(file: Union[IO, bytes, str]) -> Tuple[int, int]: + attrs = xml_etree.parse(file).getroot().attrib + + try: + width = round(float(attrs.get("width", attrs["viewBox"].split()[3]))) + except (KeyError, IndexError, ValueError, TypeError): + width = 256 + + try: + height = round(float(attrs.get("height", attrs["viewBox"].split()[4]))) + except (KeyError, IndexError, ValueError, TypeError): + height = 256 + + return (width, height) + + def guess_mime(file: IO) -> str: if is_svg(file): return "image/svg+xml"