commit 2d22d01d5b1a640a08658527cc744d895b9baf13 Author: Edward Chang Date: Wed Apr 22 14:47:03 2026 +0800 Initial commit: sprite tool project diff --git a/shiny_maker.py b/shiny_maker.py new file mode 100644 index 0000000..c8f9a9b --- /dev/null +++ b/shiny_maker.py @@ -0,0 +1,214 @@ +import traceback +from fastapi import FastAPI, UploadFile, File, Form, Request +from fastapi.responses import StreamingResponse, HTMLResponse, FileResponse, JSONResponse +import cv2 +import numpy as np +from PIL import Image +import io, os, base64 + +app = FastAPI() +BASE = os.getcwd() +OUTPUT_DIR = os.path.join(BASE, 'shiny_output') +os.makedirs(OUTPUT_DIR, exist_ok=True) + +HTML = ''' + + + + + + + +
+
+

Shiny Monster Maker Pro

+

全維度色違調整工具 (HSV 模式)

+
+ +
+
+
+ + +
+ +
+
+ + 0 +
+ +
+ +
+
+ + 1.0 +
+ +
+ +
+
+ + 1.0 +
+ +
+ +
+ + +

避免影響灰色/黑色區塊

+
+ +
+ + +
+
+ +
+

AWAITING IMAGE...

+ +
+
+
+ + + +''' + +@app.get('/', response_class=HTMLResponse) +def home(): + return HTML + +@app.middleware("http") +async def catch_exceptions_middleware(request: Request, call_next): + try: + return await call_next(request) + except Exception as e: + # 在終端機印出完整的錯誤追蹤 + traceback.print_exc() + return JSONResponse( + status_code=500, + content={"message": str(e), "traceback": traceback.format_exc()} + ) + +@app.post('/process_shiny') +async def process_shiny( + file: UploadFile = File(...), + hue_shift: int = Form(...), + sat_min: int = Form(...), + sat_scale: float = Form(...), # 新增:飽和度倍率 + val_scale: float = Form(...) # 新增:亮度倍率 +): + try: + data = await file.read() + nparr = np.frombuffer(data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) + + if img is None: return JSONResponse(status_code=400, content={"message": "解碼失敗"}) + + # 頻道判斷 + if img.ndim == 2: + bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR); alpha = None + elif img.shape[2] == 3: + bgr = img; alpha = None + else: + bgr = img[:, :, :3]; alpha = img[:, :, 3] + + hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + h, s, v = cv2.split(hsv) + + # 遮罩邏輯 + mask = s > (float(sat_min) * 2.55) + + # --- 核心變色邏輯 --- + # 1. 色相旋轉 + h[mask] = (h[mask] + float(hue_shift)) % 180 + + # 2. 飽和度調整 (乘法運算並限制範圍 0-255) + s[mask] = np.clip(s[mask] * sat_scale, 0, 255) + + # 3. 亮度調整 (乘法運算並限制範圍 0-255) + v[mask] = np.clip(v[mask] * val_scale, 0, 255) + + # 合併與轉換回 BGR + hsv_new = cv2.merge([h, s, v]).astype(np.uint8) + bgr_new = cv2.cvtColor(hsv_new, cv2.COLOR_HSV2BGR) + + if alpha is not None: + res = cv2.merge([bgr_new, alpha]) + else: + res = bgr_new + + _, buffer = cv2.imencode('.png', res) + return StreamingResponse(io.BytesIO(buffer.tobytes()), media_type='image/png') + except Exception as e: + traceback.print_exc() + return JSONResponse(status_code=500, content={"message": str(e)}) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) \ No newline at end of file diff --git a/sprite_tool_fullstack.py b/sprite_tool_fullstack.py new file mode 100644 index 0000000..c5a34eb --- /dev/null +++ b/sprite_tool_fullstack.py @@ -0,0 +1,202 @@ +from fastapi import FastAPI, UploadFile, File, Form +from fastapi.responses import HTMLResponse, FileResponse +from PIL import Image +import io, math, json, os + +app = FastAPI() +BASE = os.getcwd() +OUTPUT_DIR = os.path.join(BASE, 'output') +os.makedirs(OUTPUT_DIR, exist_ok=True) + +HTML = ''' + + + + + + + +
+
+

Sprite Tool Suite

+
+

Total Tiles: 0

+
+

操作:直接拖拽平移。Cmd+點擊選取兩點定一區。

+ +
+ + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +''' + +@app.get('/', response_class=HTMLResponse) +def home(): + return HTML + +@app.post('/export') +async def export(file: UploadFile = File(...), cols: int = Form(...), rows: int = Form(...), outCols: int = Form(...), outRows: int = Form(...), name: str = Form('output.png'), regions: str = Form(...)): + data = await file.read() + img = Image.open(io.BytesIO(data)).convert('RGBA') + cw, ch = img.width // cols, img.height // rows + reg_list = json.loads(regions) + tiles = [] + for r in reg_list: + for y in range(r['y1'], r['y2'] + 1): + for x in range(r['x1'], r['x2'] + 1): + tiles.append((x, y)) + + if not tiles: return HTMLResponse('No tiles selected', status_code=400) + needed_rows = math.ceil(len(tiles) / outCols) + out = Image.new('RGBA', (outCols * cw, needed_rows * ch), (0, 0, 0, 0)) + for i, (tx, ty) in enumerate(tiles): + tile = img.crop((tx * cw, ty * ch, (tx + 1) * cw, (ty + 1) * ch)) + out.paste(tile, ((i % outCols) * cw, (i // outCols) * ch)) + + path = os.path.join(OUTPUT_DIR, name) + out.save(path) + return FileResponse(path, filename=name, media_type='image/png') + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/sprite_webtool.py b/sprite_webtool.py new file mode 100644 index 0000000..49f3e20 --- /dev/null +++ b/sprite_webtool.py @@ -0,0 +1,278 @@ +import zipfile +from fastapi import FastAPI, UploadFile, File, Form +from fastapi.responses import HTMLResponse, FileResponse +from PIL import Image +import io, math, json, os + +app = FastAPI() +BASE = os.getcwd() +OUTPUT_DIR = os.path.join(BASE, 'output') +os.makedirs(OUTPUT_DIR, exist_ok=True) + +HTML = ''' + + + + + + + +
+
+

Sprite Picker

+ +
+

Total Selected

+ 0 +
+ +
+
+ WH: + +
+
+ Cols: + +
+ +
+ + + + +
+ +
+
+
+ +
+ +
+
+ + + +''' + + +@app.get('/', response_class=HTMLResponse) +def home(): + return HTML + +@app.post('/export_picker') +async def export_picker( + file: UploadFile = File(...), + outCols: int = Form(...), + name: str = Form(...), + picks: str = Form(...) +): + data = await file.read() + img = Image.open(io.BytesIO(data)).convert('RGBA') + pick_list = json.loads(picks) + + if not pick_list: + return HTMLResponse('None', status_code=400) + + tw, th = pick_list[0]['w'], pick_list[0]['h'] + base_name = os.path.splitext(name)[0] + + # 計算總共可以組成幾張完整的圖片 (捨棄餘數) + num_picks = len(pick_list) + num_images = num_picks // outCols + + if num_images == 0: + return HTMLResponse('選取數量不足以構成一列', status_code=400) + + # 儲存產出的路徑清單 + generated_files = [] + + for img_idx in range(num_images): + # 建立單張輸出圖 (高度固定為 1 row) + out_img = Image.new('RGBA', (outCols * tw, th), (0, 0, 0, 0)) + + for col_idx in range(outCols): + p_idx = img_idx * outCols + col_idx + p = pick_list[p_idx] + box = (p['x'], p['y'], p['x'] + p['w'], p['y'] + p['h']) + tile = img.crop(box) + out_img.paste(tile, (col_idx * tw, 0)) + + # 存檔 + file_path = os.path.join(OUTPUT_DIR, f"{base_name}_{img_idx+1}.png") + out_img.save(file_path) + generated_files.append(file_path) + + # 如果只有一張圖,直接回傳圖片 + if len(generated_files) == 1: + return FileResponse(generated_files[0], filename=f"{base_name}.png", media_type='image/png') + + # 如果有多張圖,打包成 ZIP + zip_path = os.path.join(OUTPUT_DIR, f"{base_name}_batch.zip") + with zipfile.ZipFile(zip_path, 'w') as zipf: + for f in generated_files: + zipf.write(f, os.path.basename(f)) + + return FileResponse(zip_path, filename=f"{base_name}.zip", media_type='application/zip') + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/tool.py b/tool.py new file mode 100644 index 0000000..179f059 --- /dev/null +++ b/tool.py @@ -0,0 +1,82 @@ +import uvicorn +import re +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, Response +from starlette.middleware.base import BaseHTTPMiddleware + +# 匯入原本的三個檔案 +from sprite_tool_fullstack import app as grid_app +from sprite_webtool import app as picker_app +from shiny_maker import app as shiny_app + +app = FastAPI(title="Game Dev Suite") + +# --- 定義導覽列 HTML --- +NAVBAR_HTML = """ + +""" + +class NavbarMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + + # 排除首頁,避免重複注入 + if request.url.path == "/": + return response + + # 針對所有子工具的 HTML 回傳進行注入 + if "text/html" in response.headers.get("content-type", ""): + body = b"" + async for chunk in response.body_iterator: + body += chunk + + html_content = body.decode("utf-8") + + # 使用正則表達式匹配 標籤,不論它有沒有帶 class 或其他屬性 + # 這會匹配 並在其後方插入 Navbar + new_content = re.sub(r'(]*>)', r'\1' + NAVBAR_HTML, html_content, flags=re.IGNORECASE) + + return HTMLResponse(content=new_content, status_code=response.status_code) + + return response + +app.add_middleware(NavbarMiddleware) + +# --- 掛載子應用程式 --- +app.mount("/grid", grid_app) +app.mount("/picker", picker_app) +app.mount("/shiny", shiny_app) + +# 首頁入口 +@app.get("/", response_class=HTMLResponse) +async def index(): + return f""" + + + + + + + + {NAVBAR_HTML} +
+

Game Developer Tool Suite

+

請從上方導覽列選擇要使用的工具

+
+
📏 Grid Tool
+
🎯 Picker Tool
+
✨ Shiny Maker
+
+
+ + + """ + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file