Initial commit
This commit is contained in:
commit
7dc30b95cf
11 changed files with 367 additions and 0 deletions
92
.forgejo/workflows/build-release.yaml
Normal file
92
.forgejo/workflows/build-release.yaml
Normal file
|
@ -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 }}
|
34
.forgejo/workflows/check.yaml
Normal file
34
.forgejo/workflows/check.yaml
Normal file
|
@ -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 .
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Dev environment
|
||||||
|
*venv/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# python files
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
2
atlasimagecomposer/.gitignore
vendored
Normal file
2
atlasimagecomposer/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.out/
|
||||||
|
.temp/
|
2
atlasimagecomposer/__init__.py
Normal file
2
atlasimagecomposer/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import importlib.metadata
|
||||||
|
__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer")
|
3
atlasimagecomposer/__main__.py
Normal file
3
atlasimagecomposer/__main__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .compose import run
|
||||||
|
|
||||||
|
run()
|
42
atlasimagecomposer/atlas.py
Normal file
42
atlasimagecomposer/atlas.py
Normal 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
41
atlasimagecomposer/cli.py
Normal 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
|
88
atlasimagecomposer/compose.py
Normal file
88
atlasimagecomposer/compose.py
Normal 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)
|
0
atlasimagecomposer/py.typed
Normal file
0
atlasimagecomposer/py.typed
Normal file
56
pyproject.toml
Normal file
56
pyproject.toml
Normal file
|
@ -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"
|
Loading…
Reference in a new issue