sticker-convert 2.1.6__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.
Files changed (57) hide show
  1. sticker_convert/__init__.py +1 -1
  2. sticker_convert/__main__.py +7 -4
  3. sticker_convert/cli.py +39 -31
  4. sticker_convert/converter.py +432 -0
  5. sticker_convert/downloaders/download_base.py +40 -16
  6. sticker_convert/downloaders/download_kakao.py +103 -136
  7. sticker_convert/downloaders/download_line.py +16 -6
  8. sticker_convert/downloaders/download_signal.py +48 -32
  9. sticker_convert/downloaders/download_telegram.py +71 -26
  10. sticker_convert/gui.py +78 -129
  11. sticker_convert/{gui_frames → gui_components/frames}/comp_frame.py +2 -3
  12. sticker_convert/{gui_frames → gui_components/frames}/config_frame.py +3 -4
  13. sticker_convert/{gui_frames → gui_components/frames}/control_frame.py +2 -2
  14. sticker_convert/{gui_frames → gui_components/frames}/cred_frame.py +4 -4
  15. sticker_convert/{gui_frames → gui_components/frames}/input_frame.py +4 -4
  16. sticker_convert/{gui_frames → gui_components/frames}/output_frame.py +3 -3
  17. sticker_convert/{gui_frames → gui_components/frames}/progress_frame.py +1 -1
  18. sticker_convert/{utils → gui_components}/gui_utils.py +38 -21
  19. sticker_convert/{gui_windows → gui_components/windows}/advanced_compression_window.py +3 -2
  20. sticker_convert/{gui_windows → gui_components/windows}/base_window.py +3 -2
  21. sticker_convert/{gui_windows → gui_components/windows}/kakao_get_auth_window.py +3 -3
  22. sticker_convert/{gui_windows → gui_components/windows}/line_get_auth_window.py +2 -2
  23. sticker_convert/{gui_windows → gui_components/windows}/signal_get_auth_window.py +2 -2
  24. sticker_convert/{flow.py → job.py} +91 -102
  25. sticker_convert/job_option.py +301 -0
  26. sticker_convert/resources/compression.json +1 -1
  27. sticker_convert/uploaders/compress_wastickers.py +95 -74
  28. sticker_convert/uploaders/upload_base.py +16 -4
  29. sticker_convert/uploaders/upload_signal.py +100 -62
  30. sticker_convert/uploaders/upload_telegram.py +168 -128
  31. sticker_convert/uploaders/xcode_imessage.py +202 -132
  32. sticker_convert/{auth → utils/auth}/get_kakao_auth.py +7 -5
  33. sticker_convert/{auth → utils/auth}/get_line_auth.py +3 -3
  34. sticker_convert/{auth → utils/auth}/get_signal_auth.py +1 -1
  35. sticker_convert/utils/fake_cb_msg.py +5 -2
  36. sticker_convert/utils/{cache_store.py → files/cache_store.py} +7 -3
  37. sticker_convert/utils/files/dir_utils.py +64 -0
  38. sticker_convert/utils/{json_manager.py → files/json_manager.py} +5 -4
  39. sticker_convert/utils/files/metadata_handler.py +226 -0
  40. sticker_convert/utils/files/run_bin.py +58 -0
  41. sticker_convert/utils/{apple_png_normalize.py → media/apple_png_normalize.py} +23 -20
  42. sticker_convert/utils/{codec_info.py → media/codec_info.py} +41 -35
  43. sticker_convert/utils/media/decrypt_kakao.py +68 -0
  44. sticker_convert/utils/media/format_verify.py +184 -0
  45. sticker_convert/utils/url_detect.py +16 -14
  46. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/METADATA +8 -9
  47. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/RECORD +52 -50
  48. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/WHEEL +1 -1
  49. sticker_convert/utils/converter.py +0 -407
  50. sticker_convert/utils/curr_dir.py +0 -70
  51. sticker_convert/utils/format_verify.py +0 -188
  52. sticker_convert/utils/metadata_handler.py +0 -190
  53. sticker_convert/utils/run_bin.py +0 -46
  54. /sticker_convert/{gui_frames → gui_components/frames}/right_clicker.py +0 -0
  55. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/LICENSE +0 -0
  56. {sticker_convert-2.1.6.dist-info → sticker_convert-2.1.7.dist-info}/entry_points.txt +0 -0
  57. {sticker_convert-2.1.6.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] # type: ignore[operator]
32
- chunk_crc = old_png[chunk_pos+chunk_length+8:chunk_pos+chunk_length+12] # type: ignore[operator]
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 # type: ignore[operator]
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] = chunk_data[offset+4*x+2],chunk_data[offset+4*x]
61
- offset += 1+4*width
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) # type: ignore[assignment]
67
- chunk_crc = zlib.crc32(b'IDAT') # type: ignore[assignment]
68
- chunk_crc = zlib.crc32(chunk_data, chunk_crc) # type: ignore[assignment, arg-type]
69
- chunk_crc = (chunk_crc + 0x100000000) % 0x100000000 # type: ignore[operator]
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'IDAT'
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) # type: ignore[assignment]
79
+ chunk_crc = zlib.crc32(chunk_type) # type: ignore[assignment]
77
80
  new_png += struct.pack(">L", 0)
78
- new_png += b'IEND'
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: # type: ignore[operator]
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 # type: ignore
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('/')[0] == 'video':
18
+ if mimetypes.types_map[ext].split("/")[0] == "video":
17
19
  vid_ext.append(ext)
18
- vid_ext.append('.webm')
19
- vid_ext.append('.webp')
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 '.tgs':
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 == '.webp':
31
- with open(file, 'r+b') as f:
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'ANMF')
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(int(frame_duration_32[-1]) & 0b11111100)
39
- fps = 1000 / int.from_bytes(frame_duration, 'little')
40
- elif file_ext in ('.gif', '.apng', '.png'):
41
- metadata = iio.immeta(file, index=0, plugin='pillow', exclude_applied=False)
42
- fps = int(1000 / metadata.get('duration', 1000))
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='pyav', exclude_applied=False)
45
- fps = metadata.get('fps', 1)
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 == '.webp':
54
- plugin = 'pillow'
59
+ if file_ext == ".webp":
60
+ plugin = "pillow"
55
61
  else:
56
- plugin = 'pyav'
62
+ plugin = "pyav"
57
63
  metadata = iio.immeta(file, plugin=plugin, exclude_applied=False)
58
- codec = metadata.get('codec', None)
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 == '.tgs':
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 == '.webp':
71
- plugin = 'pillow'
76
+ if file_ext == ".webp":
77
+ plugin = "pillow"
72
78
  else:
73
- plugin = 'pyav'
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 == '.tgs':
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 == '.webp':
95
+ if file_ext == ".webp":
90
96
  frames = Image.open(file).n_frames
91
97
  else:
92
- frames = len(iio.imread(file, plugin='pyav'))
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
+