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.
- sticker_convert/converter.py +91 -43
- sticker_convert/uploaders/compress_wastickers.py +3 -3
- sticker_convert/utils/files/metadata_handler.py +5 -0
- sticker_convert/version.py +1 -1
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/METADATA +1 -1
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/RECORD +10 -10
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/LICENSE +0 -0
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/WHEEL +0 -0
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.8.2.dist-info → sticker_convert-2.8.3.dist-info}/top_level.txt +0 -0
sticker_convert/converter.py
CHANGED
@@ -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"
|
699
|
+
elif self.out_f.suffix == ".webp":
|
682
700
|
self._frames_export_webp()
|
683
701
|
elif self.out_f.suffix == ".gif":
|
684
|
-
self.
|
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 (".
|
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
|
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
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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,
|
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, "
|
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:
|
sticker_convert/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sticker-convert
|
3
|
-
Version: 2.8.
|
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=
|
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=
|
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=
|
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=
|
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.
|
96
|
-
sticker_convert-2.8.
|
97
|
-
sticker_convert-2.8.
|
98
|
-
sticker_convert-2.8.
|
99
|
-
sticker_convert-2.8.
|
100
|
-
sticker_convert-2.8.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|