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.
Files changed (56) hide show
  1. sticker_convert/__init__.py +1 -0
  2. sticker_convert/__main__.py +3 -1
  3. sticker_convert/cli.py +20 -24
  4. sticker_convert/converter.py +108 -119
  5. sticker_convert/definitions.py +8 -12
  6. sticker_convert/downloaders/download_base.py +14 -31
  7. sticker_convert/downloaders/download_kakao.py +25 -39
  8. sticker_convert/downloaders/download_line.py +24 -33
  9. sticker_convert/downloaders/download_signal.py +7 -16
  10. sticker_convert/downloaders/download_telegram.py +6 -15
  11. sticker_convert/gui.py +53 -61
  12. sticker_convert/gui_components/frames/comp_frame.py +11 -20
  13. sticker_convert/gui_components/frames/config_frame.py +9 -9
  14. sticker_convert/gui_components/frames/control_frame.py +3 -3
  15. sticker_convert/gui_components/frames/cred_frame.py +12 -18
  16. sticker_convert/gui_components/frames/input_frame.py +9 -15
  17. sticker_convert/gui_components/frames/output_frame.py +9 -15
  18. sticker_convert/gui_components/frames/progress_frame.py +8 -8
  19. sticker_convert/gui_components/frames/right_clicker.py +2 -2
  20. sticker_convert/gui_components/gui_utils.py +6 -8
  21. sticker_convert/gui_components/windows/advanced_compression_window.py +23 -32
  22. sticker_convert/gui_components/windows/base_window.py +6 -6
  23. sticker_convert/gui_components/windows/kakao_get_auth_window.py +5 -11
  24. sticker_convert/gui_components/windows/line_get_auth_window.py +5 -5
  25. sticker_convert/gui_components/windows/signal_get_auth_window.py +6 -6
  26. sticker_convert/job.py +84 -90
  27. sticker_convert/job_option.py +36 -32
  28. sticker_convert/resources/emoji.json +334 -70
  29. sticker_convert/resources/help.json +1 -1
  30. sticker_convert/uploaders/compress_wastickers.py +19 -30
  31. sticker_convert/uploaders/upload_base.py +19 -13
  32. sticker_convert/uploaders/upload_signal.py +20 -33
  33. sticker_convert/uploaders/upload_telegram.py +21 -28
  34. sticker_convert/uploaders/xcode_imessage.py +30 -95
  35. sticker_convert/utils/auth/get_kakao_auth.py +7 -8
  36. sticker_convert/utils/auth/get_line_auth.py +5 -6
  37. sticker_convert/utils/auth/get_signal_auth.py +7 -7
  38. sticker_convert/utils/callback.py +31 -23
  39. sticker_convert/utils/files/cache_store.py +6 -8
  40. sticker_convert/utils/files/json_manager.py +6 -7
  41. sticker_convert/utils/files/json_resources_loader.py +12 -0
  42. sticker_convert/utils/files/metadata_handler.py +93 -84
  43. sticker_convert/utils/files/run_bin.py +11 -10
  44. sticker_convert/utils/files/sanitize_filename.py +30 -28
  45. sticker_convert/utils/media/apple_png_normalize.py +3 -2
  46. sticker_convert/utils/media/codec_info.py +41 -44
  47. sticker_convert/utils/media/decrypt_kakao.py +7 -7
  48. sticker_convert/utils/media/format_verify.py +14 -14
  49. sticker_convert/utils/url_detect.py +4 -5
  50. sticker_convert/version.py +2 -1
  51. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/METADATA +19 -17
  52. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/RECORD +56 -55
  53. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/WHEEL +1 -1
  54. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/LICENSE +0 -0
  55. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/entry_points.txt +0 -0
  56. {sticker_convert-2.7.2.dist-info → sticker_convert-2.7.4.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env python3
2
2
  """sticker-convert"""
3
+
3
4
  from sticker_convert.version import __version__ # noqa
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- def main():
2
+
3
+
4
+ def main() -> None:
3
5
  import multiprocessing
4
6
  import sys
5
7
 
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, ROOT_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
- self.help: dict[str, dict[str, str]] = JsonManager.load_json(
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.__str__)
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: dict[str, Any]
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=tuple(
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=tuple(
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
@@ -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
- max: Optional[int],
28
- min: Optional[int],
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 max is not None and min is not None:
45
- v = round((max - min) * step / steps * factor + min)
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
- else:
49
- return v
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, tuple[Path, bytes]],
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: list[str] = []
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: list[np.ndarray[Any, Any]] = []
129
- self.frames_processed: list[np.ndarray[Any, Any]] = []
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, tuple[Path, bytes]],
142
+ in_f: Union[Path, Tuple[Path, bytes]],
152
143
  out_f: Path,
153
144
  opt_comp: CompOption,
154
- cb: Union[
155
- Queue[
156
- Union[
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) -> tuple[bool, Path, Union[None, bytes, Path], int]:
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((self.MSG_START_COMP.format(self.in_f_name, self.out_f_name)))
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 = self.MSG_COMP.format(
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((self.MSG_SKIP_COMP.format(self.in_f_name, self.out_f_name)))
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
- def generate_steps_list(self) -> list[tuple[Optional[int], ...]]:
289
- steps_list: list[tuple[Optional[int], ...]] = []
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 = self.MSG_REDO_COMP.format(
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
- ) -> tuple[bool, Path, Union[None, bytes, Path], int]:
344
- msg = self.MSG_FAIL_COMP.format(
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
- ) -> tuple[bool, Path, Union[None, bytes, Path], int]:
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 = self.MSG_DONE_COMP.format(
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.__dir__():
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: list[np.ndarray[Any, Any]]
526
- ) -> list[np.ndarray[Any, Any]]:
527
- frames_out: list[np.ndarray[Any, Any]] = []
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
- if self.res_w is None:
546
- self.res_w = width
547
- if self.res_h is None:
548
- self.res_h = height
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
- if width > height:
551
- width_new = self.res_w
552
- height_new = height * self.res_w // width
553
- else:
554
- height_new = self.res_h
555
- width_new = width * self.res_h // height
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
- with (
558
- im.resize((width_new, height_new), resample=resample) as im_resized,
559
- Image.new("RGBA", (self.res_w, self.res_h), (0, 0, 0, 0)) as im_new,
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: list[np.ndarray[Any, Any]]
571
- ) -> list[np.ndarray[Any, Any]]:
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: list[np.ndarray[Any, Any]] = []
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
- elif self.opt_comp.quantize_method == "fastoctree":
763
+ if self.opt_comp.quantize_method == "fastoctree":
775
764
  return self._quantize_by_fastoctree(image)
776
- else:
777
- return image
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
- elif self.out_f.suffix in (".webp", ".apng", ".png"):
811
+ if self.out_f.suffix in (".webp", ".apng", ".png"):
823
812
  return self._fix_fps_duration(fps, 1000)
824
- else:
825
- return self._fix_fps_pyav(fps)
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
- elif self.opt_comp.fps_min and fps_fraction < self.opt_comp.fps_min:
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
 
@@ -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
- else:
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
- else:
62
- home_dir = Path.home()
63
- desktop_dir = home_dir / "Desktop"
64
- if desktop_dir.is_dir():
65
- return desktop_dir
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
- else:
83
- os.makedirs(fallback_dir, exist_ok=True)
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