add basic files

This commit is contained in:
Julian Freeman
2025-06-26 13:54:35 -04:00
parent efdd76d11e
commit e6b7c8d34d
5 changed files with 521 additions and 0 deletions

128
main.py Normal file
View File

@@ -0,0 +1,128 @@
from fastapi import FastAPI, File, UploadFile, Form
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
from typing import List
from pathlib import Path
from processing import process_image
from zipfile import ZipFile
import shutil
import uuid
from rembg import remove
import os
app = FastAPI()
# 文件路径设置
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static"
OUTPUT_DIR = STATIC_DIR / "output"
UPLOAD_DIR = STATIC_DIR / "uploads"
TEMPLATE_DIR = BASE_DIR / "templates"
# 确保目录存在
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# 挂载静态目录和模板
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
templates = Jinja2Templates(directory=TEMPLATE_DIR)
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/upload")
async def upload_images(files: List[UploadFile] = File(...)):
session_id = str(uuid.uuid4())
session_upload_dir = UPLOAD_DIR / session_id
session_output_dir = OUTPUT_DIR / session_id
session_zip_path = OUTPUT_DIR / f"{session_id}.zip"
session_upload_dir.mkdir(parents=True)
session_output_dir.mkdir(parents=True)
for file in files:
contents = await file.read()
input_path = session_upload_dir / file.filename
with open(input_path, "wb") as f:
f.write(contents)
output_path = session_output_dir / file.filename
process_image(input_path, output_path)
# 打包为 zip
with ZipFile(session_zip_path, 'w') as zipf:
for image_file in session_output_dir.iterdir():
zipf.write(image_file, arcname=image_file.name)
# 清理上传目录(可选)
shutil.rmtree(session_upload_dir)
shutil.rmtree(session_output_dir)
return {"download_url": f"/static/output/{session_zip_path.name}"}
@app.get("/download/{zip_filename}")
async def download_zip(zip_filename: str):
file_path = OUTPUT_DIR / zip_filename
if file_path.exists():
return FileResponse(path=file_path, filename=zip_filename, media_type='application/zip')
return {"error": "File not found"}
@app.post("/remove-bg")
async def remove_bg(files: List[UploadFile] = File(...)):
session_id = str(uuid.uuid4())
session_upload_dir = UPLOAD_DIR / session_id
session_output_dir = OUTPUT_DIR / session_id
session_zip_path = OUTPUT_DIR / f"{session_id}.zip"
session_upload_dir.mkdir(parents=True, exist_ok=True)
session_output_dir.mkdir(parents=True, exist_ok=True)
result_files = []
for file in files:
if file.filename == '':
return JSONResponse(content={"error": "No selected file"}, status_code=400)
input_path = session_upload_dir / file.filename
output_path = session_output_dir / file.filename
contents = await file.read()
with open(input_path, "wb") as f:
f.write(contents)
with open(input_path, 'rb') as input_file:
with open(output_path, 'wb') as output_file:
input_data = input_file.read()
output_data = remove(
input_data,
alpha_matting=True,
alpha_matting_erode_size=15,
alpha_matting_background_threshold=5,
alpha_matting_foreground_threshold=250,
)
output_file.write(output_data)
result_files.append(f'/static/output/{session_id}/{file.filename}')
# 打包为 zip 文件
with ZipFile(session_zip_path, 'w') as zipf:
for image_file in session_output_dir.iterdir():
zipf.write(image_file, arcname=image_file.name)
# 清理临时目录
shutil.rmtree(session_upload_dir)
shutil.rmtree(session_output_dir)
return JSONResponse(content={"download_url": f"/static/output/{session_zip_path.name}"})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=7310)

46
processing.py Normal file
View File

@@ -0,0 +1,46 @@
from PIL import Image, ImageDraw
from pathlib import Path
def create_soft_edge_mask(width: int, height: int, transition_ratio: float = 0.02) -> Image:
transition_height = int(height * transition_ratio)
mask = Image.new("L", (width, height), 255) # 默认全白(不透明)
draw = ImageDraw.Draw(mask)
draw.rectangle([0, 0, width, height // 2 - transition_height // 2], fill=0) # 上半黑
draw.rectangle([0, height // 2 + transition_height // 2, width, height], fill=255) # 下半白
# 顶部和底部之间 2% 的区域渐变
for i in range(transition_height):
alpha = int(255 * (i / transition_height))
y = height // 2 - transition_height // 2 + i
draw.line([(0, y), (width, y)], fill=alpha)
return mask
def process_image(input_path: str | Path, output_path: str | Path):
base_width, base_height = 800, 1000
original = Image.open(input_path).convert("RGBA")
# 缩放图像宽度适配底图宽度
scale_ratio = base_width / original.width
new_height = int(original.height * scale_ratio)
resized = original.resize((base_width, new_height), Image.LANCZOS)
# 创建底图并粘贴第一个图像(对齐顶部)
base_image = Image.new("RGBA", (base_width, base_height), "white")
base_image.paste(resized, (0, 0), resized)
# 创建 A2 图层(对齐底部)
A2 = resized.copy()
A2_image = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0))
A2_y = base_height - A2.height
A2_image.paste(A2, (0, A2_y), A2)
# 创建遮罩并加为 alpha 通道
mask = create_soft_edge_mask(base_width, base_height, 0.02)
A2_image.putalpha(mask)
# 合成
final = Image.alpha_composite(base_image, A2_image)
final.convert("RGB").save(output_path, format="PNG")

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi
uvicorn
python-multipart
pillow
jinja2
rembg
onnxruntime

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

340
templates/index.html Normal file
View File

@@ -0,0 +1,340 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>图像处理工具集</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
body {
font-family: "Segoe UI", sans-serif;
background: linear-gradient(to right, #f0f2f5, #dfe4ea);
padding: 40px;
}
.container {
max-width: 600px;
margin: auto;
background: white;
padding: 30px 40px;
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
animation: fadeIn 0.8s ease-in-out;
margin-bottom: 30px; /* 增加卡片之间的间隙 */
}
.logo {
text-align: center;
font-size: 28px;
color: #4a90e2;
margin-bottom: 20px;
}
.logo i {
margin-right: 10px;
}
h2 {
text-align: center;
font-size: 22px;
margin-bottom: 12px;
}
.notice {
font-size: 14px;
color: #666;
margin-bottom: 18px;
text-align: center;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 30px;
text-align: center;
color: #888;
cursor: pointer;
margin-bottom: 16px;
transition: all 0.3s ease;
}
.drop-zone.dragover {
background: #eef6ff;
border-color: #4a90e2;
color: #4a90e2;
}
.drop-zone i {
font-size: 36px;
margin-bottom: 10px;
display: block;
color: #bbb;
}
button {
background-color: #4a90e2;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 0.3s, transform 0.2s;
}
button:hover {
background-color: #357abd;
transform: scale(1.02);
}
.progress-container {
margin-top: 15px;
height: 12px;
background: #e0e0e0;
border-radius: 8px;
overflow: hidden;
display: none;
}
.progress-bar {
width: 0%;
height: 100%;
background-color: #4a90e2;
transition: width 0.3s ease;
}
.spinner {
text-align: center;
font-size: 15px;
color: #555;
margin-top: 12px;
display: none;
}
.spinner i {
margin-right: 6px;
}
.download-link {
margin-top: 20px;
text-align: center;
font-size: 16px;
}
.download-link i {
margin-right: 6px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<i class="fas fa-image"></i> 边框背景图像调整尺寸
</div>
<h2>将不同比例的边框图片调整为 800x1000 尺寸</h2>
<p class="notice">支持批量上传 PNG / JPG / WebP 图片,推荐使用竖图(比例 9:16 或 3:4</p>
<div class="drop-zone" id="dropZone" title="支持拖拽上传图片或点击选择文件">
<i class="fas fa-cloud-upload-alt"></i>
拖拽图片到这里,或点击选择
</div>
<form id="uploadForm" enctype="multipart/form-data" style="display: none;">
<input type="file" id="fileInput" name="files" multiple accept="image/png, image/jpeg, image/webp">
</form>
<div class="progress-container" id="progressContainer">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="spinner" id="loadingSpinner">
<i class="fas fa-circle-notch fa-spin"></i> 正在处理,请稍候...
</div>
<div class="download-link" id="result"></div>
</div>
<div class="container">
<div class="logo">
<i class="fas fa-image"></i> 图像背景移除
</div>
<h2>批量移除图片背景</h2>
<p class="notice">支持批量上传 PNG / JPG 图片,移除背景后可下载结果</p>
<div class="drop-zone" id="bgRemoveDropZone" title="支持拖拽上传图片或点击选择文件">
<i class="fas fa-cloud-upload-alt"></i>
拖拽图片到这里,或点击选择
</div>
<form id="bgRemoveForm" enctype="multipart/form-data" style="display: none;">
<input type="file" id="bgRemoveFileInput" name="files" multiple accept="image/png, image/jpeg">
</form>
<div class="progress-container" id="bgRemoveProgressContainer">
<div class="progress-bar" id="bgRemoveProgressBar"></div>
</div>
<div class="spinner" id="bgRemoveLoadingSpinner">
<i class="fas fa-circle-notch fa-spin"></i> 正在处理,请稍候...
</div>
<div class="download-link" id="bgRemoveResult"></div>
</div>
<script>
const dropZone = document.getElementById("dropZone");
const fileInput = document.getElementById("fileInput");
const progressBar = document.getElementById("progressBar");
const progressContainer = document.getElementById("progressContainer");
const resultBox = document.getElementById("result");
const spinner = document.getElementById("loadingSpinner");
dropZone.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("dragover", e => {
e.preventDefault();
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", () => {
dropZone.classList.remove("dragover");
});
dropZone.addEventListener("drop", e => {
e.preventDefault();
dropZone.classList.remove("dragover");
fileInput.files = e.dataTransfer.files;
uploadFiles(fileInput.files);
});
fileInput.addEventListener("change", () => {
if (fileInput.files.length > 0) {
uploadFiles(fileInput.files);
}
});
async function uploadFiles(files) {
resultBox.innerHTML = "";
progressBar.style.width = "0%";
progressContainer.style.display = "block";
spinner.style.display = "block";
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", "/upload", true);
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + "%";
}
};
xhr.onload = function () {
spinner.style.display = "none";
progressContainer.style.display = "none";
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
if (res.download_url) {
resultBox.innerHTML = `<i class="fas fa-check-circle" style="color:green;"></i> <a href='${res.download_url}' download>点击下载处理结果</a>`;
} else {
resultBox.innerHTML = `<i class="fas fa-times-circle" style="color:red;"></i> 上传失败,请检查文件格式。`;
}
} else {
resultBox.innerHTML = `<i class="fas fa-exclamation-circle" style="color:orange;"></i> 上传错误,请稍后重试。`;
}
};
xhr.send(formData);
}
const bgRemoveDropZone = document.getElementById("bgRemoveDropZone");
const bgRemoveFileInput = document.getElementById("bgRemoveFileInput");
const bgRemoveProgressBar = document.getElementById("bgRemoveProgressBar");
const bgRemoveProgressContainer = document.getElementById("bgRemoveProgressContainer");
const bgRemoveResultBox = document.getElementById("bgRemoveResult");
const bgRemoveSpinner = document.getElementById("bgRemoveLoadingSpinner");
bgRemoveDropZone.addEventListener("click", () => bgRemoveFileInput.click());
bgRemoveDropZone.addEventListener("dragover", e => {
e.preventDefault();
bgRemoveDropZone.classList.add("dragover");
});
bgRemoveDropZone.addEventListener("dragleave", () => {
bgRemoveDropZone.classList.remove("dragover");
});
bgRemoveDropZone.addEventListener("drop", e => {
e.preventDefault();
bgRemoveDropZone.classList.remove("dragover");
bgRemoveFileInput.files = e.dataTransfer.files;
uploadBgRemoveFiles(bgRemoveFileInput.files);
});
bgRemoveFileInput.addEventListener("change", () => {
if (bgRemoveFileInput.files.length > 0) {
uploadBgRemoveFiles(bgRemoveFileInput.files);
}
});
async function uploadBgRemoveFiles(files) {
bgRemoveResultBox.innerHTML = "";
bgRemoveProgressBar.style.width = "0%";
bgRemoveProgressContainer.style.display = "block";
bgRemoveSpinner.style.display = "block";
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", "/remove-bg", true);
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
bgRemoveProgressBar.style.width = percent + "%";
}
};
xhr.onload = function () {
bgRemoveSpinner.style.display = "none";
bgRemoveProgressContainer.style.display = "none";
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
if (res.download_url) {
bgRemoveResultBox.innerHTML = `<i class="fas fa-check-circle" style="color:green;"></i> <a href='${res.download_url}' download>点击下载处理结果</a>`;
} else {
bgRemoveResultBox.innerHTML = `<i class="fas fa-times-circle" style="color:red;"></i> 上传失败,请检查文件格式。`;
}
} else {
bgRemoveResultBox.innerHTML = `<i class="fas fa-exclamation-circle" style="color:orange;"></i> 上传错误,请稍后重试。`;
}
};
xhr.send(formData);
}
</script>
</body>
</html>