diff --git a/atlasimagecomposer/__main__.py b/atlasimagecomposer/__main__.py deleted file mode 100644 index 96b0672..0000000 --- a/atlasimagecomposer/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .compose import run - -run() diff --git a/atlasimagecomposer/backend/__init__.py b/atlasimagecomposer/backend/__init__.py new file mode 100644 index 0000000..e595ea5 --- /dev/null +++ b/atlasimagecomposer/backend/__init__.py @@ -0,0 +1 @@ +from .compose import compose diff --git a/atlasimagecomposer/atlas.py b/atlasimagecomposer/backend/atlas.py similarity index 74% rename from atlasimagecomposer/atlas.py rename to atlasimagecomposer/backend/atlas.py index dbf5500..3c7f89b 100644 --- a/atlasimagecomposer/atlas.py +++ b/atlasimagecomposer/backend/atlas.py @@ -1,10 +1,10 @@ import requests -from .cli import Paths, ExpressionDefaults +from ..config import Paths, ExpressionDefaults, AtlasDefaults def fetch_image(servantid: str, imageid: str): - atlasurl = f"https://static.atlasacademy.io/JP/CharaFigure/{imageid}/{imageid}_merged.png" + atlasurl = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}/{imageid}_merged.png" savefolder = Paths.IMAGES / servantid if not savefolder.is_dir(): @@ -21,7 +21,7 @@ def fetch_image(servantid: str, imageid: str): handle.write(block) def fetch_info(servantid: str): - atlasurl = f"https://api.atlasacademy.io/basic/JP/servant/{servantid}?lang=en" + atlasurl = f"https://api.atlasacademy.io/basic/{AtlasDefaults.REGION}/servant/{servantid}?lang=en" response = requests.get(atlasurl, timeout=10) if not response.ok: @@ -32,7 +32,7 @@ def fetch_info(servantid: str): def fetch_data(servantid: str): fetch_info(servantid) - atlasurl = f"https://api.atlasacademy.io/raw/JP/servant/{servantid}?lore=false&expand=true&lang=en" + atlasurl = f"https://api.atlasacademy.io/raw/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&expand=true&lang=en" response = requests.get(atlasurl, timeout=10) if not response.ok: diff --git a/atlasimagecomposer/compose.py b/atlasimagecomposer/backend/compose.py similarity index 56% rename from atlasimagecomposer/compose.py rename to atlasimagecomposer/backend/compose.py index 409bdca..058be84 100644 --- a/atlasimagecomposer/compose.py +++ b/atlasimagecomposer/backend/compose.py @@ -1,23 +1,16 @@ import math import pathlib import sys +from typing import Tuple import numpy as np from PIL import Image from tqdm.contrib import itertools as tqdm_itertools from .atlas import fetch_data, fetch_image -from .cli import ExpressionDefaults, Paths +from ..config import ExpressionDefaults, Paths - -def welcome(): - print("-------------------------------------------------") - print(" Welcome to the FGO Sprite loader and composer ") - print(" developed by Firq ") - print("-------------------------------------------------") - -def run(): - welcome() +def compose(): Paths.IMAGES.mkdir(exist_ok=True) Paths.OUTPUT.mkdir(exist_ok=True) @@ -39,7 +32,36 @@ def run(): process_sprite(f, sprites[f.stem]["position"], sprites[f.stem]["faceSize"], servantid) print(f"Files have been saved at: {(Paths.OUTPUT / servantid).absolute()}") -def process_sprite(filepath: pathlib.Path, position: tuple, faceSize: int, servantid: str): + +def override_expressions(expressions: Image.Image, facesize: int) -> Tuple[Image.Image, int]: + exp_w, exp_h = expressions.size + composites, offset, floating_offset = [], facesize / 5, 0 + per_composite = (ExpressionDefaults.faceSize * 4) // facesize + + # Why .......... + composite_count = math.ceil((exp_h - 1280) / (offset + facesize * per_composite)) + 1 + + # Remove top offset + expressions = expressions.crop((0, 256, exp_w, exp_h)) + + for count in range(0, composite_count): + # width_topleft, height_topleft, width_botright, height_botright + composites.append((0, 3 * count * facesize + floating_offset, exp_w, (3 * count + 3) * facesize + floating_offset - 1 )) + floating_offset += offset + + # Create new expression image, then add all compositions to it + overrides = Image.new("RGBA", (exp_w, facesize * composite_count * 3), (255, 0, 0, 0)) + for idx, c in enumerate(composites): + crop = expressions.crop(c) + pos = (0, idx * 3 * facesize) + overrides.paste(crop, pos, crop) + + # Override expressions sheet + return overrides.copy(), per_composite + + +# pylint: disable=too-many-locals +def process_sprite(filepath: pathlib.Path, position: tuple, facesize: int, servantid: str): im = Image.open(filepath) width, height = im.size @@ -48,44 +70,16 @@ def process_sprite(filepath: pathlib.Path, position: tuple, faceSize: int, serva expressions = im.crop((0, 768, width, height)) expressionwidth, expressionheight = expressions.size - rows = expressionheight // faceSize - expressions_p_row = expressionwidth // faceSize + rows = expressionheight // facesize + expressions_p_row = expressionwidth // facesize exp_per_row = 4 - if faceSize != ExpressionDefaults.faceSize: - w, h = expressions.size - composites = [] - - per_composite = (1280 - ExpressionDefaults.faceSize) // faceSize - exp_per_row = per_composite - - offset = (faceSize * ExpressionDefaults.faceSize) / 1280 - composite_count = math.ceil((h - 1280) / (offset + faceSize * per_composite)) + 1 - - expressions = expressions.crop((0, 256, w, h)) - floating_offset = 0 - for count in range(0, composite_count + 1): - composites.append(( - 0, - 3 * count * faceSize + floating_offset, - w, - (3 * count + 3) * faceSize + floating_offset - 1 - )) - floating_offset += offset - - overrides = Image.new("RGBA", (w, faceSize * composite_count * 3), (255, 0, 0, 0)) - for idx, c in enumerate(composites): - crop = expressions.crop(c) - pos = ( - 0, - idx * 3 * faceSize, - ) - overrides.paste(crop, pos, crop) - expressions = overrides.copy() + if facesize != ExpressionDefaults.faceSize: + expressions, exp_per_row = override_expressions(expressions, facesize) for x, y in tqdm_itertools.product(range(0, rows), range(0, expressions_p_row, 1), ascii="-="): - img = generate_sprite(main_sprite, expressions, x, y, position, faceSize) + img = generate_sprite(main_sprite, expressions, x, y, position, facesize) if img is not None: save_sprite(img, servantid, filepath.stem, (x, y, exp_per_row)) @@ -97,21 +91,22 @@ def is_empty(img: Image.Image): return True return False -def generate_sprite(sprite: Image.Image, expressions: Image.Image, row: int, col: int, position: tuple, faceSize: int) -> Image.Image | None: +# pylint: disable=too-many-arguments +def generate_sprite(sprite: Image.Image, expressions: Image.Image, row: int, col: int, position: tuple, facesize: int) -> Image.Image | None: area = ( - col * faceSize, - row * faceSize, - (col + 1) * faceSize - 1, - (row + 1) * faceSize - 1 + col * facesize, + row * facesize, + (col + 1) * facesize - 1, + (row + 1) * facesize - 1 ) expression = expressions.crop(area) if is_empty(expression): return None - compose = sprite.copy() - compose.paste(expression, position, expression) - return compose + composition = sprite.copy() + composition.paste(expression, position, expression) + return composition def save_sprite(image: Image.Image, folder: str, name: str, rowcol: tuple | None = None): savefolder = Paths.OUTPUT / folder diff --git a/atlasimagecomposer/cli/__init__.py b/atlasimagecomposer/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/atlasimagecomposer/cli.py b/atlasimagecomposer/cli/cli.py similarity index 66% rename from atlasimagecomposer/cli.py rename to atlasimagecomposer/cli/cli.py index 86660d0..a0d9657 100644 --- a/atlasimagecomposer/cli.py +++ b/atlasimagecomposer/cli/cli.py @@ -2,7 +2,9 @@ import argparse import pathlib import sys -from . import __version__ +from .. import __version__ +from ..backend import compose +from ..config import Paths # pylint: disable=too-few-public-methods @@ -28,13 +30,16 @@ def parse_arguments(): args, extra_args = parser.parse_known_args(sys.argv[1:], namespace=args) return args, extra_args -class Paths: - _root = pathlib.Path(__file__).parent - _args, _ = parse_arguments() - IMAGES = _root / ".temp" - OUTPUT = _root / ".out" - if _args.output: - OUTPUT = pathlib.Path(_args.output) +def welcome(): + print("-------------------------------------------------") + print(" Welcome to the FGO Sprite loader and composer ") + print(" developed by Firq ") + print("-------------------------------------------------") -class ExpressionDefaults: - faceSize = 256 +def run(): + args, _ = parse_arguments() + if args.output: + Paths.OUTPUT = pathlib.Path(args.output) + + welcome() + compose() diff --git a/atlasimagecomposer/config/__init__.py b/atlasimagecomposer/config/__init__.py new file mode 100644 index 0000000..38a49fd --- /dev/null +++ b/atlasimagecomposer/config/__init__.py @@ -0,0 +1 @@ +from .config import AtlasDefaults, ExpressionDefaults, Paths diff --git a/atlasimagecomposer/config/config.py b/atlasimagecomposer/config/config.py new file mode 100644 index 0000000..091f8b7 --- /dev/null +++ b/atlasimagecomposer/config/config.py @@ -0,0 +1,13 @@ +# pylint: disable=too-few-public-methods +import pathlib + +class Paths: + _root = pathlib.Path(__file__).parents[1] + IMAGES = _root / ".temp" + OUTPUT = _root / ".out" + +class ExpressionDefaults: + faceSize = 256 + +class AtlasDefaults: + REGION = "JP" diff --git a/pyproject.toml b/pyproject.toml index dafa1bd..692e76e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "atlasimagecomposer" -version = "0.1.0-a.4" +version = "0.1.0-a.5" dependencies = [ "numpy~=2.0.1", "pillow~=10.4.0", @@ -20,7 +20,7 @@ classifiers = [ ] [project.scripts] -atlasimagecomposer = "atlasimagecomposer.compose:run" +atlasimagecomposer = "atlasimagecomposer.cli.cli:run" [tool.setuptools.packages.find] where = ["."]