"""
Inset Crop Tool + Pink BG Remover
1. 上傳圖片,設定底色與 threshold → 即時預覽去底色效果
2. 確認後設定 cols / rows / inset → 下載 ZIP
"""
from __future__ import annotations
import io
import zipfile
import numpy as np
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.responses import HTMLResponse, PlainTextResponse, StreamingResponse
from PIL import Image
app = FastAPI()
# ──────────────────────────────────────────────
# BG Removal helper (from remove_pink_background.py)
# ──────────────────────────────────────────────
def _parse_hex(value: str) -> tuple[int, int, int]:
v = value.strip().lstrip("#")
if len(v) != 6:
raise ValueError("需要 6 位 hex 色碼,例如 #ff00ff")
return int(v[0:2], 16), int(v[2:4], 16), int(v[4:6], 16)
def remove_background(img: Image.Image, bg_color: str, threshold: float) -> Image.Image:
"""將 img 中 RGB 距離 bg_color 在 threshold 內的像素設為透明。"""
img = img.convert("RGBA")
pixels = np.array(img, dtype=np.int32)
key = np.array(_parse_hex(bg_color), dtype=np.int32)
dist = np.sqrt(np.sum((pixels[..., :3] - key) ** 2, axis=-1))
out = pixels.copy()
out[..., 3] = np.where(dist <= threshold, 0, pixels[..., 3])
return Image.fromarray(out.astype(np.uint8), "RGBA")
# ──────────────────────────────────────────────
# HTML 前端
# ──────────────────────────────────────────────
HTML = """
Inset Crop Tool
原始圖片
尚未上傳圖片
預覽將在此顯示
"""
# ──────────────────────────────────────────────
# Routes
# ──────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML
@app.post("/preview")
async def preview_bg(
file: UploadFile = File(...),
bg_color: str = Form("#ff00ff"),
threshold: float = Form(100.0),
):
"""去底色後回傳 PNG,供前端即時預覽。"""
raw = await file.read()
try:
src = Image.open(io.BytesIO(raw))
result = remove_background(src, bg_color, threshold)
except ValueError as e:
return PlainTextResponse(str(e), status_code=400)
buf = io.BytesIO()
result.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
@app.post("/process")
async def process(
file: UploadFile = File(...),
bg_color: str = Form("#ff00ff"),
threshold: float = Form(100.0),
cols: int = Form(...),
rows: int = Form(...),
inset: int = Form(...),
):
"""去底色 → 內縮裁切 → 打包 ZIP 下載。"""
raw = await file.read()
try:
src = Image.open(io.BytesIO(raw))
src = remove_background(src, bg_color, threshold)
except ValueError as e:
return PlainTextResponse(str(e), status_code=400)
W, H = src.size
tile_w = W // cols
tile_h = H // rows
if inset * 2 >= tile_w or inset * 2 >= tile_h:
return PlainTextResponse("內縮距離過大,超出格子尺寸", status_code=400)
tiles: list[Image.Image] = []
for r in range(rows):
for c in range(cols):
x0, y0 = c * tile_w, r * tile_h
cropped = src.crop((x0 + inset, y0 + inset, x0 + tile_w - inset, y0 + tile_h - inset))
resized = cropped.resize((tile_w, tile_h), Image.NEAREST)
tiles.append(resized)
combined = Image.new("RGBA", (cols * tile_w, rows * tile_h), (0, 0, 0, 0))
for idx, tile in enumerate(tiles):
r, c = divmod(idx, cols)
combined.paste(tile, (c * tile_w, r * tile_h))
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for idx, tile in enumerate(tiles):
r, c = divmod(idx, cols)
tb = io.BytesIO()
tile.save(tb, format="PNG")
zf.writestr(f"tiles/tile_r{r:02d}_c{c:02d}.png", tb.getvalue())
cb = io.BytesIO()
combined.save(cb, format="PNG")
zf.writestr("combined.png", cb.getvalue())
buf.seek(0)
return StreamingResponse(
buf,
media_type="application/zip",
headers={"Content-Disposition": "attachment; filename=inset_crop_output.zip"},
)
# ──────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run("inset_crop_tool:app", host="0.0.0.0", port=8003, reload=True)