sticker-convert 2.1.5__py3-none-any.whl → 2.1.7__py3-none-any.whl
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/__init__.py +1 -1
- sticker_convert/__main__.py +7 -4
- sticker_convert/cli.py +42 -32
- sticker_convert/converter.py +432 -0
- sticker_convert/downloaders/download_base.py +40 -16
- sticker_convert/downloaders/download_kakao.py +103 -136
- sticker_convert/downloaders/download_line.py +30 -12
- sticker_convert/downloaders/download_signal.py +48 -32
- sticker_convert/downloaders/download_telegram.py +71 -26
- sticker_convert/gui.py +79 -130
- sticker_convert/{gui_frames → gui_components/frames}/comp_frame.py +2 -3
- sticker_convert/{gui_frames → gui_components/frames}/config_frame.py +3 -4
- sticker_convert/{gui_frames → gui_components/frames}/control_frame.py +2 -2
- sticker_convert/{gui_frames → gui_components/frames}/cred_frame.py +4 -4
- sticker_convert/{gui_frames → gui_components/frames}/input_frame.py +4 -4
- sticker_convert/{gui_frames → gui_components/frames}/output_frame.py +3 -3
- sticker_convert/{gui_frames → gui_components/frames}/progress_frame.py +1 -1
- sticker_convert/{utils → gui_components}/gui_utils.py +38 -21
- sticker_convert/{gui_windows → gui_components/windows}/advanced_compression_window.py +3 -2
- sticker_convert/{gui_windows → gui_components/windows}/base_window.py +3 -2
- sticker_convert/{gui_windows → gui_components/windows}/kakao_get_auth_window.py +3 -3
- sticker_convert/{gui_windows → gui_components/windows}/line_get_auth_window.py +2 -2
- sticker_convert/{gui_windows → gui_components/windows}/signal_get_auth_window.py +2 -2
- sticker_convert/{flow.py → job.py} +91 -102
- sticker_convert/job_option.py +301 -0
- sticker_convert/resources/compression.json +1 -1
- sticker_convert/uploaders/compress_wastickers.py +95 -74
- sticker_convert/uploaders/upload_base.py +16 -4
- sticker_convert/uploaders/upload_signal.py +100 -62
- sticker_convert/uploaders/upload_telegram.py +168 -128
- sticker_convert/uploaders/xcode_imessage.py +202 -132
- sticker_convert/{auth → utils/auth}/get_kakao_auth.py +7 -5
- sticker_convert/{auth → utils/auth}/get_line_auth.py +6 -5
- sticker_convert/{auth → utils/auth}/get_signal_auth.py +1 -1
- sticker_convert/utils/fake_cb_msg.py +5 -2
- sticker_convert/utils/{cache_store.py → files/cache_store.py} +7 -3
- sticker_convert/utils/files/dir_utils.py +64 -0
- sticker_convert/utils/{json_manager.py → files/json_manager.py} +5 -4
- sticker_convert/utils/files/metadata_handler.py +226 -0
- sticker_convert/utils/files/run_bin.py +58 -0
- sticker_convert/utils/{apple_png_normalize.py → media/apple_png_normalize.py} +23 -20
- sticker_convert/utils/{codec_info.py → media/codec_info.py} +41 -35
- sticker_convert/utils/media/decrypt_kakao.py +68 -0
- sticker_convert/utils/media/format_verify.py +184 -0
- sticker_convert/utils/url_detect.py +16 -14
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/METADATA +11 -11
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/RECORD +52 -50
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/WHEEL +1 -1
- sticker_convert/utils/converter.py +0 -399
- sticker_convert/utils/curr_dir.py +0 -70
- sticker_convert/utils/format_verify.py +0 -188
- sticker_convert/utils/metadata_handler.py +0 -190
- sticker_convert/utils/run_bin.py +0 -46
- /sticker_convert/{gui_frames → gui_components/frames}/right_clicker.py +0 -0
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/LICENSE +0 -0
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.1.5.dist-info → sticker_convert-2.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
from __future__ import annotations
|
3
|
+
import os
|
4
|
+
import json
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
from ..media.codec_info import CodecInfo # type: ignore
|
8
|
+
from .json_manager import JsonManager # type: ignore
|
9
|
+
|
10
|
+
|
11
|
+
class MetadataHandler:
|
12
|
+
@staticmethod
|
13
|
+
def get_stickers_present(dir: str) -> list[str]:
|
14
|
+
from ...uploaders.xcode_imessage import XcodeImessageIconset # type: ignore
|
15
|
+
|
16
|
+
blacklist_prefix = ('cover',)
|
17
|
+
blacklist_suffix = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store")
|
18
|
+
xcode_iconset = XcodeImessageIconset().iconset
|
19
|
+
|
20
|
+
stickers_present = [
|
21
|
+
i
|
22
|
+
for i in sorted(os.listdir(dir))
|
23
|
+
if os.path.isfile(os.path.join(dir, i)) and
|
24
|
+
not i.startswith(blacklist_prefix) and
|
25
|
+
not i.endswith(blacklist_suffix) and
|
26
|
+
not i in xcode_iconset
|
27
|
+
]
|
28
|
+
|
29
|
+
return stickers_present
|
30
|
+
|
31
|
+
@staticmethod
|
32
|
+
def get_cover(dir: str) -> Optional[str]:
|
33
|
+
stickers_present = sorted(os.listdir(dir))
|
34
|
+
for i in stickers_present:
|
35
|
+
if os.path.splitext(i)[0] == "cover":
|
36
|
+
return os.path.join(dir, i)
|
37
|
+
|
38
|
+
return None
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def get_metadata(
|
42
|
+
dir: str,
|
43
|
+
title: Optional[str] = None,
|
44
|
+
author: Optional[str] = None,
|
45
|
+
emoji_dict: Optional[dict[str, str]] = None,
|
46
|
+
) -> tuple[Optional[str], Optional[str], Optional[dict[str, str]]]:
|
47
|
+
title_path = os.path.join(dir, "title.txt")
|
48
|
+
if not title and os.path.isfile(title_path):
|
49
|
+
with open(title_path, encoding="utf-8") as f:
|
50
|
+
title = f.read().strip()
|
51
|
+
|
52
|
+
author_path = os.path.join(dir, "author.txt")
|
53
|
+
if not author and os.path.isfile(author_path):
|
54
|
+
with open(author_path, encoding="utf-8") as f:
|
55
|
+
author = f.read().strip()
|
56
|
+
|
57
|
+
emoji_path = os.path.join(dir, "emoji.txt")
|
58
|
+
if not emoji_dict and os.path.isfile(emoji_path):
|
59
|
+
with open(emoji_path, "r", encoding="utf-8") as f:
|
60
|
+
emoji_dict = json.load(f)
|
61
|
+
|
62
|
+
return title, author, emoji_dict
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
def set_metadata(
|
66
|
+
dir: str,
|
67
|
+
title: Optional[str] = None,
|
68
|
+
author: Optional[str] = None,
|
69
|
+
emoji_dict: Optional[dict[str, str]] = None,
|
70
|
+
):
|
71
|
+
title_path = os.path.join(dir, "title.txt")
|
72
|
+
if title != None:
|
73
|
+
with open(title_path, "w+", encoding="utf-8") as f:
|
74
|
+
f.write(title) # type: ignore[arg-type]
|
75
|
+
|
76
|
+
author_path = os.path.join(dir, "author.txt")
|
77
|
+
if author != None:
|
78
|
+
with open(author_path, "w+", encoding="utf-8") as f:
|
79
|
+
f.write(author) # type: ignore[arg-type]
|
80
|
+
|
81
|
+
emoji_path = os.path.join(dir, "emoji.txt")
|
82
|
+
if emoji_dict != None:
|
83
|
+
with open(emoji_path, "w+", encoding="utf-8") as f:
|
84
|
+
json.dump(emoji_dict, f, indent=4, ensure_ascii=False)
|
85
|
+
|
86
|
+
@staticmethod
|
87
|
+
def check_metadata_provided(
|
88
|
+
input_dir: str, input_option: str, metadata: str
|
89
|
+
) -> bool:
|
90
|
+
"""
|
91
|
+
Check if metadata provided via .txt file (if from local)
|
92
|
+
or will be provided by input source (if not from local)
|
93
|
+
Does not check if metadata provided via user input in GUI or flag options
|
94
|
+
metadata = 'title' or 'author'
|
95
|
+
"""
|
96
|
+
input_presets = JsonManager.load_json("resources/input.json")
|
97
|
+
|
98
|
+
if input_option == "local":
|
99
|
+
metadata_file_path = os.path.join(input_dir, f"{metadata}.txt")
|
100
|
+
metadata_provided = os.path.isfile(metadata_file_path)
|
101
|
+
if metadata_provided:
|
102
|
+
with open(metadata_file_path, encoding="utf-8") as f:
|
103
|
+
metadata_provided = True if f.read() else False
|
104
|
+
else:
|
105
|
+
metadata_provided = input_presets[input_option]["metadata_provides"][
|
106
|
+
metadata
|
107
|
+
]
|
108
|
+
|
109
|
+
return metadata_provided
|
110
|
+
|
111
|
+
@staticmethod
|
112
|
+
def check_metadata_required(output_option: str, metadata: str) -> bool:
|
113
|
+
# metadata = 'title' or 'author'
|
114
|
+
output_presets = JsonManager.load_json("resources/output.json")
|
115
|
+
return output_presets[output_option]["metadata_requirements"][metadata]
|
116
|
+
|
117
|
+
@staticmethod
|
118
|
+
def generate_emoji_file(dir: str, default_emoji: str = ""):
|
119
|
+
emoji_path = os.path.join(dir, "emoji.txt")
|
120
|
+
emoji_dict = None
|
121
|
+
if os.path.isfile(emoji_path):
|
122
|
+
with open(emoji_path, "r", encoding="utf-8") as f:
|
123
|
+
emoji_dict = json.load(f)
|
124
|
+
|
125
|
+
emoji_dict_new = {}
|
126
|
+
for file in sorted(os.listdir(dir)):
|
127
|
+
if not os.path.isfile(os.path.join(dir, file)) and CodecInfo.get_file_ext(
|
128
|
+
file
|
129
|
+
) in (".txt", ".m4a"):
|
130
|
+
continue
|
131
|
+
file_name = os.path.splitext(file)[0]
|
132
|
+
if emoji_dict and file_name in emoji_dict:
|
133
|
+
emoji_dict_new[file_name] = emoji_dict[file_name]
|
134
|
+
else:
|
135
|
+
emoji_dict_new[file_name] = default_emoji
|
136
|
+
|
137
|
+
with open(emoji_path, "w+", encoding="utf-8") as f:
|
138
|
+
json.dump(emoji_dict_new, f, indent=4, ensure_ascii=False)
|
139
|
+
|
140
|
+
@staticmethod
|
141
|
+
def split_sticker_packs(
|
142
|
+
dir: str,
|
143
|
+
title: str,
|
144
|
+
file_per_pack: Optional[int] = None,
|
145
|
+
file_per_anim_pack: Optional[int] = None,
|
146
|
+
file_per_image_pack: Optional[int] = None,
|
147
|
+
separate_image_anim: bool = True,
|
148
|
+
) -> dict:
|
149
|
+
# {pack_1: [sticker1_path, sticker2_path]}
|
150
|
+
packs = {}
|
151
|
+
|
152
|
+
if file_per_pack == None:
|
153
|
+
file_per_pack = (
|
154
|
+
file_per_anim_pack
|
155
|
+
if file_per_anim_pack != None
|
156
|
+
else file_per_image_pack
|
157
|
+
)
|
158
|
+
else:
|
159
|
+
file_per_anim_pack = file_per_pack
|
160
|
+
file_per_image_pack = file_per_pack
|
161
|
+
|
162
|
+
stickers_present = MetadataHandler.get_stickers_present(dir)
|
163
|
+
|
164
|
+
processed = 0
|
165
|
+
|
166
|
+
if separate_image_anim == True:
|
167
|
+
image_stickers = []
|
168
|
+
anim_stickers = []
|
169
|
+
|
170
|
+
image_pack_count = 0
|
171
|
+
anim_pack_count = 0
|
172
|
+
|
173
|
+
anim_present = False
|
174
|
+
image_present = False
|
175
|
+
|
176
|
+
for processed, file in enumerate(stickers_present):
|
177
|
+
file_path = os.path.join(dir, file)
|
178
|
+
|
179
|
+
if CodecInfo.is_anim(file_path):
|
180
|
+
anim_stickers.append(file_path)
|
181
|
+
else:
|
182
|
+
image_stickers.append(file_path)
|
183
|
+
|
184
|
+
anim_present = anim_present or len(anim_stickers) > 0
|
185
|
+
image_present = image_present or len(image_stickers) > 0
|
186
|
+
|
187
|
+
finished_all = True if processed == len(stickers_present) else False
|
188
|
+
|
189
|
+
if len(anim_stickers) == file_per_anim_pack or (
|
190
|
+
finished_all and len(anim_stickers) > 0
|
191
|
+
):
|
192
|
+
suffix = f'{"-anim" if image_present else ""}{"-" + str(anim_pack_count) if anim_pack_count > 0 else ""}'
|
193
|
+
title_current = str(title) + suffix
|
194
|
+
packs[title_current] = anim_stickers.copy()
|
195
|
+
anim_stickers = []
|
196
|
+
anim_pack_count += 1
|
197
|
+
if len(image_stickers) == file_per_image_pack or (
|
198
|
+
finished_all and len(image_stickers) > 0
|
199
|
+
):
|
200
|
+
suffix = f'{"-image" if anim_present else ""}{"-" + str(image_pack_count) if image_pack_count > 0 else ""}'
|
201
|
+
title_current = str(title) + suffix
|
202
|
+
packs[title_current] = image_stickers.copy()
|
203
|
+
image_stickers = []
|
204
|
+
image_pack_count += 1
|
205
|
+
|
206
|
+
else:
|
207
|
+
stickers = []
|
208
|
+
pack_count = 0
|
209
|
+
|
210
|
+
for processed, file in enumerate(stickers_present):
|
211
|
+
file_path = os.path.join(dir, file)
|
212
|
+
|
213
|
+
stickers.append(file_path)
|
214
|
+
|
215
|
+
finished_all = True if processed == len(stickers_present) else False
|
216
|
+
|
217
|
+
if len(stickers) == file_per_pack or (
|
218
|
+
finished_all and len(stickers) > 0
|
219
|
+
):
|
220
|
+
suffix = f'{"-" + str(pack_count) if pack_count > 0 else ""}'
|
221
|
+
title_current = str(title) + suffix
|
222
|
+
packs[title_current] = stickers.copy()
|
223
|
+
stickers = []
|
224
|
+
pack_count += 1
|
225
|
+
|
226
|
+
return packs
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import subprocess
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
import platform
|
6
|
+
from typing import Union, AnyStr
|
7
|
+
|
8
|
+
|
9
|
+
class RunBin:
|
10
|
+
@staticmethod
|
11
|
+
def get_bin(
|
12
|
+
bin: str, silent: bool = False, cb_msg=print
|
13
|
+
) -> Union[str, AnyStr, None]:
|
14
|
+
if os.path.isfile(bin):
|
15
|
+
return bin
|
16
|
+
|
17
|
+
if platform.system() == "Windows":
|
18
|
+
bin = bin + ".exe"
|
19
|
+
|
20
|
+
which_result = shutil.which(bin)
|
21
|
+
if which_result != None:
|
22
|
+
return os.path.abspath(which_result) # type: ignore[type-var]
|
23
|
+
elif silent == False:
|
24
|
+
cb_msg(f"Warning: Cannot find binary file {bin}")
|
25
|
+
|
26
|
+
return None
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def run_cmd(
|
30
|
+
cmd_list: list[str], silence: bool = False, cb_msg=print
|
31
|
+
) -> Union[bool, str]:
|
32
|
+
bin_path = RunBin.get_bin(cmd_list[0]) # type: ignore[assignment]
|
33
|
+
|
34
|
+
if bin_path:
|
35
|
+
cmd_list[0] = bin_path
|
36
|
+
else:
|
37
|
+
if silence == False:
|
38
|
+
cb_msg(
|
39
|
+
f"Error while executing {' '.join(cmd_list)} : Command not found"
|
40
|
+
)
|
41
|
+
return False
|
42
|
+
|
43
|
+
# sp = subprocess.Popen(cmd_list, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
44
|
+
sp = subprocess.run(
|
45
|
+
cmd_list,
|
46
|
+
stdin=subprocess.PIPE,
|
47
|
+
stdout=subprocess.PIPE,
|
48
|
+
stderr=subprocess.PIPE,
|
49
|
+
)
|
50
|
+
|
51
|
+
output_str = sp.stdout.decode()
|
52
|
+
error_str = sp.stderr.decode()
|
53
|
+
|
54
|
+
if silence == False and error_str != "":
|
55
|
+
cb_msg(f"Error while executing {' '.join(cmd_list)} : {error_str}")
|
56
|
+
return False
|
57
|
+
|
58
|
+
return output_str
|
@@ -1,11 +1,12 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
|
2
|
+
"""
|
3
3
|
Required for pack_id < 775 (https://github.com/star-39/moe-sticker-bot/blob/ef2f06f3eb7a833e011e0a5201e007fc130978e7/pkg/msbimport/import_line.go#L68)
|
4
4
|
Reference: https://stackoverflow.com/a/53622211
|
5
|
-
|
5
|
+
"""
|
6
6
|
import struct
|
7
7
|
import zlib
|
8
8
|
|
9
|
+
|
9
10
|
class ApplePngNormalize:
|
10
11
|
@staticmethod
|
11
12
|
def normalize(old_png: bytes) -> bytes:
|
@@ -23,15 +24,14 @@ class ApplePngNormalize:
|
|
23
24
|
|
24
25
|
# For each chunk in the PNG file
|
25
26
|
while chunk_pos < len(old_png):
|
26
|
-
|
27
27
|
# Reading chunk
|
28
|
-
chunk_length = old_png[chunk_pos:chunk_pos+4]
|
28
|
+
chunk_length = old_png[chunk_pos : chunk_pos + 4]
|
29
29
|
chunk_length = struct.unpack(">L", chunk_length)[0]
|
30
|
-
chunk_type = old_png[chunk_pos+4 : chunk_pos+8]
|
31
|
-
chunk_data = old_png[chunk_pos+8:chunk_pos+8+chunk_length]
|
32
|
-
chunk_crc = old_png[chunk_pos+chunk_length+8:chunk_pos+chunk_length+12]
|
30
|
+
chunk_type = old_png[chunk_pos + 4 : chunk_pos + 8]
|
31
|
+
chunk_data = old_png[chunk_pos + 8 : chunk_pos + 8 + chunk_length] # type: ignore[operator]
|
32
|
+
chunk_crc = old_png[chunk_pos + chunk_length + 8 : chunk_pos + chunk_length + 12] # type: ignore[operator]
|
33
33
|
chunk_crc = struct.unpack(">L", chunk_crc)[0]
|
34
|
-
chunk_pos += chunk_length + 12
|
34
|
+
chunk_pos += chunk_length + 12 # type: ignore[operator]
|
35
35
|
|
36
36
|
# Parsing the header chunk
|
37
37
|
if chunk_type == b"IHDR":
|
@@ -57,25 +57,28 @@ class ApplePngNormalize:
|
|
57
57
|
offset = 1
|
58
58
|
for y in range(height):
|
59
59
|
for x in range(width):
|
60
|
-
chunk_data[offset+4*x],chunk_data[offset+4*x+2] =
|
61
|
-
|
60
|
+
chunk_data[offset + 4 * x], chunk_data[offset + 4 * x + 2] = (
|
61
|
+
chunk_data[offset + 4 * x + 2],
|
62
|
+
chunk_data[offset + 4 * x],
|
63
|
+
)
|
64
|
+
offset += 1 + 4 * width
|
62
65
|
|
63
66
|
# Compressing the image chunk
|
64
|
-
#chunk_data = newdata
|
67
|
+
# chunk_data = newdata
|
65
68
|
chunk_data = zlib.compress(chunk_data)
|
66
|
-
chunk_length = len(chunk_data)
|
67
|
-
chunk_crc = zlib.crc32(b
|
68
|
-
chunk_crc = zlib.crc32(chunk_data, chunk_crc)
|
69
|
-
chunk_crc = (chunk_crc + 0x100000000) % 0x100000000
|
69
|
+
chunk_length = len(chunk_data) # type: ignore[assignment]
|
70
|
+
chunk_crc = zlib.crc32(b"IDAT") # type: ignore[assignment]
|
71
|
+
chunk_crc = zlib.crc32(chunk_data, chunk_crc) # type: ignore[assignment, arg-type]
|
72
|
+
chunk_crc = (chunk_crc + 0x100000000) % 0x100000000 # type: ignore[operator]
|
70
73
|
|
71
74
|
new_png += struct.pack(">L", chunk_length)
|
72
|
-
new_png += b
|
75
|
+
new_png += b"IDAT"
|
73
76
|
new_png += chunk_data
|
74
77
|
new_png += struct.pack(">L", chunk_crc)
|
75
78
|
|
76
|
-
chunk_crc = zlib.crc32(chunk_type)
|
79
|
+
chunk_crc = zlib.crc32(chunk_type) # type: ignore[assignment]
|
77
80
|
new_png += struct.pack(">L", 0)
|
78
|
-
new_png += b
|
81
|
+
new_png += b"IEND"
|
79
82
|
new_png += struct.pack(">L", chunk_crc)
|
80
83
|
break
|
81
84
|
|
@@ -85,8 +88,8 @@ class ApplePngNormalize:
|
|
85
88
|
else:
|
86
89
|
new_png += struct.pack(">L", chunk_length)
|
87
90
|
new_png += chunk_type
|
88
|
-
if chunk_length > 0:
|
91
|
+
if chunk_length > 0: # type: ignore[operator]
|
89
92
|
new_png += chunk_data
|
90
93
|
new_png += struct.pack(">L", chunk_crc)
|
91
94
|
|
92
|
-
return new_png
|
95
|
+
return new_png
|
@@ -1,103 +1,109 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
+
from __future__ import annotations
|
2
3
|
import os
|
3
4
|
import mimetypes
|
4
5
|
from typing import Optional
|
5
6
|
|
6
7
|
import imageio.v3 as iio
|
7
|
-
from rlottie_python import LottieAnimation
|
8
|
+
from rlottie_python import LottieAnimation # type: ignore
|
8
9
|
from PIL import Image
|
9
10
|
import mmap
|
10
11
|
|
12
|
+
|
11
13
|
class CodecInfo:
|
12
14
|
def __init__(self):
|
13
15
|
mimetypes.init()
|
14
16
|
vid_ext = []
|
15
17
|
for ext in mimetypes.types_map:
|
16
|
-
if mimetypes.types_map[ext].split(
|
18
|
+
if mimetypes.types_map[ext].split("/")[0] == "video":
|
17
19
|
vid_ext.append(ext)
|
18
|
-
vid_ext.append(
|
19
|
-
vid_ext.append(
|
20
|
+
vid_ext.append(".webm")
|
21
|
+
vid_ext.append(".webp")
|
20
22
|
self.vid_ext = tuple(vid_ext)
|
21
23
|
|
22
24
|
@staticmethod
|
23
25
|
def get_file_fps(file: str) -> float:
|
24
26
|
file_ext = CodecInfo.get_file_ext(file)
|
25
27
|
|
26
|
-
if file_ext in
|
28
|
+
if file_ext in ".tgs":
|
27
29
|
with LottieAnimation.from_tgs(file) as anim:
|
28
30
|
fps = anim.lottie_animation_get_framerate()
|
29
31
|
else:
|
30
|
-
if file_ext ==
|
31
|
-
with open(file,
|
32
|
+
if file_ext == ".webp":
|
33
|
+
with open(file, "r+b") as f:
|
32
34
|
mm = mmap.mmap(f.fileno(), 0)
|
33
|
-
anmf_pos = mm.find(b
|
35
|
+
anmf_pos = mm.find(b"ANMF")
|
34
36
|
if anmf_pos == -1:
|
35
37
|
return 1
|
36
|
-
mm.seek(anmf_pos+20)
|
38
|
+
mm.seek(anmf_pos + 20)
|
37
39
|
frame_duration_32 = mm.read(4)
|
38
|
-
frame_duration = frame_duration_32[:-1] + bytes(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
frame_duration = frame_duration_32[:-1] + bytes(
|
41
|
+
int(frame_duration_32[-1]) & 0b11111100
|
42
|
+
)
|
43
|
+
fps = 1000 / int.from_bytes(frame_duration, "little")
|
44
|
+
elif file_ext in (".gif", ".apng", ".png"):
|
45
|
+
metadata = iio.immeta(
|
46
|
+
file, index=0, plugin="pillow", exclude_applied=False
|
47
|
+
)
|
48
|
+
fps = int(1000 / metadata.get("duration", 1000))
|
43
49
|
else:
|
44
|
-
metadata = iio.immeta(file, plugin=
|
45
|
-
fps = metadata.get(
|
50
|
+
metadata = iio.immeta(file, plugin="pyav", exclude_applied=False)
|
51
|
+
fps = metadata.get("fps", 1)
|
46
52
|
|
47
53
|
return fps
|
48
|
-
|
54
|
+
|
49
55
|
@staticmethod
|
50
56
|
def get_file_codec(file: str) -> Optional[str]:
|
51
57
|
file_ext = CodecInfo.get_file_ext(file)
|
52
58
|
|
53
|
-
if file_ext ==
|
54
|
-
plugin =
|
59
|
+
if file_ext == ".webp":
|
60
|
+
plugin = "pillow"
|
55
61
|
else:
|
56
|
-
plugin =
|
62
|
+
plugin = "pyav"
|
57
63
|
metadata = iio.immeta(file, plugin=plugin, exclude_applied=False)
|
58
|
-
codec = metadata.get(
|
64
|
+
codec = metadata.get("codec", None)
|
59
65
|
|
60
66
|
return codec
|
61
|
-
|
67
|
+
|
62
68
|
@staticmethod
|
63
69
|
def get_file_res(file: str) -> tuple[int, int]:
|
64
70
|
file_ext = CodecInfo.get_file_ext(file)
|
65
71
|
|
66
|
-
if file_ext ==
|
72
|
+
if file_ext == ".tgs":
|
67
73
|
with LottieAnimation.from_tgs(file) as anim:
|
68
74
|
width, height = anim.lottie_animation_get_size()
|
69
75
|
else:
|
70
|
-
if file_ext ==
|
71
|
-
plugin =
|
76
|
+
if file_ext == ".webp":
|
77
|
+
plugin = "pillow"
|
72
78
|
else:
|
73
|
-
plugin =
|
79
|
+
plugin = "pyav"
|
74
80
|
frame = iio.imread(file, plugin=plugin, index=0)
|
75
81
|
width, height, _ = frame.shape
|
76
|
-
|
82
|
+
|
77
83
|
return width, height
|
78
|
-
|
84
|
+
|
79
85
|
@staticmethod
|
80
86
|
def get_file_frames(file: str) -> int:
|
81
87
|
file_ext = CodecInfo.get_file_ext(file)
|
82
88
|
|
83
89
|
frames = None
|
84
90
|
|
85
|
-
if file_ext ==
|
91
|
+
if file_ext == ".tgs":
|
86
92
|
with LottieAnimation.from_tgs(file) as anim:
|
87
93
|
frames = anim.lottie_animation_get_totalframe()
|
88
94
|
else:
|
89
|
-
if file_ext ==
|
95
|
+
if file_ext == ".webp":
|
90
96
|
frames = Image.open(file).n_frames
|
91
97
|
else:
|
92
|
-
frames = len(iio.imread(file, plugin=
|
93
|
-
|
98
|
+
frames = len(iio.imread(file, plugin="pyav"))
|
99
|
+
|
94
100
|
return frames
|
95
|
-
|
101
|
+
|
96
102
|
@staticmethod
|
97
103
|
def get_file_duration(file: str) -> float:
|
98
104
|
# Return duration in miliseconds
|
99
105
|
return CodecInfo.get_file_frames(file) / CodecInfo.get_file_fps(file) * 1000
|
100
|
-
|
106
|
+
|
101
107
|
@staticmethod
|
102
108
|
def get_file_ext(file: str) -> str:
|
103
109
|
return os.path.splitext(file)[-1].lower()
|
@@ -107,4 +113,4 @@ class CodecInfo:
|
|
107
113
|
if CodecInfo.get_file_frames(file) > 1:
|
108
114
|
return True
|
109
115
|
else:
|
110
|
-
return False
|
116
|
+
return False
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
from __future__ import annotations
|
3
|
+
"""
|
4
|
+
References:
|
5
|
+
https://github.com/blluv/KakaoTalkEmoticonDownloader
|
6
|
+
https://github.com/star-39/moe-sticker-bot
|
7
|
+
"""
|
8
|
+
|
9
|
+
class DecryptKakao:
|
10
|
+
@staticmethod
|
11
|
+
def generate_lfsr(key: str) -> list[int]:
|
12
|
+
d = list(key * 2)
|
13
|
+
seq = [0, 0, 0]
|
14
|
+
|
15
|
+
seq[0] = 301989938
|
16
|
+
seq[1] = 623357073
|
17
|
+
seq[2] = -2004086252
|
18
|
+
|
19
|
+
i = 0
|
20
|
+
|
21
|
+
for i in range(0, 4):
|
22
|
+
seq[0] = ord(d[i]) | (seq[0] << 8)
|
23
|
+
seq[1] = ord(d[4 + i]) | (seq[1] << 8)
|
24
|
+
seq[2] = ord(d[8 + i]) | (seq[2] << 8)
|
25
|
+
|
26
|
+
seq[0] = seq[0] & 0xFFFFFFFF
|
27
|
+
seq[1] = seq[1] & 0xFFFFFFFF
|
28
|
+
seq[2] = seq[2] & 0xFFFFFFFF
|
29
|
+
|
30
|
+
return seq
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def xor_byte(b: int, seq: list) -> int:
|
34
|
+
flag1 = 1
|
35
|
+
flag2 = 0
|
36
|
+
result = 0
|
37
|
+
for _ in range(0, 8):
|
38
|
+
v10 = seq[0] >> 1
|
39
|
+
if (seq[0] << 31) & 0xFFFFFFFF:
|
40
|
+
seq[0] = v10 ^ 0xC0000031
|
41
|
+
v12 = seq[1] >> 1
|
42
|
+
if (seq[1] << 31) & 0xFFFFFFFF:
|
43
|
+
seq[1] = (v12 | 0xC0000000) ^ 0x20000010
|
44
|
+
flag1 = 1
|
45
|
+
else:
|
46
|
+
seq[1] = v12 & 0x3FFFFFFF
|
47
|
+
flag1 = 0
|
48
|
+
else:
|
49
|
+
seq[0] = v10
|
50
|
+
v11 = seq[2] >> 1
|
51
|
+
if (seq[2] << 31) & 0xFFFFFFFF:
|
52
|
+
seq[2] = (v11 | 0xF0000000) ^ 0x8000001
|
53
|
+
flag2 = 1
|
54
|
+
else:
|
55
|
+
seq[2] = v11 & 0xFFFFFFF
|
56
|
+
flag2 = 0
|
57
|
+
|
58
|
+
result = flag1 ^ flag2 | 2 * result
|
59
|
+
return result ^ b
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def xor_data(data: bytes) -> bytes:
|
63
|
+
dat = list(data)
|
64
|
+
s = DecryptKakao.generate_lfsr("a271730728cbe141e47fd9d677e9006d")
|
65
|
+
for i in range(0, 128):
|
66
|
+
dat[i] = DecryptKakao.xor_byte(dat[i], s)
|
67
|
+
return bytes(dat)
|
68
|
+
|