sticker-convert 2.7.2__py3-none-any.whl → 2.7.4__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 -0
- sticker_convert/__main__.py +3 -1
- sticker_convert/cli.py +20 -24
- sticker_convert/converter.py +108 -119
- sticker_convert/definitions.py +8 -12
- sticker_convert/downloaders/download_base.py +14 -31
- sticker_convert/downloaders/download_kakao.py +25 -39
- sticker_convert/downloaders/download_line.py +24 -33
- sticker_convert/downloaders/download_signal.py +7 -16
- sticker_convert/downloaders/download_telegram.py +6 -15
- sticker_convert/gui.py +53 -61
- sticker_convert/gui_components/frames/comp_frame.py +11 -20
- sticker_convert/gui_components/frames/config_frame.py +9 -9
- sticker_convert/gui_components/frames/control_frame.py +3 -3
- sticker_convert/gui_components/frames/cred_frame.py +12 -18
- sticker_convert/gui_components/frames/input_frame.py +9 -15
- sticker_convert/gui_components/frames/output_frame.py +9 -15
- sticker_convert/gui_components/frames/progress_frame.py +8 -8
- sticker_convert/gui_components/frames/right_clicker.py +2 -2
- sticker_convert/gui_components/gui_utils.py +6 -8
- sticker_convert/gui_components/windows/advanced_compression_window.py +23 -32
- sticker_convert/gui_components/windows/base_window.py +6 -6
- sticker_convert/gui_components/windows/kakao_get_auth_window.py +5 -11
- sticker_convert/gui_components/windows/line_get_auth_window.py +5 -5
- sticker_convert/gui_components/windows/signal_get_auth_window.py +6 -6
- sticker_convert/job.py +84 -90
- sticker_convert/job_option.py +36 -32
- sticker_convert/resources/emoji.json +334 -70
- sticker_convert/resources/help.json +1 -1
- sticker_convert/uploaders/compress_wastickers.py +19 -30
- sticker_convert/uploaders/upload_base.py +19 -13
- sticker_convert/uploaders/upload_signal.py +20 -33
- sticker_convert/uploaders/upload_telegram.py +21 -28
- sticker_convert/uploaders/xcode_imessage.py +30 -95
- sticker_convert/utils/auth/get_kakao_auth.py +7 -8
- sticker_convert/utils/auth/get_line_auth.py +5 -6
- sticker_convert/utils/auth/get_signal_auth.py +7 -7
- sticker_convert/utils/callback.py +31 -23
- sticker_convert/utils/files/cache_store.py +6 -8
- sticker_convert/utils/files/json_manager.py +6 -7
- sticker_convert/utils/files/json_resources_loader.py +12 -0
- sticker_convert/utils/files/metadata_handler.py +93 -84
- sticker_convert/utils/files/run_bin.py +11 -10
- sticker_convert/utils/files/sanitize_filename.py +30 -28
- sticker_convert/utils/media/apple_png_normalize.py +3 -2
- sticker_convert/utils/media/codec_info.py +41 -44
- sticker_convert/utils/media/decrypt_kakao.py +7 -7
- sticker_convert/utils/media/format_verify.py +14 -14
- sticker_convert/utils/url_detect.py +4 -5
- sticker_convert/version.py +2 -1
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/METADATA +19 -17
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/RECORD +56 -55
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/WHEEL +1 -1
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/LICENSE +0 -0
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/top_level.txt +0 -0
sticker_convert/__init__.py
CHANGED
sticker_convert/__main__.py
CHANGED
sticker_convert/cli.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
import argparse
|
3
3
|
import signal
|
4
|
+
import sys
|
4
5
|
from argparse import Namespace
|
5
6
|
from json.decoder import JSONDecodeError
|
6
7
|
from math import ceil
|
7
8
|
from multiprocessing import cpu_count
|
8
9
|
from pathlib import Path
|
9
|
-
from typing import Any
|
10
|
+
from typing import Any, Dict
|
10
11
|
|
11
|
-
from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR
|
12
|
+
from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR
|
12
13
|
from sticker_convert.job import Job
|
13
14
|
from sticker_convert.job_option import CompOption, CredOption, InputOption, OutputOption
|
14
15
|
from sticker_convert.utils.auth.get_kakao_auth import GetKakaoAuth
|
@@ -21,27 +22,22 @@ from sticker_convert.version import __version__
|
|
21
22
|
|
22
23
|
|
23
24
|
class CLI:
|
24
|
-
def __init__(self):
|
25
|
+
def __init__(self) -> None:
|
25
26
|
self.cb = Callback()
|
26
27
|
|
27
|
-
def cli(self):
|
28
|
+
def cli(self) -> None:
|
28
29
|
try:
|
29
|
-
|
30
|
-
ROOT_DIR / "resources/help.json"
|
31
|
-
)
|
32
|
-
self.input_presets = JsonManager.load_json(
|
33
|
-
ROOT_DIR / "resources/input.json"
|
34
|
-
)
|
35
|
-
self.compression_presets = JsonManager.load_json(
|
36
|
-
ROOT_DIR / "resources/compression.json"
|
37
|
-
)
|
38
|
-
self.output_presets = JsonManager.load_json(
|
39
|
-
ROOT_DIR / "resources/output.json"
|
40
|
-
)
|
30
|
+
from sticker_convert.utils.files.json_resources_loader import COMPRESSION_JSON, EMOJI_JSON, HELP_JSON, INPUT_JSON, OUTPUT_JSON
|
41
31
|
except RuntimeError as e:
|
42
|
-
self.cb.msg(e
|
32
|
+
self.cb.msg(str(e))
|
43
33
|
return
|
44
34
|
|
35
|
+
self.help = HELP_JSON
|
36
|
+
self.input_presets = INPUT_JSON
|
37
|
+
self.compression_presets = COMPRESSION_JSON
|
38
|
+
self.output_presets = OUTPUT_JSON
|
39
|
+
self.emoji_list = EMOJI_JSON
|
40
|
+
|
45
41
|
parser = argparse.ArgumentParser(
|
46
42
|
description="CLI for stickers-convert",
|
47
43
|
formatter_class=argparse.RawTextHelpFormatter,
|
@@ -125,7 +121,7 @@ class CLI:
|
|
125
121
|
"quantize_method",
|
126
122
|
)
|
127
123
|
flags_comp_bool = ("fake_vid",)
|
128
|
-
keyword_args:
|
124
|
+
keyword_args: Dict[str, Any]
|
129
125
|
for k, v in self.help["comp"].items():
|
130
126
|
if k in flags_comp_int:
|
131
127
|
keyword_args = {"type": int, "default": None}
|
@@ -186,7 +182,7 @@ class CLI:
|
|
186
182
|
|
187
183
|
signal.signal(signal.SIGINT, job.cancel)
|
188
184
|
status = job.start()
|
189
|
-
exit(status)
|
185
|
+
sys.exit(status)
|
190
186
|
|
191
187
|
def get_opt_input(self, args: Namespace) -> InputOption:
|
192
188
|
download_options = {
|
@@ -212,7 +208,7 @@ class CLI:
|
|
212
208
|
self.cb.msg(f"Detected URL input source: {download_option}")
|
213
209
|
else:
|
214
210
|
self.cb.msg(f"Error: Unrecognied URL input source for url: {url}")
|
215
|
-
exit()
|
211
|
+
sys.exit()
|
216
212
|
|
217
213
|
opt_input = InputOption(
|
218
214
|
option=download_option,
|
@@ -296,15 +292,15 @@ class CLI:
|
|
296
292
|
size_max_vid=self.compression_presets[preset]["size_max"]["vid"]
|
297
293
|
if args.vid_size_max is None
|
298
294
|
else args.vid_size_max,
|
299
|
-
format_img=
|
295
|
+
format_img=(
|
300
296
|
self.compression_presets[preset]["format"]["img"]
|
301
297
|
if args.img_format is None
|
302
|
-
else args.img_format
|
298
|
+
else args.img_format,
|
303
299
|
),
|
304
|
-
format_vid=
|
300
|
+
format_vid=(
|
305
301
|
self.compression_presets[preset]["format"]["vid"]
|
306
302
|
if args.vid_format is None
|
307
|
-
else args.vid_format
|
303
|
+
else args.vid_format,
|
308
304
|
),
|
309
305
|
fps_min=self.compression_presets[preset]["fps"]["min"]
|
310
306
|
if args.fps_min is None
|
sticker_convert/converter.py
CHANGED
@@ -6,26 +6,41 @@ from io import BytesIO
|
|
6
6
|
from math import ceil, floor
|
7
7
|
from pathlib import Path
|
8
8
|
from queue import Queue
|
9
|
-
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
|
9
|
+
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, Union, cast
|
10
10
|
|
11
11
|
import numpy as np
|
12
12
|
from PIL import Image
|
13
13
|
|
14
|
-
if TYPE_CHECKING:
|
15
|
-
from av.video.plane import VideoPlane
|
16
|
-
|
17
14
|
from sticker_convert.job_option import CompOption
|
18
|
-
from sticker_convert.utils.callback import Callback, CallbackReturn
|
15
|
+
from sticker_convert.utils.callback import Callback, CallbackReturn, CbQueueItemType
|
19
16
|
from sticker_convert.utils.files.cache_store import CacheStore
|
20
17
|
from sticker_convert.utils.media.codec_info import CodecInfo
|
21
18
|
from sticker_convert.utils.media.format_verify import FormatVerify
|
22
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from av.video.plane import VideoPlane # type: ignore
|
22
|
+
|
23
|
+
MSG_START_COMP = "[I] Start compressing {} -> {}"
|
24
|
+
MSG_SKIP_COMP = "[S] Compatible file found, skip compress and just copy {} -> {}"
|
25
|
+
MSG_COMP = (
|
26
|
+
"[C] Compressing {} -> {} res={}x{}, "
|
27
|
+
"quality={}, fps={}, color={} (step {}-{}-{})"
|
28
|
+
)
|
29
|
+
MSG_REDO_COMP = "[{}] Compressed {} -> {} but size {} {} limit {}, recompressing"
|
30
|
+
MSG_DONE_COMP = "[S] Successful compression {} -> {} size {} (step {})"
|
31
|
+
MSG_FAIL_COMP = (
|
32
|
+
"[F] Failed Compression {} -> {}, "
|
33
|
+
"cannot get below limit {} with lowest quality under current settings (Best size: {})"
|
34
|
+
)
|
35
|
+
|
36
|
+
|
23
37
|
def rounding(value: float) -> Decimal:
|
24
38
|
return Decimal(value).quantize(0, ROUND_HALF_UP)
|
25
39
|
|
40
|
+
|
26
41
|
def get_step_value(
|
27
|
-
|
28
|
-
|
42
|
+
max_step: Optional[int],
|
43
|
+
min_step: Optional[int],
|
29
44
|
step: int,
|
30
45
|
steps: int,
|
31
46
|
power: float = 1.0,
|
@@ -41,58 +56,34 @@ def get_step_value(
|
|
41
56
|
else:
|
42
57
|
factor = 0
|
43
58
|
|
44
|
-
if
|
45
|
-
v = round((
|
59
|
+
if max_step is not None and min_step is not None:
|
60
|
+
v = round((max_step - min_step) * step / steps * factor + min_step)
|
46
61
|
if even is True and v % 2 == 1:
|
47
62
|
return v + 1
|
48
|
-
|
49
|
-
|
50
|
-
else:
|
51
|
-
return None
|
63
|
+
return v
|
64
|
+
return None
|
52
65
|
|
53
66
|
|
54
67
|
def useful_array(
|
55
68
|
plane: "VideoPlane", bytes_per_pixel: int = 1, dtype: str = "uint8"
|
56
|
-
) -> np.ndarray[Any, Any]:
|
69
|
+
) -> "np.ndarray[Any, Any]":
|
57
70
|
total_line_size = abs(plane.line_size)
|
58
71
|
useful_line_size = plane.width * bytes_per_pixel
|
59
|
-
arr: np.ndarray[Any, Any] = np.frombuffer(cast(bytes, plane), np.uint8)
|
72
|
+
arr: "np.ndarray[Any, Any]" = np.frombuffer(cast(bytes, plane), np.uint8)
|
60
73
|
if total_line_size != useful_line_size:
|
61
74
|
arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1)
|
62
75
|
return arr.view(np.dtype(dtype))
|
63
76
|
|
64
77
|
|
65
78
|
class StickerConvert:
|
66
|
-
MSG_START_COMP = "[I] Start compressing {} -> {}"
|
67
|
-
MSG_SKIP_COMP = "[S] Compatible file found, skip compress and just copy {} -> {}"
|
68
|
-
MSG_COMP = (
|
69
|
-
"[C] Compressing {} -> {} res={}x{}, "
|
70
|
-
"quality={}, fps={}, color={} (step {}-{}-{})"
|
71
|
-
)
|
72
|
-
MSG_REDO_COMP = "[{}] Compressed {} -> {} but size {} {} limit {}, recompressing"
|
73
|
-
MSG_DONE_COMP = "[S] Successful compression {} -> {} size {} (step {})"
|
74
|
-
MSG_FAIL_COMP = (
|
75
|
-
"[F] Failed Compression {} -> {}, "
|
76
|
-
"cannot get below limit {} with lowest quality under current settings (Best size: {})"
|
77
|
-
)
|
78
|
-
|
79
79
|
def __init__(
|
80
80
|
self,
|
81
|
-
in_f: Union[Path,
|
81
|
+
in_f: Union[Path, Tuple[Path, bytes]],
|
82
82
|
out_f: Path,
|
83
83
|
opt_comp: CompOption,
|
84
|
-
cb: Union[
|
85
|
-
Queue[
|
86
|
-
Union[
|
87
|
-
tuple[str, Optional[tuple[str]], Optional[dict[str, str]]],
|
88
|
-
str,
|
89
|
-
None,
|
90
|
-
]
|
91
|
-
],
|
92
|
-
Callback,
|
93
|
-
],
|
84
|
+
cb: "Union[Queue[CbQueueItemType], Callback]",
|
94
85
|
# cb_return: CallbackReturn
|
95
|
-
):
|
86
|
+
) -> None:
|
96
87
|
self.in_f: Union[bytes, Path]
|
97
88
|
if isinstance(in_f, Path):
|
98
89
|
self.in_f = in_f
|
@@ -105,7 +96,7 @@ class StickerConvert:
|
|
105
96
|
self.in_f_path = in_f[0]
|
106
97
|
self.codec_info_orig = CodecInfo(in_f[1], Path(in_f[0]).suffix)
|
107
98
|
|
108
|
-
valid_formats:
|
99
|
+
valid_formats: List[str] = []
|
109
100
|
for i in opt_comp.get_format():
|
110
101
|
valid_formats.extend(i)
|
111
102
|
|
@@ -125,8 +116,8 @@ class StickerConvert:
|
|
125
116
|
self.out_f_name: str = self.out_f.name
|
126
117
|
|
127
118
|
self.cb = cb
|
128
|
-
self.frames_raw:
|
129
|
-
self.frames_processed:
|
119
|
+
self.frames_raw: "List[np.ndarray[Any, Any]]" = []
|
120
|
+
self.frames_processed: "List[np.ndarray[Any, Any]]" = []
|
130
121
|
self.opt_comp: CompOption = opt_comp
|
131
122
|
if not self.opt_comp.steps:
|
132
123
|
self.opt_comp.steps = 1
|
@@ -148,32 +139,23 @@ class StickerConvert:
|
|
148
139
|
|
149
140
|
@staticmethod
|
150
141
|
def convert(
|
151
|
-
in_f: Union[Path,
|
142
|
+
in_f: Union[Path, Tuple[Path, bytes]],
|
152
143
|
out_f: Path,
|
153
144
|
opt_comp: CompOption,
|
154
|
-
cb: Union[
|
155
|
-
|
156
|
-
|
157
|
-
tuple[str, Optional[tuple[str]], Optional[dict[str, str]]],
|
158
|
-
str,
|
159
|
-
None,
|
160
|
-
]
|
161
|
-
],
|
162
|
-
Callback,
|
163
|
-
],
|
164
|
-
cb_return: CallbackReturn,
|
165
|
-
) -> tuple[bool, Path, Union[None, bytes, Path], int]:
|
145
|
+
cb: "Union[Queue[CbQueueItemType], Callback]",
|
146
|
+
_cb_return: CallbackReturn,
|
147
|
+
) -> Tuple[bool, Path, Union[None, bytes, Path], int]:
|
166
148
|
sticker = StickerConvert(in_f, out_f, opt_comp, cb)
|
167
149
|
result = sticker._convert()
|
168
150
|
cb.put("update_bar")
|
169
151
|
return result
|
170
152
|
|
171
|
-
def _convert(self) ->
|
153
|
+
def _convert(self) -> Tuple[bool, Path, Union[None, bytes, Path], int]:
|
172
154
|
result = self.check_if_compatible()
|
173
155
|
if result:
|
174
156
|
return self.compress_done(result)
|
175
157
|
|
176
|
-
self.cb.put((
|
158
|
+
self.cb.put((MSG_START_COMP.format(self.in_f_name, self.out_f_name)))
|
177
159
|
|
178
160
|
steps_list = self.generate_steps_list()
|
179
161
|
|
@@ -205,7 +187,7 @@ class StickerConvert:
|
|
205
187
|
self.color = param[4]
|
206
188
|
|
207
189
|
self.tmp_f = BytesIO()
|
208
|
-
msg =
|
190
|
+
msg = MSG_COMP.format(
|
209
191
|
self.in_f_name,
|
210
192
|
self.out_f_name,
|
211
193
|
self.res_w,
|
@@ -271,7 +253,7 @@ class StickerConvert:
|
|
271
253
|
file_info=self.codec_info_orig,
|
272
254
|
)
|
273
255
|
):
|
274
|
-
self.cb.put((
|
256
|
+
self.cb.put((MSG_SKIP_COMP.format(self.in_f_name, self.out_f_name)))
|
275
257
|
|
276
258
|
if isinstance(self.in_f, Path):
|
277
259
|
with open(self.in_f, "rb") as f:
|
@@ -282,11 +264,11 @@ class StickerConvert:
|
|
282
264
|
self.result_size = len(self.in_f)
|
283
265
|
|
284
266
|
return result
|
285
|
-
else:
|
286
|
-
return None
|
287
267
|
|
288
|
-
|
289
|
-
|
268
|
+
return None
|
269
|
+
|
270
|
+
def generate_steps_list(self) -> List[Tuple[Optional[int], ...]]:
|
271
|
+
steps_list: List[Tuple[Optional[int], ...]] = []
|
290
272
|
for step in range(self.opt_comp.steps, -1, -1):
|
291
273
|
steps_list.append(
|
292
274
|
(
|
@@ -332,16 +314,16 @@ class StickerConvert:
|
|
332
314
|
|
333
315
|
return steps_list
|
334
316
|
|
335
|
-
def recompress(self, sign: str):
|
336
|
-
msg =
|
317
|
+
def recompress(self, sign: str) -> None:
|
318
|
+
msg = MSG_REDO_COMP.format(
|
337
319
|
sign, self.in_f_name, self.out_f_name, self.size, sign, self.size_max
|
338
320
|
)
|
339
321
|
self.cb.put(msg)
|
340
322
|
|
341
323
|
def compress_fail(
|
342
324
|
self,
|
343
|
-
) ->
|
344
|
-
msg =
|
325
|
+
) -> Tuple[bool, Path, Union[None, bytes, Path], int]:
|
326
|
+
msg = MSG_FAIL_COMP.format(
|
345
327
|
self.in_f_name, self.out_f_name, self.size_max, self.size
|
346
328
|
)
|
347
329
|
self.cb.put(msg)
|
@@ -350,7 +332,7 @@ class StickerConvert:
|
|
350
332
|
|
351
333
|
def compress_done(
|
352
334
|
self, data: bytes, result_step: Optional[int] = None
|
353
|
-
) ->
|
335
|
+
) -> Tuple[bool, Path, Union[None, bytes, Path], int]:
|
354
336
|
out_f: Union[None, bytes, Path]
|
355
337
|
|
356
338
|
if self.out_f.stem == "none":
|
@@ -363,14 +345,14 @@ class StickerConvert:
|
|
363
345
|
f.write(data)
|
364
346
|
|
365
347
|
if result_step:
|
366
|
-
msg =
|
348
|
+
msg = MSG_DONE_COMP.format(
|
367
349
|
self.in_f_name, self.out_f_name, self.result_size, result_step
|
368
350
|
)
|
369
351
|
self.cb.put(msg)
|
370
352
|
|
371
353
|
return True, self.in_f_path, out_f, self.result_size
|
372
354
|
|
373
|
-
def frames_import(self):
|
355
|
+
def frames_import(self) -> None:
|
374
356
|
if isinstance(self.in_f, Path):
|
375
357
|
suffix = self.in_f.suffix
|
376
358
|
else:
|
@@ -385,21 +367,21 @@ class StickerConvert:
|
|
385
367
|
else:
|
386
368
|
self._frames_import_pyav()
|
387
369
|
|
388
|
-
def _frames_import_pillow(self):
|
370
|
+
def _frames_import_pillow(self) -> None:
|
389
371
|
with Image.open(self.in_f) as im:
|
390
372
|
# Note: im.convert("RGBA") would return rgba image of current frame only
|
391
|
-
if "n_frames" in im
|
373
|
+
if "n_frames" in dir(im):
|
392
374
|
for i in range(im.n_frames):
|
393
375
|
im.seek(i)
|
394
376
|
self.frames_raw.append(np.asarray(im.convert("RGBA")))
|
395
377
|
else:
|
396
378
|
self.frames_raw.append(np.asarray(im.convert("RGBA")))
|
397
379
|
|
398
|
-
def _frames_import_pyav(self):
|
399
|
-
import av
|
400
|
-
from av.codec.context import CodecContext
|
401
|
-
from av.container.input import InputContainer
|
402
|
-
from av.video.codeccontext import VideoCodecContext
|
380
|
+
def _frames_import_pyav(self) -> None:
|
381
|
+
import av # type: ignore
|
382
|
+
from av.codec.context import CodecContext # type: ignore
|
383
|
+
from av.container.input import InputContainer # type: ignore
|
384
|
+
from av.video.codeccontext import VideoCodecContext # type: ignore
|
403
385
|
|
404
386
|
# Crashes when handling some webm in yuv420p and convert to rgba
|
405
387
|
# https://github.com/PyAV-Org/PyAV/issues/1166
|
@@ -429,7 +411,7 @@ class StickerConvert:
|
|
429
411
|
height = frame.height
|
430
412
|
if frame.format.name == "yuv420p":
|
431
413
|
rgb_array = frame.to_ndarray(format="rgb24") # type: ignore
|
432
|
-
cast(np.ndarray[Any, Any], rgb_array)
|
414
|
+
cast("np.ndarray[Any, Any]", rgb_array)
|
433
415
|
rgba_array = np.dstack(
|
434
416
|
(
|
435
417
|
rgb_array,
|
@@ -443,7 +425,7 @@ class StickerConvert:
|
|
443
425
|
width=width,
|
444
426
|
height=height,
|
445
427
|
format="yuva420p",
|
446
|
-
dst_colorspace=1,
|
428
|
+
dst_colorspace=1, # type: ignore
|
447
429
|
)
|
448
430
|
|
449
431
|
# https://stackoverflow.com/questions/72308308/converting-yuv-to-rgb-in-python-coefficients-work-with-array-dont-work-with-n
|
@@ -470,11 +452,11 @@ class StickerConvert:
|
|
470
452
|
|
471
453
|
yuv_array = yuv_array.astype(np.float32)
|
472
454
|
yuv_array[:, :, 0] = (
|
473
|
-
yuv_array[:, :, 0].clip(16, 235).astype(yuv_array.dtype)
|
455
|
+
yuv_array[:, :, 0].clip(16, 235).astype(yuv_array.dtype) # type: ignore
|
474
456
|
- 16
|
475
457
|
)
|
476
458
|
yuv_array[:, :, 1:] = (
|
477
|
-
yuv_array[:, :, 1:].clip(16, 240).astype(yuv_array.dtype)
|
459
|
+
yuv_array[:, :, 1:].clip(16, 240).astype(yuv_array.dtype) # type: ignore
|
478
460
|
- 128
|
479
461
|
)
|
480
462
|
|
@@ -492,7 +474,7 @@ class StickerConvert:
|
|
492
474
|
|
493
475
|
self.frames_raw.append(rgba_array)
|
494
476
|
|
495
|
-
def _frames_import_lottie(self):
|
477
|
+
def _frames_import_lottie(self) -> None:
|
496
478
|
from rlottie_python.rlottie_wrapper import LottieAnimation
|
497
479
|
|
498
480
|
if isinstance(self.in_f, Path):
|
@@ -522,15 +504,19 @@ class StickerConvert:
|
|
522
504
|
anim.lottie_animation_destroy()
|
523
505
|
|
524
506
|
def frames_resize(
|
525
|
-
self, frames_in:
|
526
|
-
) ->
|
527
|
-
frames_out:
|
507
|
+
self, frames_in: "List[np.ndarray[Any, Any]]"
|
508
|
+
) -> "List[np.ndarray[Any, Any]]":
|
509
|
+
frames_out: "List[np.ndarray[Any, Any]]" = []
|
528
510
|
|
529
|
-
resample: Literal[0, 1, 2, 3]
|
511
|
+
resample: Literal[0, 1, 2, 3, 4, 5]
|
530
512
|
if self.opt_comp.scale_filter == "nearest":
|
531
513
|
resample = Image.NEAREST
|
514
|
+
elif self.opt_comp.scale_filter == "box":
|
515
|
+
resample = Image.BOX
|
532
516
|
elif self.opt_comp.scale_filter == "bilinear":
|
533
517
|
resample = Image.BILINEAR
|
518
|
+
elif self.opt_comp.scale_filter == "hamming":
|
519
|
+
resample = Image.HAMMING
|
534
520
|
elif self.opt_comp.scale_filter == "bicubic":
|
535
521
|
resample = Image.BICUBIC
|
536
522
|
elif self.opt_comp.scale_filter == "lanczos":
|
@@ -542,22 +528,22 @@ class StickerConvert:
|
|
542
528
|
with Image.fromarray(frame, "RGBA") as im: # type: ignore
|
543
529
|
width, height = im.size
|
544
530
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
531
|
+
if self.res_w is None:
|
532
|
+
self.res_w = width
|
533
|
+
if self.res_h is None:
|
534
|
+
self.res_h = height
|
549
535
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
536
|
+
if width > height:
|
537
|
+
width_new = self.res_w
|
538
|
+
height_new = height * self.res_w // width
|
539
|
+
else:
|
540
|
+
height_new = self.res_h
|
541
|
+
width_new = width * self.res_h // height
|
556
542
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
):
|
543
|
+
with im.resize((width_new, height_new), resample=resample) as im_resized:
|
544
|
+
with Image.new(
|
545
|
+
"RGBA", (self.res_w, self.res_h), (0, 0, 0, 0)
|
546
|
+
) as im_new:
|
561
547
|
im_new.paste(
|
562
548
|
im_resized,
|
563
549
|
((self.res_w - width_new) // 2, (self.res_h - height_new) // 2),
|
@@ -567,8 +553,8 @@ class StickerConvert:
|
|
567
553
|
return frames_out
|
568
554
|
|
569
555
|
def frames_drop(
|
570
|
-
self, frames_in:
|
571
|
-
) ->
|
556
|
+
self, frames_in: "List[np.ndarray[Any, Any]]"
|
557
|
+
) -> "List[np.ndarray[Any, Any]]":
|
572
558
|
if (
|
573
559
|
not self.codec_info_orig.is_animated
|
574
560
|
or not self.fps
|
@@ -576,7 +562,7 @@ class StickerConvert:
|
|
576
562
|
):
|
577
563
|
return [frames_in[0]]
|
578
564
|
|
579
|
-
frames_out:
|
565
|
+
frames_out: "List[np.ndarray[Any, Any]]" = []
|
580
566
|
|
581
567
|
# fps_ratio: 1 frame in new anim equal to how many frame in old anim
|
582
568
|
# speed_ratio: How much to speed up / slow down
|
@@ -620,7 +606,7 @@ class StickerConvert:
|
|
620
606
|
frames_out.append(frames_in[-1])
|
621
607
|
return frames_out
|
622
608
|
|
623
|
-
def frames_export(self):
|
609
|
+
def frames_export(self) -> None:
|
624
610
|
is_animated = len(self.frames_processed) > 1 and self.fps
|
625
611
|
if self.out_f.suffix in (".apng", ".png"):
|
626
612
|
if is_animated:
|
@@ -634,7 +620,7 @@ class StickerConvert:
|
|
634
620
|
else:
|
635
621
|
self._frames_export_pil()
|
636
622
|
|
637
|
-
def _frames_export_pil(self):
|
623
|
+
def _frames_export_pil(self) -> None:
|
638
624
|
with Image.fromarray(self.frames_processed[0]) as im: # type: ignore
|
639
625
|
im.save(
|
640
626
|
self.tmp_f,
|
@@ -642,9 +628,10 @@ class StickerConvert:
|
|
642
628
|
quality=self.quality,
|
643
629
|
)
|
644
630
|
|
645
|
-
def _frames_export_pyav(self):
|
646
|
-
import av
|
647
|
-
from av.container import OutputContainer
|
631
|
+
def _frames_export_pyav(self) -> None:
|
632
|
+
import av # type: ignore
|
633
|
+
from av.container import OutputContainer # type: ignore
|
634
|
+
from av.video.stream import VideoStream # type: ignore
|
648
635
|
|
649
636
|
options = {}
|
650
637
|
|
@@ -675,6 +662,8 @@ class StickerConvert:
|
|
675
662
|
) as output:
|
676
663
|
output = cast(OutputContainer, output) # type: ignore
|
677
664
|
out_stream = output.add_stream(codec, rate=self.fps, options=options) # type: ignore
|
665
|
+
out_stream = cast(VideoStream, out_stream)
|
666
|
+
assert isinstance(self.res_w, int) and isinstance(self.res_h, int)
|
678
667
|
out_stream.width = self.res_w
|
679
668
|
out_stream.height = self.res_h
|
680
669
|
out_stream.pix_fmt = pixel_format
|
@@ -687,7 +676,7 @@ class StickerConvert:
|
|
687
676
|
for packet in out_stream.encode(): # type: ignore
|
688
677
|
output.mux(packet) # type: ignore
|
689
678
|
|
690
|
-
def _frames_export_webp(self):
|
679
|
+
def _frames_export_webp(self) -> None:
|
691
680
|
import webp # type: ignore
|
692
681
|
|
693
682
|
assert self.fps
|
@@ -702,7 +691,7 @@ class StickerConvert:
|
|
702
691
|
anim_data = enc.assemble(timestamp_ms) # type: ignore
|
703
692
|
self.tmp_f.write(anim_data.buffer()) # type: ignore
|
704
693
|
|
705
|
-
def _frames_export_png(self):
|
694
|
+
def _frames_export_png(self) -> None:
|
706
695
|
with Image.fromarray(self.frames_processed[0], "RGBA") as image: # type: ignore
|
707
696
|
image_quant = self.quantize(image)
|
708
697
|
|
@@ -712,7 +701,7 @@ class StickerConvert:
|
|
712
701
|
frame_optimized = self.optimize_png(f.read())
|
713
702
|
self.tmp_f.write(frame_optimized)
|
714
703
|
|
715
|
-
def _frames_export_apng(self):
|
704
|
+
def _frames_export_apng(self) -> None:
|
716
705
|
from apngasm_python._apngasm_python import APNGAsm, create_frame_from_rgba # type: ignore
|
717
706
|
|
718
707
|
assert self.fps
|
@@ -771,10 +760,10 @@ class StickerConvert:
|
|
771
760
|
return image.copy()
|
772
761
|
if self.opt_comp.quantize_method == "imagequant":
|
773
762
|
return self._quantize_by_imagequant(image)
|
774
|
-
|
763
|
+
if self.opt_comp.quantize_method == "fastoctree":
|
775
764
|
return self._quantize_by_fastoctree(image)
|
776
|
-
|
777
|
-
|
765
|
+
|
766
|
+
return image
|
778
767
|
|
779
768
|
def _quantize_by_imagequant(self, image: Image.Image) -> Image.Image:
|
780
769
|
import imagequant # type: ignore
|
@@ -819,17 +808,17 @@ class StickerConvert:
|
|
819
808
|
#
|
820
809
|
# For GIF, we need to adjust fps such that delay is matching to hundreths of second
|
821
810
|
return self._fix_fps_duration(fps, 100)
|
822
|
-
|
811
|
+
if self.out_f.suffix in (".webp", ".apng", ".png"):
|
823
812
|
return self._fix_fps_duration(fps, 1000)
|
824
|
-
|
825
|
-
|
813
|
+
|
814
|
+
return self._fix_fps_pyav(fps)
|
826
815
|
|
827
816
|
def _fix_fps_duration(self, fps: float, denominator: int) -> Fraction:
|
828
817
|
delay = int(rounding(denominator / fps))
|
829
818
|
fps_fraction = Fraction(denominator, delay)
|
830
819
|
if self.opt_comp.fps_max and fps_fraction > self.opt_comp.fps_max:
|
831
820
|
return Fraction(denominator, (delay + 1))
|
832
|
-
|
821
|
+
if self.opt_comp.fps_min and fps_fraction < self.opt_comp.fps_min:
|
833
822
|
return Fraction(denominator, (delay - 1))
|
834
823
|
return fps_fraction
|
835
824
|
|
sticker_convert/definitions.py
CHANGED
@@ -48,8 +48,7 @@ def check_root_dir_exe_writable() -> bool:
|
|
48
48
|
or "site-packages" in ROOT_DIR_EXE.as_posix()
|
49
49
|
):
|
50
50
|
return False
|
51
|
-
|
52
|
-
return True
|
51
|
+
return True
|
53
52
|
|
54
53
|
|
55
54
|
ROOT_DIR_EXE_WRITABLE = check_root_dir_exe_writable()
|
@@ -58,13 +57,11 @@ ROOT_DIR_EXE_WRITABLE = check_root_dir_exe_writable()
|
|
58
57
|
def get_default_dir() -> Path:
|
59
58
|
if ROOT_DIR_EXE_WRITABLE:
|
60
59
|
return ROOT_DIR_EXE
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
else:
|
67
|
-
return home_dir
|
60
|
+
home_dir = Path.home()
|
61
|
+
desktop_dir = home_dir / "Desktop"
|
62
|
+
if desktop_dir.is_dir():
|
63
|
+
return desktop_dir
|
64
|
+
return home_dir
|
68
65
|
|
69
66
|
|
70
67
|
# Default directory for stickers_input and stickers_output
|
@@ -79,9 +76,8 @@ def get_config_dir() -> Path:
|
|
79
76
|
|
80
77
|
if ROOT_DIR_EXE_WRITABLE:
|
81
78
|
return ROOT_DIR_EXE
|
82
|
-
|
83
|
-
|
84
|
-
return fallback_dir
|
79
|
+
os.makedirs(fallback_dir, exist_ok=True)
|
80
|
+
return fallback_dir
|
85
81
|
|
86
82
|
|
87
83
|
# Directory for saving configs
|