Initial commit

This commit is contained in:
Firq 2024-08-09 18:57:33 +02:00
commit 7dc30b95cf
Signed by: Firq
GPG key ID: 3ACC61C8CEC83C20
11 changed files with 367 additions and 0 deletions

2
atlasimagecomposer/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.out/
.temp/

View file

@ -0,0 +1,2 @@
import importlib.metadata
__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer")

View file

@ -0,0 +1,3 @@
from .compose import run
run()

View file

@ -0,0 +1,42 @@
import requests
from .cli import Paths
def fetch_image(servantid: str, imageid: str):
atlasurl = f"https://static.atlasacademy.io/JP/CharaFigure/{imageid}/{imageid}_merged.png"
savefolder = Paths.IMAGES / servantid
if not savefolder.is_dir():
savefolder.mkdir()
filelocation = savefolder / f"{imageid}.png"
with open(filelocation, 'wb') as handle:
response = requests.get(atlasurl, stream=True, timeout=10)
if not response.ok:
print(response)
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
def fetch_info(servantid: str):
atlasurl = f"https://api.atlasacademy.io/basic/JP/servant/{servantid}?lang=en"
response = requests.get(atlasurl, timeout=10)
if not response.ok:
print(response)
data = response.json()
print(f"Fetching data and sprites for {data['name']} (ID: {servantid})")
def fetch_data(servantid: str):
fetch_info(servantid)
atlasurl = f"https://api.atlasacademy.io/raw/JP/servant/{servantid}?lore=false&expand=true&lang=en"
response = requests.get(atlasurl, timeout=10)
if not response.ok:
print(response)
data = response.json()
return { str(spritesheet["id"]): (spritesheet["faceX"], spritesheet["faceY"]) for spritesheet in data["mstSvtScript"] }

41
atlasimagecomposer/cli.py Normal file
View file

@ -0,0 +1,41 @@
import argparse
import pathlib
import sys
from . import __version__
# pylint: disable=too-few-public-methods
class Arguments(argparse.Namespace):
"""
Default Arguments when calling the CLI
"""
output: str
def parse_arguments():
"""
Create a parser and parse the arguments of sys.argv based on the Arguments Namespace
Returns arguments and extra arguments separately
"""
parser = argparse.ArgumentParser(
prog="atlasimagecomposer",
description="CLI tool to automatically generate expression sheets for servants",)
parser.add_argument("--output", action="store", type=str, default=None, dest="output", help="Set the output location. This can be an absolute or relative path")
parser.add_argument("--version", action="version", version=f"atlasimagecomposer {__version__}")
args = 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)
class Expressions:
height = 256
width = 256

View file

@ -0,0 +1,88 @@
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 Expressions, 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], servantid)
print(f"Files have been saved at: {Paths.OUTPUT / servantid}")
def process_sprite(filepath: pathlib.Path, position: tuple, 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))
_, expressionheight = expressions.size
rows = expressionheight // Expressions.height
for x, y in tqdm_itertools.product(range(0, rows), range(0, 4, 1), ascii="-="):
img = generate_sprite(main_sprite, expressions, x, y, position)
if img is not None:
save_sprite(img, servantid, filepath.stem, (x, y))
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) -> Image.Image | None:
area = (
col * Expressions.width,
row * Expressions.height,
(col + 1) * Expressions.width - 1,
(row + 1) * Expressions.height - 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] * 4 + 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)

View file