sticker-convert 2.7.3__py3-none-any.whl → 2.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sticker_convert/cli.py +12 -16
- sticker_convert/converter.py +46 -50
- sticker_convert/definitions.py +8 -12
- sticker_convert/downloaders/download_base.py +13 -30
- sticker_convert/downloaders/download_kakao.py +19 -25
- sticker_convert/downloaders/download_line.py +7 -8
- sticker_convert/downloaders/download_signal.py +4 -5
- sticker_convert/downloaders/download_telegram.py +4 -5
- sticker_convert/gui.py +28 -29
- sticker_convert/gui_components/frames/comp_frame.py +7 -7
- sticker_convert/gui_components/frames/config_frame.py +7 -7
- sticker_convert/gui_components/frames/control_frame.py +1 -1
- sticker_convert/gui_components/frames/cred_frame.py +8 -8
- sticker_convert/gui_components/frames/input_frame.py +6 -6
- sticker_convert/gui_components/frames/output_frame.py +6 -6
- sticker_convert/gui_components/frames/progress_frame.py +6 -6
- sticker_convert/gui_components/gui_utils.py +2 -4
- sticker_convert/gui_components/windows/advanced_compression_window.py +13 -11
- sticker_convert/gui_components/windows/base_window.py +3 -3
- sticker_convert/gui_components/windows/kakao_get_auth_window.py +2 -2
- sticker_convert/gui_components/windows/line_get_auth_window.py +2 -2
- sticker_convert/gui_components/windows/signal_get_auth_window.py +1 -1
- sticker_convert/job.py +66 -75
- sticker_convert/job_option.py +1 -1
- sticker_convert/resources/help.json +1 -1
- sticker_convert/uploaders/compress_wastickers.py +3 -3
- sticker_convert/uploaders/upload_base.py +2 -3
- sticker_convert/uploaders/upload_signal.py +5 -5
- sticker_convert/uploaders/upload_telegram.py +5 -4
- sticker_convert/uploaders/xcode_imessage.py +14 -69
- sticker_convert/utils/auth/get_kakao_auth.py +4 -5
- sticker_convert/utils/auth/get_line_auth.py +1 -2
- sticker_convert/utils/auth/get_signal_auth.py +4 -4
- sticker_convert/utils/callback.py +25 -17
- sticker_convert/utils/files/cache_store.py +4 -6
- sticker_convert/utils/files/json_manager.py +3 -4
- sticker_convert/utils/files/json_resources_loader.py +12 -0
- sticker_convert/utils/files/metadata_handler.py +83 -74
- sticker_convert/utils/files/run_bin.py +8 -7
- sticker_convert/utils/files/sanitize_filename.py +30 -28
- sticker_convert/utils/media/apple_png_normalize.py +2 -2
- sticker_convert/utils/media/codec_info.py +22 -29
- sticker_convert/utils/media/decrypt_kakao.py +3 -5
- sticker_convert/utils/media/format_verify.py +6 -8
- sticker_convert/utils/url_detect.py +4 -5
- sticker_convert/version.py +1 -1
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/METADATA +16 -14
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/RECORD +52 -51
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/WHEEL +1 -1
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/LICENSE +0 -0
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.7.3.dist-info → sticker_convert-2.7.4.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,7 @@ class GetKakaoAuth:
|
|
17
17
|
cb_msg: Callable[..., None] = print,
|
18
18
|
cb_msg_block: Callable[..., Any] = input,
|
19
19
|
cb_ask_str: Callable[..., str] = input,
|
20
|
-
):
|
20
|
+
) -> None:
|
21
21
|
self.username = opt_cred.kakao_username
|
22
22
|
self.password = opt_cred.kakao_password
|
23
23
|
self.country_code = opt_cred.kakao_country_code
|
@@ -117,13 +117,12 @@ class GetKakaoAuth:
|
|
117
117
|
|
118
118
|
if self.verify_method == "passcode":
|
119
119
|
return self.verify_receive_sms()
|
120
|
-
|
120
|
+
if self.verify_method == "mo-send":
|
121
121
|
dest_number = response_json["viewData"]["moNumber"]
|
122
122
|
msg = response_json["viewData"]["moMessage"]
|
123
123
|
return self.verify_send_sms(dest_number, msg)
|
124
|
-
|
125
|
-
|
126
|
-
return False
|
124
|
+
self.cb_msg_block(f"Unknown verification method: {response.text}")
|
125
|
+
return False
|
127
126
|
|
128
127
|
def verify_send_sms(self, dest_number: str, msg: str) -> bool:
|
129
128
|
self.cb_msg("Verification by sending SMS")
|
@@ -45,10 +45,10 @@ class GetSignalAuth:
|
|
45
45
|
|
46
46
|
if Path(signal_bin_path_prod).is_file():
|
47
47
|
return signal_bin_path_prod, signal_user_data_dir_prod
|
48
|
-
|
48
|
+
if Path(signal_bin_path_beta).is_file():
|
49
49
|
return signal_bin_path_beta, signal_user_data_dir_beta
|
50
|
-
|
51
|
-
|
50
|
+
|
51
|
+
return None, None
|
52
52
|
|
53
53
|
def get_cred(
|
54
54
|
self,
|
@@ -76,7 +76,7 @@ class GetSignalAuth:
|
|
76
76
|
msg += f"{signal_user_data_dir=}\n"
|
77
77
|
return None, None, msg
|
78
78
|
|
79
|
-
with open(signal_config) as f:
|
79
|
+
with open(signal_config, encoding="utf-8") as f:
|
80
80
|
config = json.load(f)
|
81
81
|
key = config.get("key")
|
82
82
|
db_key = f"x'{key}'"
|
@@ -4,6 +4,15 @@ from typing import Any, Callable, Dict, Optional, Tuple, Union
|
|
4
4
|
|
5
5
|
from tqdm import tqdm
|
6
6
|
|
7
|
+
CbQueueTupleType = Tuple[
|
8
|
+
Optional[str], Optional[Tuple[Any, ...]], Optional[Dict[str, Any]]
|
9
|
+
]
|
10
|
+
CbQueueItemType = Union[
|
11
|
+
CbQueueTupleType,
|
12
|
+
str,
|
13
|
+
None,
|
14
|
+
]
|
15
|
+
|
7
16
|
|
8
17
|
class CallbackReturn:
|
9
18
|
def __init__(self) -> None:
|
@@ -34,7 +43,7 @@ class Callback:
|
|
34
43
|
ask_str: Optional[Callable[..., str]] = None,
|
35
44
|
silent: bool = False,
|
36
45
|
no_confirm: bool = False,
|
37
|
-
):
|
46
|
+
) -> None:
|
38
47
|
self.progress_bar: Optional[tqdm[Any]] = None
|
39
48
|
|
40
49
|
if msg:
|
@@ -88,7 +97,7 @@ class Callback:
|
|
88
97
|
set_progress_mode: Optional[str] = None,
|
89
98
|
steps: int = 0,
|
90
99
|
update_bar: bool = False,
|
91
|
-
):
|
100
|
+
) -> None:
|
92
101
|
if self.silent:
|
93
102
|
return
|
94
103
|
|
@@ -122,16 +131,15 @@ class Callback:
|
|
122
131
|
'"--no-confirm" flag is set. Continue with this run without asking questions'
|
123
132
|
)
|
124
133
|
return True
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
return True
|
134
|
+
|
135
|
+
self.msg(
|
136
|
+
'[If you do not want to get asked by this question, add "--no-confirm" flag]'
|
137
|
+
)
|
138
|
+
self.msg()
|
139
|
+
result = input("Continue? [y/N] > ")
|
140
|
+
if result.lower() != "y":
|
141
|
+
return False
|
142
|
+
return True
|
135
143
|
|
136
144
|
def cb_ask_str(
|
137
145
|
self,
|
@@ -155,7 +163,7 @@ class Callback:
|
|
155
163
|
def put(
|
156
164
|
self,
|
157
165
|
i: Union[
|
158
|
-
|
166
|
+
CbQueueItemType,
|
159
167
|
str,
|
160
168
|
],
|
161
169
|
) -> Union[str, bool, None]:
|
@@ -166,18 +174,18 @@ class Callback:
|
|
166
174
|
else:
|
167
175
|
args = tuple()
|
168
176
|
if len(i) >= 3:
|
169
|
-
kwargs: Dict[str, Any] = i[2] if i[2] else
|
177
|
+
kwargs: Dict[str, Any] = i[2] if i[2] else {}
|
170
178
|
else:
|
171
|
-
kwargs =
|
179
|
+
kwargs = {}
|
172
180
|
else:
|
173
181
|
action = i
|
174
182
|
args = tuple()
|
175
|
-
kwargs =
|
183
|
+
kwargs = {}
|
176
184
|
|
177
185
|
# Fake implementation for Queue.put()
|
178
186
|
if action is None:
|
179
187
|
return None
|
180
|
-
|
188
|
+
if action == "msg":
|
181
189
|
self.msg(*args, **kwargs)
|
182
190
|
elif action == "bar":
|
183
191
|
self.bar(**kwargs)
|
@@ -2,7 +2,10 @@
|
|
2
2
|
import os
|
3
3
|
import platform
|
4
4
|
import shutil
|
5
|
+
from contextlib import contextmanager
|
5
6
|
from pathlib import Path
|
7
|
+
from tempfile import TemporaryDirectory
|
8
|
+
from typing import Any, ContextManager, Generator, Optional, Union
|
6
9
|
from uuid import uuid4
|
7
10
|
|
8
11
|
if platform.system() == "Linux":
|
@@ -12,10 +15,6 @@ if platform.system() == "Linux":
|
|
12
15
|
else:
|
13
16
|
import tempfile
|
14
17
|
|
15
|
-
from contextlib import contextmanager
|
16
|
-
from tempfile import TemporaryDirectory
|
17
|
-
from typing import Any, ContextManager, Generator, Optional, Union
|
18
|
-
|
19
18
|
|
20
19
|
def debug_cache_dir(path: str) -> ContextManager[Path]:
|
21
20
|
@contextmanager
|
@@ -37,5 +36,4 @@ class CacheStore:
|
|
37
36
|
) -> "Union[ContextManager[Path], TemporaryDirectory[str]]":
|
38
37
|
if path:
|
39
38
|
return debug_cache_dir(path)
|
40
|
-
|
41
|
-
return tempfile.TemporaryDirectory() # type: ignore
|
39
|
+
return tempfile.TemporaryDirectory() # type: ignore
|
@@ -9,10 +9,9 @@ class JsonManager:
|
|
9
9
|
def load_json(path: Path) -> Dict[Any, Any]:
|
10
10
|
if not path.is_file():
|
11
11
|
raise RuntimeError(f"{path} cannot be found")
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
return data
|
12
|
+
with open(path, encoding="utf-8") as f:
|
13
|
+
data = json.load(f)
|
14
|
+
return data
|
16
15
|
|
17
16
|
@staticmethod
|
18
17
|
def save_json(path: Path, data: Dict[Any, Any]) -> None:
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from typing import Dict
|
2
|
+
|
3
|
+
from sticker_convert.definitions import ROOT_DIR
|
4
|
+
from sticker_convert.utils.files.json_manager import JsonManager
|
5
|
+
|
6
|
+
HELP_JSON: Dict[str, Dict[str, str]] = JsonManager.load_json(
|
7
|
+
ROOT_DIR / "resources/help.json"
|
8
|
+
)
|
9
|
+
INPUT_JSON = JsonManager.load_json(ROOT_DIR / "resources/input.json")
|
10
|
+
COMPRESSION_JSON = JsonManager.load_json(ROOT_DIR / "resources/compression.json")
|
11
|
+
OUTPUT_JSON = JsonManager.load_json(ROOT_DIR / "resources/output.json")
|
12
|
+
EMOJI_JSON = JsonManager.load_json(ROOT_DIR / "resources/emoji.json")
|
@@ -5,10 +5,55 @@ import json
|
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import Dict, List, Optional, Tuple
|
7
7
|
|
8
|
-
from sticker_convert.
|
9
|
-
from sticker_convert.utils.files.json_manager import JsonManager
|
8
|
+
from sticker_convert.utils.files.json_resources_loader import INPUT_JSON, OUTPUT_JSON
|
10
9
|
from sticker_convert.utils.media.codec_info import CodecInfo
|
11
10
|
|
11
|
+
RELATED_EXTENSIONS = (
|
12
|
+
".png",
|
13
|
+
".apng",
|
14
|
+
".jpg",
|
15
|
+
".jpeg",
|
16
|
+
".gif",
|
17
|
+
".tgs",
|
18
|
+
".lottie",
|
19
|
+
".json",
|
20
|
+
".mp4",
|
21
|
+
".mkv",
|
22
|
+
".mov",
|
23
|
+
".webm",
|
24
|
+
".webp",
|
25
|
+
".avi",
|
26
|
+
".m4a",
|
27
|
+
".wastickers",
|
28
|
+
)
|
29
|
+
RELATED_NAME = (
|
30
|
+
"title.txt",
|
31
|
+
"author.txt",
|
32
|
+
"emoji.txt",
|
33
|
+
"export-result.txt",
|
34
|
+
".DS_Store",
|
35
|
+
"._.DS_Store",
|
36
|
+
)
|
37
|
+
|
38
|
+
BLACKLIST_PREFIX = ("cover",)
|
39
|
+
BLACKLIST_SUFFIX = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store")
|
40
|
+
|
41
|
+
XCODE_IMESSAGE_ICONSET = {
|
42
|
+
"App-Store-1024x1024pt.png": (1024, 1024),
|
43
|
+
"iPad-Settings-29pt@2x.png": (58, 58),
|
44
|
+
"iPhone-settings-29pt@2x.png": (58, 58),
|
45
|
+
"iPhone-settings-29pt@3x.png": (87, 87),
|
46
|
+
"Messages27x20pt@2x.png": (54, 40),
|
47
|
+
"Messages27x20pt@3x.png": (81, 60),
|
48
|
+
"Messages32x24pt@2x.png": (64, 48),
|
49
|
+
"Messages32x24pt@3x.png": (96, 72),
|
50
|
+
"Messages-App-Store-1024x768pt.png": (1024, 768),
|
51
|
+
"Messages-iPad-67x50pt@2x.png": (134, 100),
|
52
|
+
"Messages-iPad-Pro-74x55pt@2x.png": (148, 110),
|
53
|
+
"Messages-iPhone-60x45pt@2x.png": (120, 90),
|
54
|
+
"Messages-iPhone-60x45pt@3x.png": (180, 135),
|
55
|
+
}
|
56
|
+
|
12
57
|
|
13
58
|
def check_if_xcodeproj(path: Path) -> bool:
|
14
59
|
if not path.is_dir():
|
@@ -22,44 +67,14 @@ def check_if_xcodeproj(path: Path) -> bool:
|
|
22
67
|
class MetadataHandler:
|
23
68
|
@staticmethod
|
24
69
|
def get_files_related_to_sticker_convert(
|
25
|
-
|
70
|
+
directory: Path, include_archive: bool = True
|
26
71
|
) -> List[Path]:
|
27
|
-
from sticker_convert.uploaders.xcode_imessage import XcodeImessageIconset
|
28
|
-
|
29
|
-
xcode_iconset = XcodeImessageIconset().iconset
|
30
|
-
related_extensions = (
|
31
|
-
".png",
|
32
|
-
".apng",
|
33
|
-
".jpg",
|
34
|
-
".jpeg",
|
35
|
-
".gif",
|
36
|
-
".tgs",
|
37
|
-
".lottie",
|
38
|
-
".json",
|
39
|
-
".mp4",
|
40
|
-
".mkv",
|
41
|
-
".mov",
|
42
|
-
".webm",
|
43
|
-
".webp",
|
44
|
-
".avi",
|
45
|
-
".m4a",
|
46
|
-
".wastickers",
|
47
|
-
)
|
48
|
-
related_name = (
|
49
|
-
"title.txt",
|
50
|
-
"author.txt",
|
51
|
-
"emoji.txt",
|
52
|
-
"export-result.txt",
|
53
|
-
".DS_Store",
|
54
|
-
"._.DS_Store",
|
55
|
-
)
|
56
|
-
|
57
72
|
files = [
|
58
73
|
i
|
59
|
-
for i in sorted(
|
60
|
-
if i.stem in
|
61
|
-
or i.name in
|
62
|
-
or i.suffix in
|
74
|
+
for i in sorted(directory.iterdir())
|
75
|
+
if i.stem in RELATED_NAME
|
76
|
+
or i.name in XCODE_IMESSAGE_ICONSET
|
77
|
+
or i.suffix in RELATED_EXTENSIONS
|
63
78
|
or (include_archive and i.name.startswith("archive_"))
|
64
79
|
or check_if_xcodeproj(i)
|
65
80
|
]
|
@@ -67,51 +82,45 @@ class MetadataHandler:
|
|
67
82
|
return files
|
68
83
|
|
69
84
|
@staticmethod
|
70
|
-
def get_stickers_present(
|
71
|
-
from sticker_convert.uploaders.xcode_imessage import XcodeImessageIconset
|
72
|
-
|
73
|
-
blacklist_prefix = ("cover",)
|
74
|
-
blacklist_suffix = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store")
|
75
|
-
xcode_iconset = XcodeImessageIconset().iconset
|
76
|
-
|
85
|
+
def get_stickers_present(directory: Path) -> List[Path]:
|
77
86
|
stickers_present = [
|
78
87
|
i
|
79
|
-
for i in sorted(
|
80
|
-
if Path(
|
81
|
-
and not i.name.startswith(
|
82
|
-
and i.suffix not in
|
83
|
-
and i.name not in
|
88
|
+
for i in sorted(directory.iterdir())
|
89
|
+
if Path(directory, i.name).is_file()
|
90
|
+
and not i.name.startswith(BLACKLIST_PREFIX)
|
91
|
+
and i.suffix not in BLACKLIST_SUFFIX
|
92
|
+
and i.name not in XCODE_IMESSAGE_ICONSET
|
84
93
|
]
|
85
94
|
|
86
95
|
return stickers_present
|
87
96
|
|
88
97
|
@staticmethod
|
89
|
-
def get_cover(
|
90
|
-
stickers_present = sorted(
|
98
|
+
def get_cover(directory: Path) -> Optional[Path]:
|
99
|
+
stickers_present = sorted(directory.iterdir())
|
91
100
|
for i in stickers_present:
|
92
101
|
if Path(i).stem == "cover":
|
93
|
-
return Path(
|
102
|
+
return Path(directory, i.name)
|
94
103
|
|
95
104
|
return None
|
96
105
|
|
97
106
|
@staticmethod
|
98
107
|
def get_metadata(
|
99
|
-
|
108
|
+
directory: Path,
|
100
109
|
title: Optional[str] = None,
|
101
110
|
author: Optional[str] = None,
|
102
111
|
emoji_dict: Optional[Dict[str, str]] = None,
|
103
112
|
) -> Tuple[Optional[str], Optional[str], Optional[Dict[str, str]]]:
|
104
|
-
title_path = Path(
|
113
|
+
title_path = Path(directory, "title.txt")
|
105
114
|
if not title and title_path.is_file():
|
106
115
|
with open(title_path, encoding="utf-8") as f:
|
107
116
|
title = f.read().strip()
|
108
117
|
|
109
|
-
author_path = Path(
|
118
|
+
author_path = Path(directory, "author.txt")
|
110
119
|
if not author and author_path.is_file():
|
111
120
|
with open(author_path, encoding="utf-8") as f:
|
112
121
|
author = f.read().strip()
|
113
122
|
|
114
|
-
emoji_path = Path(
|
123
|
+
emoji_path = Path(directory, "emoji.txt")
|
115
124
|
if not emoji_dict and emoji_path.is_file():
|
116
125
|
with open(emoji_path, "r", encoding="utf-8") as f:
|
117
126
|
emoji_dict = json.load(f)
|
@@ -120,22 +129,22 @@ class MetadataHandler:
|
|
120
129
|
|
121
130
|
@staticmethod
|
122
131
|
def set_metadata(
|
123
|
-
|
132
|
+
directory: Path,
|
124
133
|
title: Optional[str] = None,
|
125
134
|
author: Optional[str] = None,
|
126
135
|
emoji_dict: Optional[Dict[str, str]] = None,
|
127
|
-
):
|
128
|
-
title_path = Path(
|
136
|
+
) -> None:
|
137
|
+
title_path = Path(directory, "title.txt")
|
129
138
|
if title is not None:
|
130
139
|
with open(title_path, "w+", encoding="utf-8") as f:
|
131
140
|
f.write(title)
|
132
141
|
|
133
|
-
author_path = Path(
|
142
|
+
author_path = Path(directory, "author.txt")
|
134
143
|
if author is not None:
|
135
144
|
with open(author_path, "w+", encoding="utf-8") as f:
|
136
145
|
f.write(author)
|
137
146
|
|
138
|
-
emoji_path = Path(
|
147
|
+
emoji_path = Path(directory, "emoji.txt")
|
139
148
|
if emoji_dict is not None:
|
140
149
|
with open(emoji_path, "w+", encoding="utf-8") as f:
|
141
150
|
json.dump(emoji_dict, f, indent=4, ensure_ascii=False)
|
@@ -150,7 +159,7 @@ class MetadataHandler:
|
|
150
159
|
Does not check if metadata provided via user input in GUI or flag options
|
151
160
|
metadata = 'title' or 'author'
|
152
161
|
"""
|
153
|
-
input_presets =
|
162
|
+
input_presets = INPUT_JSON
|
154
163
|
assert input_presets
|
155
164
|
|
156
165
|
if input_option == "local":
|
@@ -158,7 +167,7 @@ class MetadataHandler:
|
|
158
167
|
metadata_provided = metadata_file_path.is_file()
|
159
168
|
if metadata_provided:
|
160
169
|
with open(metadata_file_path, encoding="utf-8") as f:
|
161
|
-
metadata_provided =
|
170
|
+
metadata_provided = bool(f.read())
|
162
171
|
else:
|
163
172
|
metadata_provided = input_presets[input_option]["metadata_provides"][
|
164
173
|
metadata
|
@@ -169,21 +178,21 @@ class MetadataHandler:
|
|
169
178
|
@staticmethod
|
170
179
|
def check_metadata_required(output_option: str, metadata: str) -> bool:
|
171
180
|
# metadata = 'title' or 'author'
|
172
|
-
output_presets =
|
181
|
+
output_presets = OUTPUT_JSON
|
173
182
|
assert output_presets
|
174
183
|
return output_presets[output_option]["metadata_requirements"][metadata]
|
175
184
|
|
176
185
|
@staticmethod
|
177
|
-
def generate_emoji_file(
|
178
|
-
emoji_path = Path(
|
186
|
+
def generate_emoji_file(directory: Path, default_emoji: str = "") -> None:
|
187
|
+
emoji_path = Path(directory, "emoji.txt")
|
179
188
|
emoji_dict = None
|
180
189
|
if emoji_path.is_file():
|
181
190
|
with open(emoji_path, "r", encoding="utf-8") as f:
|
182
191
|
emoji_dict = json.load(f)
|
183
192
|
|
184
193
|
emoji_dict_new = {}
|
185
|
-
for file in sorted(
|
186
|
-
if not Path(
|
194
|
+
for file in sorted(directory.iterdir()):
|
195
|
+
if not Path(directory, file).is_file() and CodecInfo.get_file_ext(file) in (
|
187
196
|
".txt",
|
188
197
|
".m4a",
|
189
198
|
):
|
@@ -199,7 +208,7 @@ class MetadataHandler:
|
|
199
208
|
|
200
209
|
@staticmethod
|
201
210
|
def split_sticker_packs(
|
202
|
-
|
211
|
+
directory: Path,
|
203
212
|
title: str,
|
204
213
|
file_per_pack: Optional[int] = None,
|
205
214
|
file_per_anim_pack: Optional[int] = None,
|
@@ -219,7 +228,7 @@ class MetadataHandler:
|
|
219
228
|
file_per_anim_pack = file_per_pack
|
220
229
|
file_per_image_pack = file_per_pack
|
221
230
|
|
222
|
-
stickers_present = MetadataHandler.get_stickers_present(
|
231
|
+
stickers_present = MetadataHandler.get_stickers_present(directory)
|
223
232
|
|
224
233
|
processed = 0
|
225
234
|
|
@@ -234,7 +243,7 @@ class MetadataHandler:
|
|
234
243
|
image_present = False
|
235
244
|
|
236
245
|
for processed, file in enumerate(stickers_present):
|
237
|
-
file_path =
|
246
|
+
file_path = directory / file
|
238
247
|
|
239
248
|
if CodecInfo.is_anim(file_path):
|
240
249
|
anim_stickers.append(file_path)
|
@@ -244,7 +253,7 @@ class MetadataHandler:
|
|
244
253
|
anim_present = anim_present or len(anim_stickers) > 0
|
245
254
|
image_present = image_present or len(image_stickers) > 0
|
246
255
|
|
247
|
-
finished_all =
|
256
|
+
finished_all = processed == len(stickers_present) - 1
|
248
257
|
|
249
258
|
if len(anim_stickers) == file_per_anim_pack or (
|
250
259
|
finished_all and len(anim_stickers) > 0
|
@@ -268,11 +277,11 @@ class MetadataHandler:
|
|
268
277
|
pack_count = 0
|
269
278
|
|
270
279
|
for processed, file in enumerate(stickers_present):
|
271
|
-
file_path = Path(
|
280
|
+
file_path = Path(directory, file)
|
272
281
|
|
273
282
|
stickers.append(file_path)
|
274
283
|
|
275
|
-
finished_all =
|
284
|
+
finished_all = processed == len(stickers_present) - 1
|
276
285
|
|
277
286
|
if len(stickers) == file_per_pack or (
|
278
287
|
finished_all and len(stickers) > 0
|
@@ -9,19 +9,19 @@ from typing import Any, Callable, List, Tuple, Union
|
|
9
9
|
class RunBin:
|
10
10
|
@staticmethod
|
11
11
|
def get_bin(
|
12
|
-
|
12
|
+
executable: str, silent: bool = False, cb_msg: Callable[..., Any] = print
|
13
13
|
) -> Union[str, None]:
|
14
|
-
if Path(
|
15
|
-
return
|
14
|
+
if Path(executable).is_file():
|
15
|
+
return executable
|
16
16
|
|
17
17
|
if platform.system() == "Windows":
|
18
|
-
|
18
|
+
executable = executable + ".exe"
|
19
19
|
|
20
|
-
which_result = shutil.which(
|
20
|
+
which_result = shutil.which(executable)
|
21
21
|
if which_result is not None:
|
22
22
|
return str(Path(which_result).resolve())
|
23
|
-
|
24
|
-
cb_msg(f"Warning: Cannot find binary file {
|
23
|
+
if silent is False:
|
24
|
+
cb_msg(f"Warning: Cannot find binary file {executable}")
|
25
25
|
|
26
26
|
return None
|
27
27
|
|
@@ -46,6 +46,7 @@ class RunBin:
|
|
46
46
|
stdin=subprocess.PIPE,
|
47
47
|
stdout=subprocess.PIPE,
|
48
48
|
stderr=subprocess.PIPE,
|
49
|
+
check=False,
|
49
50
|
)
|
50
51
|
|
51
52
|
output_str = sp.stdout.decode()
|
@@ -2,6 +2,32 @@
|
|
2
2
|
import re
|
3
3
|
import unicodedata
|
4
4
|
|
5
|
+
BLACKLIST_CHAR = ("\\", "/", ":", "*", "?", '"', "<", ">", "|", "\0")
|
6
|
+
RESERVED_FILENAME = (
|
7
|
+
"CON",
|
8
|
+
"PRN",
|
9
|
+
"AUX",
|
10
|
+
"NUL",
|
11
|
+
"COM1",
|
12
|
+
"COM2",
|
13
|
+
"COM3",
|
14
|
+
"COM4",
|
15
|
+
"COM5",
|
16
|
+
"COM6",
|
17
|
+
"COM7",
|
18
|
+
"COM8",
|
19
|
+
"COM9",
|
20
|
+
"LPT1",
|
21
|
+
"LPT2",
|
22
|
+
"LPT3",
|
23
|
+
"LPT4",
|
24
|
+
"LPT5",
|
25
|
+
"LPT6",
|
26
|
+
"LPT7",
|
27
|
+
"LPT8",
|
28
|
+
"LPT9",
|
29
|
+
) # Reserved words on Windows
|
30
|
+
|
5
31
|
|
6
32
|
def sanitize_filename(filename: str) -> str:
|
7
33
|
# Based on https://gitlab.com/jplusplus/sanitize-filename/-/blob/master/sanitize_filename/sanitize_filename.py
|
@@ -13,40 +39,16 @@ def sanitize_filename(filename: str) -> str:
|
|
13
39
|
and make sure we do not exceed Windows filename length limits.
|
14
40
|
Hence a less safe blacklist, rather than a whitelist.
|
15
41
|
"""
|
16
|
-
|
17
|
-
|
18
|
-
"CON",
|
19
|
-
"PRN",
|
20
|
-
"AUX",
|
21
|
-
"NUL",
|
22
|
-
"COM1",
|
23
|
-
"COM2",
|
24
|
-
"COM3",
|
25
|
-
"COM4",
|
26
|
-
"COM5",
|
27
|
-
"COM6",
|
28
|
-
"COM7",
|
29
|
-
"COM8",
|
30
|
-
"COM9",
|
31
|
-
"LPT1",
|
32
|
-
"LPT2",
|
33
|
-
"LPT3",
|
34
|
-
"LPT4",
|
35
|
-
"LPT5",
|
36
|
-
"LPT6",
|
37
|
-
"LPT7",
|
38
|
-
"LPT8",
|
39
|
-
"LPT9",
|
40
|
-
] # Reserved words on Windows
|
41
|
-
filename = "".join(c if c not in blacklist else "_" for c in filename)
|
42
|
+
|
43
|
+
filename = "".join(c if c not in BLACKLIST_CHAR else "_" for c in filename)
|
42
44
|
# Remove all charcters below code point 32
|
43
45
|
filename = "".join(c if 31 < ord(c) else "_" for c in filename)
|
44
46
|
filename = unicodedata.normalize("NFKD", filename)
|
45
47
|
filename = filename.rstrip(". ") # Windows does not allow these at end
|
46
48
|
filename = filename.strip()
|
47
|
-
if all(
|
49
|
+
if all(x == "." for x in filename):
|
48
50
|
filename = "__" + filename
|
49
|
-
if filename in
|
51
|
+
if filename in RESERVED_FILENAME:
|
50
52
|
filename = "__" + filename
|
51
53
|
if len(filename) == 0:
|
52
54
|
filename = "__"
|
@@ -56,8 +56,8 @@ class ApplePngNormalize:
|
|
56
56
|
|
57
57
|
assert width
|
58
58
|
assert height
|
59
|
-
|
60
|
-
chunk_data = zlib.decompress(chunk_d, -8,
|
59
|
+
buf_size = width * height * 4 + height
|
60
|
+
chunk_data = zlib.decompress(chunk_d, -8, buf_size)
|
61
61
|
|
62
62
|
# Swapping red & blue bytes for each pixel
|
63
63
|
chunk_data = bytearray(chunk_data)
|