Release Candidate 1 for the people
All checks were successful
All checks were successful
This commit is contained in:
parent
174dd557e5
commit
fbda98805f
10 changed files with 235 additions and 117 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
# Dev environment
|
# Dev environment
|
||||||
*venv/
|
*venv/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.out/
|
||||||
|
|
||||||
# python files
|
# python files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# atlasimagecomposer
|
||||||
|
|
||||||
|
A small CLI toolkit that allows you to easily download and compose any FGO expression sheets
|
||||||
|
|
||||||
|
## Installations
|
||||||
|
|
||||||
|
Install it using `pip` - You need to specify my extra index for it to be found
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ atlasimagecomposer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use it via the CLI command `atlasimagecomposer`
|
||||||
|
|
||||||
|
```plain
|
||||||
|
usage: atlasimagecomposer [-h] [--version] [--output OUTPUT] [--filter FILTER [FILTER ...]] [--timeout TIMEOUT] [--clear-cache]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--version show program's version number and exit
|
||||||
|
--output OUTPUT Set the output location. This can be an absolute or relative path
|
||||||
|
--filter FILTER [FILTER ...] Specify one or more spritesheet ids that will be fetched
|
||||||
|
--timeout TIMEOUT Set the timeout for all requests towards AtlasAcademy, default is 10s
|
||||||
|
--clear-cache Clear cached assets before downloading files for a servant
|
||||||
|
```
|
|
@ -1,2 +1,3 @@
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer")
|
__version__ = importlib.metadata.version(__package__ or "atlasimagecomposer")
|
||||||
|
|
|
@ -1,47 +1,88 @@
|
||||||
|
from typing import List, Tuple, TypedDict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ..config import Paths, ExpressionDefaults, AtlasDefaults
|
from ..config import AtlasDefaults, ExpressionDefaults, Paths
|
||||||
|
|
||||||
|
|
||||||
def fetch_image(servantid: str, imageid: str):
|
class SpritesheetData(TypedDict):
|
||||||
atlasurl = f"https://static.atlasacademy.io/{AtlasDefaults.REGION}/CharaFigure/{imageid}/{imageid}_merged.png"
|
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():
|
if not savefolder.is_dir():
|
||||||
savefolder.mkdir()
|
savefolder.mkdir(exist_ok=True, parents=True)
|
||||||
filelocation = savefolder / f"{imageid}.png"
|
|
||||||
|
idx, status = 0, 200
|
||||||
|
|
||||||
|
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:
|
with open(filelocation, 'wb') as handle:
|
||||||
response = requests.get(atlasurl, stream=True, timeout=10)
|
response = requests.get(atlasurl, stream=True, timeout=AtlasDefaults.TIMEOUT)
|
||||||
if not response.ok:
|
status = response.status_code
|
||||||
print(response)
|
if status != 200:
|
||||||
|
continue
|
||||||
for block in response.iter_content(1024):
|
for block in response.iter_content(1024):
|
||||||
if not block:
|
if not block:
|
||||||
break
|
break
|
||||||
handle.write(block)
|
handle.write(block)
|
||||||
|
print(f"Finished downloading {filename}")
|
||||||
|
idx += 1
|
||||||
|
p = savefolder / f"{idx}.png"
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
def fetch_info(servantid: str):
|
|
||||||
|
def fetch_info(servantid: int):
|
||||||
atlasurl = f"https://api.atlasacademy.io/basic/{AtlasDefaults.REGION}/servant/{servantid}?lang=en"
|
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:
|
if not response.ok:
|
||||||
print(response)
|
print(response)
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
print(f"Fetching data and sprites for {data['name']} (ID: {servantid})")
|
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)
|
fetch_info(servantid)
|
||||||
atlasurl = f"https://api.atlasacademy.io/raw/{AtlasDefaults.REGION}/servant/{servantid}?lore=false&expand=true&lang=en"
|
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:
|
if not response.ok:
|
||||||
print(response)
|
print(response)
|
||||||
|
|
||||||
data = response.json()
|
data: List[ReturnData] = response.json()["mstSvtScript"]
|
||||||
return {
|
|
||||||
|
spritesheet_data: dict[str, SpritesheetData] = {
|
||||||
str(spritesheet["id"]): {
|
str(spritesheet["id"]): {
|
||||||
"faceSize": spritesheet["extendData"].get("faceSize", ExpressionDefaults.faceSize),
|
"facesize": spritesheet["extendData"].get("faceSize", ExpressionDefaults.FACESIZE),
|
||||||
"position": (spritesheet["faceX"], spritesheet["faceY"])
|
"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
|
||||||
|
|
|
@ -1,87 +1,112 @@
|
||||||
import math
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
from typing import List, Optional
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from tqdm.contrib import itertools as tqdm_itertools
|
from tqdm.contrib import itertools as tqdm_itertools
|
||||||
|
|
||||||
from .atlas import fetch_data, fetch_image
|
from ..config import Paths
|
||||||
from ..config import ExpressionDefaults, 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.IMAGES.mkdir(exist_ok=True)
|
||||||
Paths.OUTPUT.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)
|
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:
|
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():
|
for f in path.iterdir():
|
||||||
process_sprite(f, sprites[f.stem]["position"], sprites[f.stem]["faceSize"], servantid)
|
if filters is not None and str(f.stem) not in filters:
|
||||||
print(f"Files have been saved at: {(Paths.OUTPUT / servantid).absolute()}")
|
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]:
|
def calculate_counts(width: int, height: int, facesize: int):
|
||||||
exp_w, exp_h = expressions.size
|
return height // facesize, width // facesize
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
def process_sprite(images_folder: pathlib.Path, servantid: int, configdata: SpritesheetData):
|
||||||
def process_sprite(filepath: pathlib.Path, position: tuple, facesize: int, servantid: str):
|
main = Image.open(images_folder / "0.png")
|
||||||
im = Image.open(filepath)
|
|
||||||
width, height = im.size
|
|
||||||
|
|
||||||
main_sprite = im.crop((0, 0, width, 768))
|
width, _ = main.size
|
||||||
save_sprite(main_sprite, servantid, filepath.stem)
|
main_sprite = main.crop((0, 0, width, 756))
|
||||||
expressions = im.crop((0, 768, width, height))
|
|
||||||
|
|
||||||
expressionwidth, expressionheight = expressions.size
|
save_sprite(main_sprite, servantid, f"{images_folder.stem}")
|
||||||
rows = expressionheight // facesize
|
|
||||||
expressions_p_row = expressionwidth // facesize
|
|
||||||
|
|
||||||
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:
|
rowcount, colcount = calculate_counts(*expressions.size, configdata["facesize"])
|
||||||
expressions, exp_per_row = override_expressions(expressions, facesize)
|
|
||||||
|
|
||||||
for x, y in tqdm_itertools.product(range(0, rows), range(0, expressions_p_row, 1), ascii="-="):
|
if i.name == "0.png":
|
||||||
img = generate_sprite(main_sprite, expressions, x, y, position, facesize)
|
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:
|
if img is not None:
|
||||||
save_sprite(img, servantid, filepath.stem, (x, y, exp_per_row))
|
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)
|
||||||
|
|
||||||
|
|
||||||
def is_empty(img: Image.Image):
|
def is_empty(img: Image.Image):
|
||||||
data = np.asarray(img.crop((96, 96, 160, 160)).convert('LA'))
|
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:
|
if count_unique.size < 10:
|
||||||
return True
|
return True
|
||||||
return False
|
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)
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import argparse
|
import argparse
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..backend import compose
|
from ..backend import compose
|
||||||
from ..config import Paths
|
from ..config import Paths, AtlasDefaults
|
||||||
|
from ..utils.filesystem import rmdir
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
@ -13,6 +15,9 @@ class Arguments(argparse.Namespace):
|
||||||
Default Arguments when calling the CLI
|
Default Arguments when calling the CLI
|
||||||
"""
|
"""
|
||||||
output: str
|
output: str
|
||||||
|
cacheclear: bool
|
||||||
|
filter: List[str]
|
||||||
|
timeout: int
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""
|
"""
|
||||||
|
@ -21,10 +26,15 @@ def parse_arguments():
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="atlasimagecomposer",
|
prog="atlasimagecomposer",
|
||||||
description="CLI tool to automatically generate expression sheets for servants",)
|
description="CLI tool to automatically generate expression sheets for servants",
|
||||||
|
epilog="If there are any issues during execution, it helps to clear the cache from time to time")
|
||||||
|
|
||||||
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__}")
|
parser.add_argument("--version", action="version", version=f"atlasimagecomposer {__version__}")
|
||||||
|
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("--filter", action="extend", nargs="+", dest="filter", help='Specify one or more spritesheet ids that will be fetched')
|
||||||
|
parser.add_argument("--timeout", action="store", type=int, default=None, dest="timeout", help="Set the timeout for all requests towards AtlasAcademy, default is 10s")
|
||||||
|
parser.add_argument("--clear-cache", action="store_true", default=False, dest="cacheclear", help="Clear cached assets before downloading files for a servant")
|
||||||
|
|
||||||
|
|
||||||
args = Arguments()
|
args = Arguments()
|
||||||
args, extra_args = parser.parse_known_args(sys.argv[1:], namespace=args)
|
args, extra_args = parser.parse_known_args(sys.argv[1:], namespace=args)
|
||||||
|
@ -36,10 +46,31 @@ def welcome():
|
||||||
print(" developed by Firq ")
|
print(" developed by Firq ")
|
||||||
print("-------------------------------------------------")
|
print("-------------------------------------------------")
|
||||||
|
|
||||||
def run():
|
def run_cli():
|
||||||
args, _ = parse_arguments()
|
args, _ = parse_arguments()
|
||||||
if args.output:
|
if args.output:
|
||||||
Paths.OUTPUT = pathlib.Path(args.output)
|
Paths.OUTPUT = pathlib.Path(args.output)
|
||||||
|
|
||||||
|
if args.timeout and args.timeout >= 0:
|
||||||
|
AtlasDefaults.TIMEOUT = args.timeout
|
||||||
|
|
||||||
welcome()
|
welcome()
|
||||||
compose()
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if args.cacheclear:
|
||||||
|
cachepath = Paths.IMAGES / str(servantid)
|
||||||
|
if cachepath.exists():
|
||||||
|
rmdir(cachepath)
|
||||||
|
print("Successfully cleared cached assets")
|
||||||
|
else:
|
||||||
|
print("No cache to clear was found, continuing")
|
||||||
|
|
||||||
|
compose(servantid, args.filter)
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
class Paths:
|
class Paths:
|
||||||
_root = pathlib.Path(__file__).parents[1]
|
_root = pathlib.Path(__file__).parents[1]
|
||||||
IMAGES = _root / ".temp"
|
IMAGES = _root / ".temp"
|
||||||
OUTPUT = _root / ".out"
|
OUTPUT = _root / ".out"
|
||||||
|
|
||||||
class ExpressionDefaults:
|
class ExpressionDefaults:
|
||||||
faceSize = 256
|
FACESIZE = 256
|
||||||
|
SHEETSIZE = 1024
|
||||||
|
|
||||||
class AtlasDefaults:
|
class AtlasDefaults:
|
||||||
REGION = "JP"
|
REGION = "JP"
|
||||||
|
TIMEOUT = 10
|
||||||
|
|
0
atlasimagecomposer/utils/__init__.py
Normal file
0
atlasimagecomposer/utils/__init__.py
Normal file
15
atlasimagecomposer/utils/filesystem.py
Normal file
15
atlasimagecomposer/utils/filesystem.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
def rmdir(directory: pathlib.Path):
|
||||||
|
"""
|
||||||
|
Recursively deletes all files and folders in a given directory
|
||||||
|
|
||||||
|
From: https://stackoverflow.com/a/49782093 (thanks mitch)
|
||||||
|
"""
|
||||||
|
for item in directory.iterdir():
|
||||||
|
if item.is_dir():
|
||||||
|
rmdir(item)
|
||||||
|
else:
|
||||||
|
item.unlink()
|
||||||
|
directory.rmdir()
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "atlasimagecomposer"
|
name = "atlasimagecomposer"
|
||||||
version = "0.1.0-a.6"
|
version = "0.1.0-c.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy~=2.0.1",
|
"numpy~=2.0.1",
|
||||||
"pillow~=10.4.0",
|
"pillow~=10.4.0",
|
||||||
|
@ -10,7 +10,7 @@ dependencies = [
|
||||||
requires-python = ">= 3.10"
|
requires-python = ">= 3.10"
|
||||||
authors = [{name = "Firq", email = "firelp42@gmail.com"}]
|
authors = [{name = "Firq", email = "firelp42@gmail.com"}]
|
||||||
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
|
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
|
||||||
description = "Tool to manage requests for supports"
|
description = "Package that enables peopßle to quickly download and generate all potential spritesheet expressions with a single command"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
@ -20,7 +20,7 @@ classifiers = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
atlasimagecomposer = "atlasimagecomposer.cli.cli:run"
|
atlasimagecomposer = "atlasimagecomposer.cli.cli:run_cli"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
|
|
Loading…
Reference in a new issue