diff --git a/scripts/opt-comp-experiment.py b/scripts/opt-comp-experiment.py index 0ce3fed..389367b 100755 --- a/scripts/opt-comp-experiment.py +++ b/scripts/opt-comp-experiment.py @@ -49,7 +49,7 @@ opt_comp_template.set_color(50) opt_comp_template.set_duration(3000) -formats = [ +formats = ( ("img", ".webp"), ("vid", ".webp"), ("vidlong", ".webp"), @@ -59,7 +59,7 @@ ("vid618", ".apng"), ("vid", ".webm"), ("vid", ".gif"), -] +) def generate_random_apng(res: int, fps: float, duration: float, out_f: str) -> None: diff --git a/scripts/update-xcode-imessage-iconset.py b/scripts/update-xcode-imessage-iconset.py new file mode 100644 index 0000000..eef8894 --- /dev/null +++ b/scripts/update-xcode-imessage-iconset.py @@ -0,0 +1,27 @@ +from typing import Dict, Tuple +import json +from pathlib import Path + +ROOT_DIR = Path(__file__).parents[1] + +def main(): + xcode_imessage_iconset: Dict[str, Tuple[int, int]] = {} + + with open(ROOT_DIR / "src/sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json") as f: + dict = json.load(f) + + for i in dict["images"]: + filename = i["filename"] + size = i["size"] + size_w = int(size.split("x")[0]) + size_h = int(size.split("x")[1]) + scale = int(i["scale"].replace("x", "")) + size_w_scaled = size_w * scale + size_h_scaled = size_h * scale + + xcode_imessage_iconset[filename] = (size_w_scaled, size_h_scaled) + + print(xcode_imessage_iconset) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/sticker_convert/cli.py b/src/sticker_convert/cli.py index b314294..f383b36 100755 --- a/src/sticker_convert/cli.py +++ b/src/sticker_convert/cli.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any, Dict -from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR, ROOT_DIR +from sticker_convert.definitions import CONFIG_DIR, DEFAULT_DIR from sticker_convert.job import Job from sticker_convert.job_option import CompOption, CredOption, InputOption, OutputOption from sticker_convert.utils.auth.get_kakao_auth import GetKakaoAuth @@ -26,21 +26,16 @@ def __init__(self) -> None: def cli(self) -> None: try: - self.help: Dict[str, Dict[str, str]] = JsonManager.load_json( - ROOT_DIR / "resources/help.json" - ) - self.input_presets = JsonManager.load_json( - ROOT_DIR / "resources/input.json" - ) - self.compression_presets = JsonManager.load_json( - ROOT_DIR / "resources/compression.json" - ) - self.output_presets = JsonManager.load_json( - ROOT_DIR / "resources/output.json" - ) + from sticker_convert.utils.files.json_resources_loader import HELP_JSON, INPUT_JSON, COMPRESSION_JSON, OUTPUT_JSON, EMOJI_JSON except RuntimeError as e: - self.cb.msg(e.__str__) + self.cb.msg(e.__str__()) return + + self.help = HELP_JSON + self.input_presets = INPUT_JSON + self.compression_presets = COMPRESSION_JSON + self.output_presets = OUTPUT_JSON + self.emoji_list = EMOJI_JSON parser = argparse.ArgumentParser( description="CLI for stickers-convert", diff --git a/src/sticker_convert/converter.py b/src/sticker_convert/converter.py index 036a865..7756a9b 100755 --- a/src/sticker_convert/converter.py +++ b/src/sticker_convert/converter.py @@ -26,6 +26,18 @@ None, ] +MSG_START_COMP = "[I] Start compressing {} -> {}" +MSG_SKIP_COMP = "[S] Compatible file found, skip compress and just copy {} -> {}" +MSG_COMP = ( + "[C] Compressing {} -> {} res={}x{}, " + "quality={}, fps={}, color={} (step {}-{}-{})" +) +MSG_REDO_COMP = "[{}] Compressed {} -> {} but size {} {} limit {}, recompressing" +MSG_DONE_COMP = "[S] Successful compression {} -> {} size {} (step {})" +MSG_FAIL_COMP = ( + "[F] Failed Compression {} -> {}, " + "cannot get below limit {} with lowest quality under current settings (Best size: {})" +) def rounding(value: float) -> Decimal: return Decimal(value).quantize(0, ROUND_HALF_UP) @@ -71,19 +83,6 @@ def useful_array( class StickerConvert: - MSG_START_COMP = "[I] Start compressing {} -> {}" - MSG_SKIP_COMP = "[S] Compatible file found, skip compress and just copy {} -> {}" - MSG_COMP = ( - "[C] Compressing {} -> {} res={}x{}, " - "quality={}, fps={}, color={} (step {}-{}-{})" - ) - MSG_REDO_COMP = "[{}] Compressed {} -> {} but size {} {} limit {}, recompressing" - MSG_DONE_COMP = "[S] Successful compression {} -> {} size {} (step {})" - MSG_FAIL_COMP = ( - "[F] Failed Compression {} -> {}, " - "cannot get below limit {} with lowest quality under current settings (Best size: {})" - ) - def __init__( self, in_f: Union[Path, Tuple[Path, bytes]], @@ -163,7 +162,7 @@ def _convert(self) -> Tuple[bool, Path, Union[None, bytes, Path], int]: if result: return self.compress_done(result) - self.cb.put((self.MSG_START_COMP.format(self.in_f_name, self.out_f_name))) + self.cb.put((MSG_START_COMP.format(self.in_f_name, self.out_f_name))) steps_list = self.generate_steps_list() @@ -195,7 +194,7 @@ def _convert(self) -> Tuple[bool, Path, Union[None, bytes, Path], int]: self.color = param[4] self.tmp_f = BytesIO() - msg = self.MSG_COMP.format( + msg = MSG_COMP.format( self.in_f_name, self.out_f_name, self.res_w, @@ -261,7 +260,7 @@ def check_if_compatible(self) -> Optional[bytes]: file_info=self.codec_info_orig, ) ): - self.cb.put((self.MSG_SKIP_COMP.format(self.in_f_name, self.out_f_name))) + self.cb.put((MSG_SKIP_COMP.format(self.in_f_name, self.out_f_name))) if isinstance(self.in_f, Path): with open(self.in_f, "rb") as f: @@ -323,7 +322,7 @@ def generate_steps_list(self) -> List[Tuple[Optional[int], ...]]: return steps_list def recompress(self, sign: str) -> None: - msg = self.MSG_REDO_COMP.format( + msg = MSG_REDO_COMP.format( sign, self.in_f_name, self.out_f_name, self.size, sign, self.size_max ) self.cb.put(msg) @@ -331,7 +330,7 @@ def recompress(self, sign: str) -> None: def compress_fail( self, ) -> Tuple[bool, Path, Union[None, bytes, Path], int]: - msg = self.MSG_FAIL_COMP.format( + msg = MSG_FAIL_COMP.format( self.in_f_name, self.out_f_name, self.size_max, self.size ) self.cb.put(msg) @@ -353,7 +352,7 @@ def compress_done( f.write(data) if result_step: - msg = self.MSG_DONE_COMP.format( + msg = MSG_DONE_COMP.format( self.in_f_name, self.out_f_name, self.result_size, result_step ) self.cb.put(msg) diff --git a/src/sticker_convert/gui.py b/src/sticker_convert/gui.py index 18a83da..1bfe636 100755 --- a/src/sticker_convert/gui.py +++ b/src/sticker_convert/gui.py @@ -221,13 +221,17 @@ def warn_tkinter_bug(self) -> None: self.cb_msg(msg) def load_jsons(self) -> None: - self.help = JsonManager.load_json(ROOT_DIR / "resources/help.json") - self.input_presets = JsonManager.load_json(ROOT_DIR / "resources/input.json") - self.compression_presets: Dict[str, Dict[str, Any]] = JsonManager.load_json( - ROOT_DIR / "resources/compression.json" - ) - self.output_presets = JsonManager.load_json(ROOT_DIR / "resources/output.json") - self.emoji_list = JsonManager.load_json(ROOT_DIR / "resources/emoji.json") + try: + from sticker_convert.utils.files.json_resources_loader import HELP_JSON, INPUT_JSON, COMPRESSION_JSON, OUTPUT_JSON, EMOJI_JSON + except RuntimeError as e: + self.cb_msg(e.__str__()) + return + + self.help = HELP_JSON + self.input_presets = INPUT_JSON + self.compression_presets = COMPRESSION_JSON + self.output_presets = OUTPUT_JSON + self.emoji_list = EMOJI_JSON if not ( self.compression_presets and self.input_presets and self.output_presets diff --git a/src/sticker_convert/job.py b/src/sticker_convert/job.py index 2287fcc..1e1533e 100755 --- a/src/sticker_convert/job.py +++ b/src/sticker_convert/job.py @@ -15,7 +15,6 @@ from urllib.parse import urlparse from sticker_convert.converter import StickerConvert -from sticker_convert.definitions import ROOT_DIR from sticker_convert.downloaders.download_kakao import DownloadKakao from sticker_convert.downloaders.download_line import DownloadLine from sticker_convert.downloaders.download_signal import DownloadSignal @@ -26,7 +25,7 @@ from sticker_convert.uploaders.upload_telegram import UploadTelegram from sticker_convert.uploaders.xcode_imessage import XcodeImessage from sticker_convert.utils.callback import CallbackReturn -from sticker_convert.utils.files.json_manager import JsonManager +from sticker_convert.utils.files.json_resources_loader import OUTPUT_JSON from sticker_convert.utils.files.metadata_handler import MetadataHandler from sticker_convert.utils.media.codec_info import CodecInfo @@ -335,7 +334,7 @@ def verify_input(self) -> bool: error_msg += "[X] Uploading to signal requires uuid and password.\n" error_msg += save_to_local_tip - output_presets = JsonManager.load_json(ROOT_DIR / "resources/output.json") + output_presets = OUTPUT_JSON input_option = self.opt_input.option output_option = self.opt_output.option diff --git a/src/sticker_convert/uploaders/xcode_imessage.py b/src/sticker_convert/uploaders/xcode_imessage.py index 111fe1b..2540753 100755 --- a/src/sticker_convert/uploaders/xcode_imessage.py +++ b/src/sticker_convert/uploaders/xcode_imessage.py @@ -7,7 +7,7 @@ import zipfile from pathlib import Path from queue import Queue -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Union from sticker_convert.converter import CbQueueItemType, StickerConvert from sticker_convert.definitions import ROOT_DIR @@ -19,65 +19,25 @@ from sticker_convert.utils.media.codec_info import CodecInfo from sticker_convert.utils.media.format_verify import FormatVerify - -class XcodeImessageIconset: - iconset: Dict[str, Tuple[int, int]] = {} - - def __init__(self) -> None: - if self.iconset != {}: - return - - if (ROOT_DIR / "ios-message-stickers-template").is_dir(): - with open( - ROOT_DIR - / "ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json" - ) as f: - dict = json.load(f) - elif (ROOT_DIR / "ios-message-stickers-template.zip").is_file(): - with zipfile.ZipFile( - (ROOT_DIR / "ios-message-stickers-template.zip"), "r" - ) as f: - dict = json.loads( - f.read( - "stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json" - ).decode() - ) - else: - raise FileNotFoundError("ios-message-stickers-template not found") - - for i in dict["images"]: - filename = i["filename"] - size = i["size"] - size_w = int(size.split("x")[0]) - size_h = int(size.split("x")[1]) - scale = int(i["scale"].replace("x", "")) - size_w_scaled = size_w * scale - size_h_scaled = size_h * scale - - self.iconset[filename] = (size_w_scaled, size_h_scaled) - - # self.iconset = { - # 'App-Store-1024x1024pt.png': (1024, 1024), - # 'iPad-Settings-29pt@2x.png': (58, 58), - # 'iPhone-settings-29pt@2x.png': (58, 58), - # 'iPhone-settings-29pt@3x.png': (87, 87), - # 'Messages27x20pt@2x.png': (54, 40), - # 'Messages27x20pt@3x.png': (81, 60), - # 'Messages32x24pt@2x.png': (64, 48), - # 'Messages32x24pt@3x.png': (96, 72), - # 'Messages-App-Store-1024x768pt.png': (1024, 768), - # 'Messages-iPad-67x50pt@2x.png': (134, 100), - # 'Messages-iPad-Pro-74x55pt@2x.png': (148, 110), - # 'Messages-iPhone-60x45pt@2x.png': (120, 90), - # 'Messages-iPhone-60x45pt@3x.png': (180, 135) - # } - +XCODE_IMESSAGE_ICONSET = { + 'App-Store-1024x1024pt.png': (1024, 1024), + 'iPad-Settings-29pt@2x.png': (58, 58), + 'iPhone-settings-29pt@2x.png': (58, 58), + 'iPhone-settings-29pt@3x.png': (87, 87), + 'Messages27x20pt@2x.png': (54, 40), + 'Messages27x20pt@3x.png': (81, 60), + 'Messages32x24pt@2x.png': (64, 48), + 'Messages32x24pt@3x.png': (96, 72), + 'Messages-App-Store-1024x768pt.png': (1024, 768), + 'Messages-iPad-67x50pt@2x.png': (134, 100), + 'Messages-iPad-Pro-74x55pt@2x.png': (148, 110), + 'Messages-iPhone-60x45pt@2x.png': (120, 90), + 'Messages-iPhone-60x45pt@3x.png': (180, 135) +} class XcodeImessage(UploadBase): def __init__(self, *args: Any, **kwargs: Any) -> None: super(XcodeImessage, self).__init__(*args, **kwargs) - self.iconset = XcodeImessageIconset().iconset - self.base_spec.set_size_max(500000) self.base_spec.set_res(300) self.base_spec.set_format(("png", ".apng", ".gif", ".jpeg", "jpg")) @@ -169,7 +129,7 @@ def add_metadata(self, author: str, title: str) -> None: else: icon_source = first_image_path - for icon, res in self.iconset.items(): + for icon, res in XCODE_IMESSAGE_ICONSET.items(): spec_cover = CompOption() spec_cover.set_res_w(res[0]) spec_cover.set_res_h(res[1]) @@ -264,7 +224,7 @@ def create_xcode_proj(self, author: str, title: str) -> None: if ( CodecInfo.get_file_ext(i) == ".png" and i.stem != "cover" - and i.name not in self.iconset + and i.name not in XCODE_IMESSAGE_ICONSET ): sticker_dir = f"{i.stem}.sticker" # 0.sticker stickers_lst.append(sticker_dir) @@ -308,7 +268,7 @@ def create_xcode_proj(self, author: str, title: str) -> None: os.remove(iconset_path / iconfile_name) icons_lst: List[str] = [] - for icon in self.iconset: + for icon in XCODE_IMESSAGE_ICONSET: shutil.copy(self.opt_output.dir / icon, iconset_path / icon) icons_lst.append(icon) diff --git a/src/sticker_convert/utils/files/json_resources_loader.py b/src/sticker_convert/utils/files/json_resources_loader.py new file mode 100644 index 0000000..d5df1a2 --- /dev/null +++ b/src/sticker_convert/utils/files/json_resources_loader.py @@ -0,0 +1,10 @@ +from typing import Dict + +from sticker_convert.definitions import ROOT_DIR +from sticker_convert.utils.files.json_manager import JsonManager + +HELP_JSON: Dict[str, Dict[str, str]] = JsonManager.load_json(ROOT_DIR / "resources/help.json") +INPUT_JSON = JsonManager.load_json(ROOT_DIR / "resources/input.json") +COMPRESSION_JSON = JsonManager.load_json(ROOT_DIR / "resources/compression.json") +OUTPUT_JSON = JsonManager.load_json(ROOT_DIR / "resources/output.json") +EMOJI_JSON = JsonManager.load_json(ROOT_DIR / "resources/emoji.json") diff --git a/src/sticker_convert/utils/files/metadata_handler.py b/src/sticker_convert/utils/files/metadata_handler.py index 20f1dc3..12f8c1b 100755 --- a/src/sticker_convert/utils/files/metadata_handler.py +++ b/src/sticker_convert/utils/files/metadata_handler.py @@ -5,11 +5,40 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple -from sticker_convert.definitions import ROOT_DIR -from sticker_convert.utils.files.json_manager import JsonManager +from sticker_convert.utils.files.json_resources_loader import INPUT_JSON, OUTPUT_JSON from sticker_convert.utils.media.codec_info import CodecInfo +RELATED_EXTENSIONS = ( + ".png", + ".apng", + ".jpg", + ".jpeg", + ".gif", + ".tgs", + ".lottie", + ".json", + ".mp4", + ".mkv", + ".mov", + ".webm", + ".webp", + ".avi", + ".m4a", + ".wastickers", +) +RELATED_NAME = ( + "title.txt", + "author.txt", + "emoji.txt", + "export-result.txt", + ".DS_Store", + "._.DS_Store", +) + +BLACKLIST_PREFIX = ("cover",) +BLACKLIST_SUFFIX = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store") + def check_if_xcodeproj(path: Path) -> bool: if not path.is_dir(): return False @@ -24,42 +53,14 @@ class MetadataHandler: def get_files_related_to_sticker_convert( dir: Path, include_archive: bool = True ) -> List[Path]: - from sticker_convert.uploaders.xcode_imessage import XcodeImessageIconset - - xcode_iconset = XcodeImessageIconset().iconset - related_extensions = ( - ".png", - ".apng", - ".jpg", - ".jpeg", - ".gif", - ".tgs", - ".lottie", - ".json", - ".mp4", - ".mkv", - ".mov", - ".webm", - ".webp", - ".avi", - ".m4a", - ".wastickers", - ) - related_name = ( - "title.txt", - "author.txt", - "emoji.txt", - "export-result.txt", - ".DS_Store", - "._.DS_Store", - ) + from sticker_convert.uploaders.xcode_imessage import XCODE_IMESSAGE_ICONSET files = [ i for i in sorted(dir.iterdir()) - if i.stem in related_name - or i.name in xcode_iconset - or i.suffix in related_extensions + if i.stem in RELATED_NAME + or i.name in XCODE_IMESSAGE_ICONSET + or i.suffix in RELATED_EXTENSIONS or (include_archive and i.name.startswith("archive_")) or check_if_xcodeproj(i) ] @@ -68,19 +69,15 @@ def get_files_related_to_sticker_convert( @staticmethod def get_stickers_present(dir: Path) -> List[Path]: - from sticker_convert.uploaders.xcode_imessage import XcodeImessageIconset - - blacklist_prefix = ("cover",) - blacklist_suffix = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store") - xcode_iconset = XcodeImessageIconset().iconset + from sticker_convert.uploaders.xcode_imessage import XCODE_IMESSAGE_ICONSET stickers_present = [ i for i in sorted(dir.iterdir()) if Path(dir, i.name).is_file() - and not i.name.startswith(blacklist_prefix) - and i.suffix not in blacklist_suffix - and i.name not in xcode_iconset + and not i.name.startswith(BLACKLIST_PREFIX) + and i.suffix not in BLACKLIST_SUFFIX + and i.name not in XCODE_IMESSAGE_ICONSET ] return stickers_present @@ -150,7 +147,7 @@ def check_metadata_provided( Does not check if metadata provided via user input in GUI or flag options metadata = 'title' or 'author' """ - input_presets = JsonManager.load_json(ROOT_DIR / "resources/input.json") + input_presets = INPUT_JSON assert input_presets if input_option == "local": @@ -169,7 +166,7 @@ def check_metadata_provided( @staticmethod def check_metadata_required(output_option: str, metadata: str) -> bool: # metadata = 'title' or 'author' - output_presets = JsonManager.load_json(ROOT_DIR / "resources/output.json") + output_presets = OUTPUT_JSON assert output_presets return output_presets[output_option]["metadata_requirements"][metadata] diff --git a/src/sticker_convert/utils/files/sanitize_filename.py b/src/sticker_convert/utils/files/sanitize_filename.py index 431d475..12b2785 100755 --- a/src/sticker_convert/utils/files/sanitize_filename.py +++ b/src/sticker_convert/utils/files/sanitize_filename.py @@ -2,6 +2,31 @@ import re import unicodedata +BLACKLIST_CHAR = ("\\", "/", ":", "*", "?", '"', "<", ">", "|", "\0") +RESERVED_FILENAME = ( + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + ) # Reserved words on Windows def sanitize_filename(filename: str) -> str: # Based on https://gitlab.com/jplusplus/sanitize-filename/-/blob/master/sanitize_filename/sanitize_filename.py @@ -13,32 +38,8 @@ def sanitize_filename(filename: str) -> str: and make sure we do not exceed Windows filename length limits. Hence a less safe blacklist, rather than a whitelist. """ - blacklist = ["\\", "/", ":", "*", "?", '"', "<", ">", "|", "\0"] - reserved = [ - "CON", - "PRN", - "AUX", - "NUL", - "COM1", - "COM2", - "COM3", - "COM4", - "COM5", - "COM6", - "COM7", - "COM8", - "COM9", - "LPT1", - "LPT2", - "LPT3", - "LPT4", - "LPT5", - "LPT6", - "LPT7", - "LPT8", - "LPT9", - ] # Reserved words on Windows - filename = "".join(c if c not in blacklist else "_" for c in filename) + + filename = "".join(c if c not in BLACKLIST_CHAR else "_" for c in filename) # Remove all charcters below code point 32 filename = "".join(c if 31 < ord(c) else "_" for c in filename) filename = unicodedata.normalize("NFKD", filename) @@ -46,7 +47,7 @@ def sanitize_filename(filename: str) -> str: filename = filename.strip() if all([x == "." for x in filename]): filename = "__" + filename - if filename in reserved: + if filename in RESERVED_FILENAME: filename = "__" + filename if len(filename) == 0: filename = "__"