From 77ba732eec7660fbc3ebf059effa128b9fe9a049 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sat, 21 Sep 2019 01:45:56 +0300 Subject: [PATCH 01/19] Added function to convert tgs to png. XXX: there is the bug in tgs lib, it crashes on some tgs files. Also cairo svg2png need to be called not from tgs.exporters because there is no option to set image size --- mautrix_telegram/util/file_transfer.py | 44 +++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 02a3e7ca..a40c280e 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Optional, Tuple, Union, Dict -from io import BytesIO +from io import BytesIO, StringIO import time import logging import asyncio @@ -48,6 +48,17 @@ try: except ImportError: VideoFileClip = random = string = os = mimetypes = None +try: + import cairosvg + from tgs.parsers.tgs import parse_tgs as tgs_importer + from tgs.exporters import svg as tgs_svg_exporter +# from tgs.exporters import gif as tgs_gif_exporter +except (ImportError, OSError): + cairosvg = None + tgs_importer = None + tgs_svg_exporter = None +# tgs_gif_exporter = None + log: logging.Logger = logging.getLogger("mau.util") TypeLocation = Union[Document, InputDocumentFileLocation, InputPeerPhotoFileLocation, @@ -72,6 +83,32 @@ def convert_image(file: bytes, source_mime: str = "image/webp", target_type: str return source_mime, file, None, None +def convert_tgs(file: bytes) -> Tuple[str, bytes, Optional[int], Optional[int]]: + if cairosvg and tgs_importer and tgs_svg_exporter: + try: + with BytesIO(file) as fi: + animation = tgs_importer(fi) + """ + It's possible to convert to gif, but out animation is too big (~500KB), + Convert to mp4 needs opencv2 to be installed... + TODO: Maybe should create config parameter + """ + with StringIO() as svg, BytesIO() as fo: + frame = int(animation.out_point * 0.3) + w, h = 256, 256 + tgs_svg_exporter.export_svg(animation, svg, frame=frame) + svg.seek(0) + cairosvg.svg2png(file_obj=svg, write_to=fo, output_width=w, output_height=h) + out = fo.getvalue() + return "image/png", out, w, h + # Yep... some animations crash library... + except AttributeError: + log.exception("Error occurred while converting animated sticker") + else: + log.warning("Unable to convert animated sticker, install tgs and cairosvg packages") + return "application/gzip", file, None, None + + def _temp_file_name(ext: str) -> str: return ("/tmp/mxtg-video-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) @@ -197,6 +234,11 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten mime_type = magic.from_buffer(file, mime=True) image_converted = False + if mime_type == "application/gzip" and is_sticker: + mime_type, file, width, height = convert_tgs(file) + image_converted = width is not None + thumbnail = None + if mime_type == "image/webp": new_mime_type, file, width, height = convert_image( file, source_mime="image/webp", target_type="png", From fc241b1cdc4581f11cd7f7db2f421b48aeff1a4d Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sun, 22 Sep 2019 01:23:00 +0300 Subject: [PATCH 02/19] Moved converters to other file, added methods for video and gif, which supports resize. XXX: videos don't want to be played by riot, i don't know why... --- mautrix_telegram/util/file_transfer.py | 55 ++------- mautrix_telegram/util/tgs_converter.py | 148 +++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 45 deletions(-) create mode 100644 mautrix_telegram/util/tgs_converter.py diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index a40c280e..95b932d6 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Optional, Tuple, Union, Dict -from io import BytesIO, StringIO +from io import BytesIO import time import logging import asyncio @@ -30,6 +30,7 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio from mautrix.appservice import IntentAPI + from ..tgclient import MautrixTelegramClient from ..db import TelegramFile as DBTelegramFile from ..util import sane_mimetypes @@ -48,16 +49,7 @@ try: except ImportError: VideoFileClip = random = string = os = mimetypes = None -try: - import cairosvg - from tgs.parsers.tgs import parse_tgs as tgs_importer - from tgs.exporters import svg as tgs_svg_exporter -# from tgs.exporters import gif as tgs_gif_exporter -except (ImportError, OSError): - cairosvg = None - tgs_importer = None - tgs_svg_exporter = None -# tgs_gif_exporter = None +from .tgs_converter import convert_tgs log: logging.Logger = logging.getLogger("mau.util") @@ -83,32 +75,6 @@ def convert_image(file: bytes, source_mime: str = "image/webp", target_type: str return source_mime, file, None, None -def convert_tgs(file: bytes) -> Tuple[str, bytes, Optional[int], Optional[int]]: - if cairosvg and tgs_importer and tgs_svg_exporter: - try: - with BytesIO(file) as fi: - animation = tgs_importer(fi) - """ - It's possible to convert to gif, but out animation is too big (~500KB), - Convert to mp4 needs opencv2 to be installed... - TODO: Maybe should create config parameter - """ - with StringIO() as svg, BytesIO() as fo: - frame = int(animation.out_point * 0.3) - w, h = 256, 256 - tgs_svg_exporter.export_svg(animation, svg, frame=frame) - svg.seek(0) - cairosvg.svg2png(file_obj=svg, write_to=fo, output_width=w, output_height=h) - out = fo.getvalue() - return "image/png", out, w, h - # Yep... some animations crash library... - except AttributeError: - log.exception("Error occurred while converting animated sticker") - else: - log.warning("Unable to convert animated sticker, install tgs and cairosvg packages") - return "application/gzip", file, None, None - - def _temp_file_name(ext: str) -> str: return ("/tmp/mxtg-video-" + "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) @@ -200,9 +166,9 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA if not location_id: return None - db_file = DBTelegramFile.get(location_id) - if db_file: - return db_file + #db_file = DBTelegramFile.get(location_id) + #if db_file: + # return db_file try: lock = transfer_locks[location_id] @@ -218,9 +184,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten loc_id: str, location: TypeLocation, thumbnail: TypeThumbnail, is_sticker: bool ) -> Optional[DBTelegramFile]: - db_file = DBTelegramFile.get(loc_id) - if db_file: - return db_file + #db_file = DBTelegramFile.get(loc_id) + #if db_file: + # return db_file try: file = await client.download_file(location) @@ -235,9 +201,8 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten image_converted = False if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height = convert_tgs(file) + mime_type, file, width, height, thumbnail = convert_tgs(file, "gif", 128, 128) image_converted = width is not None - thumbnail = None if mime_type == "image/webp": new_mime_type, file, width, height = convert_image( diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py new file mode 100644 index 00000000..34d0299b --- /dev/null +++ b/mautrix_telegram/util/tgs_converter.py @@ -0,0 +1,148 @@ +# Generated by Netbeans +# Author: Eramde +# Date: 09.2019 + +from typing import Optional, Tuple, Union, Dict + + +import logging + +LOG: logging.Logger = logging.getLogger("mau.util.tgs") + +try: + + import cairosvg + from io import BytesIO, StringIO + from tgs.objects import Animation + from tgs.parsers.tgs import parse_tgs as tgs_importer + from tgs.exporters import svg as tgs_svg_exporter + + def _tgs_to_png(animation: Animation, width: int = None, + height: int = None, frame: int = None) -> Tuple[bytes, Optional[bytes]]: + if not frame: + frame = int(animation.out_point * 0.3) + if not (width and height): + width = animation.width + height = animation.height + svg = StringIO() + tgs_svg_exporter.export_svg(animation, svg, frame=frame) + svg.seek(0) + fo = BytesIO() + cairosvg.svg2png(file_obj=svg, write_to=fo, output_width=width, output_height=height) + return fo.getvalue(), None + + TGS_CONVERTERS = {"image": _tgs_to_png} + + try: + from PIL import Image + + def _tgs_to_gif(animation: Animation, width: int = None, height: int = None) \ + -> Tuple[bytes, Optional[bytes]]: + """ + FIXME: copy-pasted from tgs.exporters.gif, because it's method don't resize images + """ + start = int(animation.in_point) + end = int(animation.out_point) + skip_frames = 5 + frames = [] + first_frame = None + for i in range(start, end + 1, skip_frames): + frame, _ = _tgs_to_png(animation, width, height, i) + if not first_frame: + first_frame = frame + image = Image.open(BytesIO(frame)) + if image.mode not in ["RGBA", "RGBa"]: + image = image.convert("RGBA") + alpha = image.getchannel("A") + image = image.convert('P', palette=Image.ADAPTIVE, colors=255) + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + image.paste(255, mask) + frames.append(image) + + duration = 1000 / animation.frame_rate + fo = BytesIO() + frames[0].save( + fo, + format='GIF', + append_images=frames[1:], + save_all=True, + duration=duration, + loop=0, + transparency=255, + disposal=2, + ) + return fo.getvalue(), first_frame + + TGS_CONVERTERS.update({"gif": _tgs_to_gif}) + except ImportError: + LOG.warn("Unable to create tgs to gif converter, install PIL") + + try: + import cv2 + import numpy + import tempfile + import os + from PIL import Image + + def _tgs_to_video(animation: Animation, width: int = None, height: int = None) \ + -> Tuple[bytes, Optional[bytes]]: + """ + FIXME: copy-pasted from tgs.exporters.video, because it's method don't resize images + """ + start = int(animation.in_point) + end = int(animation.out_point) + with tempfile.NamedTemporaryFile(mode="r+b", suffix=".mp4") as tmp: + video_tmp_file = tmp.name + video = None + first_frame = None + try: + video = cv2.VideoWriter(filename=video_tmp_file, apiPreference=cv2.CAP_ANY, + fourcc=cv2.VideoWriter_fourcc(*'vp09'), + fps=int(animation.frame_rate), + frameSize=(width or animation.width, + height or animation.height)) + + for i in range(start, end + 1): + frame, _ = _tgs_to_png(animation, width, height, i) + if not first_frame: + first_frame = frame + video.write(cv2.cvtColor(numpy.array(Image.open(BytesIO(frame))), + cv2.COLOR_RGB2BGR)) + + finally: + if video: + video.release() + with open(video_tmp_file, "rb") as video_file: + out = video_file.read() + os.remove(video_tmp_file) + return out, first_frame + """ + It seems, that riot don't wont to play converted videos... + """ + TGS_CONVERTERS.update({"video": _tgs_to_video}) + except ImportError: + LOG.warn("Unable to create tgs to video converter, " + "install PIL, numpy and opencv-python-headless") + +except (ImportError, OSError): + LOG.exception("Unable to init tgs converters, possibly missing tgs and/or cairo libraries") + TGS_CONVERTERS = {} + +TYPE_TO_MIME = {"image": "image/png", "gif": "image/gif", "video": "video/mp4"} + + +def convert_tgs(file: bytes, convert_to: str, width: int = None, height: int = None) \ + -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: + if convert_to in TGS_CONVERTERS: + converter = TGS_CONVERTERS[convert_to] + mime = TYPE_TO_MIME[convert_to] + try: + animation = tgs_importer(BytesIO(file)) + out, preview = converter(animation, width, height) + return mime, out, width or animation.width, height or animation.height, preview + # Yep... some animations crash library... + except AttributeError: + LOG.exception("Error occurred while converting animated sticker") + else: + LOG.warning(f"Unable to convert animated sticker, no converter for type {convert_to}") + return "application/gzip", file, None, None, None From dff5903c534cac14873ca1809840aa3af319c4c5 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sun, 22 Sep 2019 01:28:49 +0300 Subject: [PATCH 03/19] Forgot uncomment db fetch --- mautrix_telegram/util/file_transfer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 95b932d6..0230ffe4 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -166,9 +166,9 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA if not location_id: return None - #db_file = DBTelegramFile.get(location_id) - #if db_file: - # return db_file + db_file = DBTelegramFile.get(location_id) + if db_file: + return db_file try: lock = transfer_locks[location_id] @@ -184,9 +184,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten loc_id: str, location: TypeLocation, thumbnail: TypeThumbnail, is_sticker: bool ) -> Optional[DBTelegramFile]: - #db_file = DBTelegramFile.get(loc_id) - #if db_file: - # return db_file + db_file = DBTelegramFile.get(loc_id) + if db_file: + return db_file try: file = await client.download_file(location) From dc71f74c0ccd8e8a7d741022d2e18fbe6346dcc5 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Wed, 25 Sep 2019 12:53:42 +0300 Subject: [PATCH 04/19] Changed default convert type and image size --- mautrix_telegram/util/file_transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 0230ffe4..94da7fc3 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -201,7 +201,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten image_converted = False if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height, thumbnail = convert_tgs(file, "gif", 128, 128) + mime_type, file, width, height, thumbnail = convert_tgs(file, "image", 256, 256) image_converted = width is not None if mime_type == "image/webp": From f5c008c1a76b18abfe1813ca29dbb0c42b73099c Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Wed, 25 Sep 2019 13:09:21 +0300 Subject: [PATCH 05/19] Added parameter in config for selecting convert type --- example-config.yaml | 6 ++++++ mautrix_telegram/config.py | 1 + mautrix_telegram/portal/telegram.py | 4 +++- mautrix_telegram/util/file_transfer.py | 10 ++++++---- mautrix_telegram/util/tgs_converter.py | 6 +----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index e5e18d53..507d0d76 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -163,6 +163,12 @@ bridge: image_as_file_size: 10 # Maximum size of Telegram documents in megabytes to bridge. max_document_size: 100 + # Format, animated sticker convert to (unstable). + # Supported values: + # image - converts to png (preferred), + # gif - converts to gif animation (sometimes loses alpha), + # video - VP9 video in mp4 container + animated_sticker_target_type: image # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index fb80a26f..3d4cf621 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -101,6 +101,7 @@ class Config(BaseBridgeConfig): copy("bridge.inline_images") copy("bridge.image_as_file_size") copy("bridge.max_document_size") + copy("bridge.animated_sticker_target_type") copy("bridge.bot_messages_as_notices") if isinstance(self["bridge.bridge_notices"], bool): diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index 6ccf62cd..fde07f24 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -182,7 +182,9 @@ class PortalTelegram(BasePortal, ABC): thumb_loc = None thumb_size = None file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc, - is_sticker=attrs.is_sticker) + is_sticker=attrs.is_sticker, + tgs_convert_type= + self.get_config("animated_sticker_target_type")) if not file: return None diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 94da7fc3..b7dfe002 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -161,7 +161,8 @@ TypeThumbnail = Optional[Union[TypeLocation, TypePhotoSize]] async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, location: TypeLocation, thumbnail: TypeThumbnail = None, - is_sticker: bool = False) -> Optional[DBTelegramFile]: + is_sticker: bool = False, tgs_convert_type: str = "image") \ + -> Optional[DBTelegramFile]: location_id = _location_to_id(location) if not location_id: return None @@ -177,12 +178,13 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA transfer_locks[location_id] = lock async with lock: return await _unlocked_transfer_file_to_matrix(client, intent, location_id, location, - thumbnail, is_sticker) + thumbnail, is_sticker, tgs_convert_type) async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, loc_id: str, location: TypeLocation, - thumbnail: TypeThumbnail, is_sticker: bool + thumbnail: TypeThumbnail, is_sticker: bool, + tgs_convert_type: str ) -> Optional[DBTelegramFile]: db_file = DBTelegramFile.get(loc_id) if db_file: @@ -201,7 +203,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten image_converted = False if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height, thumbnail = convert_tgs(file, "image", 256, 256) + mime_type, file, width, height, thumbnail = convert_tgs(file, tgs_convert_type, 256, 256) image_converted = width is not None if mime_type == "image/webp": diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 34d0299b..ad08ac3f 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -20,7 +20,7 @@ try: def _tgs_to_png(animation: Animation, width: int = None, height: int = None, frame: int = None) -> Tuple[bytes, Optional[bytes]]: if not frame: - frame = int(animation.out_point * 0.3) + frame = int(animation.out_point * 0.9) if not (width and height): width = animation.width height = animation.height @@ -82,13 +82,9 @@ try: import numpy import tempfile import os - from PIL import Image def _tgs_to_video(animation: Animation, width: int = None, height: int = None) \ -> Tuple[bytes, Optional[bytes]]: - """ - FIXME: copy-pasted from tgs.exporters.video, because it's method don't resize images - """ start = int(animation.in_point) end = int(animation.out_point) with tempfile.NamedTemporaryFile(mode="r+b", suffix=".mp4") as tmp: From ed4e34b808893fadbc50fe3114539a13c1181b16 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Wed, 25 Sep 2019 13:31:56 +0300 Subject: [PATCH 06/19] Changed to 30% frame in image convert --- mautrix_telegram/util/tgs_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index ad08ac3f..edacadbf 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -20,7 +20,7 @@ try: def _tgs_to_png(animation: Animation, width: int = None, height: int = None, frame: int = None) -> Tuple[bytes, Optional[bytes]]: if not frame: - frame = int(animation.out_point * 0.9) + frame = int(animation.out_point * 0.3) if not (width and height): width = animation.width height = animation.height From 7d224ec5aca118305b7f586ac5aba16d64e7ea23 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Wed, 25 Sep 2019 15:34:34 +0300 Subject: [PATCH 07/19] Switched to puppeter-lottie npm library --- example-config.yaml | 6 +- mautrix_telegram/util/file_transfer.py | 5 +- mautrix_telegram/util/tgs_converter.py | 158 ++++--------------------- 3 files changed, 32 insertions(+), 137 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 507d0d76..26a4e9e9 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -165,9 +165,9 @@ bridge: max_document_size: 100 # Format, animated sticker convert to (unstable). # Supported values: - # image - converts to png (preferred), - # gif - converts to gif animation (sometimes loses alpha), - # video - VP9 video in mp4 container + # image - converts to png (fastest and preferred), + # gif - converts to gif animation (requires gifski binary and takes a lot of time), + # video - video in mp4 container (requires gifski and ffmpeg binary and takes a lot of time, but less than gif) animated_sticker_target_type: image # Whether to bridge Telegram bot messages as m.notices or m.texts. diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index b7dfe002..29e6cf15 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -49,7 +49,7 @@ try: except ImportError: VideoFileClip = random = string = os = mimetypes = None -from .tgs_converter import convert_tgs +from .tgs_converter import convert_tgs_to log: logging.Logger = logging.getLogger("mau.util") @@ -203,8 +203,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten image_converted = False if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height, thumbnail = convert_tgs(file, tgs_convert_type, 256, 256) + mime_type, file, width, height = convert_tgs_to(file, tgs_convert_type, 256, 256) image_converted = width is not None + thumbnail = None if mime_type == "image/webp": new_mime_type, file, width, height = convert_image( diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index edacadbf..c62bea5b 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -1,144 +1,38 @@ # Generated by Netbeans # Author: Eramde # Date: 09.2019 - -from typing import Optional, Tuple, Union, Dict - - +import tempfile import logging +import gzip +import subprocess +import os +from io import BytesIO +from typing import Optional, Tuple + LOG: logging.Logger = logging.getLogger("mau.util.tgs") -try: - - import cairosvg - from io import BytesIO, StringIO - from tgs.objects import Animation - from tgs.parsers.tgs import parse_tgs as tgs_importer - from tgs.exporters import svg as tgs_svg_exporter - - def _tgs_to_png(animation: Animation, width: int = None, - height: int = None, frame: int = None) -> Tuple[bytes, Optional[bytes]]: - if not frame: - frame = int(animation.out_point * 0.3) - if not (width and height): - width = animation.width - height = animation.height - svg = StringIO() - tgs_svg_exporter.export_svg(animation, svg, frame=frame) - svg.seek(0) - fo = BytesIO() - cairosvg.svg2png(file_obj=svg, write_to=fo, output_width=width, output_height=height) - return fo.getvalue(), None - - TGS_CONVERTERS = {"image": _tgs_to_png} - - try: - from PIL import Image - - def _tgs_to_gif(animation: Animation, width: int = None, height: int = None) \ - -> Tuple[bytes, Optional[bytes]]: - """ - FIXME: copy-pasted from tgs.exporters.gif, because it's method don't resize images - """ - start = int(animation.in_point) - end = int(animation.out_point) - skip_frames = 5 - frames = [] - first_frame = None - for i in range(start, end + 1, skip_frames): - frame, _ = _tgs_to_png(animation, width, height, i) - if not first_frame: - first_frame = frame - image = Image.open(BytesIO(frame)) - if image.mode not in ["RGBA", "RGBa"]: - image = image.convert("RGBA") - alpha = image.getchannel("A") - image = image.convert('P', palette=Image.ADAPTIVE, colors=255) - mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) - image.paste(255, mask) - frames.append(image) - - duration = 1000 / animation.frame_rate - fo = BytesIO() - frames[0].save( - fo, - format='GIF', - append_images=frames[1:], - save_all=True, - duration=duration, - loop=0, - transparency=255, - disposal=2, - ) - return fo.getvalue(), first_frame - - TGS_CONVERTERS.update({"gif": _tgs_to_gif}) - except ImportError: - LOG.warn("Unable to create tgs to gif converter, install PIL") - - try: - import cv2 - import numpy - import tempfile - import os - - def _tgs_to_video(animation: Animation, width: int = None, height: int = None) \ - -> Tuple[bytes, Optional[bytes]]: - start = int(animation.in_point) - end = int(animation.out_point) - with tempfile.NamedTemporaryFile(mode="r+b", suffix=".mp4") as tmp: - video_tmp_file = tmp.name - video = None - first_frame = None - try: - video = cv2.VideoWriter(filename=video_tmp_file, apiPreference=cv2.CAP_ANY, - fourcc=cv2.VideoWriter_fourcc(*'vp09'), - fps=int(animation.frame_rate), - frameSize=(width or animation.width, - height or animation.height)) - - for i in range(start, end + 1): - frame, _ = _tgs_to_png(animation, width, height, i) - if not first_frame: - first_frame = frame - video.write(cv2.cvtColor(numpy.array(Image.open(BytesIO(frame))), - cv2.COLOR_RGB2BGR)) - - finally: - if video: - video.release() - with open(video_tmp_file, "rb") as video_file: - out = video_file.read() - os.remove(video_tmp_file) - return out, first_frame - """ - It seems, that riot don't wont to play converted videos... - """ - TGS_CONVERTERS.update({"video": _tgs_to_video}) - except ImportError: - LOG.warn("Unable to create tgs to video converter, " - "install PIL, numpy and opencv-python-headless") - -except (ImportError, OSError): - LOG.exception("Unable to init tgs converters, possibly missing tgs and/or cairo libraries") - TGS_CONVERTERS = {} - TYPE_TO_MIME = {"image": "image/png", "gif": "image/gif", "video": "video/mp4"} +TYPE_TO_FORMAT = {"image": ".png", "gif": ".gif", "video": ".mp4"} -def convert_tgs(file: bytes, convert_to: str, width: int = None, height: int = None) \ - -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: - if convert_to in TGS_CONVERTERS: - converter = TGS_CONVERTERS[convert_to] +def convert_tgs_to(file: bytes, convert_to: str, width: int = 200, height: int = 200) \ + -> Tuple[str, bytes, Optional[int], Optional[int]]: + if convert_to in TYPE_TO_FORMAT: + file_ext = TYPE_TO_FORMAT[convert_to] mime = TYPE_TO_MIME[convert_to] - try: - animation = tgs_importer(BytesIO(file)) - out, preview = converter(animation, width, height) - return mime, out, width or animation.width, height or animation.height, preview - # Yep... some animations crash library... - except AttributeError: - LOG.exception("Error occurred while converting animated sticker") + lottie = gzip.open(BytesIO(file)) + with tempfile.NamedTemporaryFile(mode="w+b", suffix=".json") as json_out: + with tempfile.NamedTemporaryFile(mode="r+b", suffix=file_ext) as tmp: + tmp_output_file = tmp.name + json_out.write(lottie.read()) + json_out.flush() + subprocess.run(["puppeteer-lottie", "-q", "-i", json_out.name, "-o", tmp_output_file, + "-w", str(width), "-h", str(height)], capture_output=True) + with open(tmp_output_file, mode="r+b") as out_file: + out = out_file.read() + os.remove(tmp_output_file) + return mime, out, width, height else: - LOG.warning(f"Unable to convert animated sticker, no converter for type {convert_to}") - return "application/gzip", file, None, None, None + LOG.warning(f"Unable to convert animated sticker, type {convert_to} not supported") + return "application/gzip", file, None, None From 9d3c15f284685bce2db7330a6fb18e3f7c7932e4 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Wed, 25 Sep 2019 16:10:52 +0300 Subject: [PATCH 08/19] Added info in example-config how to install library for lottie --- example-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example-config.yaml b/example-config.yaml index 26a4e9e9..519791b3 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -168,6 +168,12 @@ bridge: # image - converts to png (fastest and preferred), # gif - converts to gif animation (requires gifski binary and takes a lot of time), # video - video in mp4 container (requires gifski and ffmpeg binary and takes a lot of time, but less than gif) + # To install library: + # source venv/bin/activate + # pip install nodeenv + # nodeenv -p + # source venv/bin/activate + # npm install -g puppeteer-lottie-cli animated_sticker_target_type: image # Whether to bridge Telegram bot messages as m.notices or m.texts. From 2a3f70eb4a434ebb377a94492125a11edb5b271c Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sun, 29 Sep 2019 19:44:13 +0300 Subject: [PATCH 09/19] Migrated to rlottie utility --- example-config.yaml | 17 ++- mautrix_telegram/util/file_transfer.py | 15 ++- mautrix_telegram/util/tgs_converter.py | 143 +++++++++++++++++++++---- 3 files changed, 134 insertions(+), 41 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 519791b3..115d2298 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -163,18 +163,13 @@ bridge: image_as_file_size: 10 # Maximum size of Telegram documents in megabytes to bridge. max_document_size: 100 - # Format, animated sticker convert to (unstable). + # Format, animated sticker convert to. # Supported values: - # image - converts to png (fastest and preferred), - # gif - converts to gif animation (requires gifski binary and takes a lot of time), - # video - video in mp4 container (requires gifski and ffmpeg binary and takes a lot of time, but less than gif) - # To install library: - # source venv/bin/activate - # pip install nodeenv - # nodeenv -p - # source venv/bin/activate - # npm install -g puppeteer-lottie-cli - animated_sticker_target_type: image + # png - converts to png (fastest and preferred), + # gif - converts to gif animation, requires PIL, slow, sometimes loses transparency, + # gifc - uses same utility as png, faster but without transparency at all + # mp4 - video in mp4 container (ffmpeg binary and takes a lot of time, but less than gif) + animated_sticker_target_type: gif # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 29e6cf15..8a8077cb 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -167,9 +167,9 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA if not location_id: return None - db_file = DBTelegramFile.get(location_id) - if db_file: - return db_file + #db_file = DBTelegramFile.get(location_id) + #if db_file: + # return db_file try: lock = transfer_locks[location_id] @@ -186,9 +186,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten thumbnail: TypeThumbnail, is_sticker: bool, tgs_convert_type: str ) -> Optional[DBTelegramFile]: - db_file = DBTelegramFile.get(loc_id) - if db_file: - return db_file + #db_file = DBTelegramFile.get(loc_id) + #if db_file: + # return db_file try: file = await client.download_file(location) @@ -203,9 +203,8 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten image_converted = False if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height = convert_tgs_to(file, tgs_convert_type, 256, 256) + mime_type, file, width, height, thumbnail = convert_tgs_to(file, tgs_convert_type, 256, 256) image_converted = width is not None - thumbnail = None if mime_type == "image/webp": new_mime_type, file, width, height = convert_image( diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index c62bea5b..1fdf3546 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -1,38 +1,137 @@ # Generated by Netbeans # Author: Eramde # Date: 09.2019 -import tempfile import logging -import gzip -import subprocess -import os from io import BytesIO from typing import Optional, Tuple LOG: logging.Logger = logging.getLogger("mau.util.tgs") -TYPE_TO_MIME = {"image": "image/png", "gif": "image/gif", "video": "video/mp4"} -TYPE_TO_FORMAT = {"image": ".png", "gif": ".gif", "video": ".mp4"} +try: + import gzip + import subprocess + + proc = subprocess.Popen(["lottieconverter"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _, err = proc.communicate() + if err is not None and not err.decode("utf-8").startswith("Usage"): + raise ImportError(err) + + + def _tgs_to_png(file: bytes, width: int, + height: int, frame: int = None) -> Tuple[bytes, Optional[bytes]]: + if not frame: + frame = 1 + p = subprocess.run(["lottieconverter", "-", "-", "png", + str.format(f"{width}x{height}"), str(frame)], stdout=subprocess.PIPE, + input=file, universal_newlines=False) + return p.stdout, None + + + TGS_CONVERTERS = {"png": _tgs_to_png} + + def _tgs_to_gif(file: bytes, width: int, height: int) -> Tuple[bytes, Optional[bytes]]: + p = subprocess.run(["lottieconverter", "-", "-", "gif", + str.format(f"{width}x{height}"), "0", "0x202020"], + stdout=subprocess.PIPE, + input=file, universal_newlines=False) + return p.stdout, None + + TGS_CONVERTERS.update({"gifc": _tgs_to_gif}) + + try: + from PIL import Image + + def _tgs_to_gif(file: bytes, width: int, height: int) \ + -> Tuple[bytes, Optional[bytes]]: + frames = [] + first_frame = None + for i in range(1, 100): + frame, _ = _tgs_to_png(file, width, height, i) + if not first_frame: + first_frame = frame + image = Image.open(BytesIO(frame)) + if image.mode not in ["RGBA", "RGBa"]: + image = image.convert("RGBA") + alpha = image.getchannel("A") + image = image.convert('P', palette=Image.ADAPTIVE, colors=255) + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + image.paste(255, mask) + frames.append(image) + + duration = 100 + fo = BytesIO() + frames[0].save( + fo, + format='GIF', + append_images=frames[1:], + save_all=True, + duration=duration, + loop=0, + transparency=255, + disposal=2, + ) + return fo.getvalue(), first_frame + + TGS_CONVERTERS.update({"gif": _tgs_to_gif}) + except ImportError: + LOG.warn("Unable to create tgs to gif converter, install PIL") + + try: + import cv2 + import numpy + import tempfile + import os + + def _tgs_to_video(file: bytes, width: int, height: int) \ + -> Tuple[bytes, Optional[bytes]]: + with tempfile.NamedTemporaryFile(mode="r+b", suffix=".mp4") as tmp: + video_tmp_file = tmp.name + video = None + first_frame = None + try: + video = cv2.VideoWriter(filename=video_tmp_file, apiPreference=cv2.CAP_ANY, + fourcc=cv2.VideoWriter_fourcc(*'vp09'), + fps=10, + frameSize=(width, height)) + + for i in range(1, 100): + frame, _ = _tgs_to_png(file, width, height, i) + if not first_frame: + first_frame = frame + video.write(cv2.cvtColor(numpy.array(Image.open(BytesIO(frame))), + cv2.COLOR_RGB2BGR)) + + finally: + if video: + video.release() + with open(video_tmp_file, "rb") as video_file: + out = video_file.read() + os.remove(video_tmp_file) + return out, first_frame + """ + It seems, that riot don't wont to play converted videos... + """ + TGS_CONVERTERS.update({"mp4": _tgs_to_video}) + except ImportError: + LOG.warn("Unable to create tgs to video converter, " + "install PIL, numpy and opencv-python-headless") + +except (ImportError, OSError): + LOG.exception("Unable to init tgs converters, possibly missing lottieconverter") + TGS_CONVERTERS = {} + + +TYPE_TO_MIME = {"png": "image/png", "gif": "image/gif", "gifc": "image/gif", "mp4": "video/mp4"} def convert_tgs_to(file: bytes, convert_to: str, width: int = 200, height: int = 200) \ - -> Tuple[str, bytes, Optional[int], Optional[int]]: - if convert_to in TYPE_TO_FORMAT: - file_ext = TYPE_TO_FORMAT[convert_to] + -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: + if convert_to in TGS_CONVERTERS: mime = TYPE_TO_MIME[convert_to] - lottie = gzip.open(BytesIO(file)) - with tempfile.NamedTemporaryFile(mode="w+b", suffix=".json") as json_out: - with tempfile.NamedTemporaryFile(mode="r+b", suffix=file_ext) as tmp: - tmp_output_file = tmp.name - json_out.write(lottie.read()) - json_out.flush() - subprocess.run(["puppeteer-lottie", "-q", "-i", json_out.name, "-o", tmp_output_file, - "-w", str(width), "-h", str(height)], capture_output=True) - with open(tmp_output_file, mode="r+b") as out_file: - out = out_file.read() - os.remove(tmp_output_file) - return mime, out, width, height + converter = TGS_CONVERTERS[convert_to] + out, preview = converter(file, width, height) + return mime, out, width, height, preview else: LOG.warning(f"Unable to convert animated sticker, type {convert_to} not supported") - return "application/gzip", file, None, None + return "application/gzip", file, None, None, None From 4834e2297a33399e28e1349cbd7a658ffbe92fc4 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sun, 29 Sep 2019 19:48:59 +0300 Subject: [PATCH 10/19] Forgot about db fetch... --- mautrix_telegram/util/file_transfer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index 8a8077cb..ead8d9d6 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -161,15 +161,15 @@ TypeThumbnail = Optional[Union[TypeLocation, TypePhotoSize]] async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, location: TypeLocation, thumbnail: TypeThumbnail = None, - is_sticker: bool = False, tgs_convert_type: str = "image") \ + is_sticker: bool = False, tgs_convert_type: str = "png") \ -> Optional[DBTelegramFile]: location_id = _location_to_id(location) if not location_id: return None - #db_file = DBTelegramFile.get(location_id) - #if db_file: - # return db_file + db_file = DBTelegramFile.get(location_id) + if db_file: + return db_file try: lock = transfer_locks[location_id] @@ -186,9 +186,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten thumbnail: TypeThumbnail, is_sticker: bool, tgs_convert_type: str ) -> Optional[DBTelegramFile]: - #db_file = DBTelegramFile.get(loc_id) - #if db_file: - # return db_file + db_file = DBTelegramFile.get(loc_id) + if db_file: + return db_file try: file = await client.download_file(location) From 1044298d76f513d5d449e0c68dee7a5b592ed8fb Mon Sep 17 00:00:00 2001 From: Lawrence <41361221+Eramde@users.noreply.github.com> Date: Sun, 29 Sep 2019 22:37:42 +0300 Subject: [PATCH 11/19] Update mautrix_telegram/portal/telegram.py Co-Authored-By: Tulir Asokan --- mautrix_telegram/portal/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index fde07f24..6cb860e0 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -184,7 +184,7 @@ class PortalTelegram(BasePortal, ABC): file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc, is_sticker=attrs.is_sticker, tgs_convert_type= - self.get_config("animated_sticker_target_type")) + config["bridge.animated_sticker_target_type"]) if not file: return None From c84536fef75397aea377c6c8278b566bc3739e9c Mon Sep 17 00:00:00 2001 From: Lawrence <41361221+Eramde@users.noreply.github.com> Date: Sun, 29 Sep 2019 22:43:32 +0300 Subject: [PATCH 12/19] Set licence header Deleted autogenerated header and set licence --- mautrix_telegram/util/tgs_converter.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 1fdf3546..8400ff21 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -1,6 +1,19 @@ -# Generated by Netbeans -# Author: Eramde -# Date: 09.2019 +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Telegram lottie sticker converter +# Copyright (C) 2019 Randall Eramde Lawrence +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . import logging from io import BytesIO from typing import Optional, Tuple From f430ed7169e632ac6dce8d5d34ac1952031acc3a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Oct 2019 01:28:47 +0300 Subject: [PATCH 13/19] Remove slow python converters and use asyncio subprocess --- example-config.yaml | 20 +-- mautrix_telegram/config.py | 3 +- mautrix_telegram/portal/telegram.py | 3 +- mautrix_telegram/util/file_transfer.py | 15 +-- mautrix_telegram/util/lottie2ffmpeg.sh | 26 ++++ mautrix_telegram/util/tgs_converter.py | 165 +++++++------------------ 6 files changed, 95 insertions(+), 137 deletions(-) create mode 100755 mautrix_telegram/util/lottie2ffmpeg.sh diff --git a/example-config.yaml b/example-config.yaml index fbca058b..2cba1761 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -163,16 +163,22 @@ bridge: image_as_file_size: 10 # Maximum size of Telegram documents in megabytes to bridge. max_document_size: 100 - # Format, animated sticker convert to. - # Supported values: - # png - converts to png (fastest and preferred), - # gif - converts to gif animation, requires PIL, slow, sometimes loses transparency, - # gifc - uses same utility as png, faster but without transparency at all - # mp4 - video in mp4 container (ffmpeg binary and takes a lot of time, but less than gif) - animated_sticker_target_type: gif # Whether or not created rooms should have federation enabled. # If false, created portal rooms will never be federated. federate_rooms: true + # Settings for converting animated stickers. + animated_sticker: + # Format to which animated stickers should be converted. + # disable - No conversion, send as-is (gzipped lottie) + # png - converts to non-animated png (fastest), + # gif - converts to animated gif, but loses transparency + target: gif + # Arguments for converter. All converters take width and height. + # GIF converter takes background as a hex color. + args: + width: 256 + height: 256 + background: "ffffff" # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 7361bfbf..9d3cd37c 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -101,8 +101,9 @@ class Config(BaseBridgeConfig): copy("bridge.inline_images") copy("bridge.image_as_file_size") copy("bridge.max_document_size") - copy("bridge.animated_sticker_target_type") copy("bridge.federate_rooms") + copy("bridge.animated_sticker.target") + copy("bridge.animated_sticker.args") copy("bridge.bot_messages_as_notices") if isinstance(self["bridge.bridge_notices"], bool): diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index 6cb860e0..3aa97b3a 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -183,8 +183,7 @@ class PortalTelegram(BasePortal, ABC): thumb_size = None file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc, is_sticker=attrs.is_sticker, - tgs_convert_type= - config["bridge.animated_sticker_target_type"]) + tgs_convert=config["bridge.animated_sticker"]) if not file: return None diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index ead8d9d6..bcc638ce 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -161,8 +161,8 @@ TypeThumbnail = Optional[Union[TypeLocation, TypePhotoSize]] async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, location: TypeLocation, thumbnail: TypeThumbnail = None, - is_sticker: bool = False, tgs_convert_type: str = "png") \ - -> Optional[DBTelegramFile]: + is_sticker: bool = False, tgs_convert: Optional[dict] = None + ) -> Optional[DBTelegramFile]: location_id = _location_to_id(location) if not location_id: return None @@ -178,13 +178,13 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA transfer_locks[location_id] = lock async with lock: return await _unlocked_transfer_file_to_matrix(client, intent, location_id, location, - thumbnail, is_sticker, tgs_convert_type) + thumbnail, is_sticker, tgs_convert) async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, loc_id: str, location: TypeLocation, thumbnail: TypeThumbnail, is_sticker: bool, - tgs_convert_type: str + tgs_convert: Optional[dict] ) -> Optional[DBTelegramFile]: db_file = DBTelegramFile.get(loc_id) if db_file: @@ -202,9 +202,10 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten mime_type = magic.from_buffer(file, mime=True) image_converted = False - if mime_type == "application/gzip" and is_sticker: - mime_type, file, width, height, thumbnail = convert_tgs_to(file, tgs_convert_type, 256, 256) - image_converted = width is not None + if mime_type == "application/gzip" and is_sticker and tgs_convert: + mime_type, file, width, height, thumbnail = await convert_tgs_to( + file, tgs_convert["target"], **tgs_convert["args"]) + image_converted = mime_type != "application/gzip" if mime_type == "image/webp": new_mime_type, file, width, height = convert_image( diff --git a/mautrix_telegram/util/lottie2ffmpeg.sh b/mautrix_telegram/util/lottie2ffmpeg.sh new file mode 100755 index 00000000..fd9a36b5 --- /dev/null +++ b/mautrix_telegram/util/lottie2ffmpeg.sh @@ -0,0 +1,26 @@ +#!/bin/bash +TMPDIR=$(mktemp -d) + +if [ ! -e $TMPDIR ]; then + >&2 echo "Failed to create temp directory" + exit 1 +fi + +trap "exit 1" HUP INT PIPE QUIT TERM +trap 'rm -rf "$TMPDIR"' EXIT + +cd $TMPDIR + +lottieconverter=$1 +resolution=$2 + +cat > input + +for i in {0..99}; do + padded="0$i" + $lottieconverter input frame-${padded: -2}.png png $resolution $((i+1)) +done + +ffmpeg -start_number 0 -framerate 30 -i frame-%02d.png -c:v libvpx-vp9 -pix_fmt yuva420p out.webm + +cat out.webm diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 8400ff21..3e3106d1 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -14,137 +14,62 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Dict, Callable, Awaitable, Optional, Tuple, Any +import asyncio.subprocess import logging -from io import BytesIO -from typing import Optional, Tuple +import shutil +import os.path + +log: logging.Logger = logging.getLogger("mau.util.tgs") +converters: Dict[str, Callable[[bytes, int, int, Any], Awaitable[Tuple[str, bytes]]]] = {} -LOG: logging.Logger = logging.getLogger("mau.util.tgs") +lottieconverter = os.path.abspath(shutil.which("lottieconverter")) +lottie2ffmpeg = os.path.abspath(shutil.which("lottie2ffmpeg.sh")) -try: - import gzip - import subprocess - - proc = subprocess.Popen(["lottieconverter"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - _, err = proc.communicate() - if err is not None and not err.decode("utf-8").startswith("Usage"): - raise ImportError(err) +if lottieconverter: + async def tgs_to_png(file: bytes, width: int, height: int, **_: Any) -> Tuple[str, bytes]: + frame = 1 + proc = await asyncio.create_subprocess_exec(lottieconverter, "-", "-", "png", + f"{width}x{height}", str(frame), + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate(file) + return "image/png", stdout - def _tgs_to_png(file: bytes, width: int, - height: int, frame: int = None) -> Tuple[bytes, Optional[bytes]]: - if not frame: - frame = 1 - p = subprocess.run(["lottieconverter", "-", "-", "png", - str.format(f"{width}x{height}"), str(frame)], stdout=subprocess.PIPE, - input=file, universal_newlines=False) - return p.stdout, None + async def tgs_to_gif(file: bytes, width: int, height: int, background: str = "202020", + **_: Any) -> Tuple[str, bytes]: + proc = await asyncio.create_subprocess_exec(lottieconverter, "-", "-", "gif", + f"{width}x{height}", "0", f"0x{background}", + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate(file) + return "image/gif", stdout - TGS_CONVERTERS = {"png": _tgs_to_png} + converters["png"] = tgs_to_png + converters["gif"] = tgs_to_gif - def _tgs_to_gif(file: bytes, width: int, height: int) -> Tuple[bytes, Optional[bytes]]: - p = subprocess.run(["lottieconverter", "-", "-", "gif", - str.format(f"{width}x{height}"), "0", "0x202020"], - stdout=subprocess.PIPE, - input=file, universal_newlines=False) - return p.stdout, None - - TGS_CONVERTERS.update({"gifc": _tgs_to_gif}) - - try: - from PIL import Image - - def _tgs_to_gif(file: bytes, width: int, height: int) \ - -> Tuple[bytes, Optional[bytes]]: - frames = [] - first_frame = None - for i in range(1, 100): - frame, _ = _tgs_to_png(file, width, height, i) - if not first_frame: - first_frame = frame - image = Image.open(BytesIO(frame)) - if image.mode not in ["RGBA", "RGBa"]: - image = image.convert("RGBA") - alpha = image.getchannel("A") - image = image.convert('P', palette=Image.ADAPTIVE, colors=255) - mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) - image.paste(255, mask) - frames.append(image) - - duration = 100 - fo = BytesIO() - frames[0].save( - fo, - format='GIF', - append_images=frames[1:], - save_all=True, - duration=duration, - loop=0, - transparency=255, - disposal=2, - ) - return fo.getvalue(), first_frame - - TGS_CONVERTERS.update({"gif": _tgs_to_gif}) - except ImportError: - LOG.warn("Unable to create tgs to gif converter, install PIL") - - try: - import cv2 - import numpy - import tempfile - import os - - def _tgs_to_video(file: bytes, width: int, height: int) \ - -> Tuple[bytes, Optional[bytes]]: - with tempfile.NamedTemporaryFile(mode="r+b", suffix=".mp4") as tmp: - video_tmp_file = tmp.name - video = None - first_frame = None - try: - video = cv2.VideoWriter(filename=video_tmp_file, apiPreference=cv2.CAP_ANY, - fourcc=cv2.VideoWriter_fourcc(*'vp09'), - fps=10, - frameSize=(width, height)) - - for i in range(1, 100): - frame, _ = _tgs_to_png(file, width, height, i) - if not first_frame: - first_frame = frame - video.write(cv2.cvtColor(numpy.array(Image.open(BytesIO(frame))), - cv2.COLOR_RGB2BGR)) - - finally: - if video: - video.release() - with open(video_tmp_file, "rb") as video_file: - out = video_file.read() - os.remove(video_tmp_file) - return out, first_frame - """ - It seems, that riot don't wont to play converted videos... - """ - TGS_CONVERTERS.update({"mp4": _tgs_to_video}) - except ImportError: - LOG.warn("Unable to create tgs to video converter, " - "install PIL, numpy and opencv-python-headless") - -except (ImportError, OSError): - LOG.exception("Unable to init tgs converters, possibly missing lottieconverter") - TGS_CONVERTERS = {} +if lottieconverter and lottie2ffmpeg: + async def tgs_to_webm(file: bytes, width: int, height: int, **_: Any) -> Tuple[str, bytes]: + proc = await asyncio.create_subprocess_exec(lottie2ffmpeg, lottieconverter, + f"{width}x{height}", + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate(file) + return "video/webm", stdout -TYPE_TO_MIME = {"png": "image/png", "gif": "image/gif", "gifc": "image/gif", "mp4": "video/mp4"} + converters["webm"] = tgs_to_webm -def convert_tgs_to(file: bytes, convert_to: str, width: int = 200, height: int = 200) \ - -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: - if convert_to in TGS_CONVERTERS: - mime = TYPE_TO_MIME[convert_to] - converter = TGS_CONVERTERS[convert_to] - out, preview = converter(file, width, height) - return mime, out, width, height, preview - else: - LOG.warning(f"Unable to convert animated sticker, type {convert_to} not supported") +async def convert_tgs_to(file: bytes, convert_to: str, width: int, height: int, **kwargs: Any + ) -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: + if convert_to in converters: + converter = converters[convert_to] + mime, out = await converter(file, width, height, **kwargs) + return mime, out, width, height, None + elif convert_to != "disable": + log.warning(f"Unable to convert animated sticker, type {convert_to} not supported") return "application/gzip", file, None, None, None From a8982cf8c74598d453c3508973ed804716d844df Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Oct 2019 10:14:58 +0300 Subject: [PATCH 14/19] Remove extension from lottie2ffmpeg and fix crash when lottieconverter not present --- .../util/{lottie2ffmpeg.sh => lottie2ffmpeg} | 0 mautrix_telegram/util/tgs_converter.py | 9 +++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) rename mautrix_telegram/util/{lottie2ffmpeg.sh => lottie2ffmpeg} (100%) diff --git a/mautrix_telegram/util/lottie2ffmpeg.sh b/mautrix_telegram/util/lottie2ffmpeg similarity index 100% rename from mautrix_telegram/util/lottie2ffmpeg.sh rename to mautrix_telegram/util/lottie2ffmpeg diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 3e3106d1..84f5696f 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -24,8 +24,13 @@ log: logging.Logger = logging.getLogger("mau.util.tgs") converters: Dict[str, Callable[[bytes, int, int, Any], Awaitable[Tuple[str, bytes]]]] = {} -lottieconverter = os.path.abspath(shutil.which("lottieconverter")) -lottie2ffmpeg = os.path.abspath(shutil.which("lottie2ffmpeg.sh")) +def abswhich(program: Optional[str]) -> Optional[str]: + path = shutil.which(program) + return os.path.abspath(path) if path else None + + +lottieconverter = abswhich("lottieconverter") +lottie2ffmpeg = abswhich("lottie2ffmpeg") if lottieconverter: async def tgs_to_png(file: bytes, width: int, height: int, **_: Any) -> Tuple[str, bytes]: From 9694fb901acfa6e856284a2c6c5473186c678351 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Oct 2019 10:15:12 +0300 Subject: [PATCH 15/19] Add lottieconverter to docker image --- Dockerfile | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Dockerfile b/Dockerfile index 5d170ba6..05992cb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,33 @@ +FROM docker.io/alpine:3.10 AS lottieconverter + +WORKDIR /build + +RUN apk add --no-cache git build-base cmake \ + && git clone https://github.com/Samsung/rlottie.git \ + && cd rlottie \ + && mkdir build \ + && cd build \ + && cmake .. \ + && make -j2 \ + && make install \ + && cd ../.. + +RUN apk add --no-cache libpng libpng-dev zlib zlib-dev \ + && git clone https://github.com/Eramde/LottieConverter.git \ + && cd LottieConverter \ + && make + FROM docker.io/alpine:3.10 ENV UID=1337 \ GID=1337 \ FFMPEG_BINARY=/usr/bin/ffmpeg +#COPY --from=lottieconverter /usr/local/include/rlottie* /usr/local/include/ +COPY --from=lottieconverter /usr/lib/librlottie* /usr/lib/ +COPY --from=lottieconverter /build/LottieConverter/dist/Debug/GNU-Linux/lottieconverter /usr/local/bin/lottieconverter +COPY ./mautrix_telegram/util/lottie2ffmpeg /usr/local/bin/lottie2ffmpeg + COPY . /opt/mautrix-telegram WORKDIR /opt/mautrix-telegram RUN apk add --no-cache --virtual .build-deps \ @@ -41,6 +65,8 @@ RUN apk add --no-cache --virtual .build-deps \ ca-certificates \ su-exec \ netcat-openbsd \ + # lottieconverter + zlib libpng \ && pip3 install .[speedups,hq_thumbnails,metrics] \ && apk del .build-deps From d2edf12fdfb2179054752691665a0d4c746ceabd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 3 Oct 2019 10:57:52 +0300 Subject: [PATCH 16/19] Fix weird mime type bug in alpine/magic --- example-config.yaml | 2 +- mautrix_telegram/util/file_transfer.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 2cba1761..010dbb2c 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -178,7 +178,7 @@ bridge: args: width: 256 height: 256 - background: "ffffff" + background: "020202" # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index bcc638ce..f08ed177 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -202,7 +202,10 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten mime_type = magic.from_buffer(file, mime=True) image_converted = False - if mime_type == "application/gzip" and is_sticker and tgs_convert: + # A weird bug in alpine/magic makes it return application/octet-stream for gzips... + if is_sticker and tgs_convert and (mime_type == "application/gzip" or ( + mime_type == "application/octet-stream" + and magic.from_buffer(file).startswith("gzip"))): mime_type, file, width, height, thumbnail = await convert_tgs_to( file, tgs_convert["target"], **tgs_convert["args"]) image_converted = mime_type != "application/gzip" From 0726289c7a0d8b4b3cc05a0e5c993b5398660a68 Mon Sep 17 00:00:00 2001 From: Randall Lawrence Date: Sat, 5 Oct 2019 20:09:14 +0300 Subject: [PATCH 17/19] Modified converters to support pngs option of lottieconverter See https://github.com/Eramde/LottieConverter/commit/37e73d8dc15152e050288ea0a55541546dde84d1 --- example-config.yaml | 4 +- mautrix_telegram/util/file_transfer.py | 3 +- mautrix_telegram/util/lottie2ffmpeg | 4 +- mautrix_telegram/util/tgs_converter.py | 66 +++++++++++++++++++------- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 010dbb2c..5f7e9753 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -172,13 +172,15 @@ bridge: # disable - No conversion, send as-is (gzipped lottie) # png - converts to non-animated png (fastest), # gif - converts to animated gif, but loses transparency + # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support target: gif # Arguments for converter. All converters take width and height. # GIF converter takes background as a hex color. args: width: 256 height: 256 - background: "020202" + background: "020202" # only for gif + fps: 30 # only for webm # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index f08ed177..e89c3465 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -206,8 +206,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten if is_sticker and tgs_convert and (mime_type == "application/gzip" or ( mime_type == "application/octet-stream" and magic.from_buffer(file).startswith("gzip"))): - mime_type, file, width, height, thumbnail = await convert_tgs_to( + mime_type, file, width, height = await convert_tgs_to( file, tgs_convert["target"], **tgs_convert["args"]) + thumbnail = None image_converted = mime_type != "application/gzip" if mime_type == "image/webp": diff --git a/mautrix_telegram/util/lottie2ffmpeg b/mautrix_telegram/util/lottie2ffmpeg index fd9a36b5..c5af7620 100755 --- a/mautrix_telegram/util/lottie2ffmpeg +++ b/mautrix_telegram/util/lottie2ffmpeg @@ -21,6 +21,4 @@ for i in {0..99}; do $lottieconverter input frame-${padded: -2}.png png $resolution $((i+1)) done -ffmpeg -start_number 0 -framerate 30 -i frame-%02d.png -c:v libvpx-vp9 -pix_fmt yuva420p out.webm - -cat out.webm +ffmpeg -start_number 0 -framerate 30 -i frame-%02d.png -c:v libvpx-vp9 -pix_fmt yuva420p out.webm -f webm - diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 84f5696f..8496bb68 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -19,6 +19,7 @@ import asyncio.subprocess import logging import shutil import os.path +import tempfile log: logging.Logger = logging.getLogger("mau.util.tgs") converters: Dict[str, Callable[[bytes, int, int, Any], Awaitable[Tuple[str, bytes]]]] = {} @@ -30,7 +31,7 @@ def abswhich(program: Optional[str]) -> Optional[str]: lottieconverter = abswhich("lottieconverter") -lottie2ffmpeg = abswhich("lottie2ffmpeg") +ffmpeg = abswhich("ffmpeg") if lottieconverter: async def tgs_to_png(file: bytes, width: int, height: int, **_: Any) -> Tuple[str, bytes]: @@ -39,42 +40,73 @@ if lottieconverter: f"{width}x{height}", str(frame), stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) - stdout, _ = await proc.communicate(file) - return "image/png", stdout + stdout, stderr = await proc.communicate(file) + if proc.returncode == 0: + return "image/png", stdout + else: + log.error("lottieconverter error: " + stderr.decode("utf-8") if stderr is not None + else "unknown") + return "application/gzip", file async def tgs_to_gif(file: bytes, width: int, height: int, background: str = "202020", **_: Any) -> Tuple[str, bytes]: proc = await asyncio.create_subprocess_exec(lottieconverter, "-", "-", "gif", - f"{width}x{height}", "0", f"0x{background}", + f"{width}x{height}", f"0x{background}", stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) - stdout, _ = await proc.communicate(file) - return "image/gif", stdout + stdout, stderr = await proc.communicate(file) + if proc.returncode == 0: + return "image/gif", stdout + else: + log.error("lottieconverter error: " + stderr.decode("utf-8") if stderr is not None + else "unknown") + return "application/gzip", file converters["png"] = tgs_to_png converters["gif"] = tgs_to_gif -if lottieconverter and lottie2ffmpeg: - async def tgs_to_webm(file: bytes, width: int, height: int, **_: Any) -> Tuple[str, bytes]: - proc = await asyncio.create_subprocess_exec(lottie2ffmpeg, lottieconverter, - f"{width}x{height}", - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE) - stdout, _ = await proc.communicate(file) - return "video/webm", stdout +if lottieconverter and ffmpeg: + async def tgs_to_webm(file: bytes, width: int, height: int, fps: int = 30, + **_: Any) -> Tuple[str, bytes]: + with tempfile.TemporaryDirectory(prefix="tgs_") as tmpdir: + file_template = tmpdir + "/out_" + proc = await asyncio.create_subprocess_exec(lottieconverter, "-", file_template, + "pngs", f"{width}x{height}", str(fps), + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE) + _, stderr = await proc.communicate(file) + if proc.returncode == 0: + proc = await asyncio.create_subprocess_exec(ffmpeg, "-hide_banner", "-loglevel", + "error", "-framerate", str(fps), + "-pattern_type", "glob", "-i", + file_template + "*.png", + "-c:v", "libvpx-vp9", "-pix_fmt", + "yuva420p", "-f", "webm", "-", + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + if proc.returncode == 0: + return "video/webm", stdout + else: + log.error("ffmpeg error: " + stderr.decode("utf-8") if stderr is not None + else "unknown") + else: + log.error("lottieconverter error: " + stderr.decode("utf-8") if stderr is not None + else "unknown") + return "application/gzip", file converters["webm"] = tgs_to_webm async def convert_tgs_to(file: bytes, convert_to: str, width: int, height: int, **kwargs: Any - ) -> Tuple[str, bytes, Optional[int], Optional[int], Optional[bytes]]: + ) -> Tuple[str, bytes, Optional[int], Optional[int]]: if convert_to in converters: converter = converters[convert_to] mime, out = await converter(file, width, height, **kwargs) - return mime, out, width, height, None + return mime, out, width, height elif convert_to != "disable": log.warning(f"Unable to convert animated sticker, type {convert_to} not supported") - return "application/gzip", file, None, None, None + return "application/gzip", file, None, None From 0192fb8308f802eae1882e6b473a46254d64559f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 27 Oct 2019 15:37:42 +0200 Subject: [PATCH 18/19] Fix minor things --- example-config.yaml | 4 ++-- mautrix_telegram/portal/telegram.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 61a444e1..440c262e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -183,8 +183,8 @@ bridge: args: width: 256 height: 256 - background: "020202" # only for gif - fps: 30 # only for webm + background: "020202" # only for gif + fps: 30 # only for webm # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index f88d1380..fa6bdfad 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -193,7 +193,10 @@ class PortalTelegram(BasePortal, ABC): await intent.set_typing(self.mxid, is_typing=False) - event_type = EventType.STICKER if attrs.is_sticker else EventType.ROOM_MESSAGE + event_type = EventType.ROOM_MESSAGE + # Riot only supports images as stickers, so send animated webm stickers as m.video + if attrs.is_sticker and file.mime_type.startswith("image/"): + event_type = EventType.STICKER content = MediaMessageEventContent( body=name or "unnamed file", info=info, url=file.mxc, relates_to=relates_to, external_url=self._get_external_url(evt), From cbbc5e850085879a2e3dee060a9052dfbe614634 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 27 Oct 2019 15:40:35 +0200 Subject: [PATCH 19/19] Remove unused lottie2ffmpeg script --- Dockerfile | 2 -- mautrix_telegram/util/lottie2ffmpeg | 24 ------------------------ 2 files changed, 26 deletions(-) delete mode 100755 mautrix_telegram/util/lottie2ffmpeg diff --git a/Dockerfile b/Dockerfile index 05992cb3..81fc6abc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,8 @@ ENV UID=1337 \ GID=1337 \ FFMPEG_BINARY=/usr/bin/ffmpeg -#COPY --from=lottieconverter /usr/local/include/rlottie* /usr/local/include/ COPY --from=lottieconverter /usr/lib/librlottie* /usr/lib/ COPY --from=lottieconverter /build/LottieConverter/dist/Debug/GNU-Linux/lottieconverter /usr/local/bin/lottieconverter -COPY ./mautrix_telegram/util/lottie2ffmpeg /usr/local/bin/lottie2ffmpeg COPY . /opt/mautrix-telegram WORKDIR /opt/mautrix-telegram diff --git a/mautrix_telegram/util/lottie2ffmpeg b/mautrix_telegram/util/lottie2ffmpeg deleted file mode 100755 index c5af7620..00000000 --- a/mautrix_telegram/util/lottie2ffmpeg +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -TMPDIR=$(mktemp -d) - -if [ ! -e $TMPDIR ]; then - >&2 echo "Failed to create temp directory" - exit 1 -fi - -trap "exit 1" HUP INT PIPE QUIT TERM -trap 'rm -rf "$TMPDIR"' EXIT - -cd $TMPDIR - -lottieconverter=$1 -resolution=$2 - -cat > input - -for i in {0..99}; do - padded="0$i" - $lottieconverter input frame-${padded: -2}.png png $resolution $((i+1)) -done - -ffmpeg -start_number 0 -framerate 30 -i frame-%02d.png -c:v libvpx-vp9 -pix_fmt yuva420p out.webm -f webm -