Files
SpriteTool/rotate_webtool.py

205 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import cv2
import io
import numpy as np
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import HTMLResponse, StreamingResponse
app = FastAPI()
HTML = '''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sprite Flipper</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.pixelated { image-rendering: pixelated; }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #1e293b; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
</style>
</head>
<body class="bg-slate-900 text-white p-6">
<div class="max-w-4xl mx-auto space-y-6">
<div class="bg-slate-800 p-6 rounded-2xl border border-slate-700">
<h1 class="text-2xl font-bold text-blue-400 mb-1">🔄 Sprite Flipper</h1>
<p class="text-slate-400 text-sm mb-6">將 Sprite Sheet 中每個格子水平翻轉(左右鏡像),並輸出新圖片。</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-xs text-slate-400 mb-1">Columns列數</label>
<input id="cols" type="number" value="10" min="1"
class="w-full p-2 rounded-lg bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-blue-500">
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">Rows行數</label>
<input id="rows" type="number" value="6" min="1"
class="w-full p-2 rounded-lg bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-blue-500">
</div>
</div>
<div class="mb-6">
<label class="block text-xs text-slate-400 mb-2">上傳 Sprite SheetPNG / GIF 等)</label>
<div id="dropzone"
class="border-2 border-dashed border-slate-600 rounded-xl p-8 text-center cursor-pointer hover:border-blue-500 transition"
onclick="document.getElementById('fileInput').click()">
<p id="dropLabel" class="text-slate-400 text-sm">點擊或拖曳圖片至此處</p>
</div>
<input id="fileInput" type="file" accept="image/*" class="hidden">
</div>
<div id="previewSection" class="hidden mb-6 space-y-2">
<p class="text-xs text-slate-400">預覽(原圖)</p>
<img id="preview" class="max-w-full rounded-lg border border-slate-700 pixelated">
</div>
<button id="flipBtn" onclick="doFlip()"
class="w-full py-3 bg-blue-600 hover:bg-blue-500 rounded-xl font-bold transition disabled:opacity-50 disabled:cursor-not-allowed"
disabled>
🔄 執行翻轉並下載
</button>
<div id="status" class="mt-4 text-sm text-center text-slate-400 hidden"></div>
</div>
<div id="resultSection" class="hidden bg-slate-800 p-6 rounded-2xl border border-slate-700 space-y-3">
<p class="text-sm text-slate-400">輸出結果預覽</p>
<img id="resultImg" class="max-w-full rounded-lg border border-slate-700 pixelated">
<a id="downloadLink" class="inline-block mt-2 px-4 py-2 bg-green-600 hover:bg-green-500 rounded-lg font-bold transition text-sm">
⬇ 下載翻轉後圖片
</a>
</div>
</div>
<script>
let fileBlob = null;
const fileInput = document.getElementById('fileInput');
const dropzone = document.getElementById('dropzone');
const flipBtn = document.getElementById('flipBtn');
function loadFile(file) {
if (!file) return;
fileBlob = file;
document.getElementById('dropLabel').textContent = file.name;
const url = URL.createObjectURL(file);
document.getElementById('preview').src = url;
document.getElementById('previewSection').classList.remove('hidden');
flipBtn.disabled = false;
document.getElementById('resultSection').classList.add('hidden');
}
fileInput.addEventListener('change', e => loadFile(e.target.files[0]));
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('border-blue-500'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('border-blue-500'));
dropzone.addEventListener('drop', e => {
e.preventDefault();
dropzone.classList.remove('border-blue-500');
loadFile(e.dataTransfer.files[0]);
});
async function doFlip() {
if (!fileBlob) return;
const cols = parseInt(document.getElementById('cols').value) || 10;
const rows = parseInt(document.getElementById('rows').value) || 6;
flipBtn.disabled = true;
const status = document.getElementById('status');
status.textContent = '⏳ 處理中...';
status.classList.remove('hidden');
const fd = new FormData();
fd.append('file', fileBlob);
fd.append('cols', cols);
fd.append('rows', rows);
try {
const r = await fetch('flip', { method: 'POST', body: fd });
if (!r.ok) {
status.textContent = '❌ 處理失敗:' + await r.text();
flipBtn.disabled = false;
return;
}
const blob = await r.blob();
const url = URL.createObjectURL(blob);
document.getElementById('resultImg').src = url;
const dl = document.getElementById('downloadLink');
dl.href = url;
const ext = fileBlob.name.split('.').pop();
dl.download = fileBlob.name.replace(/\.[^.]+$/, '') + '_flipped.' + ext;
document.getElementById('resultSection').classList.remove('hidden');
status.textContent = '✅ 完成!';
} catch(e) {
status.textContent = '❌ 發生錯誤:' + e.message;
}
flipBtn.disabled = false;
}
</script>
</body>
</html>'''
def _flip_sprite_sheet(img_bytes: bytes, cols: int, rows: int) -> bytes:
"""核心翻轉邏輯(來自 rotate.py將每個 cell 水平翻轉。"""
arr = np.frombuffer(img_bytes, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
if img is None:
raise ValueError("無法解碼圖片")
h, w = img.shape[:2]
cell_w = w / cols
cell_h = h / rows
output = img.copy()
for r in range(rows):
for c in range(cols):
x1 = int(c * cell_w)
y1 = int(r * cell_h)
x2 = int((c + 1) * cell_w)
y2 = int((r + 1) * cell_h)
cell = img[y1:y2, x1:x2]
if cell.size == 0:
continue
output[y1:y2, x1:x2] = cv2.flip(cell, 1)
success, buf = cv2.imencode(".png", output)
if not success:
raise ValueError("圖片編碼失敗")
return buf.tobytes()
@app.get("/", response_class=HTMLResponse)
def home():
return HTML
@app.post("/flip")
async def flip(
file: UploadFile = File(...),
cols: int = Form(10),
rows: int = Form(6),
):
data = await file.read()
try:
result = _flip_sprite_sheet(data, cols, rows)
except ValueError as e:
from fastapi.responses import PlainTextResponse
return PlainTextResponse(str(e), status_code=400)
return StreamingResponse(
io.BytesIO(result),
media_type="image/png",
headers={"Content-Disposition": 'attachment; filename="flipped.png"'},
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)