sticker-convert 2.8.2__py3-none-any.whl → 2.8.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,