From 805ee0a2fc78d362f4b7d469c4f7c2287e73ef86 Mon Sep 17 00:00:00 2001 From: Ovler Date: Sun, 3 Mar 2024 14:50:40 +0800 Subject: [PATCH 01/10] Refactor GIF compression logic for wechat slave channel fix https://github.com/ehforwarderbot/efb-wechat-slave/issues/55#issuecomment-1207145598 --- efb_telegram_master/utils.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index f6b9cc77..1f4058e6 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -294,11 +294,26 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: file.seek(0) metadata = ffmpeg.probe(file.name) stream = ffmpeg.input(file.name) - if channel_id.startswith("blueset.wechat") and metadata.get('width', 0) > 600: + if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future - stream = stream.filter("scale", 600, -2) - stream.output(gif_file.name).overwrite_output().run() + stream = stream.filter("scale", 600, -2, flags="lanczos") + if metadata.get('fps', 0) > 12: + stream = stream.filter("fps", 12, round='up') + + stream.output(gif_file.name).overwrite_output().run() + # get the gif file size + new_file_size = os.path.getsize(gif_file.name) + scales = [600, 512, 480, 400, 360, 300, 256, 200, 150, 100] + if new_file_size > 1024 * 1024: + for scale in scales: + stream = ffmpeg.input(file.name).filter("scale", scale, -2, flags="lanczos").filter("fps", 12, round='up') + stream.output(gif_file.name).overwrite_output().run() + new_file_size = os.path.getsize(gif_file.name) + if new_file_size < 1024 * 1024: + break + else: + stream.output(gif_file.name).overwrite_output().run() file.close() gif_file.seek(0) return gif_file From b101fc52943df0efe77971206cdc0685d8ff85dc Mon Sep 17 00:00:00 2001 From: Ovler Date: Sun, 3 Mar 2024 22:43:19 +0800 Subject: [PATCH 02/10] Fix video encoding issue for VP9 codec --- efb_telegram_master/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 1f4058e6..1e3a0ba3 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -294,6 +294,9 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: file.seek(0) metadata = ffmpeg.probe(file.name) stream = ffmpeg.input(file.name) + # 检查视频编码类型是否为VP9 + if metadata['streams'][0]['codec_name'] == 'vp9': + stream = ffmpeg.input(file.name, vcodec='libvpx') if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future From af539836620e784913a0b575073c17c9a5497c07 Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 00:02:29 +0800 Subject: [PATCH 03/10] Refactor compression logic and add support for generating GIF palettes --- efb_telegram_master/utils.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 1e3a0ba3..89de9f7b 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -296,22 +296,37 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: stream = ffmpeg.input(file.name) # 检查视频编码类型是否为VP9 if metadata['streams'][0]['codec_name'] == 'vp9': - stream = ffmpeg.input(file.name, vcodec='libvpx') + stream = ffmpeg.input(file.name, vcodec='libvpx-vp9') + # generate a palettegen + palettegen_file = NamedTemporaryFile(suffix='.png') + ( + stream + .output(palettegen_file.name, vf='palettegen=reserve_transparent=on') + .overwrite_output() + .run() + ) + # generate a gif + palettegen = ffmpeg.input(palettegen_file.name) + + stream = ( + ffmpeg + .filter([stream, palettegen], 'paletteuse') + ) if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future - stream = stream.filter("scale", 600, -2, flags="lanczos") if metadata.get('fps', 0) > 12: stream = stream.filter("fps", 12, round='up') + stream_scale = stream.filter("scale", 600, -2, flags="lanczos") - stream.output(gif_file.name).overwrite_output().run() + stream_scale.output(gif_file.name).overwrite_output().run() # get the gif file size new_file_size = os.path.getsize(gif_file.name) scales = [600, 512, 480, 400, 360, 300, 256, 200, 150, 100] if new_file_size > 1024 * 1024: for scale in scales: - stream = ffmpeg.input(file.name).filter("scale", scale, -2, flags="lanczos").filter("fps", 12, round='up') - stream.output(gif_file.name).overwrite_output().run() + stream_scale = stream.filter("scale", scale, -2, flags="lanczos") + stream_scale.output(gif_file.name).overwrite_output().run() new_file_size = os.path.getsize(gif_file.name) if new_file_size < 1024 * 1024: break From 951d789a73c0ea58a972bd9c3e291fc20f595d85 Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 02:31:07 +0800 Subject: [PATCH 04/10] Refactor GIF compression logic in utils.py --- efb_telegram_master/utils.py | 69 +++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 89de9f7b..e558b8d8 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -297,41 +297,62 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: # 检查视频编码类型是否为VP9 if metadata['streams'][0]['codec_name'] == 'vp9': stream = ffmpeg.input(file.name, vcodec='libvpx-vp9') - # generate a palettegen - palettegen_file = NamedTemporaryFile(suffix='.png') - ( - stream - .output(palettegen_file.name, vf='palettegen=reserve_transparent=on') - .overwrite_output() - .run() - ) - # generate a gif - palettegen = ffmpeg.input(palettegen_file.name) - - stream = ( - ffmpeg - .filter([stream, palettegen], 'paletteuse') - ) if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future if metadata.get('fps', 0) > 12: stream = stream.filter("fps", 12, round='up') - stream_scale = stream.filter("scale", 600, -2, flags="lanczos") - - stream_scale.output(gif_file.name).overwrite_output().run() - # get the gif file size - new_file_size = os.path.getsize(gif_file.name) + if metadata.get('width', 0) > 600: + stream = stream.filter("scale", 600, -2, flags="lanczos") + split = ( + stream + .split() + ) + stream_paletteuse = ( + ffmpeg + .filter( + [ + split[0], + split[1] + .filter( + filter_name='palettegen', + reserve_transparent='on', + ) + ], + filter_name='paletteuse', + ) + ) + stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() + # get the gif file size + new_file_size = os.path.getsize(gif_file.name) + if new_file_size > 1024 * 1024: scales = [600, 512, 480, 400, 360, 300, 256, 200, 150, 100] - if new_file_size > 1024 * 1024: + scales = [scale for scale in scales if scale < metadata['streams'][0]['width']] + if channel_id.startswith("blueset.wechat"): for scale in scales: stream_scale = stream.filter("scale", scale, -2, flags="lanczos") - stream_scale.output(gif_file.name).overwrite_output().run() + split = ( + stream_scale + .split() + ) + stream_paletteuse = ( + ffmpeg + .filter( + [ + split[0], + split[1] + .filter( + filter_name='palettegen', + reserve_transparent='on', + ) + ], + filter_name='paletteuse', + ) + ) + stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() new_file_size = os.path.getsize(gif_file.name) if new_file_size < 1024 * 1024: break - else: - stream.output(gif_file.name).overwrite_output().run() file.close() gif_file.seek(0) return gif_file From 6c6c44060652c18537e907d9de4abfeaedd0dbcb Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 02:31:46 +0800 Subject: [PATCH 05/10] no size limit for the first run --- efb_telegram_master/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index e558b8d8..2ae4af42 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -322,7 +322,7 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: filter_name='paletteuse', ) ) - stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() + stream_paletteuse.output(gif_file.name).overwrite_output().run() # get the gif file size new_file_size = os.path.getsize(gif_file.name) if new_file_size > 1024 * 1024: From 426da3cbcbc942ba80b52e9768d08c7c5f161884 Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 02:33:16 +0800 Subject: [PATCH 06/10] Update scales for gif file size check --- efb_telegram_master/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 2ae4af42..951338cb 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -326,7 +326,7 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: # get the gif file size new_file_size = os.path.getsize(gif_file.name) if new_file_size > 1024 * 1024: - scales = [600, 512, 480, 400, 360, 300, 256, 200, 150, 100] + scales = [600, 512, 480, 400, 360, 300, 256, 250, 200, 150, 100] scales = [scale for scale in scales if scale < metadata['streams'][0]['width']] if channel_id.startswith("blueset.wechat"): for scale in scales: From 2a01b0345e767981680a292c08cfb0a2ca21e352 Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 02:50:46 +0800 Subject: [PATCH 07/10] Refactor GIF compression logic to make code simple --- efb_telegram_master/utils.py | 76 +++++++++++++----------------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 951338cb..926934ee 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -296,63 +296,41 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: stream = ffmpeg.input(file.name) # 检查视频编码类型是否为VP9 if metadata['streams'][0]['codec_name'] == 'vp9': - stream = ffmpeg.input(file.name, vcodec='libvpx-vp9') + stream = ffmpeg.input(file.name, vcodec='libvpx-vp9') # 只有这个能保持透明背景 if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future if metadata.get('fps', 0) > 12: - stream = stream.filter("fps", 12, round='up') + stream = stream.filter("fps", 12, round='up') # 限制帧率 if metadata.get('width', 0) > 600: - stream = stream.filter("scale", 600, -2, flags="lanczos") - split = ( - stream - .split() - ) - stream_paletteuse = ( - ffmpeg - .filter( - [ - split[0], - split[1] - .filter( - filter_name='palettegen', - reserve_transparent='on', - ) - ], - filter_name='paletteuse', + metadata['streams'][0]['width'] = 600 # 限制宽度,更宽的不会再出现 + scales = [metadata['streams'][0]['width'], 600, 512, 480, 400, 360, 300, 256, 250, 200, 150, 100] + scales = [scale for scale in scales if scale <= metadata['streams'][0]['width']] + scales = sorted(scales, reverse=True) + for scale in scales: + stream_scale = stream.filter("scale", scale, -2, flags="lanczos") # scale要放在split和palettegen之前,否则透明会变成黑色或绿色 + split = ( + stream_scale + .split() ) - ) - stream_paletteuse.output(gif_file.name).overwrite_output().run() - # get the gif file size - new_file_size = os.path.getsize(gif_file.name) - if new_file_size > 1024 * 1024: - scales = [600, 512, 480, 400, 360, 300, 256, 250, 200, 150, 100] - scales = [scale for scale in scales if scale < metadata['streams'][0]['width']] - if channel_id.startswith("blueset.wechat"): - for scale in scales: - stream_scale = stream.filter("scale", scale, -2, flags="lanczos") - split = ( - stream_scale - .split() - ) - stream_paletteuse = ( - ffmpeg + stream_paletteuse = ( + ffmpeg + .filter( + [ + split[0], + split[1] .filter( - [ - split[0], - split[1] - .filter( - filter_name='palettegen', - reserve_transparent='on', - ) - ], - filter_name='paletteuse', + filter_name='palettegen', + reserve_transparent='on', ) - ) - stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() - new_file_size = os.path.getsize(gif_file.name) - if new_file_size < 1024 * 1024: - break + ], + filter_name='paletteuse', + ) + ) + stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() + new_file_size = os.path.getsize(gif_file.name) + if new_file_size < 1024 * 1024 or not channel_id.startswith("blueset.wechat"): + break file.close() gif_file.seek(0) return gif_file From cc14084b9ffd918dc5322b2e2900a6ebce0a797e Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 16:01:16 +0800 Subject: [PATCH 08/10] Add gifsicle to lossy compress gif --- efb_telegram_master/utils.py | 65 +++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 926934ee..e376b302 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -303,34 +303,45 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: if metadata.get('fps', 0) > 12: stream = stream.filter("fps", 12, round='up') # 限制帧率 if metadata.get('width', 0) > 600: - metadata['streams'][0]['width'] = 600 # 限制宽度,更宽的不会再出现 - scales = [metadata['streams'][0]['width'], 600, 512, 480, 400, 360, 300, 256, 250, 200, 150, 100] - scales = [scale for scale in scales if scale <= metadata['streams'][0]['width']] - scales = sorted(scales, reverse=True) - for scale in scales: - stream_scale = stream.filter("scale", scale, -2, flags="lanczos") # scale要放在split和palettegen之前,否则透明会变成黑色或绿色 - split = ( - stream_scale - .split() + stream = stream.filter("scale", 600, -2) # 限制宽度 + split = ( + stream + .split() + ) + stream_paletteuse = ( + ffmpeg + .filter( + [ + split[0], + split[1] + .filter( + filter_name='palettegen', + reserve_transparent='on', + ) + ], + filter_name='paletteuse', ) - stream_paletteuse = ( - ffmpeg - .filter( - [ - split[0], - split[1] - .filter( - filter_name='palettegen', - reserve_transparent='on', - ) - ], - filter_name='paletteuse', - ) - ) - stream_paletteuse.output(gif_file.name, fs=1400000).overwrite_output().run() - new_file_size = os.path.getsize(gif_file.name) - if new_file_size < 1024 * 1024 or not channel_id.startswith("blueset.wechat"): - break + ) + stream_paletteuse.output(gif_file.name).overwrite_output().run() + new_file_size = os.path.getsize(gif_file.name) + print(f"file_size: {new_file_size/1024}KB") + if new_file_size > 1024 * 1024 and channel_id.startswith("blueset.wechat"): + # try to use gifsicle lossy compression + compress_file = NamedTemporaryFile(suffix='.gif') + subprocess.run(["gifsicle", "--resize-method=catrom", "--lossy=100", "-O2", "-o", compress_file.name, gif_file.name], check=True) + new_file_size = os.path.getsize(compress_file.name) + if new_file_size > 1024 * 1024: + scales = [600, 512, 480, 400, 360, 300, 256, 250, 200, 150, 100] + scales = [scale for scale in scales if scale < metadata['streams'][0]['width']] + scales = sorted(scales, reverse=True) + for scale in scales: + subprocess.run(["gifsicle", "--resize-method=catrom", "--resize-fit", f"{scale}x{scale}", "--lossy=100", "-O2", "-o", compress_file.name, gif_file.name], check=True) + new_file_size = os.path.getsize(compress_file.name) + print(f"new_file_size: {new_file_size/1024}KB after resize to {scale}x{scale}") + if new_file_size < 1024 * 1024: + break + gif_file.close() + gif_file = compress_file file.close() gif_file.seek(0) return gif_file From bd648dd1e9054ef4b66580f55b387400288c461e Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 22:16:38 +0800 Subject: [PATCH 09/10] Fix tgs to gif's transparency issue --- efb_telegram_master/utils.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index e376b302..0971e73f 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -160,6 +160,22 @@ def chat_id_str_to_id(s: EFBChannelChatIDStr) -> Tuple[ModuleID, ChatID, Optiona group_id = ChatID(ids[2]) return channel_id, chat_uid, group_id +def _png_gif_prepare(image): + """ Fork of lottie.exporters.gif.export_gif + Adapted from eltiempoes/python-lottie + https://github.com/eltiempoes/python-lottie/blob/a9f8be4858adb7eb0bc0e406a870b19c309c8a36/lib/lottie/exporters/gif.py#L10 + License: + AGPL 3.0 (Python Lottie) + """ + if image.mode not in ["RGBA", "RGBa"]: + image = image.convert("RGBA") + alpha = image.getchannel("A") + image = image.convert(image.mode[:-1]) \ + .convert('P', palette=Image.ADAPTIVE, colors=255) # changed + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + image.paste(255, mask=mask) + image.info['transparency'] = 255 # added + return image def export_gif(animation, fp, dpi=96, skip_frames=5): """ Fork of lottie.exporters.gif.export_gif @@ -172,7 +188,7 @@ def export_gif(animation, fp, dpi=96, skip_frames=5): # Import only upon calling the method due to added binary dependencies # (libcairo) from lottie.exporters.cairo import export_png - from lottie.exporters.gif import _png_gif_prepare + # from lottie.exporters.gif import _png_gif_prepare # The code here have some problem, so I copy the function abo ve start = int(animation.in_point) end = int(animation.out_point) From eeaaaf5563a7f3941e51767c333a8330e1ca59b0 Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 4 Mar 2024 22:29:46 +0800 Subject: [PATCH 10/10] add the missing part on windows --- efb_telegram_master/utils.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 0971e73f..d3c8d6a9 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -274,12 +274,35 @@ def gif_conversion(file: IO[bytes], channel_id: str) -> IO[bytes]: # Set input/output of ffmpeg to stream stream = ffmpeg.input("pipe:") - if channel_id.startswith("blueset.wechat") and metadata.get('width', 0) > 600: + if metadata['streams'][0]['codec_name'] == 'vp9': + stream = ffmpeg.input(file.name, vcodec='libvpx-vp9') + if channel_id.startswith("blueset.wechat"): # Workaround: Compress GIF for slave channel `blueset.wechat` # TODO: Move this logic to `blueset.wechat` in the future - stream = stream.filter("scale", 600, -2) + if metadata.get('width', 0) > 600: + stream = stream.filter("scale", 600, -2) + if metadata.get('fps', 0) > 12: + stream = stream.filter("fps", 12, round='up') + split = ( + stream + .split() + ) + stream_paletteuse = ( + ffmpeg + .filter( + [ + split[0], + split[1] + .filter( + filter_name='palettegen', + reserve_transparent='on', + ) + ], + filter_name='paletteuse', + ) + ) # Need to specify file format here as no extension hint presents. - args = stream.output("pipe:", format="gif").compile() + args = stream_paletteuse.output("pipe:", format="gif").compile() file.seek(0) # subprocess.Popen would still try to access the file handle instead of