import io
import math
import os
import re
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
from PIL import Image
app = FastAPI()
ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
RESAMPLE_LANCZOS = getattr(getattr(Image, "Resampling", Image), "LANCZOS", Image.BICUBIC)
def natural_name_key(full_name: str) -> tuple[list[object], str]:
normalized = (full_name or "").replace("\\", "/")
base = os.path.basename(normalized)
parts = re.split(r"(\d+)", base.lower())
key: list[object] = [int(p) if p.isdigit() else p for p in parts]
return key, normalized.lower()
HTML = """
Folder Sprite Builder
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML
@app.post("/build")
async def build_folder_sprite(
files: list[UploadFile] = File(...),
cols: int = Form(...),
total_width: int = Form(...),
output_name: str = Form("folder_sprite.png"),
):
if cols <= 0:
return PlainTextResponse("Columns 必須大於 0", status_code=400)
if total_width <= 0:
return PlainTextResponse("總寬度必須大於 0", status_code=400)
tile_w = total_width // cols
if tile_w <= 0:
return PlainTextResponse("總寬度過小,請提高總寬度或降低 Columns", status_code=400)
image_files: list[UploadFile] = []
for f in files:
_, ext = os.path.splitext((f.filename or "").lower())
if ext in ALLOWED_EXTENSIONS:
image_files.append(f)
if not image_files:
return PlainTextResponse("找不到可用圖片檔", status_code=400)
image_files.sort(key=lambda f: natural_name_key(f.filename or ""))
resized_images: list[Image.Image] = []
resized_heights: list[int] = []
for f in image_files:
try:
data = await f.read()
img = Image.open(io.BytesIO(data)).convert("RGBA")
except Exception:
continue
if img.width <= 0 or img.height <= 0:
continue
ratio = min(1.0, tile_w / float(img.width))
new_w = max(1, int(round(img.width * ratio)))
new_h = max(1, int(round(img.height * ratio)))
resized = img.resize((new_w, new_h), RESAMPLE_LANCZOS)
resized_images.append(resized)
resized_heights.append(new_h)
if not resized_images:
return PlainTextResponse("圖片讀取失敗,請確認檔案格式", status_code=400)
cell_h = max(resized_heights)
rows = math.ceil(len(resized_images) / cols)
out_h = rows * cell_h
sheet = Image.new("RGBA", (total_width, out_h), (0, 0, 0, 0))
for idx, img in enumerate(resized_images):
row = idx // cols
col = idx % cols
x = col * tile_w + (tile_w - img.width) // 2
y = row * cell_h + (cell_h - img.height) // 2
sheet.paste(img, (x, y), img)
final_name = output_name.strip() or "folder_sprite.png"
if not final_name.lower().endswith(".png"):
final_name += ".png"
buf = io.BytesIO()
sheet.save(buf, format="PNG")
buf.seek(0)
return Response(
content=buf.read(),
media_type="image/png",
headers={"Content-Disposition": f'attachment; filename="{final_name}"'},
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("folder_sprite_builder:app", host="0.0.0.0", port=8005, reload=True)