sticker-convert 2.1.6__tar.gz → 2.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sticker-convert-2.1.6/src/sticker_convert.egg-info → sticker-convert-2.1.7}/PKG-INFO +8 -9
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/requirements.txt +7 -8
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/__init__.py +1 -1
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/__main__.py +7 -4
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/cli.py +39 -31
- sticker-convert-2.1.7/src/sticker_convert/converter.py +432 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/downloaders/download_base.py +40 -16
- sticker-convert-2.1.7/src/sticker_convert/downloaders/download_kakao.py +241 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/downloaders/download_line.py +16 -6
- sticker-convert-2.1.7/src/sticker_convert/downloaders/download_signal.py +89 -0
- sticker-convert-2.1.7/src/sticker_convert/downloaders/download_telegram.py +120 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/gui.py +78 -129
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/comp_frame.py +2 -3
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/config_frame.py +3 -4
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/control_frame.py +2 -2
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/cred_frame.py +4 -4
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/input_frame.py +4 -4
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/output_frame.py +3 -3
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/progress_frame.py +1 -1
- {sticker-convert-2.1.6/src/sticker_convert/utils → sticker-convert-2.1.7/src/sticker_convert/gui_components}/gui_utils.py +38 -21
- {sticker-convert-2.1.6/src/sticker_convert/gui_windows → sticker-convert-2.1.7/src/sticker_convert/gui_components/windows}/advanced_compression_window.py +3 -2
- {sticker-convert-2.1.6/src/sticker_convert/gui_windows → sticker-convert-2.1.7/src/sticker_convert/gui_components/windows}/base_window.py +3 -2
- {sticker-convert-2.1.6/src/sticker_convert/gui_windows → sticker-convert-2.1.7/src/sticker_convert/gui_components/windows}/kakao_get_auth_window.py +3 -3
- {sticker-convert-2.1.6/src/sticker_convert/gui_windows → sticker-convert-2.1.7/src/sticker_convert/gui_components/windows}/line_get_auth_window.py +2 -2
- {sticker-convert-2.1.6/src/sticker_convert/gui_windows → sticker-convert-2.1.7/src/sticker_convert/gui_components/windows}/signal_get_auth_window.py +2 -2
- sticker-convert-2.1.6/src/sticker_convert/flow.py → sticker-convert-2.1.7/src/sticker_convert/job.py +91 -102
- sticker-convert-2.1.7/src/sticker_convert/job_option.py +301 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/compression.json +1 -1
- sticker-convert-2.1.7/src/sticker_convert/uploaders/compress_wastickers.py +151 -0
- sticker-convert-2.1.7/src/sticker_convert/uploaders/upload_base.py +28 -0
- sticker-convert-2.1.7/src/sticker_convert/uploaders/upload_signal.py +171 -0
- sticker-convert-2.1.7/src/sticker_convert/uploaders/upload_telegram.py +280 -0
- sticker-convert-2.1.7/src/sticker_convert/uploaders/xcode_imessage.py +348 -0
- {sticker-convert-2.1.6/src/sticker_convert → sticker-convert-2.1.7/src/sticker_convert/utils}/auth/get_kakao_auth.py +7 -5
- {sticker-convert-2.1.6/src/sticker_convert → sticker-convert-2.1.7/src/sticker_convert/utils}/auth/get_line_auth.py +3 -3
- {sticker-convert-2.1.6/src/sticker_convert → sticker-convert-2.1.7/src/sticker_convert/utils}/auth/get_signal_auth.py +1 -1
- sticker-convert-2.1.7/src/sticker_convert/utils/fake_cb_msg.py +11 -0
- {sticker-convert-2.1.6/src/sticker_convert/utils → sticker-convert-2.1.7/src/sticker_convert/utils/files}/cache_store.py +7 -3
- sticker-convert-2.1.7/src/sticker_convert/utils/files/dir_utils.py +64 -0
- {sticker-convert-2.1.6/src/sticker_convert/utils → sticker-convert-2.1.7/src/sticker_convert/utils/files}/json_manager.py +5 -4
- sticker-convert-2.1.7/src/sticker_convert/utils/files/metadata_handler.py +226 -0
- sticker-convert-2.1.7/src/sticker_convert/utils/files/run_bin.py +58 -0
- {sticker-convert-2.1.6/src/sticker_convert/utils → sticker-convert-2.1.7/src/sticker_convert/utils/media}/apple_png_normalize.py +23 -20
- {sticker-convert-2.1.6/src/sticker_convert/utils → sticker-convert-2.1.7/src/sticker_convert/utils/media}/codec_info.py +41 -35
- sticker-convert-2.1.7/src/sticker_convert/utils/media/decrypt_kakao.py +68 -0
- sticker-convert-2.1.7/src/sticker_convert/utils/media/format_verify.py +184 -0
- sticker-convert-2.1.7/src/sticker_convert/utils/url_detect.py +31 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7/src/sticker_convert.egg-info}/PKG-INFO +8 -9
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert.egg-info/SOURCES.txt +30 -28
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert.egg-info/requires.txt +7 -8
- sticker-convert-2.1.6/src/sticker_convert/downloaders/download_kakao.py +0 -274
- sticker-convert-2.1.6/src/sticker_convert/downloaders/download_signal.py +0 -73
- sticker-convert-2.1.6/src/sticker_convert/downloaders/download_telegram.py +0 -75
- sticker-convert-2.1.6/src/sticker_convert/uploaders/compress_wastickers.py +0 -130
- sticker-convert-2.1.6/src/sticker_convert/uploaders/upload_base.py +0 -16
- sticker-convert-2.1.6/src/sticker_convert/uploaders/upload_signal.py +0 -133
- sticker-convert-2.1.6/src/sticker_convert/uploaders/upload_telegram.py +0 -240
- sticker-convert-2.1.6/src/sticker_convert/uploaders/xcode_imessage.py +0 -278
- sticker-convert-2.1.6/src/sticker_convert/utils/converter.py +0 -407
- sticker-convert-2.1.6/src/sticker_convert/utils/curr_dir.py +0 -70
- sticker-convert-2.1.6/src/sticker_convert/utils/fake_cb_msg.py +0 -8
- sticker-convert-2.1.6/src/sticker_convert/utils/format_verify.py +0 -188
- sticker-convert-2.1.6/src/sticker_convert/utils/metadata_handler.py +0 -190
- sticker-convert-2.1.6/src/sticker_convert/utils/run_bin.py +0 -46
- sticker-convert-2.1.6/src/sticker_convert/utils/url_detect.py +0 -29
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/LICENSE +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/MANIFEST.in +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/README.md +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/pyproject.toml +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/setup.cfg +0 -0
- {sticker-convert-2.1.6/src/sticker_convert/gui_frames → sticker-convert-2.1.7/src/sticker_convert/gui_components/frames}/right_clicker.py +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/.gitignore +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/README.md +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers/Info.plist +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Sticker 1.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Sticker 2.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Sticker 3.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/App-Store-1024x1024pt.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-App-Store-1024x768pt.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-67x50pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-Pro-74x55pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@3x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@3x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@3x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPad-Settings-29pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-Settings-29pt@3x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-settings-29pt@2x.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcuserdata/niklaspeterson.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/NotoColorEmoji.ttf +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/appicon.icns +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/appicon.ico +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/appicon.png +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/emoji.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/help.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/input.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert/resources/output.json +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert.egg-info/dependency_links.txt +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert.egg-info/entry_points.txt +0 -0
- {sticker-convert-2.1.6 → sticker-convert-2.1.7}/src/sticker_convert.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sticker-convert
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.7
|
4
4
|
Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, iMessage. Written in Python.
|
5
5
|
Author-email: laggykiller <chaudominic2@gmail.com>
|
6
6
|
Maintainer-email: laggykiller <chaudominic2@gmail.com>
|
@@ -365,19 +365,18 @@ Requires-Python: >=3.7
|
|
365
365
|
Description-Content-Type: text/markdown
|
366
366
|
License-File: LICENSE
|
367
367
|
Requires-Dist: apngasm_python~=1.2.1
|
368
|
-
Requires-Dist: pyav~=11.
|
368
|
+
Requires-Dist: pyav~=11.4.1
|
369
369
|
Requires-Dist: beautifulsoup4~=4.12.2
|
370
|
-
Requires-Dist: rookiepy~=0.
|
371
|
-
Requires-Dist: imageio~=2.
|
370
|
+
Requires-Dist: rookiepy~=0.3.3
|
371
|
+
Requires-Dist: imageio~=2.33.1
|
372
372
|
Requires-Dist: memory-tempfile~=2.2.3
|
373
|
-
Requires-Dist:
|
374
|
-
Requires-Dist:
|
375
|
-
Requires-Dist:
|
376
|
-
Requires-Dist: pyoxipng~=8.0.1
|
373
|
+
Requires-Dist: numpy~=1.26.2
|
374
|
+
Requires-Dist: Pillow~=10.1.0
|
375
|
+
Requires-Dist: pyoxipng~=9.0.0
|
377
376
|
Requires-Dist: python-telegram-bot~=20.5
|
378
377
|
Requires-Dist: requests~=2.31.0
|
379
378
|
Requires-Dist: rlottie_python~=1.2.1
|
380
|
-
Requires-Dist: selenium~=4.
|
379
|
+
Requires-Dist: selenium~=4.16.0
|
381
380
|
Requires-Dist: signalstickers-client~=3.3.0
|
382
381
|
Requires-Dist: tqdm~=4.66.1
|
383
382
|
Requires-Dist: ttkbootstrap-fork-laggykiller~=1.5.1
|
@@ -1,17 +1,16 @@
|
|
1
1
|
apngasm_python~=1.2.1
|
2
|
-
pyav~=11.
|
2
|
+
pyav~=11.4.1
|
3
3
|
beautifulsoup4~=4.12.2
|
4
|
-
rookiepy~=0.
|
5
|
-
imageio~=2.
|
4
|
+
rookiepy~=0.3.3
|
5
|
+
imageio~=2.33.1
|
6
6
|
memory-tempfile~=2.2.3
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
pyoxipng~=8.0.1
|
7
|
+
numpy~=1.26.2
|
8
|
+
Pillow~=10.1.0
|
9
|
+
pyoxipng~=9.0.0
|
11
10
|
python-telegram-bot~=20.5
|
12
11
|
requests~=2.31.0
|
13
12
|
rlottie_python~=1.2.1
|
14
|
-
selenium~=4.
|
13
|
+
selenium~=4.16.0
|
15
14
|
signalstickers-client~=3.3.0
|
16
15
|
tqdm~=4.66.1
|
17
16
|
ttkbootstrap-fork-laggykiller~=1.5.1
|
@@ -10,12 +10,15 @@ def main():
|
|
10
10
|
script_path = os.path.dirname(sys.argv[0])
|
11
11
|
os.chdir(script_path)
|
12
12
|
if len(sys.argv) == 1:
|
13
|
-
print(
|
14
|
-
from sticker_convert.gui import GUI
|
13
|
+
print("Launching GUI...")
|
14
|
+
from sticker_convert.gui import GUI # type: ignore
|
15
|
+
|
15
16
|
GUI().gui()
|
16
17
|
else:
|
17
18
|
from sticker_convert.cli import CLI
|
19
|
+
|
18
20
|
CLI().cli()
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
+
|
23
|
+
if __name__ == "__main__":
|
24
|
+
main()
|
@@ -7,12 +7,13 @@ from typing import Optional
|
|
7
7
|
|
8
8
|
from tqdm import tqdm
|
9
9
|
|
10
|
-
from .
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
from .auth.
|
14
|
-
from .auth.
|
15
|
-
from .utils.
|
10
|
+
from .job import Job # type: ignore
|
11
|
+
from .job_option import InputOption, CompOption, OutputOption, CredOption # type: ignore
|
12
|
+
from .utils.files.json_manager import JsonManager # type: ignore
|
13
|
+
from .utils.auth.get_kakao_auth import GetKakaoAuth # type: ignore
|
14
|
+
from .utils.auth.get_signal_auth import GetSignalAuth # type: ignore
|
15
|
+
from .utils.auth.get_line_auth import GetLineAuth # type: ignore
|
16
|
+
from .utils.files.dir_utils import DirUtils # type: ignore
|
16
17
|
from .utils.url_detect import UrlDetect # type: ignore
|
17
18
|
from .__init__ import __version__ # type: ignore
|
18
19
|
|
@@ -92,25 +93,24 @@ class CLI:
|
|
92
93
|
|
93
94
|
self.no_confirm = args.no_confirm
|
94
95
|
|
95
|
-
self.get_opt_input(args)
|
96
|
-
self.get_opt_output(args)
|
97
|
-
self.get_opt_comp(args)
|
98
|
-
self.get_opt_cred(args)
|
96
|
+
self.opt_input = InputOption(self.get_opt_input(args))
|
97
|
+
self.opt_output = OutputOption(self.get_opt_output(args))
|
98
|
+
self.opt_comp = CompOption(self.get_opt_comp(args))
|
99
|
+
self.opt_cred = CredOption(self.get_opt_cred(args))
|
99
100
|
|
100
|
-
|
101
|
+
job = Job(
|
101
102
|
self.opt_input, self.opt_comp, self.opt_output, self.opt_cred,
|
102
|
-
self.input_presets, self.output_presets,
|
103
103
|
self.cb_msg, self.cb_msg_block, self.cb_bar, self.cb_ask_bool
|
104
104
|
)
|
105
105
|
|
106
|
-
status =
|
106
|
+
status = job.start()
|
107
107
|
|
108
108
|
if status == 1:
|
109
109
|
self.cb_msg(msg='An error occured during this run.')
|
110
110
|
elif status == 2:
|
111
111
|
self.cb_msg(msg='Job cancelled.')
|
112
112
|
|
113
|
-
def get_opt_input(self, args):
|
113
|
+
def get_opt_input(self, args) -> dict:
|
114
114
|
download_options = {
|
115
115
|
'auto': args.download_auto,
|
116
116
|
'signal': args.download_signal,
|
@@ -134,13 +134,15 @@ class CLI:
|
|
134
134
|
self.cb_msg(f'Error: Unrecognied URL input source for url: {url}')
|
135
135
|
exit()
|
136
136
|
|
137
|
-
|
137
|
+
opt_input = {
|
138
138
|
'option': download_option,
|
139
139
|
'url': url,
|
140
|
-
'dir': args.input_dir if args.input_dir else os.path.join(
|
140
|
+
'dir': args.input_dir if args.input_dir else os.path.join(DirUtils.get_curr_dir(), 'stickers_input')
|
141
141
|
}
|
142
142
|
|
143
|
-
|
143
|
+
return opt_input
|
144
|
+
|
145
|
+
def get_opt_output(self, args) -> dict:
|
144
146
|
if args.export_whatsapp:
|
145
147
|
export_option = 'whatsapp'
|
146
148
|
elif args.export_signal:
|
@@ -152,14 +154,16 @@ class CLI:
|
|
152
154
|
else:
|
153
155
|
export_option = 'local'
|
154
156
|
|
155
|
-
|
157
|
+
opt_output = {
|
156
158
|
'option': export_option,
|
157
|
-
'dir': args.output_dir if args.output_dir else os.path.join(
|
159
|
+
'dir': args.output_dir if args.output_dir else os.path.join(DirUtils.get_curr_dir(), 'stickers_output'),
|
158
160
|
'title': args.title,
|
159
161
|
'author': args.author
|
160
162
|
}
|
161
163
|
|
162
|
-
|
164
|
+
return opt_output
|
165
|
+
|
166
|
+
def get_opt_comp(self, args) -> dict:
|
163
167
|
preset = args.preset
|
164
168
|
if args.preset == 'custom':
|
165
169
|
if sum((args.export_whatsapp, args.export_signal, args.export_telegram, args.export_imessage)) > 1:
|
@@ -174,7 +178,7 @@ class CLI:
|
|
174
178
|
elif args.export_imessage:
|
175
179
|
preset = 'imessage_small'
|
176
180
|
elif args.preset == 'auto':
|
177
|
-
output_option = self.opt_output
|
181
|
+
output_option = self.opt_output.option
|
178
182
|
if output_option == 'local':
|
179
183
|
preset = 'custom'
|
180
184
|
args.no_compress = True
|
@@ -186,7 +190,7 @@ class CLI:
|
|
186
190
|
preset = output_option
|
187
191
|
self.cb_msg(f'Auto compression option set to {preset}')
|
188
192
|
|
189
|
-
|
193
|
+
opt_comp = {
|
190
194
|
'preset': preset,
|
191
195
|
'size_max': {
|
192
196
|
'img': self.compression_presets[preset]['size_max']['img'] if args.img_size_max == None else args.img_size_max,
|
@@ -230,15 +234,17 @@ class CLI:
|
|
230
234
|
'processes': args.processes if args.processes else math.ceil(cpu_count() / 2)
|
231
235
|
}
|
232
236
|
|
233
|
-
|
234
|
-
|
237
|
+
return opt_comp
|
238
|
+
|
239
|
+
def get_opt_cred(self, args) -> dict:
|
240
|
+
creds_path = os.path.join(DirUtils.get_config_dir(), 'creds.json')
|
235
241
|
creds = JsonManager.load_json(creds_path)
|
236
242
|
if creds:
|
237
243
|
self.cb_msg('Loaded credentials from creds.json')
|
238
244
|
else:
|
239
245
|
creds = {}
|
240
246
|
|
241
|
-
|
247
|
+
opt_cred = {
|
242
248
|
'signal': {
|
243
249
|
'uuid': args.signal_uuid if args.signal_uuid else creds.get('signal', {}).get('uuid'),
|
244
250
|
'password': args.signal_password if args.signal_password else creds.get('signal', {}).get('password')
|
@@ -264,7 +270,7 @@ class CLI:
|
|
264
270
|
auth_token = m.get_cred()
|
265
271
|
|
266
272
|
if auth_token:
|
267
|
-
|
273
|
+
opt_cred['kakao']['auth_token'] = auth_token
|
268
274
|
|
269
275
|
self.cb_msg(f'Got auth_token successfully: {auth_token}')
|
270
276
|
|
@@ -276,8 +282,8 @@ class CLI:
|
|
276
282
|
uuid, password = m.get_cred()
|
277
283
|
|
278
284
|
if uuid and password:
|
279
|
-
|
280
|
-
|
285
|
+
opt_cred['signal']['uuid'] = uuid
|
286
|
+
opt_cred['signal']['password'] = password
|
281
287
|
|
282
288
|
self.cb_msg(f'Got uuid and password successfully: {uuid}, {password}')
|
283
289
|
break
|
@@ -288,16 +294,18 @@ class CLI:
|
|
288
294
|
line_cookies = m.get_cred()
|
289
295
|
|
290
296
|
if line_cookies:
|
291
|
-
|
297
|
+
opt_cred['line']['cookies'] = line_cookies
|
292
298
|
|
293
299
|
self.cb_msg('Got Line cookies successfully')
|
294
300
|
else:
|
295
301
|
self.cb_msg('Failed to get Line cookies. Have you logged in the web browser?')
|
296
302
|
|
297
303
|
if args.save_cred:
|
298
|
-
creds_path = os.path.join(
|
299
|
-
JsonManager.save_json(creds_path,
|
304
|
+
creds_path = os.path.join(DirUtils.get_config_dir(), 'creds.json')
|
305
|
+
JsonManager.save_json(creds_path, opt_cred)
|
300
306
|
self.cb_msg('Saved credentials to creds.json')
|
307
|
+
|
308
|
+
return opt_cred
|
301
309
|
|
302
310
|
def cb_ask_str(self, msg: Optional[str] = None, initialvalue: Optional[str] = None, cli_show_initialvalue: bool = True) -> str:
|
303
311
|
self.cb_msg(msg)
|
@@ -0,0 +1,432 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import os
|
3
|
+
import io
|
4
|
+
from multiprocessing.queues import Queue as QueueType
|
5
|
+
from typing import Optional, Union
|
6
|
+
|
7
|
+
import imageio.v3 as iio
|
8
|
+
from rlottie_python import LottieAnimation # type: ignore
|
9
|
+
from apngasm_python._apngasm_python import APNGAsm, create_frame_from_rgba
|
10
|
+
import numpy as np
|
11
|
+
from PIL import Image
|
12
|
+
import av # type: ignore
|
13
|
+
from av.codec.context import CodecContext # type: ignore
|
14
|
+
import webp # type: ignore
|
15
|
+
import oxipng
|
16
|
+
|
17
|
+
from .utils.media.codec_info import CodecInfo # type: ignore
|
18
|
+
from .utils.files.cache_store import CacheStore # type: ignore
|
19
|
+
from .utils.media.format_verify import FormatVerify # type: ignore
|
20
|
+
from .utils.fake_cb_msg import FakeCbMsg # type: ignore
|
21
|
+
from .job_option import CompOption
|
22
|
+
|
23
|
+
def get_step_value(
|
24
|
+
max: Optional[int], min: Optional[int],
|
25
|
+
step: int, steps: int
|
26
|
+
) -> Optional[int]:
|
27
|
+
|
28
|
+
if max and min:
|
29
|
+
return round((max - min) * step / steps + min)
|
30
|
+
else:
|
31
|
+
return None
|
32
|
+
|
33
|
+
class StickerConvert:
|
34
|
+
MSG_START_COMP = '[I] Start compressing {} -> {}'
|
35
|
+
MSG_SKIP_COMP = '[S] Compatible file found, skip compress and just copy {} -> {}'
|
36
|
+
MSG_COMP = ('[C] Compressing {} -> {} res={}x{}, '
|
37
|
+
'quality={}, fps={}, color={} (step {}-{}-{})')
|
38
|
+
MSG_REDO_COMP = '[{}] Compressed {} -> {} but size {} {} limit {}, recompressing'
|
39
|
+
MSG_DONE_COMP = '[S] Successful compression {} -> {} size {} (step {})'
|
40
|
+
MSG_FAIL_COMP = ('[F] Failed Compression {} -> {}, '
|
41
|
+
'cannot get below limit {} with lowest quality under current settings')
|
42
|
+
|
43
|
+
def __init__(self,
|
44
|
+
in_f: Union[str, list[str, io.BytesIO]],
|
45
|
+
out_f: str,
|
46
|
+
opt_comp: CompOption,
|
47
|
+
cb_msg: Union[FakeCbMsg, bool] = True):
|
48
|
+
|
49
|
+
if not isinstance(cb_msg, QueueType):
|
50
|
+
if cb_msg == False:
|
51
|
+
silent = True
|
52
|
+
else:
|
53
|
+
silent = False
|
54
|
+
cb_msg = FakeCbMsg(print, silent=silent)
|
55
|
+
|
56
|
+
if isinstance(in_f, str):
|
57
|
+
self.in_f = in_f
|
58
|
+
self.in_f_name = os.path.split(self.in_f)[1]
|
59
|
+
self.in_f_ext = CodecInfo.get_file_ext(self.in_f)
|
60
|
+
else:
|
61
|
+
self.in_f = in_f[1]
|
62
|
+
self.in_f_name = os.path.split(in_f[0])[1]
|
63
|
+
self.in_f_ext = CodecInfo.get_file_ext(in_f[0])
|
64
|
+
|
65
|
+
self.out_f = out_f
|
66
|
+
self.out_f_name = os.path.split(self.out_f)[1]
|
67
|
+
self.out_f_ext = os.path.splitext(out_f)[1]
|
68
|
+
|
69
|
+
self.cb_msg = cb_msg
|
70
|
+
self.frames_raw: list[np.ndarray] = []
|
71
|
+
self.frames_processed: list[np.ndarray] = []
|
72
|
+
self.opt_comp = opt_comp
|
73
|
+
if not self.opt_comp.steps:
|
74
|
+
self.opt_comp.steps = 1
|
75
|
+
|
76
|
+
self.size = 0
|
77
|
+
self.size_max = None
|
78
|
+
self.res_w = None
|
79
|
+
self.res_h = None
|
80
|
+
self.quality = None
|
81
|
+
self.fps = None
|
82
|
+
self.color = None
|
83
|
+
|
84
|
+
self.frames_orig = CodecInfo.get_file_frames(self.in_f)
|
85
|
+
self.fps_orig = CodecInfo.get_file_fps(self.in_f)
|
86
|
+
self.duration_orig = self.frames_orig / self.fps_orig * 1000
|
87
|
+
|
88
|
+
self.tmp_f = None
|
89
|
+
self.result = None
|
90
|
+
self.result_size = 0
|
91
|
+
self.result_step = None
|
92
|
+
|
93
|
+
self.apngasm = APNGAsm() # type: ignore[call-arg]
|
94
|
+
|
95
|
+
def convert(self) -> tuple[bool, str, Union[None, bytes, str], int]:
|
96
|
+
if (FormatVerify.check_format(self.in_f, fmt=self.out_f_ext) and
|
97
|
+
FormatVerify.check_file_res(self.in_f, res=self.opt_comp.res) and
|
98
|
+
FormatVerify.check_file_fps(self.in_f, fps=self.opt_comp.fps) and
|
99
|
+
FormatVerify.check_file_size(self.in_f, size=self.opt_comp.size_max) and
|
100
|
+
FormatVerify.check_duration(self.in_f, duration=self.opt_comp.duration)):
|
101
|
+
self.cb_msg.put(self.MSG_SKIP_COMP.format(self.in_f_name, self.out_f_name))
|
102
|
+
|
103
|
+
with open(self.in_f, 'rb') as f:
|
104
|
+
self.result = f.read()
|
105
|
+
self.result_size = os.path.getsize(self.in_f)
|
106
|
+
|
107
|
+
return self.compress_done(self.result)
|
108
|
+
|
109
|
+
self.cb_msg.put(self.MSG_START_COMP.format(self.in_f_name, self.out_f_name))
|
110
|
+
|
111
|
+
steps_list = []
|
112
|
+
for step in range(self.opt_comp.steps, -1, -1):
|
113
|
+
steps_list.append((
|
114
|
+
get_step_value(self.opt_comp.res_w_max, self.opt_comp.res_w_min, step, self.opt_comp.steps),
|
115
|
+
get_step_value(self.opt_comp.res_h_max, self.opt_comp.res_h_min, step, self.opt_comp.steps),
|
116
|
+
get_step_value(self.opt_comp.quality_max, self.opt_comp.quality_min, step, self.opt_comp.steps),
|
117
|
+
get_step_value(self.opt_comp.fps_max, self.opt_comp.fps_min, step, self.opt_comp.steps),
|
118
|
+
get_step_value(self.opt_comp.color_max, self.opt_comp.color_min, step, self.opt_comp.steps)
|
119
|
+
))
|
120
|
+
|
121
|
+
step_lower = 0
|
122
|
+
step_upper = self.opt_comp.steps
|
123
|
+
|
124
|
+
if self.opt_comp.size_max == [None, None]:
|
125
|
+
# No limit to size, create the best quality result
|
126
|
+
step_current = 0
|
127
|
+
else:
|
128
|
+
step_current = round((step_lower + step_upper) / 2)
|
129
|
+
|
130
|
+
self.frames_import()
|
131
|
+
while True:
|
132
|
+
param = steps_list[step_current]
|
133
|
+
self.res_w = param[0]
|
134
|
+
self.res_h = param[1]
|
135
|
+
self.quality = param[2]
|
136
|
+
self.fps = param[3]
|
137
|
+
self.color = param[4]
|
138
|
+
|
139
|
+
self.tmp_f = io.BytesIO()
|
140
|
+
msg = self.MSG_COMP.format(
|
141
|
+
self.in_f_name, self.out_f_name,
|
142
|
+
self.res_w, self.res_h,
|
143
|
+
self.quality, self.fps, self.color,
|
144
|
+
step_lower, step_current, step_upper
|
145
|
+
)
|
146
|
+
self.cb_msg.put(msg)
|
147
|
+
|
148
|
+
self.frames_processed = self.frames_drop(self.frames_raw)
|
149
|
+
self.frames_processed = self.frames_resize(self.frames_processed)
|
150
|
+
self.frames_export()
|
151
|
+
|
152
|
+
self.tmp_f.seek(0)
|
153
|
+
self.size = self.tmp_f.getbuffer().nbytes
|
154
|
+
if CodecInfo.is_anim(self.in_f):
|
155
|
+
self.size_max = self.opt_comp.size_max_vid
|
156
|
+
else:
|
157
|
+
self.size_max = self.opt_comp.size_max_img
|
158
|
+
|
159
|
+
if (not self.size_max or
|
160
|
+
(self.size <= self.size_max and self.size >= self.result_size)):
|
161
|
+
self.result = self.tmp_f.read()
|
162
|
+
self.result_size = self.size
|
163
|
+
self.result_step = step_current
|
164
|
+
|
165
|
+
if step_upper - step_lower > 1:
|
166
|
+
if self.size <= self.size_max:
|
167
|
+
sign = '<'
|
168
|
+
step_upper = step_current
|
169
|
+
else:
|
170
|
+
sign = '>'
|
171
|
+
step_lower = step_current
|
172
|
+
step_current = int((step_lower + step_upper) / 2)
|
173
|
+
self.recompress(sign)
|
174
|
+
elif self.result or not self.size_max:
|
175
|
+
return self.compress_done(self.result, self.result_step)
|
176
|
+
else:
|
177
|
+
return self.compress_fail()
|
178
|
+
|
179
|
+
def recompress(self, sign: str):
|
180
|
+
msg = self.MSG_REDO_COMP.format(
|
181
|
+
sign, self.in_f_name, self.out_f_name, self.size, sign, self.size_max
|
182
|
+
)
|
183
|
+
self.cb_msg.put(msg)
|
184
|
+
|
185
|
+
def compress_fail(self) -> tuple[bool, str, Union[None, bytes, str], int]:
|
186
|
+
msg = self.MSG_FAIL_COMP.format(
|
187
|
+
self.in_f_name, self.out_f_name, self.size_max
|
188
|
+
)
|
189
|
+
self.cb_msg.put(msg)
|
190
|
+
|
191
|
+
return False, self.in_f, self.out_f, self.size
|
192
|
+
|
193
|
+
def compress_done(self,
|
194
|
+
data: bytes,
|
195
|
+
result_step: Optional[int] = None
|
196
|
+
) -> tuple[bool, str, Union[None, bytes, str], int]:
|
197
|
+
|
198
|
+
if os.path.splitext(self.out_f_name)[0] == 'none':
|
199
|
+
self.out_f = None
|
200
|
+
elif os.path.splitext(self.out_f_name)[0] == 'bytes':
|
201
|
+
self.out_f = data
|
202
|
+
else:
|
203
|
+
with open(self.out_f, 'wb+') as f:
|
204
|
+
f.write(data)
|
205
|
+
|
206
|
+
if result_step:
|
207
|
+
msg = self.MSG_DONE_COMP.format(
|
208
|
+
self.in_f_name, self.out_f_name, self.result_size, result_step
|
209
|
+
)
|
210
|
+
self.cb_msg.put(msg)
|
211
|
+
|
212
|
+
return True, self.in_f, self.out_f, self.result_size
|
213
|
+
|
214
|
+
def frames_import(self):
|
215
|
+
if self.in_f_ext in ('.tgs', '.lottie', '.json'):
|
216
|
+
self.frames_import_lottie()
|
217
|
+
else:
|
218
|
+
self.frames_import_imageio()
|
219
|
+
|
220
|
+
def frames_import_imageio(self):
|
221
|
+
if self.in_f_ext in '.webp':
|
222
|
+
# ffmpeg do not support webp decoding (yet)
|
223
|
+
for frame in iio.imiter(self.in_f, plugin='pillow', mode='RGBA'):
|
224
|
+
self.frames_raw.append(frame)
|
225
|
+
return
|
226
|
+
|
227
|
+
frame_format = 'rgba'
|
228
|
+
# Crashes when handling some webm in yuv420p and convert to rgba
|
229
|
+
# https://github.com/PyAV-Org/PyAV/issues/1166
|
230
|
+
metadata = iio.immeta(self.in_f, plugin='pyav', exclude_applied=False)
|
231
|
+
context = None
|
232
|
+
if metadata.get('video_format') == 'yuv420p':
|
233
|
+
if metadata.get('alpha_mode') != '1':
|
234
|
+
frame_format = 'rgb24'
|
235
|
+
if metadata.get('codec') == 'vp8':
|
236
|
+
context = CodecContext.create('v8', 'r')
|
237
|
+
elif metadata.get('codec') == 'vp9':
|
238
|
+
context = CodecContext.create('libvpx-vp9', 'r')
|
239
|
+
|
240
|
+
with av.open(self.in_f) as container:
|
241
|
+
if not context:
|
242
|
+
context = container.streams.video[0].codec_context
|
243
|
+
for packet in container.demux(video=0):
|
244
|
+
for frame in context.decode(packet):
|
245
|
+
frame = frame.to_ndarray(format=frame_format)
|
246
|
+
if frame_format == 'rgb24':
|
247
|
+
frame = np.dstack(
|
248
|
+
(frame, np.zeros(frame.shape[:2], dtype=np.uint8)+255)
|
249
|
+
)
|
250
|
+
self.frames_raw.append(frame)
|
251
|
+
|
252
|
+
def frames_import_lottie(self):
|
253
|
+
if self.in_f_ext == '.tgs':
|
254
|
+
anim = LottieAnimation.from_tgs(self.in_f)
|
255
|
+
else:
|
256
|
+
if isinstance(self.in_f, str):
|
257
|
+
anim = LottieAnimation.from_file(self.in_f)
|
258
|
+
else:
|
259
|
+
anim = LottieAnimation.from_data(self.in_f.read().decode('utf-8'))
|
260
|
+
|
261
|
+
for i in range(anim.lottie_animation_get_totalframe()):
|
262
|
+
frame = np.asarray(anim.render_pillow_frame(frame_num=i))
|
263
|
+
self.frames_raw.append(frame)
|
264
|
+
|
265
|
+
anim.lottie_animation_destroy()
|
266
|
+
|
267
|
+
def frames_resize(self, frames_in: list[np.ndarray]) -> list[np.ndarray]:
|
268
|
+
frames_out = []
|
269
|
+
|
270
|
+
for frame in frames_in:
|
271
|
+
im = Image.fromarray(frame, 'RGBA')
|
272
|
+
width, height = im.size
|
273
|
+
|
274
|
+
if self.res_w == None:
|
275
|
+
self.res_w = width
|
276
|
+
if self.res_h == None:
|
277
|
+
self.res_h = height
|
278
|
+
|
279
|
+
if width > height:
|
280
|
+
width_new = self.res_w
|
281
|
+
height_new = height * self.res_w // width
|
282
|
+
else:
|
283
|
+
height_new = self.res_h
|
284
|
+
width_new = width * self.res_h // height
|
285
|
+
im = im.resize((width_new, height_new), resample=Image.LANCZOS)
|
286
|
+
im_new = Image.new('RGBA', (self.res_w, self.res_h), (0, 0, 0, 0))
|
287
|
+
im_new.paste(
|
288
|
+
im, ((self.res_w - width_new) // 2, (self.res_h - height_new) // 2)
|
289
|
+
)
|
290
|
+
frames_out.append(np.asarray(im_new))
|
291
|
+
|
292
|
+
return frames_out
|
293
|
+
|
294
|
+
def frames_drop(self, frames_in: list[np.ndarray]) -> list[np.ndarray]:
|
295
|
+
if not self.fps:
|
296
|
+
return [frames_in[0]]
|
297
|
+
|
298
|
+
frames_out = []
|
299
|
+
|
300
|
+
# fps_ratio: 1 frame in new anim equal to how many frame in old anim
|
301
|
+
# speed_ratio: How much to speed up / slow down
|
302
|
+
fps_ratio = self.fps_orig / self.fps
|
303
|
+
if (self.opt_comp.duration_min and
|
304
|
+
self.duration_orig < self.opt_comp.duration_min):
|
305
|
+
|
306
|
+
speed_ratio = self.duration_orig / self.opt_comp.duration_min
|
307
|
+
elif (self.opt_comp.duration_max and
|
308
|
+
self.duration_orig > self.opt_comp.duration_max):
|
309
|
+
|
310
|
+
speed_ratio = self.duration_orig / self.opt_comp.duration_max
|
311
|
+
else:
|
312
|
+
speed_ratio = 1
|
313
|
+
|
314
|
+
frame_current = 0
|
315
|
+
frame_current_float = 0
|
316
|
+
while frame_current < len(frames_in):
|
317
|
+
frames_out.append(frames_in[frame_current])
|
318
|
+
frame_current_float += fps_ratio * speed_ratio
|
319
|
+
frame_current = round(frame_current_float)
|
320
|
+
|
321
|
+
return frames_out
|
322
|
+
|
323
|
+
def frames_export(self):
|
324
|
+
if self.out_f_ext in ('.apng', '.png') and self.fps:
|
325
|
+
self.frames_export_apng()
|
326
|
+
elif self.out_f_ext == '.png':
|
327
|
+
self.frames_export_png()
|
328
|
+
elif self.out_f_ext == '.webp' and self.fps:
|
329
|
+
self.frames_export_webp()
|
330
|
+
elif self.fps:
|
331
|
+
self.frames_export_pyav()
|
332
|
+
else:
|
333
|
+
self.frames_export_pil()
|
334
|
+
|
335
|
+
def frames_export_pil(self):
|
336
|
+
image = Image.fromarray(self.frames_processed[0])
|
337
|
+
image.save(
|
338
|
+
self.tmp_f,
|
339
|
+
format=self.out_f_ext.replace('.', ''),
|
340
|
+
quality=self.quality
|
341
|
+
)
|
342
|
+
|
343
|
+
def frames_export_pyav(self):
|
344
|
+
options = {}
|
345
|
+
|
346
|
+
if isinstance(self.quality, int):
|
347
|
+
# Seems not actually working
|
348
|
+
options['quality'] = str(self.quality)
|
349
|
+
options['lossless'] = '0'
|
350
|
+
|
351
|
+
if self.out_f_ext == '.gif':
|
352
|
+
codec = 'gif'
|
353
|
+
pixel_format = 'rgb8'
|
354
|
+
options['loop'] = '0'
|
355
|
+
elif self.out_f_ext in ('.apng', '.png'):
|
356
|
+
codec = 'apng'
|
357
|
+
pixel_format = 'rgba'
|
358
|
+
options['plays'] = '0'
|
359
|
+
else:
|
360
|
+
codec = 'vp9'
|
361
|
+
pixel_format = 'yuva420p'
|
362
|
+
options['loop'] = '0'
|
363
|
+
|
364
|
+
with av.open(self.tmp_f, 'w', format=self.out_f_ext.replace('.', '')) as output:
|
365
|
+
out_stream = output.add_stream(codec, rate=self.fps, options=options)
|
366
|
+
out_stream.width = self.res_w
|
367
|
+
out_stream.height = self.res_h
|
368
|
+
out_stream.pix_fmt = pixel_format
|
369
|
+
|
370
|
+
for frame in self.frames_processed:
|
371
|
+
av_frame = av.VideoFrame.from_ndarray(frame, format='rgba')
|
372
|
+
for packet in out_stream.encode(av_frame):
|
373
|
+
output.mux(packet)
|
374
|
+
|
375
|
+
for packet in out_stream.encode():
|
376
|
+
output.mux(packet)
|
377
|
+
|
378
|
+
def frames_export_webp(self):
|
379
|
+
config = webp.WebPConfig.new(quality=self.quality)
|
380
|
+
enc = webp.WebPAnimEncoder.new(self.res_w, self.res_h)
|
381
|
+
timestamp_ms = 0
|
382
|
+
for frame in self.frames_processed:
|
383
|
+
pic = webp.WebPPicture.from_numpy(frame)
|
384
|
+
enc.encode_frame(pic, timestamp_ms, config=config)
|
385
|
+
timestamp_ms += int(1 / self.fps * 1000)
|
386
|
+
anim_data = enc.assemble(timestamp_ms)
|
387
|
+
self.tmp_f.write(anim_data.buffer())
|
388
|
+
|
389
|
+
def frames_export_png(self):
|
390
|
+
image = Image.fromarray(self.frames_processed[0], 'RGBA')
|
391
|
+
if self.color and self.color <= 256:
|
392
|
+
image_quant = image.quantize(colors=self.color, method=2)
|
393
|
+
else:
|
394
|
+
image_quant = image
|
395
|
+
with io.BytesIO() as f:
|
396
|
+
image_quant.save(f, format='png')
|
397
|
+
f.seek(0)
|
398
|
+
frame_optimized = oxipng.optimize_from_memory(f.read(), level=4)
|
399
|
+
self.tmp_f.write(frame_optimized)
|
400
|
+
|
401
|
+
def frames_export_apng(self):
|
402
|
+
frames_concat = np.concatenate(self.frames_processed)
|
403
|
+
image_concat = Image.fromarray(frames_concat, 'RGBA')
|
404
|
+
if self.color and self.color <= 256:
|
405
|
+
image_quant = image_concat.quantize(colors=self.color, method=2)
|
406
|
+
else:
|
407
|
+
image_quant = image_concat
|
408
|
+
|
409
|
+
for i in range(0, image_quant.height, self.res_h):
|
410
|
+
with io.BytesIO() as f:
|
411
|
+
crop_dimension = (0, i, image_quant.width, i+self.res_h)
|
412
|
+
image_cropped = image_quant.crop(crop_dimension)
|
413
|
+
image_cropped.save(f, format='png')
|
414
|
+
f.seek(0)
|
415
|
+
frame_optimized = oxipng.optimize_from_memory(f.read(), level=4)
|
416
|
+
image_final = Image.open(io.BytesIO(frame_optimized)).convert('RGBA')
|
417
|
+
frame_final = create_frame_from_rgba(
|
418
|
+
np.array(image_final),
|
419
|
+
image_final.width,
|
420
|
+
image_final.height
|
421
|
+
)
|
422
|
+
frame_final.delay_num = int(1000 / self.fps)
|
423
|
+
frame_final.delay_den = 1000
|
424
|
+
self.apngasm.add_frame(frame_final)
|
425
|
+
|
426
|
+
with CacheStore.get_cache_store(path=self.opt_comp.cache_dir) as tempdir:
|
427
|
+
self.apngasm.assemble(os.path.join(tempdir, f'out{self.out_f_ext}'))
|
428
|
+
|
429
|
+
with open(os.path.join(tempdir, f'out{self.out_f_ext}'), 'rb') as f:
|
430
|
+
self.tmp_f.write(f.read())
|
431
|
+
|
432
|
+
self.apngasm.reset()
|