import math import pathlib import sys 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 def welcome(): print("-------------------------------------------------") print(" Welcome to the FGO Sprite loader and composer ") print(" developed by Firq ") print("-------------------------------------------------") def run(): welcome() Paths.IMAGES.mkdir(exist_ok=True) Paths.OUTPUT.mkdir(exist_ok=True) servantid = input("Enter servant ID: ") try: t = int(servantid) if t <= 0: raise ValueError except ValueError: print("Servant ID has to be a valid integer above 0") sys.exit(1) sprites = fetch_data(servantid) for sprite in sprites: fetch_image(servantid, sprite) path = Paths.IMAGES / servantid for f in path.iterdir(): 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): im = Image.open(filepath) width, height = im.size main_sprite = im.crop((0, 0, width, 768)) save_sprite(main_sprite, servantid, filepath.stem) expressions = im.crop((0, 768, width, height)) expressionwidth, expressionheight = expressions.size 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() 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) if img is not None: save_sprite(img, servantid, filepath.stem, (x, y, exp_per_row)) def is_empty(img: Image.Image): data = np.asarray(img.crop((96, 96, 160, 160)).convert('LA')) np.reshape(data, (-1, 1)) _, count_unique = np.unique(data, return_counts=True) if count_unique.size < 10: return True return False 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 ) expression = expressions.crop(area) if is_empty(expression): return None compose = sprite.copy() compose.paste(expression, position, expression) return compose def save_sprite(image: Image.Image, folder: str, name: str, rowcol: tuple | None = None): savefolder = Paths.OUTPUT / folder if not savefolder.is_dir(): savefolder.mkdir() postfix = f"_{rowcol[0] * rowcol[2] + rowcol[1] + 1}" if rowcol is not None else "_0" outfile = savefolder / f"{name}{postfix}.png" with open(outfile, 'wb') as file: image.save(file)