From 7dc30b95cf72a4036ed61f282f5a6901b439f834 Mon Sep 17 00:00:00 2001 From: Firq Date: Fri, 9 Aug 2024 18:57:33 +0200 Subject: [PATCH] Initial commit --- .forgejo/workflows/build-release.yaml | 92 +++++++++++++++++++++++++++ .forgejo/workflows/check.yaml | 34 ++++++++++ .gitignore | 7 ++ atlasimagecomposer/.gitignore | 2 + atlasimagecomposer/__init__.py | 2 + atlasimagecomposer/__main__.py | 3 + atlasimagecomposer/atlas.py | 42 ++++++++++++ atlasimagecomposer/cli.py | 41 ++++++++++++ atlasimagecomposer/compose.py | 88 +++++++++++++++++++++++++ atlasimagecomposer/py.typed | 0 pyproject.toml | 56 ++++++++++++++++ 11 files changed, 367 insertions(+) create mode 100644 .forgejo/workflows/build-release.yaml create mode 100644 .forgejo/workflows/check.yaml create mode 100644 .gitignore create mode 100644 atlasimagecomposer/.gitignore create mode 100644 atlasimagecomposer/__init__.py create mode 100644 atlasimagecomposer/__main__.py create mode 100644 atlasimagecomposer/atlas.py create mode 100644 atlasimagecomposer/cli.py create mode 100644 atlasimagecomposer/compose.py create mode 100644 atlasimagecomposer/py.typed create mode 100644 pyproject.toml diff --git a/.forgejo/workflows/build-release.yaml b/.forgejo/workflows/build-release.yaml new file mode 100644 index 0000000..5888be7 --- /dev/null +++ b/.forgejo/workflows/build-release.yaml @@ -0,0 +1,92 @@ +on: + push: + tags: + - '[0-9]+\.[0-9]+\.[0-9]+-c\.[0-9]+' + - '[0-9]+\.[0-9]+\.[0-9]+-a\.[0-9]+' + - '[0-9]+\.[0-9]+\.[0-9]' + +jobs: + lint-and-typing: + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Checkout source code + uses: https://code.forgejo.org/actions/checkout@v3 + - name: Install packages + run: | + pip install -e .[lint,typing] -q --disable-pip-version-check -q + python -m pip list --format=columns --disable-pip-version-check + - name: Run pylint + run: | + pylint --version + pylint **/*.py --exit-zero --rc-file pyproject.toml + - name: Run mypy + run: | + mypy --version + mypy . + + build-artifacts: + needs: ["lint-and-typing"] + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Checkout source code + uses: https://code.forgejo.org/actions/checkout@v3 + - name: Install packages + run: pip install build + - name: Build package + run: python -m build + - name: Save build artifacts + uses: https://code.forgejo.org/actions/upload-artifact@v3 + with: + name: packages + path: dist/* + + publish-artifacts: + needs: ["build-artifacts"] + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Downloading static site artifacts + uses: actions/download-artifact@v3 + with: + name: packages + path: dist + - name: Install Dependencies + run: pip install twine + - name: Upload package to registry + run: python -m twine upload --repository-url ${{ secrets.REPOSITORY_URL }} -u ${{ secrets.TWINE_DEPLOY_USER }} -p ${{ secrets.TWINE_DEPLOY_PASSWORD }} dist/* + + build-and-push-container: + needs: [ "publish-artifacts" ] + runs-on: dind + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log into Docker Package Registry + uses: docker/login-action@v3 + with: + registry: forgejo.neshweb.net + username: ${{ secrets.FORGEJO_USERNAME }} + password: ${{ secrets.FORGEJO_TOKEN }} + - name: Build and push to Docker Package Registry + uses: docker/build-push-action@v5 + with: + build-args: | + PACKAGE_VERSION=${{ github.ref_name }} + push: true + tags: forgejo.neshweb.net/firq/dockge-cli:${{ github.ref_name }} + + release: + needs: [ build-and-push-container, publish-artifacts ] + if: success() + runs-on: docker + steps: + - name: Release New Version + uses: actions/forgejo-release@v2 + with: + direction: upload + url: https://forgejo.neshweb.net + release-dir: release + token: ${{ secrets.FORGEJO_TOKEN }} + tag: ${{ github.ref_name }} \ No newline at end of file diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml new file mode 100644 index 0000000..55de02a --- /dev/null +++ b/.forgejo/workflows/check.yaml @@ -0,0 +1,34 @@ +on: + push: + branches: "**" + +jobs: + pylint: + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Checkout source code + uses: https://code.forgejo.org/actions/checkout@v3 + - name: Install packages + run: | + pip install -e .[lint] -q --disable-pip-version-check -q + python -m pip list --format=columns --disable-pip-version-check + - name: Run pylint + run: | + pylint --version + pylint **/*.py --exit-zero --rc-file pyproject.toml + + mypy: + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Checkout source code + uses: https://code.forgejo.org/actions/checkout@v3 + - name: Install packages + run: | + pip install -e .[typing] -q --disable-pip-version-check -q + python -m pip list --format=columns --disable-pip-version-check + - name: Run mypy + run: | + mypy --version + mypy . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea936fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Dev environment +*venv/ +.vscode/ + +# python files +__pycache__/ +*.egg-info/ \ No newline at end of file diff --git a/atlasimagecomposer/.gitignore b/atlasimagecomposer/.gitignore new file mode 100644 index 0000000..94e756a --- /dev/null +++ b/atlasimagecomposer/.gitignore @@ -0,0 +1,2 @@ +.out/ +.temp/ diff --git a/atlasimagecomposer/__init__.py b/atlasimagecomposer/__init__.py new file mode 100644 index 0000000..89bcc1f --- /dev/null +++ b/atlasimagecomposer/__init__.py @@ -0,0 +1,2 @@ +import importlib.metadata +__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer") diff --git a/atlasimagecomposer/__main__.py b/atlasimagecomposer/__main__.py new file mode 100644 index 0000000..96b0672 --- /dev/null +++ b/atlasimagecomposer/__main__.py @@ -0,0 +1,3 @@ +from .compose import run + +run() diff --git a/atlasimagecomposer/atlas.py b/atlasimagecomposer/atlas.py new file mode 100644 index 0000000..8e03ac7 --- /dev/null +++ b/atlasimagecomposer/atlas.py @@ -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"] } diff --git a/atlasimagecomposer/cli.py b/atlasimagecomposer/cli.py new file mode 100644 index 0000000..e20c6c5 --- /dev/null +++ b/atlasimagecomposer/cli.py @@ -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 diff --git a/atlasimagecomposer/compose.py b/atlasimagecomposer/compose.py new file mode 100644 index 0000000..4f715ae --- /dev/null +++ b/atlasimagecomposer/compose.py @@ -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) diff --git a/atlasimagecomposer/py.typed b/atlasimagecomposer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2474911 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[project] +name = "atlasimagecomposer" +version = "0.1.0-a.1" +dependencies = [ + "pillow~=10.4.0", + "requests~=2.32.3", + "tqdm~=4.66.5", +] +requires-python = ">= 3.10" +authors = [{name = "Firq", email = "firelp42@gmail.com"}] +maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] +description = "Tool to manage requests for supports" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.scripts] +atlasimagecomposer = "atlasimagecomposer.compose:run" + +[tool.setuptools.packages.find] +where = ["."] +include = ["atlasimagecomposer*"] + +[tool.setuptools.package-data] +"*" = ["py.typed"] + +[project.optional-dependencies] +lint = [ + "pylint~=3.2.5", +] +typing = [ + "mypy~=1.10.1", + "types-tqdm~=4.66.0", + "types-requests~=2.32.0", +] + +[tool.pylint."MAIN"] +disable = [ + "line-too-long", + "missing-module-docstring", + "missing-function-docstring", + "missing-class-docstring", +] + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true + +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta"