diff --git a/README.md b/README.md index 8f4889b..2837b37 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,8 @@ This would generate the expressions for Scathach (Servant Id 70) in the folder o `skyeweave` can also be used in other Python scripts. ```python -from skyeweave import compose - -compose(70) +from skyeweave import SkyeWeave +SkyeWeave().compose(70) ``` This feature will be expanded upon in future releases. diff --git a/pyproject.toml b/pyproject.toml index 200173a..bed9a13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "skyeweave" -version = "1.0.0-c.2" +version = "1.0.0-c.3" requires-python = ">= 3.10" authors = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] diff --git a/skyeweave/__init__.py b/skyeweave/__init__.py index 7533508..07a550a 100644 --- a/skyeweave/__init__.py +++ b/skyeweave/__init__.py @@ -2,4 +2,4 @@ import importlib.metadata __version__ = importlib.metadata.version(__package__ or "skyeweave") -from .service import compose +from .service import SkyeWeave diff --git a/skyeweave/cli/cli.py b/skyeweave/cli/cli.py index 7622806..04cd3fa 100644 --- a/skyeweave/cli/cli.py +++ b/skyeweave/cli/cli.py @@ -5,7 +5,7 @@ import sys from typing import List from .. import __version__ -from ..service import compose +from ..service import SkyeWeave from ..config import AtlasAPIConfig, LoggingConfig, PathConfig from ..utils import rmdir, disable_tqdm @@ -91,4 +91,4 @@ def run(): rmdir(PathConfig.IMAGES) LOGGER.info("Successfully reset local storage") - compose(input_id, args.filter) + SkyeWeave().compose(input_id, args.filter) diff --git a/skyeweave/service/__init__.py b/skyeweave/service/__init__.py index e595ea5..2746888 100644 --- a/skyeweave/service/__init__.py +++ b/skyeweave/service/__init__.py @@ -1 +1 @@ -from .compose import compose +from .compose import SkyeWeave diff --git a/skyeweave/service/atlas.py b/skyeweave/service/atlas.py index 6afc754..7ca9595 100644 --- a/skyeweave/service/atlas.py +++ b/skyeweave/service/atlas.py @@ -1,4 +1,5 @@ import logging +import pathlib from typing import Annotated, List, NotRequired, Tuple, TypedDict import requests @@ -61,12 +62,11 @@ def fetch_mstsvtjson(): break handle.write(block) -def fetch_expression_sheets(basefolder: str, imageid: str): +def fetch_expression_sheets(tempfolder: pathlib.Path, imageid: str): atlasurl_base = f"https://static.atlasacademy.io/{AtlasAPIConfig.REGION}/CharaFigure/{imageid}" - savefolder = PathConfig.IMAGES / basefolder / str(imageid) - if not savefolder.is_dir(): - savefolder.mkdir(exist_ok=True, parents=True) + savefolder = tempfolder / str(imageid) + savefolder.mkdir(exist_ok=True, parents=True) idx, status = 0, 200 diff --git a/skyeweave/service/compose.py b/skyeweave/service/compose.py index c42bf69..9f1ada0 100644 --- a/skyeweave/service/compose.py +++ b/skyeweave/service/compose.py @@ -11,106 +11,115 @@ from .atlas import SpritesheetData, fetch_data, fetch_expression_sheets, fetch_c LOGGER = logging.getLogger(LoggingConfig.NAME) -def compose(input_id: int, filters: Optional[List[str]] = None): - PathConfig.IMAGES.mkdir(exist_ok=True) - PathConfig.OUTPUT.mkdir(exist_ok=True) +class SkyeWeave: + output_folder: pathlib.Path + image_folder: pathlib.Path - if input_id < 10000: - chara_ids = fetch_data(input_id) - savefolder = PathConfig.OUTPUT / str(input_id) - else: - LOGGER.info(f"Processing manually uploaded charaId {input_id}") - savefolder = PathConfig.OUTPUT / "manual" - chara_ids = [str(input_id)] + def __init__(self, output: Optional[pathlib.Path] = None, assets: Optional[pathlib.Path] = None): + self.output_folder = output or PathConfig.OUTPUT + self.image_folder = assets or PathConfig.IMAGES + + self.output_folder.mkdir(exist_ok=True) + self.image_folder.mkdir(exist_ok=True) + + def compose(self, input_id: int, filters: Optional[List[str]] = None): + if input_id < 10000: + chara_ids = fetch_data(input_id) + savefolder, tempfolder = self.output_folder / str(input_id), self.image_folder / str(input_id) + else: + LOGGER.info(f"Processing manually uploaded charaId {input_id}") + savefolder, tempfolder = self.output_folder / "manual", self.image_folder / "manual" + chara_ids = [str(input_id)] - if not savefolder.is_dir(): savefolder.mkdir(parents=True, exist_ok=True) + tempfolder.mkdir(parents=True, exist_ok=True) - if filters is not None: - chara_ids = [ v for v in chara_ids if v in filters ] - LOGGER.debug(chara_ids) + chara_ids = [ v for v in chara_ids if v in filters ] if filters else chara_ids + LOGGER.debug(chara_ids) - for char_id in chara_ids: - expfolder = fetch_expression_sheets(savefolder.stem, char_id) - config = fetch_config(char_id) - process_sprite(expfolder, config, savefolder) + for char_id in chara_ids: + expfolder = fetch_expression_sheets(tempfolder, char_id) + config = fetch_config(char_id) + self.process_sprite(expfolder, config, savefolder) - LOGGER.info(f"Files have been saved at: {savefolder.absolute()}") + LOGGER.info(f"Files have been saved at: {savefolder.absolute()}") + def process_sprite(self, images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path): + main_sprite = self._gen_main_sprite(images_folder / "0.png") + image_idx = self._save_sprite(main_sprite, outputfolder, f"{images_folder.stem}") -def calculate_counts(width: int, height: int, facesize: tuple[int, int]): - rowcount, colcount = height // facesize[1], width // facesize[0] - LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}") - LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}") - return rowcount, colcount + for i in images_folder.iterdir(): + LOGGER.debug(f"Idx: {image_idx}") + initial_row = 0 + expressions = Image.open(i) -def gen_main_sprite(folder: pathlib.Path): - image = Image.open(folder / "0.png") - width, height = image.size - LOGGER.debug(f"Main sprite ({folder}): {width}:{height}") - return image.crop((0, 0, width, height - 256)) + rowcount, colcount = self._calculate_counts(*expressions.size, configdata["facesize"]) -def process_sprite(images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path): - main_sprite = gen_main_sprite(images_folder) - image_idx = save_sprite(main_sprite, outputfolder, f"{images_folder.stem}") + if i.name == "0.png" and 256 < configdata["facesize"][1]: + continue - for i in images_folder.iterdir(): - LOGGER.debug(f"Idx: {image_idx}") - initial_row = 0 - expressions = Image.open(i) + if i.name == "0.png": + initial_row = rowcount - 1 - rowcount, colcount = calculate_counts(*expressions.size, configdata["facesize"]) + for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-=", desc=f"[PROG] [{LoggingConfig.NAME}]", bar_format="{desc} {percentage:3.0f}% |{bar}|"): + img = self._generate_sprite(main_sprite, expressions, x, y, configdata) + if img is not None: + image_idx = self._save_sprite(img, outputfolder, f"{images_folder.stem}", image_idx) + LOGGER.debug(f"{x}/{y} - {'Invalid' if img is None else 'Valid'} image") - if i.name == "0.png" and 256 < configdata["facesize"][1]: - continue + @staticmethod + def _calculate_counts(width: int, height: int, facesize: tuple[int, int]): + rowcount, colcount = height // facesize[1], width // facesize[0] + LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}") + LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}") + return rowcount, colcount - if i.name == "0.png": - initial_row = rowcount - 1 + @staticmethod + def _gen_main_sprite(imagepath: pathlib.Path): + image = Image.open(imagepath) + width, height = image.size + LOGGER.debug(f"Main sprite ({imagepath}): {width}:{height}") + return image.crop((0, 0, width, height - 256)) - for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-=", desc=f"[PROG] [{LoggingConfig.NAME}]", bar_format="{desc} {percentage:3.0f}% |{bar}|"): - img = generate_sprite(main_sprite, expressions, x, y, configdata) - if img is not None: - image_idx = save_sprite(img, outputfolder, f"{images_folder.stem}", image_idx) - LOGGER.debug(f"{x}/{y} - {'Invalid' if img is None else 'Valid'} image") + @staticmethod + def _generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int, col: int, configdata: SpritesheetData) -> Image.Image | None: + position, facesize = configdata["position"], configdata["facesize"] + roi = ( + col * facesize[0], + row * facesize[1], + (col + 1) * facesize[0], + (row + 1) * facesize[1] + ) + LOGGER.debug(roi) + expression = expressions.crop(roi) + if SkyeWeave._is_empty(expression): + LOGGER.debug("Image empty") + return None -def generate_sprite(main_sprite: Image.Image, expressions: Image.Image, row: int, col: int, configdata: SpritesheetData) -> Image.Image | None: - position, facesize = configdata["position"], configdata["facesize"] - roi = ( - col * facesize[0], - row * facesize[1], - (col + 1) * facesize[0], - (row + 1) * facesize[1] - ) - LOGGER.debug(roi) - expression = expressions.crop(roi) + mask = Image.new("RGBA", (facesize[0], facesize[1]), (255,255,255,255)) + composition = main_sprite.copy() + composition.paste(expression, position, mask) + return composition - if is_empty(expression): - LOGGER.debug("Image empty") - return None + @staticmethod + def _save_sprite(image: Image.Image, outputfolder: pathlib.Path, name: str, idx: int = 0) -> int: + savefolder = outputfolder / name + if not savefolder.is_dir(): + savefolder.mkdir() + outfile = savefolder / f"{idx}.png" - mask = Image.new("RGBA", (facesize[0], facesize[1]), (255,255,255,255)) - composition = main_sprite.copy() - composition.paste(expression, position, mask) - return composition + with open(outfile, 'wb') as file: + image.save(file) + return idx + 1 -def save_sprite(image: Image.Image, outputfolder: pathlib.Path, name: str, idx: int = 0) -> int: - savefolder = outputfolder / name - if not savefolder.is_dir(): - savefolder.mkdir() - outfile = savefolder / f"{idx}.png" - - with open(outfile, 'wb') as file: - image.save(file) - - return idx + 1 - -def is_empty(img: Image.Image): - w, h = img.size - croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 ) - data = Counter(img.crop(croparea).convert('LA').getdata()) - LOGGER.debug(f"Counts: {len(data)}") - if len(data) < 6: - return True - return False + @staticmethod + def _is_empty(img: Image.Image): + w, h = img.size + croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 ) + data = Counter(img.crop(croparea).convert('LA').getdata()) + LOGGER.debug(f"Counts: {len(data)}") + if len(data) < 6: + return True + return False diff --git a/test.py b/test.py deleted file mode 100644 index 038687a..0000000 --- a/test.py +++ /dev/null @@ -1,3 +0,0 @@ -from skyeweave import compose - -compose(70)