BlueSkyXN commited on
Commit
c8344ad
·
unverified ·
2 Parent(s): fc94a64 090921a
Files changed (2) hide show
  1. README.md +47 -3
  2. main.py +418 -32
README.md CHANGED
@@ -14,7 +14,9 @@ pinned: false # 是否在你的个人资料页置顶这个 Space (可选)
14
 
15
  ## ✨ 功能特性
16
 
 
17
  * **🎯 动态路径 API**: 通过 URL 路径直接指定目标格式、转换模式和质量参数
 
18
  * **🎬 动画支持**: 完整支持 GIF、Animated WebP/AVIF/APNG 等动画格式
19
  * **🔄 多格式转换**: 支持 AVIF、WebP、JPEG、PNG、GIF、HEIF 格式互转
20
  * **⚙️ 灵活配置**: 支持有损/无损两种模式,质量参数可在 0-100 范围自由调节
@@ -23,7 +25,41 @@ pinned: false # 是否在你的个人资料页置顶这个 Space (可选)
23
 
24
  ## 📡 API 端点
25
 
26
- ### 1. 图像转换
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  **端点**: `POST /convert/{target_format}/{mode}/{setting}`
29
 
@@ -58,7 +94,7 @@ curl -X POST "https://your-api.hf.space/convert/jpeg/lossy/75" \
58
  -o output.jpg
59
  ```
60
 
61
- ### 2. 健康检查
62
 
63
  **端点**: `GET /health`
64
 
@@ -147,7 +183,15 @@ docker run -p 8000:8000 magick-api
147
 
148
  ## 🐛 已知问题与修复
149
 
150
- ### V3 版本修复 (当前版本)
 
 
 
 
 
 
 
 
151
 
152
  1. ✅ **修复 Timeout 实现**: 超时现在正确应用于进程执行而非进程创建
153
  2. ✅ **修复 WebP 无损质量**: 无损模式下 quality 固定为 100
 
14
 
15
  ## ✨ 功能特性
16
 
17
+ * **🌐 图形化界面**: 访问根路径即可使用现代化 Web 上传界面,零编程门槛
18
  * **🎯 动态路径 API**: 通过 URL 路径直接指定目标格式、转换模式和质量参数
19
+ * **📤 双上传模式**: 支持网页表单上传和 RESTful API 调用两种方式
20
  * **🎬 动画支持**: 完整支持 GIF、Animated WebP/AVIF/APNG 等动画格式
21
  * **🔄 多格式转换**: 支持 AVIF、WebP、JPEG、PNG、GIF、HEIF 格式互转
22
  * **⚙️ 灵活配置**: 支持有损/无损两种模式,质量参数可在 0-100 范围自由调节
 
25
 
26
  ## 📡 API 端点
27
 
28
+ ### 1. Web 上传界面 (新功能!)
29
+
30
+ **端点**: `GET /`
31
+
32
+ 访问根路径即可打开用户友好的图形化上传界面,无需编程知识即可使用。
33
+
34
+ **特点**:
35
+ - 🎨 现代化响应式界面
36
+ - 📱 支持移动设备
37
+ - 🖱️ 拖拽上传支持
38
+ - 📊 实时参数预览
39
+ - 🔗 快速访问 API 文档
40
+
41
+ **表单上传端点**: `POST /`
42
+
43
+ 支持通过 HTML 表单提交转换请求(与网页界面使用相同端点)。
44
+
45
+ **表单参数**:
46
+ - `file`: 图像文件(必需)
47
+ - `target_format`: 目标格式(默认: `webp`)
48
+ - `mode`: 转换模式(默认: `lossy`)
49
+ - `setting`: 质量参数 0-100(默认: `80`)
50
+
51
+ **示例**:
52
+ ```bash
53
+ # 使用表单方式上传(与网页界面相同)
54
+ curl -X POST "http://localhost:8000/" \
55
+ -F "file=@input.jpg" \
56
+ -F "target_format=avif" \
57
+ -F "mode=lossy" \
58
+ -F "setting=85" \
59
+ -o output.avif
60
+ ```
61
+
62
+ ### 2. API 端点(程序化调用)
63
 
64
  **端点**: `POST /convert/{target_format}/{mode}/{setting}`
65
 
 
94
  -o output.jpg
95
  ```
96
 
97
+ ### 3. 健康检查
98
 
99
  **端点**: `GET /health`
100
 
 
183
 
184
  ## 🐛 已知问题与修复
185
 
186
+ ### V4 版本更新 (当前版本)
187
+
188
+ 1. ✅ **新增 Web 上传界面**: 在根路径 `/` 提供现代化图形化上传界面
189
+ 2. ✅ **新增表单上传 API**: 支持通过 `POST /` 使用表单数据上传
190
+ 3. ✅ **代码架构重构**: 提取核心转换逻辑,实现代码复用
191
+ 4. ✅ **改进用户体验**: 实时参数提示、拖拽上传、响应式设计
192
+ 5. ✅ **完善 API 文档**: 在 Web 界面提供快速访问链接
193
+
194
+ ### V3 版本修复
195
 
196
  1. ✅ **修复 Timeout 实现**: 超时现在正确应用于进程执行而非进程创建
197
  2. ✅ **修复 WebP 无损质量**: 无损模式下 quality 固定为 100
main.py CHANGED
@@ -20,9 +20,10 @@ from fastapi import (
20
  UploadFile,
21
  HTTPException,
22
  BackgroundTasks,
23
- Path
 
24
  )
25
- from fastapi.responses import FileResponse, JSONResponse
26
  import subprocess
27
  import asyncio
28
  import tempfile
@@ -57,9 +58,9 @@ ConversionMode = Literal["lossless", "lossy"]
57
  # --- 3. FastAPI 应用初始化 ---
58
 
59
  app = FastAPI(
60
- title="Magick 动态图像转换器 (V3)",
61
- description="通过动态 API 路径实现多种格式的(无)损图像转换,支持动图。",
62
- version="3.0.0"
63
  )
64
 
65
  # 启动时确保临时目录存在
@@ -102,7 +103,309 @@ def cleanup_temp_dir(temp_dir: str):
102
  except Exception as cleanup_error:
103
  logger.error(f"后台清理:删除 {temp_dir} 失败: {cleanup_error}", exc_info=True)
104
 
105
- # --- 5. API 端点 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  @app.get("/health", summary="服务健康检查")
108
  async def health_check():
@@ -143,35 +446,28 @@ async def health_check():
143
  logger.error(f"健康检查失败: {str(e)}")
144
  return JSONResponse(status_code=500, content={"status": "unhealthy", "error": str(e)})
145
 
146
- @app.post(
147
- "/convert/{target_format}/{mode}/{setting}",
148
- summary="动态转换图像 (支持动图)",
149
- response_class=FileResponse,
150
- responses={
151
- 200: {"description": "转换成功,返回图像文件"},
152
- 400: {"description": "请求无效(例如文件过大)"},
153
- 422: {"description": "路径参数验证失败(例如格式不支持)"},
154
- 500: {"description": "服务器内部转换失败"},
155
- 504: {"description": "转换处理超时"}
156
- }
157
- )
158
- async def convert_image_dynamic(
159
  background_tasks: BackgroundTasks,
160
- target_format: TargetFormat,
161
- mode: ConversionMode,
162
- setting: int = Path(..., ge=0, le=100, description="质量(有损) 或 压缩速度(无损) (0-100)"),
163
- file: UploadFile = File(..., description="要转换的图像文件 (支持动图)")
164
- ):
165
  """
166
- 通过动态 URL 路径接收图像文件,执行转换并返回结果。
 
167
 
168
- - **target_format**: 目标格式 (avif, webp, jpeg, png, gif, heif)
169
- - **mode**: 转换模式 (lossless, lossy)
170
- - **setting**: 模式设置 (0-100)
171
- - mode=lossy: 0=最差质量, 100=最佳质量
172
- - mode=lossless: 0=最慢/最佳压缩, 100=最快/最差压缩
 
 
 
 
173
  """
174
- logger.info(f"收到动态转换请求: {target_format}/{mode}/{setting} (文件: {file.filename})")
175
 
176
  # 预检查: AVIF/HEIF 格式需要 heif-enc 依赖
177
  if target_format in ["avif", "heif"]:
@@ -386,4 +682,94 @@ async def convert_image_dynamic(
386
  await file.close()
387
  # 备用清理:仅当未注册后台任务时立即清理
388
  if not cleanup_scheduled and os.path.exists(temp_dir):
389
- cleanup_temp_dir(temp_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  UploadFile,
21
  HTTPException,
22
  BackgroundTasks,
23
+ Path,
24
+ Form
25
  )
26
+ from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
27
  import subprocess
28
  import asyncio
29
  import tempfile
 
58
  # --- 3. FastAPI 应用初始化 ---
59
 
60
  app = FastAPI(
61
+ title="Magick 动态图像转换器 (V4)",
62
+ description="通过 Web 界面或 API 实现多种格式的(无)损图像转换,支持动图。提供现代化图形上传界面和灵活的 RESTful API。",
63
+ version="4.0.0"
64
  )
65
 
66
  # 启动时确保临时目录存在
 
103
  except Exception as cleanup_error:
104
  logger.error(f"后台清理:删除 {temp_dir} 失败: {cleanup_error}", exc_info=True)
105
 
106
+ # --- 5. HTML 模板 ---
107
+
108
+ HTML_UPLOAD_PAGE = """
109
+ <!DOCTYPE html>
110
+ <html lang="zh-CN">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>Magick 图像转换器</title>
115
+ <style>
116
+ * { box-sizing: border-box; margin: 0; padding: 0; }
117
+ body {
118
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
119
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
+ min-height: 100vh;
121
+ padding: 20px;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ }
126
+ .container {
127
+ background: white;
128
+ border-radius: 20px;
129
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
130
+ max-width: 600px;
131
+ width: 100%;
132
+ padding: 40px;
133
+ }
134
+ h1 {
135
+ color: #333;
136
+ margin-bottom: 10px;
137
+ font-size: 28px;
138
+ text-align: center;
139
+ }
140
+ .subtitle {
141
+ color: #666;
142
+ text-align: center;
143
+ margin-bottom: 30px;
144
+ font-size: 14px;
145
+ }
146
+ .form-group {
147
+ margin-bottom: 25px;
148
+ }
149
+ label {
150
+ display: block;
151
+ color: #333;
152
+ font-weight: 600;
153
+ margin-bottom: 8px;
154
+ font-size: 14px;
155
+ }
156
+ .file-input-wrapper {
157
+ position: relative;
158
+ border: 2px dashed #667eea;
159
+ border-radius: 10px;
160
+ padding: 30px;
161
+ text-align: center;
162
+ background: #f8f9ff;
163
+ cursor: pointer;
164
+ transition: all 0.3s;
165
+ }
166
+ .file-input-wrapper:hover {
167
+ border-color: #764ba2;
168
+ background: #f0f2ff;
169
+ }
170
+ .file-input-wrapper input[type="file"] {
171
+ position: absolute;
172
+ width: 100%;
173
+ height: 100%;
174
+ top: 0;
175
+ left: 0;
176
+ opacity: 0;
177
+ cursor: pointer;
178
+ }
179
+ .file-label {
180
+ color: #667eea;
181
+ font-weight: 600;
182
+ }
183
+ select, input[type="range"] {
184
+ width: 100%;
185
+ padding: 12px;
186
+ border: 2px solid #e0e0e0;
187
+ border-radius: 8px;
188
+ font-size: 14px;
189
+ transition: border-color 0.3s;
190
+ }
191
+ select:focus {
192
+ outline: none;
193
+ border-color: #667eea;
194
+ }
195
+ .radio-group {
196
+ display: flex;
197
+ gap: 20px;
198
+ }
199
+ .radio-label {
200
+ display: flex;
201
+ align-items: center;
202
+ cursor: pointer;
203
+ font-weight: normal;
204
+ }
205
+ .radio-label input[type="radio"] {
206
+ margin-right: 8px;
207
+ cursor: pointer;
208
+ }
209
+ .slider-container {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 15px;
213
+ }
214
+ input[type="range"] {
215
+ flex: 1;
216
+ }
217
+ .slider-value {
218
+ min-width: 45px;
219
+ text-align: center;
220
+ font-weight: 600;
221
+ color: #667eea;
222
+ font-size: 18px;
223
+ }
224
+ .param-hint {
225
+ background: #f0f2ff;
226
+ padding: 12px;
227
+ border-radius: 8px;
228
+ font-size: 13px;
229
+ color: #555;
230
+ margin-top: 10px;
231
+ border-left: 4px solid #667eea;
232
+ }
233
+ .submit-btn {
234
+ width: 100%;
235
+ padding: 15px;
236
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
237
+ color: white;
238
+ border: none;
239
+ border-radius: 10px;
240
+ font-size: 16px;
241
+ font-weight: 600;
242
+ cursor: pointer;
243
+ transition: transform 0.2s, box-shadow 0.2s;
244
+ }
245
+ .submit-btn:hover {
246
+ transform: translateY(-2px);
247
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
248
+ }
249
+ .submit-btn:active {
250
+ transform: translateY(0);
251
+ }
252
+ .links {
253
+ margin-top: 25px;
254
+ text-align: center;
255
+ padding-top: 25px;
256
+ border-top: 1px solid #e0e0e0;
257
+ }
258
+ .links a {
259
+ color: #667eea;
260
+ text-decoration: none;
261
+ margin: 0 15px;
262
+ font-size: 14px;
263
+ font-weight: 500;
264
+ }
265
+ .links a:hover {
266
+ text-decoration: underline;
267
+ }
268
+ .selected-file {
269
+ margin-top: 10px;
270
+ color: #28a745;
271
+ font-size: 13px;
272
+ font-weight: 500;
273
+ }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <div class="container">
278
+ <h1>🧙‍♂️ Magick 图像转换器</h1>
279
+ <p class="subtitle">支持多格式转换 | 有损/无损模式 | 支持动画图像</p>
280
+
281
+ <form id="uploadForm" action="/" method="POST" enctype="multipart/form-data">
282
+ <div class="form-group">
283
+ <label>选择图像文件</label>
284
+ <div class="file-input-wrapper">
285
+ <input type="file" name="file" id="fileInput" accept="image/*" required>
286
+ <div class="file-label">
287
+ 📁 点击选择或拖拽文件到此处
288
+ <div style="font-size: 12px; color: #999; margin-top: 8px;">
289
+ 支持 JPG, PNG, GIF, WebP, AVIF, HEIF 等格式
290
+ </div>
291
+ </div>
292
+ </div>
293
+ <div id="selectedFile" class="selected-file"></div>
294
+ </div>
295
+
296
+ <div class="form-group">
297
+ <label for="target_format">目标格式</label>
298
+ <select name="target_format" id="target_format" required>
299
+ <option value="webp" selected>WebP - 现代高效格式</option>
300
+ <option value="avif">AVIF - 最新一代格式</option>
301
+ <option value="jpeg">JPEG - 经典有损格式</option>
302
+ <option value="png">PNG - 无损格式</option>
303
+ <option value="gif">GIF - 动画格式</option>
304
+ <option value="heif">HEIF - 高效图像格式</option>
305
+ </select>
306
+ </div>
307
+
308
+ <div class="form-group">
309
+ <label>转换模式</label>
310
+ <div class="radio-group">
311
+ <label class="radio-label">
312
+ <input type="radio" name="mode" value="lossy" checked>
313
+ 有损压缩 (更小体积)
314
+ </label>
315
+ <label class="radio-label">
316
+ <input type="radio" name="mode" value="lossless">
317
+ 无损压缩 (保持质量)
318
+ </label>
319
+ </div>
320
+ </div>
321
+
322
+ <div class="form-group">
323
+ <label for="setting">质量参数</label>
324
+ <div class="slider-container">
325
+ <input type="range" name="setting" id="setting" min="0" max="100" value="80">
326
+ <span class="slider-value" id="settingValue">80</span>
327
+ </div>
328
+ <div class="param-hint" id="paramHint">
329
+ 质量: 80 - 高质量 (0=最低质量,100=最高质量)
330
+ </div>
331
+ </div>
332
+
333
+ <button type="submit" class="submit-btn">🚀 开始转换</button>
334
+ </form>
335
+
336
+ <div class="links">
337
+ <a href="/docs" target="_blank">📖 API 文档</a>
338
+ <a href="/health" target="_blank">🏥 健康检查</a>
339
+ </div>
340
+ </div>
341
+
342
+ <script>
343
+ // 文件选择提示
344
+ const fileInput = document.getElementById('fileInput');
345
+ const selectedFile = document.getElementById('selectedFile');
346
+
347
+ fileInput.addEventListener('change', function() {
348
+ if (this.files.length > 0) {
349
+ selectedFile.textContent = '✓ 已选择: ' + this.files[0].name;
350
+ }
351
+ });
352
+
353
+ // 滑块实时更新
354
+ const slider = document.getElementById('setting');
355
+ const sliderValue = document.getElementById('settingValue');
356
+ const paramHint = document.getElementById('paramHint');
357
+ const modeRadios = document.querySelectorAll('input[name="mode"]');
358
+
359
+ function updateHint() {
360
+ const mode = document.querySelector('input[name="mode"]:checked').value;
361
+ const value = slider.value;
362
+ sliderValue.textContent = value;
363
+
364
+ if (mode === 'lossy') {
365
+ let quality = '中等';
366
+ if (value >= 90) quality = '极高';
367
+ else if (value >= 80) quality = '高';
368
+ else if (value >= 60) quality = '中等';
369
+ else if (value >= 40) quality = '中低';
370
+ else quality = '低';
371
+ paramHint.textContent = `质量: ${value} - ${quality}质量 (0=最低质量,100=最高质量)`;
372
+ } else {
373
+ let speed = '平衡';
374
+ if (value <= 20) speed = '最慢/最佳压缩';
375
+ else if (value <= 40) speed = '较慢/较好压缩';
376
+ else if (value <= 60) speed = '平衡';
377
+ else if (value <= 80) speed = '较快/较差压缩';
378
+ else speed = '最快/最差压缩';
379
+ paramHint.textContent = `压缩速度: ${value} - ${speed} (0=最慢/最佳,100=最快/最差)`;
380
+ }
381
+ }
382
+
383
+ slider.addEventListener('input', updateHint);
384
+ modeRadios.forEach(radio => radio.addEventListener('change', updateHint));
385
+
386
+ // 表单提交处理
387
+ const form = document.getElementById('uploadForm');
388
+ const submitBtn = form.querySelector('.submit-btn');
389
+ const originalBtnText = submitBtn.textContent;
390
+
391
+ form.addEventListener('submit', function() {
392
+ submitBtn.textContent = '⏳ 转换中...';
393
+ submitBtn.disabled = true;
394
+ });
395
+ </script>
396
+ </body>
397
+ </html>
398
+ """
399
+
400
+ # --- 6. API 端点 ---
401
+
402
+ @app.get("/", response_class=HTMLResponse, summary="上传界面")
403
+ async def root():
404
+ """
405
+ 返回用户友好的HTML上传表单页面。
406
+ 提供图形化界面进行图像转换,无需编程知识。
407
+ """
408
+ return HTML_UPLOAD_PAGE
409
 
410
  @app.get("/health", summary="服务健康检查")
411
  async def health_check():
 
446
  logger.error(f"健康检查失败: {str(e)}")
447
  return JSONResponse(status_code=500, content={"status": "unhealthy", "error": str(e)})
448
 
449
+ async def _perform_conversion(
 
 
 
 
 
 
 
 
 
 
 
 
450
  background_tasks: BackgroundTasks,
451
+ file: UploadFile,
452
+ target_format: str,
453
+ mode: str,
454
+ setting: int
455
+ ) -> FileResponse:
456
  """
457
+ 核心图像转换逻辑(内部函数)。
458
+ 被多个端点复用以避免代码重复。
459
 
460
+ Args:
461
+ background_tasks: FastAPI后台任务对象
462
+ file: 上传的图像文件
463
+ target_format: 目标格式 (avif, webp, jpeg, png, gif, heif)
464
+ mode: 转换模式 (lossy, lossless)
465
+ setting: 质量/压缩参数 (0-100)
466
+
467
+ Returns:
468
+ FileResponse: 转换后的图像文件
469
  """
470
+ logger.info(f"开始转换: {target_format}/{mode}/{setting} (文件: {file.filename})")
471
 
472
  # 预检查: AVIF/HEIF 格式需要 heif-enc 依赖
473
  if target_format in ["avif", "heif"]:
 
682
  await file.close()
683
  # 备用清理:仅当未注册后台任务时立即清理
684
  if not cleanup_scheduled and os.path.exists(temp_dir):
685
+ cleanup_temp_dir(temp_dir)
686
+
687
+ @app.post("/", response_class=FileResponse, summary="简化上传转换")
688
+ async def upload_convert(
689
+ background_tasks: BackgroundTasks,
690
+ file: UploadFile = File(..., description="要转换的图像文件"),
691
+ target_format: str = Form("webp", description="目标格式"),
692
+ mode: str = Form("lossy", description="转换模式"),
693
+ setting: int = Form(80, ge=0, le=100, description="质量参数")
694
+ ):
695
+ """
696
+ 通过HTML表单上传并转换图像。
697
+
698
+ 这个端点接收表单数据(而非URL路径参数),适合从网页表单调用。
699
+ 内部调用与 /convert/{format}/{mode}/{setting} 相同的转换逻辑。
700
+
701
+ - **file**: 图像文件
702
+ - **target_format**: 目标格式 (avif, webp, jpeg, png, gif, heif),默认 webp
703
+ - **mode**: 转换模式 (lossy, lossless),默认 lossy
704
+ - **setting**: 质量/压缩参数 (0-100),默认 80
705
+ """
706
+ # 验证参数
707
+ valid_formats = ["avif", "webp", "jpeg", "png", "gif", "heif"]
708
+ if target_format not in valid_formats:
709
+ raise HTTPException(
710
+ status_code=422,
711
+ detail=f"Invalid target_format: {target_format}. Must be one of {valid_formats}"
712
+ )
713
+
714
+ valid_modes = ["lossy", "lossless"]
715
+ if mode not in valid_modes:
716
+ raise HTTPException(
717
+ status_code=422,
718
+ detail=f"Invalid mode: {mode}. Must be one of {valid_modes}"
719
+ )
720
+
721
+ if not (0 <= setting <= 100):
722
+ raise HTTPException(
723
+ status_code=422,
724
+ detail=f"Invalid setting: {setting}. Must be between 0 and 100"
725
+ )
726
+
727
+ logger.info(f"收到表单上传请求: {target_format}/{mode}/{setting} (文件: {file.filename})")
728
+
729
+ # 调用核心转换逻辑
730
+ return await _perform_conversion(
731
+ background_tasks=background_tasks,
732
+ file=file,
733
+ target_format=target_format,
734
+ mode=mode,
735
+ setting=setting
736
+ )
737
+
738
+ @app.post(
739
+ "/convert/{target_format}/{mode}/{setting}",
740
+ summary="动态转换图像 (支持动图)",
741
+ response_class=FileResponse,
742
+ responses={
743
+ 200: {"description": "转换成功,返回图像文件"},
744
+ 400: {"description": "请求无效(例如文件过大)"},
745
+ 422: {"description": "路径参数验证失败(例如格式不支持)"},
746
+ 500: {"description": "服务器内部转换失败"},
747
+ 504: {"description": "转换处理超时"}
748
+ }
749
+ )
750
+ async def convert_image_dynamic(
751
+ background_tasks: BackgroundTasks,
752
+ target_format: TargetFormat,
753
+ mode: ConversionMode,
754
+ setting: int = Path(..., ge=0, le=100, description="质量(有损) 或 压缩速度(无损) (0-100)"),
755
+ file: UploadFile = File(..., description="要转换的图像文件 (支持动图)")
756
+ ):
757
+ """
758
+ 通过动态 URL 路径接收图像文件,执行转换并返回结果。
759
+
760
+ - **target_format**: 目标格式 (avif, webp, jpeg, png, gif, heif)
761
+ - **mode**: 转换模式 (lossless, lossy)
762
+ - **setting**: 模式设置 (0-100)
763
+ - mode=lossy: 0=最差质量, 100=最佳质量
764
+ - mode=lossless: 0=最慢/最佳压缩, 100=最快/最差压缩
765
+ """
766
+ logger.info(f"收到API转换请求: {target_format}/{mode}/{setting} (文件: {file.filename})")
767
+
768
+ # 调用核心转换逻辑
769
+ return await _perform_conversion(
770
+ background_tasks=background_tasks,
771
+ file=file,
772
+ target_format=target_format,
773
+ mode=mode,
774
+ setting=setting
775
+ )