Release Candidate 1 for the people

This commit is contained in:
Firq 2024-10-05 14:01:50 +02:00
parent 174dd557e5
commit fbda98805f
Signed by: Firq
GPG key ID: 3ACC61C8CEC83C20
10 changed files with 235 additions and 117 deletions
atlasimagecomposer/backend

View file

@ -1,47 +1,88 @@
from typing import List, Tuple, TypedDict
import requests
from ..config import Paths, ExpressionDefaults, AtlasDefaults
from ..config import AtlasDefaults, ExpressionDefaults, Paths
def fetch_image(servantid: str, imageid: str):
atlasurl = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}/{imageid}_merged.png"
class SpritesheetData(TypedDict):
facesize: int
position: Tuple[int, int]
savefolder = Paths.IMAGES / servantid
class ReturnData(TypedDict):
id: str
faceX: int
faceY: int
extendData: dict[str, int]
def fetch_expression_sheets(servantid: int, imageid: str):
atlasurl_base = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}"
savefolder = Paths.IMAGES / str(servantid) / imageid
if not savefolder.is_dir():
savefolder.mkdir()
filelocation = savefolder / f"{imageid}.png"
savefolder.mkdir(exist_ok=True, parents=True)
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)
idx, status = 0, 200
def fetch_info(servantid: str):
while status == 200:
filelocation = savefolder / f"{idx}.png"
postfix = ""
if idx == 1:
postfix = "f"
elif idx > 1:
postfix = f"f{idx}"
if filelocation.exists():
print(f"Found cached asset for {imageid}{postfix}.png")
idx += 1
continue
filename = f"{imageid}{postfix}.png"
atlasurl = f"{atlasurl_base}/{filename}"
with open(filelocation, 'wb') as handle:
response = requests.get(atlasurl, stream=True, timeout=AtlasDefaults.TIMEOUT)
status = response.status_code
if status != 200:
continue
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
print(f"Finished downloading {filename}")
idx += 1
p = savefolder / f"{idx}.png"
p.unlink(missing_ok=True)
def fetch_info(servantid: int):
atlasurl = f"https://api.atlasacademy.io/basic/{AtlasDefaults.REGION}/servant/{servantid}?lang=en"
response = requests.get(atlasurl, timeout=10)
response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT)
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):
def fetch_data(servantid: int) -> dict[str, SpritesheetData]:
fetch_info(servantid)
atlasurl = f"https://api.atlasacademy.io/raw/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&expand=true&lang=en"
response = requests.get(atlasurl, timeout=10)
response = requests.get(atlasurl, timeout=AtlasDefaults.TIMEOUT)
if not response.ok:
print(response)
data = response.json()
return {
data: List[ReturnData] = response.json()["mstSvtScript"]
spritesheet_data: dict[str, SpritesheetData] = {
str(spritesheet["id"]): {
"faceSize": spritesheet["extendData"].get("faceSize", ExpressionDefaults.faceSize),
"facesize": spritesheet["extendData"].get("faceSize", ExpressionDefaults.FACESIZE),
"position": (spritesheet["faceX"], spritesheet["faceY"])
} for spritesheet in data["mstSvtScript"]
} for spritesheet in data
}
print(f"Found a total of {len(spritesheet_data)} spritesheets")
return spritesheet_data

View file

@ -1,87 +1,112 @@
import math
import pathlib
import sys
from typing import Tuple
from typing import List, Optional
import numpy as np
from PIL import Image
from tqdm.contrib import itertools as tqdm_itertools
from .atlas import fetch_data, fetch_image
from ..config import ExpressionDefaults, Paths
from ..config import Paths
from .atlas import SpritesheetData, fetch_data, fetch_expression_sheets
def compose():
def compose(servantid: int, filters: Optional[List[str]] = None):
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)
if filters is not None:
sprites = { key: value for key, value in sprites.items() if key in filters }
for sprite in sprites:
fetch_image(servantid, sprite)
fetch_expression_sheets(servantid, sprite)
path = Paths.IMAGES / str(servantid)
savefolder = Paths.OUTPUT / str(servantid)
if not savefolder.is_dir():
savefolder.mkdir()
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()}")
if filters is not None and str(f.stem) not in filters:
continue
process_sprite(f, servantid, sprites[f.stem])
print(f"Files have been saved at: {(Paths.OUTPUT / str(servantid)).absolute()}")
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
def calculate_counts(width: int, height: int, facesize: int):
return height // facesize, width // facesize
# 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
def process_sprite(images_folder: pathlib.Path, servantid: int, configdata: SpritesheetData):
main = Image.open(images_folder / "0.png")
main_sprite = im.crop((0, 0, width, 768))
save_sprite(main_sprite, servantid, filepath.stem)
expressions = im.crop((0, 768, width, height))
width, _ = main.size
main_sprite = main.crop((0, 0, width, 756))
expressionwidth, expressionheight = expressions.size
rows = expressionheight // facesize
expressions_p_row = expressionwidth // facesize
save_sprite(main_sprite, servantid, f"{images_folder.stem}")
exp_per_row = 4
for i in images_folder.iterdir():
initial_row, index = 0, int(i.stem)
expressions = Image.open(i)
if facesize != ExpressionDefaults.faceSize:
expressions, exp_per_row = override_expressions(expressions, facesize)
rowcount, colcount = calculate_counts(*expressions.size, configdata["facesize"])
if i.name == "0.png":
initial_row = 3
for x, y in tqdm_itertools.product(range(initial_row, rowcount), range(0, colcount), ascii="-="):
img = generate_sprite(main_sprite, expressions, x, y, configdata)
if img is not None:
save_sprite(img, servantid, f"{images_folder.stem}", (x, y, colcount, index))
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,
row * facesize,
(col + 1) * facesize - 1,
(row + 1) * facesize - 1
)
expression = expressions.crop(roi)
if is_empty(expression):
return None
composition = main_sprite.copy()
composition.paste(expression, position, expression)
return composition
def save_sprite(image: Image.Image, folder_id: int, name: str, info: tuple | None = None):
savefolder = Paths.OUTPUT / str(folder_id) / name
if not savefolder.is_dir():
savefolder.mkdir()
postfix = "0"
if info is not None:
(row, col, column_count, file_idx) = info
if file_idx == 0 and column_count == 4:
postfix = str(col + 1)
elif file_idx == 0:
raise ValueError("Should not have any faces")
elif column_count == 4:
postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1) + column_count)
elif column_count < 4:
postfix = str((column_count * row + col + 1) + pow(column_count, 2) * (file_idx - 1))
else:
raise ValueError("Unaccounted case")
outfile = savefolder / f"{postfix}.png"
with open(outfile, 'wb') as file:
image.save(file)
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'))
@ -90,29 +115,3 @@ def is_empty(img: Image.Image):
if count_unique.size < 10:
return True
return False
# 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
)
expression = expressions.crop(area)
if is_empty(expression):
return None
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
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)