sticker-convert 2.7.7__py3-none-any.whl → 2.7.9__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/cli.py CHANGED
@@ -114,6 +114,7 @@ class CLI:
114
114
  )
115
115
  flags_comp_float = ("fps_power", "res_power", "quality_power", "color_power")
116
116
  flags_comp_str = (
117
+ "bg_color",
117
118
  "vid_format",
118
119
  "img_format",
119
120
  "cache_dir",
@@ -350,6 +351,9 @@ class CLI:
350
351
  duration_max=self.compression_presets[preset]["duration"]["max"]
351
352
  if args.duration_max is None
352
353
  else args.duration_max,
354
+ bg_color=self.compression_presets[preset]["bg_color"]
355
+ if args.bg_color is None
356
+ else args.bg_color,
353
357
  steps=self.compression_presets[preset]["steps"]
354
358
  if args.steps is None
355
359
  else args.steps,
@@ -6,10 +6,11 @@ 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, List, Literal, Optional, Tuple, Union, cast
9
+ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
10
10
 
11
11
  import numpy as np
12
12
  from PIL import Image
13
+ from PIL import __version__ as PillowVersion
13
14
 
14
15
  from sticker_convert.job_option import CompOption
15
16
  from sticker_convert.utils.callback import Callback, CallbackReturn, CbQueueItemType
@@ -378,10 +379,10 @@ class StickerConvert:
378
379
  self.frames_raw.append(np.asarray(im.convert("RGBA")))
379
380
 
380
381
  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
382
+ import av
383
+ from av.codec.context import CodecContext
384
+ from av.container.input import InputContainer
385
+ from av.video.codeccontext import VideoCodecContext
385
386
 
386
387
  # Crashes when handling some webm in yuv420p and convert to rgba
387
388
  # https://github.com/PyAV-Org/PyAV/issues/1166
@@ -390,16 +391,17 @@ class StickerConvert:
390
391
  file = self.in_f.as_posix()
391
392
  else:
392
393
  file = BytesIO(self.in_f)
393
- with av.open(file) as container: # type: ignore
394
+ with av.open(file) as container:
394
395
  container = cast(InputContainer, container)
395
396
  context = container.streams.video[0].codec_context
396
397
  if context.name == "vp8":
397
- context = CodecContext.create("libvpx", "r") # type: ignore
398
+ context = cast(VideoCodecContext, CodecContext.create("libvpx", "r"))
398
399
  elif context.name == "vp9":
399
- context = CodecContext.create("libvpx-vp9", "r") # type: ignore
400
- context = cast(VideoCodecContext, context)
400
+ context = cast(
401
+ VideoCodecContext, CodecContext.create("libvpx-vp9", "r")
402
+ )
401
403
 
402
- for packet in container.demux(container.streams.video): # type: ignore
404
+ for packet in container.demux(container.streams.video):
403
405
  for frame in context.decode(packet):
404
406
  if frame.width % 2 != 0:
405
407
  width = frame.width - 1
@@ -410,22 +412,22 @@ class StickerConvert:
410
412
  else:
411
413
  height = frame.height
412
414
  if frame.format.name == "yuv420p":
413
- rgb_array = frame.to_ndarray(format="rgb24") # type: ignore
415
+ rgb_array = frame.to_ndarray(format="rgb24")
414
416
  cast("np.ndarray[Any, Any]", rgb_array)
415
417
  rgba_array = np.dstack(
416
418
  (
417
419
  rgb_array,
418
420
  np.zeros(rgb_array.shape[:2], dtype=np.uint8) + 255,
419
- ) # type: ignore
421
+ )
420
422
  )
421
423
  else:
422
424
  # yuva420p may cause crash
423
425
  # https://github.com/laggykiller/sticker-convert/issues/114
424
- frame = frame.reformat( # type: ignore
426
+ frame = frame.reformat(
425
427
  width=width,
426
428
  height=height,
427
429
  format="yuva420p",
428
- dst_colorspace=1, # type: ignore
430
+ dst_colorspace=1,
429
431
  )
430
432
 
431
433
  # https://stackoverflow.com/questions/72308308/converting-yuv-to-rgb-in-python-coefficients-work-with-array-dont-work-with-n
@@ -522,7 +524,27 @@ class StickerConvert:
522
524
  elif self.opt_comp.scale_filter == "lanczos":
523
525
  resample = Image.LANCZOS
524
526
  else:
525
- resample = Image.LANCZOS
527
+ resample = Image.BICUBIC
528
+
529
+ if not self.opt_comp.bg_color:
530
+ # Calculate average color of all frames for selecting background color
531
+ frames_raw_np = np.array(self.frames_raw)
532
+ s = frames_raw_np.shape
533
+ colors = frames_raw_np.reshape((-1, s[3])) # type: ignore
534
+ # Do not count in alpha=0
535
+ # If alpha > 0, use alpha as weight
536
+ colors = colors[colors[:, 3] != 0]
537
+ if colors.shape[0] == 0:
538
+ bg = (0, 0, 0, 0)
539
+ else:
540
+ colors = colors * (colors[3] / 255)
541
+ if np.mean(colors[:, :3]) < 128:
542
+ bg = (255, 255, 255, 0)
543
+ else:
544
+ bg = (0, 0, 0, 0)
545
+ else:
546
+ r, g, b = bytes.fromhex(self.opt_comp.bg_color)
547
+ bg = (r, g, b, 0)
526
548
 
527
549
  for frame in frames_in:
528
550
  with Image.fromarray(frame, "RGBA") as im: # type: ignore
@@ -541,10 +563,8 @@ class StickerConvert:
541
563
  width_new = width * self.res_h // height
542
564
 
543
565
  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:
547
- im_new.paste(
566
+ with Image.new("RGBA", (self.res_w, self.res_h), bg) as im_new:
567
+ im_new.alpha_composite(
548
568
  im_resized,
549
569
  ((self.res_w - width_new) // 2, (self.res_h - height_new) // 2),
550
570
  )
@@ -615,6 +635,8 @@ class StickerConvert:
615
635
  self._frames_export_png()
616
636
  elif self.out_f.suffix == ".webp" and is_animated:
617
637
  self._frames_export_webp()
638
+ elif self.out_f.suffix == ".gif":
639
+ self._frames_export_gif()
618
640
  elif self.out_f.suffix in (".webm", ".mp4", ".mkv") or is_animated:
619
641
  self._frames_export_pyav()
620
642
  else:
@@ -629,22 +651,17 @@ class StickerConvert:
629
651
  )
630
652
 
631
653
  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
654
+ import av
655
+ from av.video.stream import VideoStream
635
656
 
636
- options = {}
657
+ options: Dict[str, str] = {}
637
658
 
638
659
  if isinstance(self.quality, int):
639
660
  # Seems not actually working
640
661
  options["quality"] = str(self.quality)
641
662
  options["lossless"] = "0"
642
663
 
643
- if self.out_f.suffix == ".gif":
644
- codec = "gif"
645
- pixel_format = "rgb8"
646
- options["loop"] = "0"
647
- elif self.out_f.suffix in (".apng", ".png"):
664
+ if self.out_f.suffix in (".apng", ".png"):
648
665
  codec = "apng"
649
666
  pixel_format = "rgba"
650
667
  options["plays"] = "0"
@@ -657,11 +674,10 @@ class StickerConvert:
657
674
  pixel_format = "yuv420p"
658
675
  options["loop"] = "0"
659
676
 
660
- with av.open( # type: ignore
677
+ with av.open(
661
678
  self.tmp_f, "w", format=self.out_f.suffix.replace(".", "")
662
679
  ) as output:
663
- output = cast(OutputContainer, output) # type: ignore
664
- out_stream = output.add_stream(codec, rate=self.fps, options=options) # type: ignore
680
+ out_stream = output.add_stream(codec, rate=self.fps, options=options)
665
681
  out_stream = cast(VideoStream, out_stream)
666
682
  assert isinstance(self.res_w, int) and isinstance(self.res_h, int)
667
683
  out_stream.width = self.res_w
@@ -669,12 +685,49 @@ class StickerConvert:
669
685
  out_stream.pix_fmt = pixel_format
670
686
 
671
687
  for frame in self.frames_processed:
672
- av_frame = av.VideoFrame.from_ndarray(frame, format="rgba") # type: ignore
673
- for packet in out_stream.encode(av_frame): # type: ignore
674
- output.mux(packet) # type: ignore
675
-
676
- for packet in out_stream.encode(): # type: ignore
677
- output.mux(packet) # type: ignore
688
+ av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
689
+ output.mux(out_stream.encode(av_frame))
690
+ output.mux(out_stream.encode())
691
+
692
+ def _frames_export_gif(self) -> None:
693
+ extra_kwargs: Dict[str, Any] = {}
694
+
695
+ # disposal=2 on gif cause flicker in image with transparency
696
+ # Occurs in Pillow == 10.2.0
697
+ # https://github.com/python-pillow/Pillow/issues/7787
698
+ if PillowVersion == "10.2.0":
699
+ extra_kwargs["optimize"] = False
700
+ else:
701
+ extra_kwargs["optimize"] = True
702
+
703
+ # Only enable transparency if all pixels have alpha channel
704
+ # with value of 0 or 255
705
+ alpha_channel_values = np.unique(np.array(self.frames_raw)[:, :, :, 3])
706
+ illegals = np.setxor1d(
707
+ alpha_channel_values, [0, 255]
708
+ ) # Find all values not 0 or 255
709
+ if illegals.size == 0:
710
+ extra_kwargs["transparency"] = 0
711
+ extra_kwargs["disposal"] = 2
712
+ im_out = [self.quantize(Image.fromarray(i)) for i in self.frames_processed]
713
+ else:
714
+ im_out = [
715
+ self.quantize(Image.fromarray(i).convert("RGB")).convert("RGB")
716
+ for i in self.frames_processed
717
+ ]
718
+
719
+ if self.fps:
720
+ extra_kwargs["save_all"] = True
721
+ extra_kwargs["append_images"] = im_out[1:]
722
+ extra_kwargs["duration"] = int(1000 / self.fps)
723
+ extra_kwargs["loop"] = 0
724
+
725
+ im_out[0].save(
726
+ self.tmp_f,
727
+ format="GIF",
728
+ quality=self.quality,
729
+ **extra_kwargs,
730
+ )
678
731
 
679
732
  def _frames_export_webp(self) -> None:
680
733
  import webp # type: ignore
sticker_convert/gui.py CHANGED
@@ -12,6 +12,7 @@ from threading import Thread
12
12
  from typing import Any, Callable, Dict, Optional, Union
13
13
  from urllib.parse import urlparse
14
14
 
15
+ from mergedeep import merge # type: ignore
15
16
  from PIL import ImageFont
16
17
  from ttkbootstrap import BooleanVar, DoubleVar, IntVar, StringVar, Toplevel, Window # type: ignore
17
18
  from ttkbootstrap.dialogs import Messagebox, Querybox # type: ignore
@@ -127,6 +128,7 @@ class GUI(Window):
127
128
  self.img_size_max_var = IntVar(self)
128
129
  self.vid_size_max_var = IntVar(self)
129
130
  self.size_disable_var = BooleanVar()
131
+ self.bg_color_var = StringVar()
130
132
  self.img_format_var = StringVar(self)
131
133
  self.vid_format_var = StringVar(self)
132
134
  self.fake_vid_var = BooleanVar()
@@ -265,7 +267,12 @@ class GUI(Window):
265
267
  def save_config(self) -> None:
266
268
  # Only update comp_custom if custom preset is selected
267
269
  if self.comp_preset_var.get() == "custom":
268
- comp_custom = self.get_opt_comp().to_dict()
270
+ comp_custom: Dict[Any, Any] = merge( # type: ignore
271
+ self.compression_presets.get("custom"), # type: ignore
272
+ self.get_opt_comp().to_dict(),
273
+ )
274
+ comp_custom["format"]["img"] = comp_custom["format"]["img"][0]
275
+ comp_custom["format"]["vid"] = comp_custom["format"]["vid"][0]
269
276
  del comp_custom["preset"]
270
277
  del comp_custom["no_compress"]
271
278
  else:
@@ -332,7 +339,9 @@ class GUI(Window):
332
339
  )
333
340
  comp_custom = self.settings.get("comp_custom")
334
341
  if comp_custom:
335
- self.compression_presets["custom"] = comp_custom
342
+ self.compression_presets["custom"] = merge(
343
+ self.compression_presets["custom"], comp_custom
344
+ )
336
345
  self.cache_dir_var.set(self.settings.get("comp", {}).get("cache_dir", ""))
337
346
  self.processes_var.set(
338
347
  self.settings.get("comp", {}).get("processes", ceil(cpu_count() / 2))
@@ -509,6 +518,7 @@ class GUI(Window):
509
518
  duration_max=self.duration_max_var.get()
510
519
  if not self.duration_disable_var.get()
511
520
  else None,
521
+ bg_color=self.bg_color_var.get(),
512
522
  steps=self.steps_var.get(),
513
523
  fake_vid=self.fake_vid_var.get(),
514
524
  scale_filter=self.scale_filter_var.get(),
@@ -110,73 +110,33 @@ class CompFrame(LabelFrame):
110
110
  else:
111
111
  self.gui.no_compress_var.set(False)
112
112
 
113
- self.gui.fps_min_var.set(self.gui.compression_presets[selection]["fps"]["min"])
114
- self.gui.fps_max_var.set(self.gui.compression_presets[selection]["fps"]["max"])
115
- self.gui.fps_power_var.set(
116
- self.gui.compression_presets[selection]["fps"]["power"]
117
- )
118
- self.gui.res_w_min_var.set(
119
- self.gui.compression_presets[selection]["res"]["w"]["min"]
120
- )
121
- self.gui.res_w_max_var.set(
122
- self.gui.compression_presets[selection]["res"]["w"]["max"]
123
- )
124
- self.gui.res_h_min_var.set(
125
- self.gui.compression_presets[selection]["res"]["h"]["min"]
126
- )
127
- self.gui.res_h_max_var.set(
128
- self.gui.compression_presets[selection]["res"]["h"]["max"]
129
- )
130
- self.gui.res_power_var.set(
131
- self.gui.compression_presets[selection]["res"]["power"]
132
- )
133
- self.gui.quality_min_var.set(
134
- self.gui.compression_presets[selection]["quality"]["min"]
135
- )
136
- self.gui.quality_max_var.set(
137
- self.gui.compression_presets[selection]["quality"]["max"]
138
- )
139
- self.gui.quality_power_var.set(
140
- self.gui.compression_presets[selection]["quality"]["power"]
141
- )
142
- self.gui.color_min_var.set(
143
- self.gui.compression_presets[selection]["color"]["min"]
144
- )
145
- self.gui.color_max_var.set(
146
- self.gui.compression_presets[selection]["color"]["max"]
147
- )
148
- self.gui.color_power_var.set(
149
- self.gui.compression_presets[selection]["color"]["power"]
150
- )
151
- self.gui.duration_min_var.set(
152
- self.gui.compression_presets[selection]["duration"]["min"]
153
- )
154
- self.gui.duration_max_var.set(
155
- self.gui.compression_presets[selection]["duration"]["max"]
156
- )
157
- self.gui.img_size_max_var.set(
158
- self.gui.compression_presets[selection]["size_max"]["img"]
159
- )
160
- self.gui.vid_size_max_var.set(
161
- self.gui.compression_presets[selection]["size_max"]["vid"]
162
- )
163
- self.gui.img_format_var.set(
164
- self.gui.compression_presets[selection]["format"]["img"]
165
- )
166
- self.gui.vid_format_var.set(
167
- self.gui.compression_presets[selection]["format"]["vid"]
168
- )
169
- self.gui.fake_vid_var.set(self.gui.compression_presets[selection]["fake_vid"])
170
- self.gui.scale_filter_var.set(
171
- self.gui.compression_presets[selection]["scale_filter"]
172
- )
173
- self.gui.quantize_method_var.set(
174
- self.gui.compression_presets[selection]["quantize_method"]
175
- )
176
- self.gui.default_emoji_var.set(
177
- self.gui.compression_presets[selection]["default_emoji"]
178
- )
179
- self.gui.steps_var.set(self.gui.compression_presets[selection]["steps"])
113
+ preset = self.gui.compression_presets[selection]
114
+ self.gui.fps_min_var.set(preset.get("fps", {}).get("min"))
115
+ self.gui.fps_max_var.set(preset.get("fps", {}).get("max"))
116
+ self.gui.fps_power_var.set(preset.get("fps", {}).get("power"))
117
+ self.gui.res_w_min_var.set(preset.get("res", {}).get("w", {}).get("min"))
118
+ self.gui.res_w_max_var.set(preset.get("res", {}).get("w", {}).get("max"))
119
+ self.gui.res_h_min_var.set(preset.get("res", {}).get("h", {}).get("min"))
120
+ self.gui.res_h_max_var.set(preset.get("res", {}).get("h", {}).get("max"))
121
+ self.gui.res_power_var.set(preset.get("res", {}).get("power"))
122
+ self.gui.quality_min_var.set(preset.get("quality", {}).get("min"))
123
+ self.gui.quality_max_var.set(preset.get("quality", {}).get("max"))
124
+ self.gui.quality_power_var.set(preset.get("quality", {}).get("power"))
125
+ self.gui.color_min_var.set(preset.get("color", {}).get("min"))
126
+ self.gui.color_max_var.set(preset.get("color", {}).get("max"))
127
+ self.gui.color_power_var.set(preset.get("color", {}).get("power"))
128
+ self.gui.duration_min_var.set(preset.get("duration", {}).get("min"))
129
+ self.gui.duration_max_var.set(preset.get("duration", {}).get("max"))
130
+ self.gui.img_size_max_var.set(preset.get("size_max", {}).get("img"))
131
+ self.gui.vid_size_max_var.set(preset.get("size_max", {}).get("vid"))
132
+ self.gui.img_format_var.set(preset.get("format", {}).get("img"))
133
+ self.gui.vid_format_var.set(preset.get("format", {}).get("vid"))
134
+ self.gui.bg_color_var.set(preset.get("bg_color"))
135
+ self.gui.fake_vid_var.set(preset.get("fake_vid"))
136
+ self.gui.scale_filter_var.set(preset.get("scale_filter"))
137
+ self.gui.quantize_method_var.set(preset.get("quantize_method"))
138
+ self.gui.default_emoji_var.set(preset.get("default_emoji"))
139
+ self.gui.steps_var.set(preset.get("steps"))
180
140
 
181
141
  self.cb_no_compress()
182
142
  self.gui.highlight_fields()
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from functools import partial
5
+ from tkinter import colorchooser
5
6
  from typing import Any, List, Tuple
6
7
 
7
8
  from PIL import Image, ImageDraw, ImageTk
@@ -13,7 +14,7 @@ from sticker_convert.gui_components.windows.base_window import BaseWindow
13
14
 
14
15
 
15
16
  class AdvancedCompressionWindow(BaseWindow):
16
- emoji_column_per_row = 10
17
+ emoji_column_per_row = 8
17
18
  emoji_visible_rows = 5
18
19
  emoji_btns: List[Tuple[Button, ImageTk.PhotoImage]] = []
19
20
 
@@ -33,15 +34,7 @@ class AdvancedCompressionWindow(BaseWindow):
33
34
 
34
35
  self.frame_advcomp.grid_columnconfigure(6, weight=1)
35
36
 
36
- cb_msg_block_adv_comp_win = partial(self.gui.cb_msg_block, parent=self)
37
-
38
- self.fps_help_btn = Button(
39
- self.frame_advcomp,
40
- text="?",
41
- width=1,
42
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["fps"]),
43
- bootstyle="secondary", # type: ignore
44
- )
37
+ self.fps_help_btn = self.add_help_btn(self.gui.help["comp"]["fps"])
45
38
  self.fps_lbl = Label(self.frame_advcomp, text="Output FPS")
46
39
  self.fps_min_lbl = Label(self.frame_advcomp, text="Min:")
47
40
  self.fps_min_entry = Entry(
@@ -63,13 +56,7 @@ class AdvancedCompressionWindow(BaseWindow):
63
56
  bootstyle="danger-round-toggle", # type: ignore
64
57
  )
65
58
 
66
- self.res_w_help_btn = Button(
67
- self.frame_advcomp,
68
- text="?",
69
- width=1,
70
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["res"]),
71
- bootstyle="secondary", # type: ignore
72
- )
59
+ self.res_w_help_btn = self.add_help_btn(self.gui.help["comp"]["res"])
73
60
  self.res_w_lbl = Label(self.frame_advcomp, text="Output resolution (Width)")
74
61
  self.res_w_min_lbl = Label(self.frame_advcomp, text="Min:")
75
62
  self.res_w_min_entry = Entry(
@@ -91,13 +78,7 @@ class AdvancedCompressionWindow(BaseWindow):
91
78
  bootstyle="danger-round-toggle", # type: ignore
92
79
  )
93
80
 
94
- self.res_h_help_btn = Button(
95
- self.frame_advcomp,
96
- text="?",
97
- width=1,
98
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["res"]),
99
- bootstyle="secondary", # type: ignore
100
- )
81
+ self.res_h_help_btn = self.add_help_btn(self.gui.help["comp"]["res"])
101
82
  self.res_h_lbl = Label(self.frame_advcomp, text="Output resolution (Height)")
102
83
  self.res_h_min_lbl = Label(self.frame_advcomp, text="Min:")
103
84
  self.res_h_min_entry = Entry(
@@ -119,13 +100,7 @@ class AdvancedCompressionWindow(BaseWindow):
119
100
  bootstyle="danger-round-toggle", # type: ignore
120
101
  )
121
102
 
122
- self.quality_help_btn = Button(
123
- self.frame_advcomp,
124
- text="?",
125
- width=1,
126
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["quality"]),
127
- bootstyle="secondary", # type: ignore
128
- )
103
+ self.quality_help_btn = self.add_help_btn(self.gui.help["comp"]["quality"])
129
104
  self.quality_lbl = Label(self.frame_advcomp, text="Output quality (0-100)")
130
105
  self.quality_min_lbl = Label(self.frame_advcomp, text="Min:")
131
106
  self.quality_min_entry = Entry(
@@ -147,13 +122,7 @@ class AdvancedCompressionWindow(BaseWindow):
147
122
  bootstyle="danger-round-toggle", # type: ignore
148
123
  )
149
124
 
150
- self.color_help_btn = Button(
151
- self.frame_advcomp,
152
- text="?",
153
- width=1,
154
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["color"]),
155
- bootstyle="secondary", # type: ignore
156
- )
125
+ self.color_help_btn = self.add_help_btn(self.gui.help["comp"]["color"])
157
126
  self.color_lbl = Label(self.frame_advcomp, text="Colors (0-256)")
158
127
  self.color_min_lbl = Label(self.frame_advcomp, text="Min:")
159
128
  self.color_min_entry = Entry(
@@ -175,15 +144,7 @@ class AdvancedCompressionWindow(BaseWindow):
175
144
  bootstyle="danger-round-toggle", # type: ignore
176
145
  )
177
146
 
178
- self.duration_help_btn = Button(
179
- self.frame_advcomp,
180
- text="?",
181
- width=1,
182
- command=lambda: cb_msg_block_adv_comp_win(
183
- self.gui.help["comp"]["duration"]
184
- ),
185
- bootstyle="secondary", # type: ignore
186
- )
147
+ self.duration_help_btn = self.add_help_btn(self.gui.help["comp"]["duration"])
187
148
  self.duration_lbl = Label(self.frame_advcomp, text="Duration (Miliseconds)")
188
149
  self.duration_min_lbl = Label(self.frame_advcomp, text="Min:")
189
150
  self.duration_min_entry = Entry(
@@ -205,13 +166,7 @@ class AdvancedCompressionWindow(BaseWindow):
205
166
  bootstyle="danger-round-toggle", # type: ignore
206
167
  )
207
168
 
208
- self.size_help_btn = Button(
209
- self.frame_advcomp,
210
- text="?",
211
- width=1,
212
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["size"]),
213
- bootstyle="secondary", # type: ignore
214
- )
169
+ self.size_help_btn = self.add_help_btn(self.gui.help["comp"]["size"])
215
170
  self.size_lbl = Label(self.frame_advcomp, text="Maximum file size (bytes)")
216
171
  self.img_size_max_lbl = Label(self.frame_advcomp, text="Img:")
217
172
  self.img_size_max_entry = Entry(
@@ -233,13 +188,7 @@ class AdvancedCompressionWindow(BaseWindow):
233
188
  bootstyle="danger-round-toggle", # type: ignore
234
189
  )
235
190
 
236
- self.format_help_btn = Button(
237
- self.frame_advcomp,
238
- text="?",
239
- width=1,
240
- command=lambda: cb_msg_block_adv_comp_win(self.gui.help["comp"]["format"]),
241
- bootstyle="secondary", # type: ignore
242
- )
191
+ self.format_help_btn = self.add_help_btn(self.gui.help["comp"]["format"])
243
192
  self.format_lbl = Label(self.frame_advcomp, text="File format")
244
193
  self.img_format_lbl = Label(self.frame_advcomp, text="Img:")
245
194
  self.img_format_entry = Entry(
@@ -252,15 +201,7 @@ class AdvancedCompressionWindow(BaseWindow):
252
201
  )
253
202
  self.vid_format_entry.bind("<Button-3><ButtonRelease-3>", RightClicker)
254
203
 
255
- self.power_help_btn1 = Button(
256
- self.frame_advcomp,
257
- text="?",
258
- width=1,
259
- command=lambda: cb_msg_block_adv_comp_win(
260
- self.gui.help["comp"]["fps_power"]
261
- ),
262
- bootstyle="secondary", # type: ignore
263
- )
204
+ self.power_help_btn1 = self.add_help_btn(self.gui.help["comp"]["fps_power"])
264
205
  self.power_lbl1 = Label(self.frame_advcomp, text="Power (Importance)")
265
206
  self.fps_power_lbl = Label(self.frame_advcomp, text="FPS:")
266
207
  self.fps_power_entry = Entry(
@@ -273,15 +214,7 @@ class AdvancedCompressionWindow(BaseWindow):
273
214
  )
274
215
  self.res_power_entry.bind("<Button-3><ButtonRelease-3>", RightClicker)
275
216
 
276
- self.power_help_btn2 = Button(
277
- self.frame_advcomp,
278
- text="?",
279
- width=1,
280
- command=lambda: cb_msg_block_adv_comp_win(
281
- self.gui.help["comp"]["fps_power"]
282
- ),
283
- bootstyle="secondary", # type: ignore
284
- )
217
+ self.power_help_btn2 = self.add_help_btn(self.gui.help["comp"]["fps_power"])
285
218
  self.power_lbl2 = Label(self.frame_advcomp, text="Power (Importance)")
286
219
  self.quality_power_lbl = Label(self.frame_advcomp, text="Quality:")
287
220
  self.quality_power_entry = Entry(
@@ -294,15 +227,7 @@ class AdvancedCompressionWindow(BaseWindow):
294
227
  )
295
228
  self.color_power_entry.bind("<Button-3><ButtonRelease-3>", RightClicker)
296
229
 
297
- self.fake_vid_help_btn = Button(
298
- self.frame_advcomp,
299
- text="?",
300
- width=1,
301
- command=lambda: cb_msg_block_adv_comp_win(
302
- self.gui.help["comp"]["fake_vid"]
303
- ),
304
- bootstyle="secondary", # type: ignore
305
- )
230
+ self.fake_vid_help_btn = self.add_help_btn(self.gui.help["comp"]["fake_vid"])
306
231
  self.fake_vid_lbl = Label(
307
232
  self.frame_advcomp, text="Convert (faking) image to video"
308
233
  )
@@ -314,15 +239,21 @@ class AdvancedCompressionWindow(BaseWindow):
314
239
  bootstyle="success-round-toggle", # type: ignore
315
240
  )
316
241
 
317
- self.scale_filter_help_btn = Button(
242
+ self.bg_color_help_btn = self.add_help_btn(self.gui.help["comp"]["bg_color"])
243
+ self.bg_color_lbl = Label(self.frame_advcomp, text="Background color")
244
+ self.bg_color_entry = Entry(
245
+ self.frame_advcomp, textvariable=self.gui.bg_color_var, width=8
246
+ )
247
+ self.bg_color_btn = Button(
318
248
  self.frame_advcomp,
319
- text="?",
320
- width=1,
321
- command=lambda: cb_msg_block_adv_comp_win(
322
- self.gui.help["comp"]["scale_filter"]
323
- ),
249
+ text="Set",
250
+ command=self.cb_bg_color,
324
251
  bootstyle="secondary", # type: ignore
325
252
  )
253
+
254
+ self.scale_filter_help_btn = self.add_help_btn(
255
+ self.gui.help["comp"]["scale_filter"]
256
+ )
326
257
  self.scale_filter_lbl = Label(self.frame_advcomp, text="Scale filter")
327
258
  self.scale_filter_opt = OptionMenu(
328
259
  self.frame_advcomp,
@@ -337,14 +268,8 @@ class AdvancedCompressionWindow(BaseWindow):
337
268
  bootstyle="secondary", # type: ignore
338
269
  )
339
270
 
340
- self.quantize_method_help_btn = Button(
341
- self.frame_advcomp,
342
- text="?",
343
- width=1,
344
- command=lambda: cb_msg_block_adv_comp_win(
345
- self.gui.help["comp"]["quantize_method"]
346
- ),
347
- bootstyle="secondary", # type: ignore
271
+ self.quantize_method_help_btn = self.add_help_btn(
272
+ self.gui.help["comp"]["quantize_method"]
348
273
  )
349
274
  self.quantize_method_lbl = Label(self.frame_advcomp, text="Quantize method")
350
275
  self.quantize_method_opt = OptionMenu(
@@ -357,29 +282,15 @@ class AdvancedCompressionWindow(BaseWindow):
357
282
  bootstyle="secondary", # type: ignore
358
283
  )
359
284
 
360
- self.cache_dir_help_btn = Button(
361
- self.frame_advcomp,
362
- text="?",
363
- width=1,
364
- command=lambda: cb_msg_block_adv_comp_win(
365
- self.gui.help["comp"]["cache_dir"]
366
- ),
367
- bootstyle="secondary", # type: ignore
368
- )
285
+ self.cache_dir_help_btn = self.add_help_btn(self.gui.help["comp"]["cache_dir"])
369
286
  self.cache_dir_lbl = Label(self.frame_advcomp, text="Custom cache directory")
370
287
  self.cache_dir_entry = Entry(
371
288
  self.frame_advcomp, textvariable=self.gui.cache_dir_var, width=30
372
289
  )
373
290
  self.cache_dir_entry.bind("<Button-3><ButtonRelease-3>", RightClicker)
374
291
 
375
- self.default_emoji_help_btn = Button(
376
- self.frame_advcomp,
377
- text="?",
378
- width=1,
379
- command=lambda: cb_msg_block_adv_comp_win(
380
- self.gui.help["comp"]["default_emoji"]
381
- ),
382
- bootstyle="secondary", # type: ignore
292
+ self.default_emoji_help_btn = self.add_help_btn(
293
+ self.gui.help["comp"]["default_emoji"]
383
294
  )
384
295
  self.default_emoji_lbl = Label(self.frame_advcomp, text="Default emoji")
385
296
  self.im: Image.Image = Image.new("RGBA", (32, 32), (255, 255, 255, 0))
@@ -467,27 +378,32 @@ class AdvancedCompressionWindow(BaseWindow):
467
378
  self.fake_vid_lbl.grid(column=1, row=12, sticky="w", padx=3, pady=3)
468
379
  self.fake_vid_cbox.grid(column=6, row=12, sticky="nes", padx=3, pady=3)
469
380
 
470
- self.scale_filter_help_btn.grid(column=0, row=13, sticky="w", padx=3, pady=3)
471
- self.scale_filter_lbl.grid(column=1, row=13, sticky="w", padx=3, pady=3)
381
+ self.bg_color_help_btn.grid(column=0, row=13, sticky="w", padx=3, pady=3)
382
+ self.bg_color_lbl.grid(column=1, row=13, sticky="w", padx=3, pady=3)
383
+ self.bg_color_entry.grid(column=5, row=13, sticky="w", padx=3, pady=3)
384
+ self.bg_color_btn.grid(column=6, row=13, sticky="nes", padx=3, pady=3)
385
+
386
+ self.scale_filter_help_btn.grid(column=0, row=14, sticky="w", padx=3, pady=3)
387
+ self.scale_filter_lbl.grid(column=1, row=14, sticky="w", padx=3, pady=3)
472
388
  self.scale_filter_opt.grid(
473
- column=2, row=13, columnspan=4, sticky="nes", padx=3, pady=3
389
+ column=2, row=14, columnspan=4, sticky="nes", padx=3, pady=3
474
390
  )
475
391
 
476
- self.quantize_method_help_btn.grid(column=0, row=14, sticky="w", padx=3, pady=3)
477
- self.quantize_method_lbl.grid(column=1, row=14, sticky="w", padx=3, pady=3)
392
+ self.quantize_method_help_btn.grid(column=0, row=15, sticky="w", padx=3, pady=3)
393
+ self.quantize_method_lbl.grid(column=1, row=15, sticky="w", padx=3, pady=3)
478
394
  self.quantize_method_opt.grid(
479
- column=2, row=14, columnspan=4, sticky="nes", padx=3, pady=3
395
+ column=2, row=15, columnspan=4, sticky="nes", padx=3, pady=3
480
396
  )
481
397
 
482
- self.cache_dir_help_btn.grid(column=0, row=15, sticky="w", padx=3, pady=3)
483
- self.cache_dir_lbl.grid(column=1, row=15, sticky="w", padx=3, pady=3)
398
+ self.cache_dir_help_btn.grid(column=0, row=16, sticky="w", padx=3, pady=3)
399
+ self.cache_dir_lbl.grid(column=1, row=16, sticky="w", padx=3, pady=3)
484
400
  self.cache_dir_entry.grid(
485
- column=2, row=15, columnspan=4, sticky="nes", padx=3, pady=3
401
+ column=2, row=16, columnspan=4, sticky="nes", padx=3, pady=3
486
402
  )
487
403
 
488
- self.default_emoji_help_btn.grid(column=0, row=16, sticky="w", padx=3, pady=3)
489
- self.default_emoji_lbl.grid(column=1, row=16, sticky="w", padx=3, pady=3)
490
- self.default_emoji_dsp.grid(column=6, row=16, sticky="nes", padx=3, pady=3)
404
+ self.default_emoji_help_btn.grid(column=0, row=17, sticky="w", padx=3, pady=3)
405
+ self.default_emoji_lbl.grid(column=1, row=17, sticky="w", padx=3, pady=3)
406
+ self.default_emoji_dsp.grid(column=6, row=17, sticky="nes", padx=3, pady=3)
491
407
 
492
408
  # https://stackoverflow.com/questions/43731784/tkinter-canvas-scrollbar-with-grid
493
409
  # Create a frame for the canvas with non-zero row&column weights
@@ -647,6 +563,42 @@ class AdvancedCompressionWindow(BaseWindow):
647
563
 
648
564
  self.fake_vid_cbox.config(state=state)
649
565
 
566
+ def cb_bg_color(self, *_: Any) -> None:
567
+ color = colorchooser.askcolor(title="Choose color")[1]
568
+ if color:
569
+ self.gui.bg_color_var.set(color.replace("#", ""))
570
+ self.lift()
571
+ self.attributes("-topmost", True) # type: ignore
572
+ self.attributes("-topmost", False) # type: ignore
573
+
574
+ def cb_bound_to_mousewheel(self, event: Any) -> None:
575
+ for i in self.mousewheel:
576
+ self.emoji_canvas.bind_all(i, self.cb_on_mousewheel)
577
+
578
+ def cb_unbound_to_mousewheel(self, event: Any) -> None:
579
+ for i in self.mousewheel:
580
+ self.emoji_canvas.unbind_all(i)
581
+
582
+ def cb_on_mousewheel(self, event: Any) -> None:
583
+ self.emoji_canvas.yview_scroll(
584
+ int(-1 * (event.delta / self.delta_divide)), "units"
585
+ ) # type: ignore
586
+
587
+ def cb_set_emoji(self, emoji: str) -> None:
588
+ self.gui.default_emoji_var.set(emoji)
589
+ self.set_emoji_btn()
590
+
591
+ def add_help_btn(self, msg: str) -> Button:
592
+ cb_msg_block_adv_comp_win = partial(self.gui.cb_msg_block, parent=self)
593
+
594
+ return Button(
595
+ self.frame_advcomp,
596
+ text="?",
597
+ width=1,
598
+ command=lambda: cb_msg_block_adv_comp_win(msg),
599
+ bootstyle="secondary", # type: ignore
600
+ )
601
+
650
602
  def set_emoji_btn(self) -> None:
651
603
  self.im = Image.new("RGBA", (128, 128), (255, 255, 255, 0))
652
604
  ImageDraw.Draw(self.im).text( # type: ignore
@@ -736,20 +688,3 @@ class AdvancedCompressionWindow(BaseWindow):
736
688
  # https://stackoverflow.com/questions/17355902/tkinter-binding-mousewheel-to-scrollbar
737
689
  self.emoji_canvas.bind("<Enter>", self.cb_bound_to_mousewheel)
738
690
  self.emoji_canvas.bind("<Leave>", self.cb_unbound_to_mousewheel)
739
-
740
- def cb_bound_to_mousewheel(self, event: Any) -> None:
741
- for i in self.mousewheel:
742
- self.emoji_canvas.bind_all(i, self.cb_on_mousewheel)
743
-
744
- def cb_unbound_to_mousewheel(self, event: Any) -> None:
745
- for i in self.mousewheel:
746
- self.emoji_canvas.unbind_all(i)
747
-
748
- def cb_on_mousewheel(self, event: Any) -> None:
749
- self.emoji_canvas.yview_scroll(
750
- int(-1 * (event.delta / self.delta_divide)), "units"
751
- ) # type: ignore
752
-
753
- def cb_set_emoji(self, emoji: str) -> None:
754
- self.gui.default_emoji_var.set(emoji)
755
- self.set_emoji_btn()
sticker_convert/job.py CHANGED
@@ -29,7 +29,6 @@ from sticker_convert.utils.files.json_resources_loader import OUTPUT_JSON
29
29
  from sticker_convert.utils.files.metadata_handler import MetadataHandler
30
30
  from sticker_convert.utils.media.codec_info import CodecInfo
31
31
 
32
- CbQueueType = Queue[CbQueueItemType]
33
32
  WorkListItemType = Optional[Tuple[Callable[..., Any], Tuple[Any, ...]]]
34
33
  if TYPE_CHECKING:
35
34
  # mypy complains about this
@@ -59,7 +58,7 @@ class Executor:
59
58
  # Especially when using scale_filter=nearest
60
59
  self.work_list: WorkListType = self.manager.list()
61
60
  self.results_queue: Queue[Any] = self.manager.Queue()
62
- self.cb_queue: CbQueueType = self.manager.Queue()
61
+ self.cb_queue: Queue[CbQueueItemType] = self.manager.Queue()
63
62
  self.cb_return = CallbackReturn()
64
63
  self.processes: List[Process] = []
65
64
 
@@ -76,7 +75,7 @@ class Executor:
76
75
 
77
76
  def cb_thread(
78
77
  self,
79
- cb_queue: CbQueueType,
78
+ cb_queue: Queue[CbQueueItemType],
80
79
  cb_return: CallbackReturn,
81
80
  ) -> None:
82
81
  for i in iter(cb_queue.get, None):
@@ -113,7 +112,7 @@ class Executor:
113
112
  def worker(
114
113
  work_list: WorkListType,
115
114
  results_queue: Queue[Any],
116
- cb_queue: CbQueueType,
115
+ cb_queue: Queue[CbQueueItemType],
117
116
  cb_return: CallbackReturn,
118
117
  ) -> None:
119
118
  while True:
@@ -412,6 +411,15 @@ class Job:
412
411
  error_msg += f"[X] quantize_method {self.opt_comp.quantize_method} is not valid option"
413
412
  error_msg += " Valid options: imagequant, fastoctree, none"
414
413
 
414
+ if self.opt_comp.bg_color:
415
+ try:
416
+ _, _, _ = bytes.fromhex(self.opt_comp.bg_color)
417
+ except ValueError:
418
+ error_msg += "\n"
419
+ error_msg += (
420
+ f"[X] bg_color {self.opt_comp.bg_color} is not valid color hex"
421
+ )
422
+
415
423
  # Warn about unable to download animated Kakao stickers with such link
416
424
  if (
417
425
  self.opt_output.option == "kakao"
@@ -67,6 +67,8 @@ class CompOption(BaseOption):
67
67
  duration_min: Optional[int] = None
68
68
  duration_max: Optional[int] = None
69
69
 
70
+ bg_color: Optional[str] = None
71
+
70
72
  steps: int = 1
71
73
  fake_vid: Optional[bool] = None
72
74
  quantize_method: Optional[str] = None
@@ -101,7 +103,9 @@ class CompOption(BaseOption):
101
103
  },
102
104
  "duration": {"min": self.duration_min, "max": self.duration_max},
103
105
  "steps": self.steps,
106
+ "bg_color": self.bg_color,
104
107
  "fake_vid": self.fake_vid,
108
+ "quantize_method": self.quantize_method,
105
109
  "scale_filter": self.scale_filter,
106
110
  "cache_dir": self.cache_dir,
107
111
  "default_emoji": self.default_emoji,
@@ -38,9 +38,10 @@
38
38
  "min": 0,
39
39
  "max": 10000
40
40
  },
41
+ "bg_color": "",
41
42
  "steps": 16,
42
43
  "fake_vid": false,
43
- "scale_filter": "lanczos",
44
+ "scale_filter": "bicubic",
44
45
  "quantize_method": "imagequant",
45
46
  "default_emoji": "😀"
46
47
  },
@@ -83,9 +84,10 @@
83
84
  "min": 0,
84
85
  "max": 3000
85
86
  },
87
+ "bg_color": "",
86
88
  "steps": 16,
87
89
  "fake_vid": false,
88
- "scale_filter": "lanczos",
90
+ "scale_filter": "bicubic",
89
91
  "quantize_method": "imagequant",
90
92
  "default_emoji": "😀"
91
93
  },
@@ -128,9 +130,10 @@
128
130
  "min": 0,
129
131
  "max": 3000
130
132
  },
133
+ "bg_color": "",
131
134
  "steps": 16,
132
135
  "fake_vid": false,
133
- "scale_filter": "lanczos",
136
+ "scale_filter": "bicubic",
134
137
  "quantize_method": "imagequant",
135
138
  "default_emoji": "😀"
136
139
  },
@@ -173,9 +176,10 @@
173
176
  "min": 0,
174
177
  "max": 3000
175
178
  },
179
+ "bg_color": "",
176
180
  "steps": 16,
177
181
  "fake_vid": false,
178
- "scale_filter": "lanczos",
182
+ "scale_filter": "bicubic",
179
183
  "quantize_method": "imagequant",
180
184
  "default_emoji": "😀"
181
185
  },
@@ -218,9 +222,10 @@
218
222
  "min": 8,
219
223
  "max": 10000
220
224
  },
225
+ "bg_color": "",
221
226
  "steps": 16,
222
227
  "fake_vid": true,
223
- "scale_filter": "lanczos",
228
+ "scale_filter": "bicubic",
224
229
  "quantize_method": "imagequant",
225
230
  "default_emoji": "😀"
226
231
  },
@@ -263,9 +268,10 @@
263
268
  "min": 83,
264
269
  "max": 4000
265
270
  },
271
+ "bg_color": "",
266
272
  "steps": 16,
267
273
  "fake_vid": false,
268
- "scale_filter": "lanczos",
274
+ "scale_filter": "bicubic",
269
275
  "quantize_method": "imagequant",
270
276
  "default_emoji": "😀"
271
277
  },
@@ -308,9 +314,10 @@
308
314
  "min": 0,
309
315
  "max": 5000
310
316
  },
317
+ "bg_color": "",
311
318
  "steps": 16,
312
319
  "fake_vid": false,
313
- "scale_filter": "lanczos",
320
+ "scale_filter": "bicubic",
314
321
  "quantize_method": "imagequant",
315
322
  "default_emoji": "😀"
316
323
  },
@@ -353,9 +360,10 @@
353
360
  "min": 0,
354
361
  "max": 2000
355
362
  },
363
+ "bg_color": "",
356
364
  "steps": 16,
357
365
  "fake_vid": false,
358
- "scale_filter": "lanczos",
366
+ "scale_filter": "bicubic",
359
367
  "quantize_method": "imagequant",
360
368
  "default_emoji": "😀"
361
369
  },
@@ -398,9 +406,10 @@
398
406
  "min": 0,
399
407
  "max": 2000
400
408
  },
409
+ "bg_color": "",
401
410
  "steps": 16,
402
411
  "fake_vid": false,
403
- "scale_filter": "lanczos",
412
+ "scale_filter": "bicubic",
404
413
  "quantize_method": "imagequant",
405
414
  "default_emoji": "😀"
406
415
  },
@@ -443,9 +452,10 @@
443
452
  "min": 0,
444
453
  "max": 2000
445
454
  },
455
+ "bg_color": "",
446
456
  "steps": 16,
447
457
  "fake_vid": false,
448
- "scale_filter": "lanczos",
458
+ "scale_filter": "bicubic",
449
459
  "quantize_method": "imagequant",
450
460
  "default_emoji": "😀"
451
461
  },
@@ -488,9 +498,10 @@
488
498
  "min": 0,
489
499
  "max": 10000
490
500
  },
501
+ "bg_color": "",
491
502
  "steps": 16,
492
503
  "fake_vid": false,
493
- "scale_filter": "lanczos",
504
+ "scale_filter": "bicubic",
494
505
  "quantize_method": "imagequant",
495
506
  "default_emoji": "😀"
496
507
  }
@@ -38,6 +38,7 @@
38
38
  "duration": "Change playback speed if outside of duration limit.\nDuration set in miliseconds.\n0 will disable limit.",
39
39
  "duration_min": "Set minimum output duration in miliseconds.",
40
40
  "duration_max": "Set maximum output duration in miliseconds.",
41
+ "bg_color": "Set custom background color.\nExample: 00ff00 for green.\nIf this is not set, background color would be auto set to black if image is bright, or white if image is dark.\nNote: The color should not be visible if output format supports transparency.",
41
42
  "size": "Set maximum file size in bytes for video and image.",
42
43
  "vid_size_max": "Set maximum file size limit for animated stickers.",
43
44
  "img_size_max": "Set maximum file size limit for static stickers.",
@@ -45,7 +46,7 @@
45
46
  "vid_format": "Set file format if input is animated.",
46
47
  "img_format": "Set file format if input is static.",
47
48
  "fake_vid": "Convert (faking) image to video.\nUseful if:\n(1) Size limit for video is larger than image;\n(2) Mix image and video into same pack.",
48
- "scale_filter": "Set scale filter. Default as lanczos. Valid options are:\n- nearest = Use nearest neighbour (Suitable for pixel art)\n-box = Similar to nearest, but better downscaling\n- bilinear = Linear interpolation\n- hamming = Similar to bilinear, but better downscaling\n- bicubic = Cubic spline interpolation\n- lanczos = A high-quality downsampling filter",
49
+ "scale_filter": "Set scale filter. Default as bicubic. Valid options are:\n- nearest = Use nearest neighbour (Suitable for pixel art)\n-box = Similar to nearest, but better downscaling\n- bilinear = Linear interpolation\n- hamming = Similar to bilinear, but better downscaling\n- bicubic = Cubic spline interpolation\n- lanczos = A high-quality downsampling filter",
49
50
  "quantize_method": "Set method for quantizing image. Default as imagequant. Valid options are:\n- imagequant = Best quality but slow\n- fastoctree = Fast but image looks chunky\n- none = No image quantizing, large image size as result",
50
51
  "cache_dir": "Set custom cache directory.\nUseful for debugging, or speed up conversion if cache_dir is on RAM disk.",
51
52
  "default_emoji": "Set the default emoji for uploading Signal and Telegram sticker packs."
@@ -230,8 +230,8 @@ class CodecInfo:
230
230
  frames_to_iterate: Optional[int] = None,
231
231
  frames_only: bool = False,
232
232
  ) -> Tuple[int, int]:
233
- import av # type: ignore
234
- from av.container.input import InputContainer # type: ignore
233
+ import av
234
+ from av.container.input import InputContainer
235
235
 
236
236
  # Getting fps and frame count from metadata is not reliable
237
237
  # Example: https://github.com/laggykiller/sticker-convert/issues/114
@@ -242,7 +242,7 @@ class CodecInfo:
242
242
  else:
243
243
  file_ref = BytesIO(file)
244
244
 
245
- with av.open(file_ref) as container: # type: ignore
245
+ with av.open(file_ref) as container:
246
246
  container = cast(InputContainer, container)
247
247
  stream = container.streams.video[0]
248
248
  if container.duration:
@@ -257,7 +257,7 @@ class CodecInfo:
257
257
 
258
258
  frame_count = 0
259
259
  last_frame = None
260
- for frame_count, frame in enumerate(container.decode(stream)): # type: ignore
260
+ for frame_count, frame in enumerate(container.decode(stream)):
261
261
  if frames_to_iterate is not None and frame_count == frames_to_iterate:
262
262
  break
263
263
  last_frame = frame
@@ -310,11 +310,11 @@ class CodecInfo:
310
310
  if codec is not None:
311
311
  return codec.lower()
312
312
 
313
- import av # type: ignore
314
- from av.error import InvalidDataError # type: ignore
313
+ import av
314
+ from av.error import InvalidDataError
315
315
 
316
316
  try:
317
- with av.open(file_ref) as container: # type: ignore
317
+ with av.open(file_ref) as container:
318
318
  return container.streams.video[0].codec_context.name.lower()
319
319
  except InvalidDataError:
320
320
  pass
@@ -354,7 +354,7 @@ class CodecInfo:
354
354
  else:
355
355
  file_ref = BytesIO(file)
356
356
 
357
- with av.open(file_ref) as container: # type: ignore
357
+ with av.open(file_ref) as container:
358
358
  stream = container.streams.video[0]
359
359
  width = stream.width
360
360
  height = stream.height
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.7.7"
3
+ __version__ = "2.7.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.7.7
3
+ Version: 2.7.9
4
4
  Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, iMessage. Written in Python.
5
5
  Author-email: laggykiller <chaudominic2@gmail.com>
6
6
  Maintainer-email: laggykiller <chaudominic2@gmail.com>
@@ -367,13 +367,14 @@ License-File: LICENSE
367
367
  Requires-Dist: aiolimiter ~=1.1.0
368
368
  Requires-Dist: anyio ~=3.7.1
369
369
  Requires-Dist: apngasm-python ~=1.2.3
370
- Requires-Dist: av ~=11.0.0
370
+ Requires-Dist: av ~=12.0.0
371
371
  Requires-Dist: beautifulsoup4 ~=4.12.3
372
372
  Requires-Dist: rookiepy ~=0.3.6
373
373
  Requires-Dist: imagequant ~=1.1.1
374
374
  Requires-Dist: memory-tempfile ~=2.2.3
375
+ Requires-Dist: mergedeep ~=1.3.4
375
376
  Requires-Dist: numpy >=1.22.4
376
- Requires-Dist: Pillow ~=10.2.0
377
+ Requires-Dist: Pillow ==10.1.0
377
378
  Requires-Dist: pyoxipng ~=9.0.0
378
379
  Requires-Dist: python-telegram-bot ~=20.5
379
380
  Requires-Dist: requests ~=2.31.0
@@ -482,18 +483,19 @@ usage: sticker-convert.py [-h] [--version] [--no-confirm] [--input-dir INPUT_DIR
482
483
  [--preset {auto,signal,telegram,telegram_emoji,whatsapp,line,kakao,imessage_small,imessage_medium,imessage_large,custom}]
483
484
  [--steps STEPS] [--processes PROCESSES] [--fps-min FPS_MIN] [--fps-max FPS_MAX]
484
485
  [--fps-power FPS_POWER] [--res-min RES_MIN] [--res-max RES_MAX] [--res-w-min RES_W_MIN]
485
- [--res-w-max RES_W_MAX] [--res-h-min RES_H_MIN] [--res-h-max RES_H_MAX] [--res-power RES_POWER]
486
+ [--res-w-max RES_W_MAX] [--res-h-min RES_H_MIN] [--res-h-max RES_H_MAX] [--res-power RES_POWER]
486
487
  [--quality-min QUALITY_MIN] [--quality-max QUALITY_MAX] [--quality-power QUALITY_POWER]
487
488
  [--color-min COLOR_MIN] [--color-max COLOR_MAX] [--color-power COLOR_POWER]
488
- [--duration-min DURATION_MIN] [--duration-max DURATION_MAX] [--vid-size-max VID_SIZE_MAX]
489
- [--img-size-max IMG_SIZE_MAX] [--vid-format VID_FORMAT] [--img-format IMG_FORMAT] [--fake-vid]
490
- [--scale-filter SCALE_FILTER] [--quantize-method QUANTIZE_METHOD] [--cache-dir CACHE_DIR]
491
- [--default-emoji DEFAULT_EMOJI] [--signal-uuid SIGNAL_UUID] [--signal-password SIGNAL_PASSWORD]
492
- [--signal-get-auth] [--signal-data-dir SIGNAL_DATA_DIR] [--telegram-token TELEGRAM_TOKEN]
489
+ [--duration-min DURATION_MIN] [--duration-max DURATION_MAX] [--bg-color BG_COLOR]
490
+ [--vid-size-max VID_SIZE_MAX] [--img-size-max IMG_SIZE_MAX] [--vid-format VID_FORMAT]
491
+ [--img-format IMG_FORMAT] [--fake-vid] [--scale-filter SCALE_FILTER]
492
+ [--quantize-method QUANTIZE_METHOD] [--cache-dir CACHE_DIR] [--default-emoji DEFAULT_EMOJI]
493
+ [--signal-uuid SIGNAL_UUID] [--signal-password SIGNAL_PASSWORD] [--signal-get-auth]
494
+ [--signal-data-dir SIGNAL_DATA_DIR] [--telegram-token TELEGRAM_TOKEN]
493
495
  [--telegram-userid TELEGRAM_USERID] [--kakao-auth-token KAKAO_AUTH_TOKEN] [--kakao-get-auth]
494
496
  [--kakao-username KAKAO_USERNAME] [--kakao-password KAKAO_PASSWORD]
495
- [--kakao-country-code KAKAO_COUNTRY_CODE] [--kakao-phone-number KAKAO_PHONE_NUMBER]
496
- [--line-get-auth] [--line-cookies LINE_COOKIES] [--save-cred SAVE_CRED]
497
+ [--kakao-country-code KAKAO_COUNTRY_CODE] [--kakao-phone-number KAKAO_PHONE_NUMBER] [--line-get-auth]
498
+ [--line-cookies LINE_COOKIES] [--save-cred SAVE_CRED]
497
499
 
498
500
  CLI for stickers-convert
499
501
 
@@ -538,7 +540,7 @@ Output options:
538
540
 
539
541
  Compression options:
540
542
  --no-compress Do not compress files. Useful for only downloading stickers.
541
- --preset {auto,signal,telegram,telegram_emoji,whatsapp,line,kakao,imessage_small,imessage_medium,imessage_large,custom}
543
+ --preset {auto,signal,telegram,telegram_emoji,whatsapp,line,kakao,imessage_small,imessage_medium,imessage_large,custom}
542
544
  Apply preset for compression.
543
545
  --steps STEPS Set number of divisions between min and max settings.
544
546
  Steps higher = Slower but yields file more closer to the specified file size limit.
@@ -579,6 +581,10 @@ Compression options:
579
581
  Set minimum output duration in miliseconds.
580
582
  --duration-max DURATION_MAX
581
583
  Set maximum output duration in miliseconds.
584
+ --bg-color BG_COLOR Set custom background color.
585
+ Example: 00ff00 for green.
586
+ If this is not set, background color would be auto set to black if image is bright, or white if image is dark.
587
+ Note: The color should not be visible if output format supports transparency.
582
588
  --vid-size-max VID_SIZE_MAX
583
589
  Set maximum file size limit for animated stickers.
584
590
  --img-size-max IMG_SIZE_MAX
@@ -592,7 +598,7 @@ Compression options:
592
598
  (1) Size limit for video is larger than image;
593
599
  (2) Mix image and video into same pack.
594
600
  --scale-filter SCALE_FILTER
595
- Set scale filter. Default as lanczos. Valid options are:
601
+ Set scale filter. Default as bicubic. Valid options are:
596
602
  - nearest = Use nearest neighbour (Suitable for pixel art)
597
603
  -box = Similar to nearest, but better downscaling
598
604
  - bilinear = Linear interpolation
@@ -622,10 +628,10 @@ Credentials options:
622
628
  --telegram-token TELEGRAM_TOKEN
623
629
  Set Telegram token. Required for uploading and downloading Telegram stickers.
624
630
  --telegram-userid TELEGRAM_USERID
625
- Set telegram user_id (From real account, not bot account). Required for uploading Telegram stickers.
631
+ Set telegram user_id (From real account, not bot account). Required for uploading Telegram stickers.
626
632
  --kakao-auth-token KAKAO_AUTH_TOKEN
627
- Set Kakao auth_token. Required for downloading animated stickers from https://e.kakao.com/t/xxxxx
628
- --kakao-get-auth Generate Kakao auth_token. Kakao username, password, country code and phone number are also required.
633
+ Set Kakao auth_token. Required for downloading animated stickers from https://e.kakao.com/t/xxxxx
634
+ --kakao-get-auth Generate Kakao auth_token. Kakao username, password, country code and phone number are also required.
629
635
  --kakao-username KAKAO_USERNAME
630
636
  Set Kakao username, which is email or phone number used for signing up Kakao account
631
637
  Example: +447700900142
@@ -1,12 +1,12 @@
1
1
  sticker_convert/__init__.py,sha256=iQnv6UOOA69c3soAn7ZOnAIubTIQSUxtq1Uhh8xRWvU,102
2
2
  sticker_convert/__main__.py,sha256=6RJauR-SCSSTT3TU7FFB6B6PVwsCxO2xZXtmZ3jc2Is,463
3
- sticker_convert/cli.py,sha256=LXX-LPlFeFKZLc7LKjdtDDLtOqmognEze0kMSr8Vj50,17543
4
- sticker_convert/converter.py,sha256=ZAuRO6Ic6e1nhJc_pBsBlPMVyHZzy87q5Gk_naCRShI,31241
3
+ sticker_convert/cli.py,sha256=Ex5Nculo1cbkfhLPibnYR6-SM431ODGPb324Bos0z6k,17702
4
+ sticker_convert/converter.py,sha256=XYt6bt7hnCJSWK-oWvVUQGCHP2Z-g2xVQWyu4j7oQek,32955
5
5
  sticker_convert/definitions.py,sha256=ZhP2ALCEud-w9ZZD4c3TDG9eHGPZyaAL7zPUsJAbjtE,2073
6
- sticker_convert/gui.py,sha256=N3JTsxYtzcHIjrY1FrMczCe3U_bt8E_-3BAdHH0t8L8,30104
7
- sticker_convert/job.py,sha256=tvK2JrL33DX4swErEihw1sLFv-IP_JQPoPpIpg6JSQ0,25833
8
- sticker_convert/job_option.py,sha256=niPTGZ4X1iUdkxyVc29oM67bpsUNBX9Um7OOJNa3piE,7521
9
- sticker_convert/version.py,sha256=jSfFumyqVCtgQ0gUWQGbYTxqs9lhLwaV1Vi6ncCy0cY,46
6
+ sticker_convert/gui.py,sha256=TqdgFbHBRYgcXWWrsfxLUJ8Zu9WeE11vYyZMX6nalik,30599
7
+ sticker_convert/job.py,sha256=hJgjMAvv50PZ4eYB_ZV7uXc8ZqF7sKnbRvCzs1YzgGE,26144
8
+ sticker_convert/job_option.py,sha256=1YVhyTfu2cWz3qpAKbdIM11jbL0CJz0ksOYAeg8v6dc,7649
9
+ sticker_convert/version.py,sha256=YtYyLos6wcoE1gtu4LiuT2txOQo9u2LqLrvBoQ9NGb4,46
10
10
  sticker_convert/downloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  sticker_convert/downloaders/download_base.py,sha256=5R7c8kwahAflOOYrtSQPnBVQ4T-DsprPUMP7G9wcJX4,2824
12
12
  sticker_convert/downloaders/download_kakao.py,sha256=RYrebTxEjKjXAr9xw18r0dMW0dFjSqZxzi8dpaq56TY,8581
@@ -16,7 +16,7 @@ sticker_convert/downloaders/download_telegram.py,sha256=OZdg7WuJV5VurZVC6dZSnzCW
16
16
  sticker_convert/gui_components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  sticker_convert/gui_components/gui_utils.py,sha256=okho2cA1Scem_m6rPiYifreFzpFrM21-yUkiAv64EUI,3431
18
18
  sticker_convert/gui_components/frames/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- sticker_convert/gui_components/frames/comp_frame.py,sha256=Bqdw1cRVIq1ub0TanXN7zBhQdxGuRzR9UvOgOMG5oWA,8137
19
+ sticker_convert/gui_components/frames/comp_frame.py,sha256=9k_UntKKi2G_g0byzoj1rdTqOq7q9mcnXiy799bYnr0,7257
20
20
  sticker_convert/gui_components/frames/config_frame.py,sha256=b3X4QAnGpde0OhthXHmjSyU_Yb5tYRUFXmy04Yi8Zmo,4316
21
21
  sticker_convert/gui_components/frames/control_frame.py,sha256=_XiOJ9JPUfiysDghGG04sEVLrXG9TMVlDZ60W0LhYVI,835
22
22
  sticker_convert/gui_components/frames/cred_frame.py,sha256=I3XrOv7kUOsvFWquuzWWpZWbLclqKQXgD7dx5pcTuY4,6499
@@ -25,7 +25,7 @@ sticker_convert/gui_components/frames/output_frame.py,sha256=n2WLk22h61DoZli8WbF
25
25
  sticker_convert/gui_components/frames/progress_frame.py,sha256=LWUZg_iL7iiNTfu7N5Ct_pklZdghxihENi7DP9YozOE,4915
26
26
  sticker_convert/gui_components/frames/right_clicker.py,sha256=dGIvSzEChrkguR80pzUemBNJ39uzJjVJQKeDNUoW3Gk,721
27
27
  sticker_convert/gui_components/windows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- sticker_convert/gui_components/windows/advanced_compression_window.py,sha256=1UulI_3ZtXdc34aq-hzFyV2WBZOC5OWGFUAMdf7ipU0,32105
28
+ sticker_convert/gui_components/windows/advanced_compression_window.py,sha256=I3I3seZ5xLTEIsJt3A4t4vsF9ZFY3DmxJVo_yD4HSfA,30639
29
29
  sticker_convert/gui_components/windows/base_window.py,sha256=xBE1peGMPvWsdrFej0CJUVhmQ57GJGvz-cX03nIIhkE,1108
30
30
  sticker_convert/gui_components/windows/kakao_get_auth_window.py,sha256=AvXB2Q8pAPkKILHTvlLGV9jfBcvskCA4arko4nvBTdo,7115
31
31
  sticker_convert/gui_components/windows/line_get_auth_window.py,sha256=S4ES_lk2-GDvPokZtYALnUc5zW1VbS4WulNqO9K1aSs,3375
@@ -66,9 +66,9 @@ sticker_convert/resources/NotoColorEmoji.ttf,sha256=LurIVaCIA8bSCfjrdO1feYr0bhKL
66
66
  sticker_convert/resources/appicon.icns,sha256=FB2DVTOQcFfQNZ9RcyG3z9c9k7eOiI1qw0IJhXMRFg4,5404
67
67
  sticker_convert/resources/appicon.ico,sha256=-ldugcl2Yq2pBRTktnhGKWInpKyWzRjCiPvMr3XPTlc,38078
68
68
  sticker_convert/resources/appicon.png,sha256=6XBEQz7PnerqS43aRkwpWolFG4WvKMuQ-st1ly-_JPg,5265
69
- sticker_convert/resources/compression.json,sha256=WxC_KejpEreXrd_3YfW0NhDKn6xqBSaRqalVohLq5eI,10569
69
+ sticker_convert/resources/compression.json,sha256=vwn6wLsfsPnNnpjhJHRvzozBI4F_kkb9-eBR8IlyYLo,10833
70
70
  sticker_convert/resources/emoji.json,sha256=sXSuKusyG1L2Stuf9BL5ZqfzOIOdeAeE3RXcrXAsLdY,413367
71
- sticker_convert/resources/help.json,sha256=PQcISwdoGTZhyxgOBhNJ47z8t3rCiawewbDFHE53f3k,5988
71
+ sticker_convert/resources/help.json,sha256=dYLh751rhhriSwc2p_xKVdWPCgUZTPyNJ1QFZdUjA2M,6259
72
72
  sticker_convert/resources/input.json,sha256=sRz8qWaLh2KTjjlPIxz2UfynVn2Bn0olywbb8-qT_Hc,2251
73
73
  sticker_convert/resources/output.json,sha256=QYP2gqDvEaAm5I9bH4NReaB1XMLboevv69u-V8YdZUs,1373
74
74
  sticker_convert/uploaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -89,12 +89,12 @@ sticker_convert/utils/files/metadata_handler.py,sha256=TJpQ-7KdnqQh09hwR6xB_scRL
89
89
  sticker_convert/utils/files/run_bin.py,sha256=QalA9je6liHxiOtxsjsFsIkc2t59quhcJCVpP1X3p50,1743
90
90
  sticker_convert/utils/files/sanitize_filename.py,sha256=HBklPGsHRJjFQUIC5rYTQsUrsuTtezZXIEA8CPhLP8A,2156
91
91
  sticker_convert/utils/media/apple_png_normalize.py,sha256=LbrQhc7LlYX4I9ek4XJsZE4l0MygBA1jB-PFiYLEkzk,3657
92
- sticker_convert/utils/media/codec_info.py,sha256=slKvwLxgJz1txJIN4gJ5dAFDLvZLhnTP29Pm8GYveSM,12984
92
+ sticker_convert/utils/media/codec_info.py,sha256=JHHlfCLSpepYFv6TSL1XqwUJoVOpOPFxurRtduplPGY,12856
93
93
  sticker_convert/utils/media/decrypt_kakao.py,sha256=4wq9ZDRnFkx1WmFZnyEogBofiLGsWQM_X69HlA36578,1947
94
94
  sticker_convert/utils/media/format_verify.py,sha256=Xf94jyqk_6M9IlFGMy0wYIgQKn_yg00nD4XW0CgAbew,5732
95
- sticker_convert-2.7.7.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
96
- sticker_convert-2.7.7.dist-info/METADATA,sha256=oLKVy7XCVQ5m6S2WDRiynT_ATYnahnL5PmG4t7tU7Ak,48379
97
- sticker_convert-2.7.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
98
- sticker_convert-2.7.7.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
99
- sticker_convert-2.7.7.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
100
- sticker_convert-2.7.7.dist-info/RECORD,,
95
+ sticker_convert-2.7.9.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
96
+ sticker_convert-2.7.9.dist-info/METADATA,sha256=zUUav4-oua996FFdpYa9TqeXRVfj2X7w1jhK-Xcpg4I,48836
97
+ sticker_convert-2.7.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
98
+ sticker_convert-2.7.9.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
99
+ sticker_convert-2.7.9.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
100
+ sticker_convert-2.7.9.dist-info/RECORD,,