sticker-convert 2.8.2__py3-none-any.whl → 2.8.3__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.
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Uni
9
9
  import numpy as np
10
10
  from PIL import Image
11
11
  from PIL import __version__ as PillowVersion
12
+ from PIL import features
12
13
 
13
14
  from sticker_convert.job_option import CompOption
14
15
  from sticker_convert.utils.callback import CallbackProtocol, CallbackReturn
@@ -32,6 +33,17 @@ MSG_FAIL_COMP = (
32
33
  "[F] Failed Compression {} -> {}, "
33
34
  "cannot get below limit {} with lowest quality under current settings (Best size: {})"
34
35
  )
36
+ MSG_PYWEBP_DUPFRAME = (
37
+ "[W] {} contains duplicated frame.\n"
38
+ " System WebP>=0.5.0 was not found, hence Pillow cannot be used\n"
39
+ " for creating animated webp. Using pywebp instead, which is known to\n"
40
+ " collapse same frames into single frame, causing problem with animation timing."
41
+ )
42
+ MSG_WEBP_PIL_DUPFRAME = (
43
+ "[W] {} contains duplicated frame.\n"
44
+ " Using Pillow to create animated webp to avoid same frames collapse\n"
45
+ " into single frame, but this is slower."
46
+ )
35
47
 
36
48
  YUV_RGB_MATRIX = np.array(
37
49
  [
@@ -41,6 +53,10 @@ YUV_RGB_MATRIX = np.array(
41
53
  ]
42
54
  )
43
55
 
56
+ # Whether animated WebP is supported
57
+ # See https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#saving-sequences
58
+ PIL_WEBP_ANIM = cast(bool, features.check("webp_anim")) # type: ignore
59
+
44
60
 
45
61
  def get_step_value(
46
62
  max_step: Optional[int],
@@ -179,6 +195,8 @@ class StickerConvert:
179
195
  self.result_step: Optional[int] = None
180
196
 
181
197
  self.apngasm = None
198
+ self.msg_pywebp_dupframe_displayed = False
199
+ self.msg_webp_pil_dupframe_displayed = False
182
200
 
183
201
  @staticmethod
184
202
  def convert(
@@ -394,7 +412,7 @@ class StickerConvert:
394
412
  with open(self.out_f, "wb+") as f:
395
413
  f.write(data)
396
414
 
397
- if result_step:
415
+ if result_step is not None:
398
416
  msg = MSG_DONE_COMP.format(
399
417
  self.in_f_name, self.out_f_name, self.result_size, result_step
400
418
  )
@@ -678,14 +696,51 @@ class StickerConvert:
678
696
  self._frames_export_apng()
679
697
  else:
680
698
  self._frames_export_png()
681
- elif self.out_f.suffix == ".webp" and is_animated:
699
+ elif self.out_f.suffix == ".webp":
682
700
  self._frames_export_webp()
683
701
  elif self.out_f.suffix == ".gif":
684
- self._frames_export_gif()
702
+ self._frames_export_pil_anim()
685
703
  elif self.out_f.suffix in (".webm", ".mp4", ".mkv") or is_animated:
686
704
  self._frames_export_pyav()
687
705
  else:
688
706
  self._frames_export_pil()
707
+
708
+ def _check_dup(self) -> bool:
709
+ if len(self.frames_processed) == 1:
710
+ return False
711
+
712
+ prev_frame = self.frames_processed[0]
713
+ for frame in self.frames_processed[1:]:
714
+ if np.array_equal(frame, prev_frame):
715
+ return True
716
+ prev_frame = frame
717
+
718
+ return False
719
+
720
+ def _frames_export_webp(self) -> None:
721
+ has_dup_frames = self._check_dup()
722
+ if self.fps:
723
+ # It was noted that pywebp would collapse all frames.
724
+ # aed005b attempted to fix this by creating webp with
725
+ # variable frame duration. However, the webp created would
726
+ # not be accepted by WhatsApp.
727
+ # Therefore, we are preferring Pillow over pywebp.
728
+ if has_dup_frames:
729
+ if PIL_WEBP_ANIM:
730
+ # Warn that using Pillow is slower
731
+ if not self.msg_webp_pil_dupframe_displayed:
732
+ self.cb.put(MSG_WEBP_PIL_DUPFRAME.format(self.in_f_name))
733
+ self.msg_webp_pil_dupframe_displayed = True
734
+ self._frames_export_pil_anim()
735
+ else:
736
+ if not self.msg_pywebp_dupframe_displayed:
737
+ self.cb.put(MSG_PYWEBP_DUPFRAME.format(self.in_f_name))
738
+ self.msg_pywebp_dupframe_displayed = True
739
+ self._frames_export_pywebp()
740
+ else:
741
+ self._frames_export_pywebp()
742
+ else:
743
+ self._frames_export_pil()
689
744
 
690
745
  def _frames_export_pil(self) -> None:
691
746
  with Image.fromarray(self.frames_processed[0]) as im: # type: ignore
@@ -710,7 +765,7 @@ class StickerConvert:
710
765
  codec = "apng"
711
766
  pixel_format = "rgba"
712
767
  options["plays"] = "0"
713
- elif self.out_f.suffix in (".webp", ".webm", ".mkv"):
768
+ elif self.out_f.suffix in (".webm", ".mkv"):
714
769
  codec = "libvpx-vp9"
715
770
  pixel_format = "yuva420p"
716
771
  options["loop"] = "0"
@@ -734,7 +789,7 @@ class StickerConvert:
734
789
  output.mux(out_stream.encode(av_frame))
735
790
  output.mux(out_stream.encode())
736
791
 
737
- def _frames_export_gif(self) -> None:
792
+ def _frames_export_pil_anim(self) -> None:
738
793
  extra_kwargs: Dict[str, Any] = {}
739
794
 
740
795
  # disposal=2 on gif cause flicker in image with transparency
@@ -745,25 +800,34 @@ class StickerConvert:
745
800
  else:
746
801
  extra_kwargs["optimize"] = True
747
802
 
748
- # GIF can only have one alpha color
749
- # Change lowest alpha to alpha=0
750
- # Only keep alpha=0 and alpha=255, nothing in between
751
- frames_processed = np.array(self.frames_processed)
752
- alpha = frames_processed[:, :, :, 3]
753
- alpha_min = np.min(alpha) # type: ignore
754
- if alpha_min < 255:
755
- alpha[alpha > alpha_min] = 255
756
- alpha[alpha == alpha_min] = 0
757
-
758
- if 0 in alpha:
759
- extra_kwargs["transparency"] = 0
760
- extra_kwargs["disposal"] = 2
761
- im_out = [self.quantize(Image.fromarray(i)) for i in frames_processed] # type: ignore
803
+ if self.out_f.suffix == ".gif":
804
+ # GIF can only have one alpha color
805
+ # Change lowest alpha to alpha=0
806
+ # Only keep alpha=0 and alpha=255, nothing in between
807
+ frames_processed = np.array(self.frames_processed)
808
+ alpha = frames_processed[:, :, :, 3]
809
+ alpha_min = np.min(alpha) # type: ignore
810
+ if alpha_min < 255:
811
+ alpha[alpha > alpha_min] = 255
812
+ alpha[alpha == alpha_min] = 0
813
+
814
+ if 0 in alpha:
815
+ extra_kwargs["transparency"] = 0
816
+ extra_kwargs["disposal"] = 2
817
+ im_out = [self.quantize(Image.fromarray(i)) for i in frames_processed] # type: ignore
818
+ else:
819
+ im_out = [
820
+ self.quantize(Image.fromarray(i).convert("RGB")).convert("RGB") # type: ignore
821
+ for i in frames_processed
822
+ ]
823
+ extra_kwargs["format"] = "GIF"
824
+ elif self.out_f.suffix == ".webp":
825
+ im_out = [Image.fromarray(i) for i in self.frames_processed] # type: ignore
826
+ extra_kwargs["format"] = "WebP"
827
+ extra_kwargs["minimize_size"] = True
828
+ extra_kwargs["method"] = 6
762
829
  else:
763
- im_out = [
764
- self.quantize(Image.fromarray(i).convert("RGB")).convert("RGB") # type: ignore
765
- for i in frames_processed
766
- ]
830
+ raise RuntimeError(f"Invalid format {self.out_f.suffix}")
767
831
 
768
832
  if self.fps:
769
833
  extra_kwargs["save_all"] = True
@@ -773,12 +837,11 @@ class StickerConvert:
773
837
 
774
838
  im_out[0].save(
775
839
  self.tmp_f,
776
- format="GIF",
777
840
  quality=self.quality,
778
841
  **extra_kwargs,
779
842
  )
780
843
 
781
- def _frames_export_webp(self) -> None:
844
+ def _frames_export_pywebp(self) -> None:
782
845
  import webp # type: ignore
783
846
 
784
847
  assert self.fps
@@ -786,25 +849,10 @@ class StickerConvert:
786
849
  config = webp.WebPConfig.new(quality=self.quality) # type: ignore
787
850
  enc = webp.WebPAnimEncoder.new(self.res_w, self.res_h) # type: ignore
788
851
  timestamp_ms = 0
789
- timestamp_inc = int(1000 / self.fps)
790
-
791
- pic = webp.WebPPicture.from_numpy(self.frames_processed[0]) # type: ignore
792
- enc.encode_frame(pic, 0, config=config) # type: ignore
793
-
794
- frame_num = 1
795
- frame_num_prev = 1
796
- frame_total = len(self.frames_processed)
797
- while frame_num < frame_total - 1:
798
- while frame_num < frame_total - 1 and np.array_equal(
799
- self.frames_processed[frame_num_prev],
800
- self.frames_processed[frame_num],
801
- ):
802
- timestamp_ms += timestamp_inc
803
- frame_num += 1
804
- pic = webp.WebPPicture.from_numpy(self.frames_processed[frame_num]) # type: ignore
852
+ for frame in self.frames_processed:
853
+ pic = webp.WebPPicture.from_numpy(frame) # type: ignore
805
854
  enc.encode_frame(pic, timestamp_ms, config=config) # type: ignore
806
- frame_num_prev = frame_num
807
-
855
+ timestamp_ms += int(1000 / self.fps)
808
856
  anim_data = enc.assemble(timestamp_ms) # type: ignore
809
857
  self.tmp_f.write(anim_data.buffer()) # type: ignore
810
858
 
@@ -79,7 +79,7 @@ class CompressWastickers(UploadBase):
79
79
  else:
80
80
  ext = ".png"
81
81
 
82
- dst = Path(tempdir, str(num) + ext)
82
+ dst = Path(tempdir, f"sticker_{num+1}{ext}")
83
83
 
84
84
  if FormatVerify.check_file(
85
85
  src, spec=self.webp_spec
@@ -114,7 +114,7 @@ class CompressWastickers(UploadBase):
114
114
  opt_comp_merged.merge(self.spec_cover)
115
115
 
116
116
  cover_path_old = MetadataHandler.get_cover(self.opt_output.dir)
117
- cover_path_new = Path(pack_dir, "100.png")
117
+ cover_path_new = Path(pack_dir, "tray.png")
118
118
  if cover_path_old:
119
119
  if FormatVerify.check_file(cover_path_old, spec=self.spec_cover):
120
120
  shutil.copy(cover_path_old, cover_path_new)
@@ -142,7 +142,7 @@ class CompressWastickers(UploadBase):
142
142
  self.cb_return,
143
143
  )
144
144
 
145
- MetadataHandler.set_metadata(pack_dir, author=author, title=title)
145
+ MetadataHandler.set_metadata(pack_dir, author=author, title=title, newline=True)
146
146
 
147
147
  @staticmethod
148
148
  def start(
@@ -133,16 +133,21 @@ class MetadataHandler:
133
133
  title: Optional[str] = None,
134
134
  author: Optional[str] = None,
135
135
  emoji_dict: Optional[Dict[str, str]] = None,
136
+ newline: bool = False,
136
137
  ) -> None:
137
138
  title_path = Path(directory, "title.txt")
138
139
  if title is not None:
139
140
  with open(title_path, "w+", encoding="utf-8") as f:
140
141
  f.write(title)
142
+ if newline:
143
+ f.write("\n")
141
144
 
142
145
  author_path = Path(directory, "author.txt")
143
146
  if author is not None:
144
147
  with open(author_path, "w+", encoding="utf-8") as f:
145
148
  f.write(author)
149
+ if newline:
150
+ f.write("\n")
146
151
 
147
152
  emoji_path = Path(directory, "emoji.txt")
148
153
  if emoji_dict is not None:
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- __version__ = "2.8.2"
3
+ __version__ = "2.8.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sticker-convert
3
- Version: 2.8.2
3
+ Version: 2.8.3
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>
@@ -1,12 +1,12 @@
1
1
  sticker_convert/__init__.py,sha256=iQnv6UOOA69c3soAn7ZOnAIubTIQSUxtq1Uhh8xRWvU,102
2
2
  sticker_convert/__main__.py,sha256=6RJauR-SCSSTT3TU7FFB6B6PVwsCxO2xZXtmZ3jc2Is,463
3
3
  sticker_convert/cli.py,sha256=KMl8G25rTpSko46SC-WdI5YDBykyluQL13PYpZW_O8M,18579
4
- sticker_convert/converter.py,sha256=tGUTcMLVvfq5BFqIev76jzDiONe0KNKC-e-cHpawuP0,35074
4
+ sticker_convert/converter.py,sha256=CwhOuH8Os5zU_cLKmK0IKLAurA_-9-4E0cccQNHLp8Y,37357
5
5
  sticker_convert/definitions.py,sha256=ZhP2ALCEud-w9ZZD4c3TDG9eHGPZyaAL7zPUsJAbjtE,2073
6
6
  sticker_convert/gui.py,sha256=TRPGwMhSMPHnZppHmw2OWHKTJtGoeLpGWD0eRYi4_yk,30707
7
7
  sticker_convert/job.py,sha256=dBo98c5dIbg5ZUY8CEE1XQOSoBuf5VOZc-8pmq0Y1Js,25608
8
8
  sticker_convert/job_option.py,sha256=JHAFCxp7-dDwD-1PbpYLAFRF3OoJu8cj_BjOm5r8Gp8,7732
9
- sticker_convert/version.py,sha256=cG0zSLjJ4KXuLjUEWES88zpDwr28nDF8AaBVALGm5Ec,46
9
+ sticker_convert/version.py,sha256=s6EC7_A6bcTAEEXvg8UKbvmODYWmAfNLeSy3bqzdqAY,46
10
10
  sticker_convert/downloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  sticker_convert/downloaders/download_base.py,sha256=CcrgZiBOYJbYcDGCPDHp-ECGXSpfmGtQCzS7KRbBl1E,2726
12
12
  sticker_convert/downloaders/download_kakao.py,sha256=UFp7EpMea62fIePY5DfhH4jThAwdeazfoC5iW1g4dAo,8516
@@ -72,7 +72,7 @@ sticker_convert/resources/help.json,sha256=7VKc4Oxw6e4zv2IIeYRQ5e_aa88UlsgIHSBm9
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
75
- sticker_convert/uploaders/compress_wastickers.py,sha256=xNua2pDPD-7Q2Fx9WLo1IlncVs3jVPMN-pxXTswjk8g,5997
75
+ sticker_convert/uploaders/compress_wastickers.py,sha256=SMPf1_ir30ZKO2ChHspDFuyaufx0XeVBVLOlHmawEdY,6021
76
76
  sticker_convert/uploaders/upload_base.py,sha256=uQupPn6r4zrlAzpKzzX7CgvZb69ATyrwPKahWOQj0ds,1203
77
77
  sticker_convert/uploaders/upload_signal.py,sha256=vFMvQ4TwDNliPuQ7ecXHzTT-qrTwPAORbSxZ7Nk9VM4,6541
78
78
  sticker_convert/uploaders/upload_telegram.py,sha256=KXQskywHjiVWLDz6qjzJhZk14PNRD3vQ-HNORNa1vNI,14634
@@ -85,16 +85,16 @@ sticker_convert/utils/auth/get_signal_auth.py,sha256=6Sx-lMuyBHeX1RpjAWI8u03qnRu
85
85
  sticker_convert/utils/files/cache_store.py,sha256=etfe614OAhAyrnM5fGeESKq6R88YLNqkqkxSzEmZ0V0,1047
86
86
  sticker_convert/utils/files/json_manager.py,sha256=Vr6pZJdLMkrJJWN99210aduVHb0ILyf0SSTaw4TZqgc,541
87
87
  sticker_convert/utils/files/json_resources_loader.py,sha256=flZFixUXRTrOAhvRQpuSQgmJ69yXL94sxukcowLT1JQ,1049
88
- sticker_convert/utils/files/metadata_handler.py,sha256=TJpQ-7KdnqQh09hwR6xB_scRLhbJ6D3zgORy3dkf858,9933
88
+ sticker_convert/utils/files/metadata_handler.py,sha256=5ED1UVoAaObgAXe4NZ8sXfsNX7JQvGIqgfCQCjpXRjc,10088
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
92
  sticker_convert/utils/media/codec_info.py,sha256=SJSFvQzXHnGkj7MH9xJ5xiC4cqiOjFKckFKE_FICdT4,15562
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.8.2.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
96
- sticker_convert-2.8.2.dist-info/METADATA,sha256=rY1d2u1zQbf4IbudJK4UxA1dnWkhdCM9BMNS8mov49k,49284
97
- sticker_convert-2.8.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
98
- sticker_convert-2.8.2.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
99
- sticker_convert-2.8.2.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
100
- sticker_convert-2.8.2.dist-info/RECORD,,
95
+ sticker_convert-2.8.3.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
96
+ sticker_convert-2.8.3.dist-info/METADATA,sha256=1Z_8zjVrd5RMAilj-dAEVVGBtbYzkchvTDgzeWsfCTA,49284
97
+ sticker_convert-2.8.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
98
+ sticker_convert-2.8.3.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
99
+ sticker_convert-2.8.3.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
100
+ sticker_convert-2.8.3.dist-info/RECORD,,