Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
7d20ae4
1
Parent(s): 9cc3a59
Fix: wrap Vue template with Jinja raw; add Logo overlay; adjust gunicorn workers
Browse files- Dockerfile +1 -1
- app.py +13 -0
- templates/index.html +52 -1
Dockerfile
CHANGED
|
@@ -21,4 +21,4 @@ ENV HOME=/home/user \
|
|
| 21 |
|
| 22 |
EXPOSE 7860
|
| 23 |
|
| 24 |
-
CMD ["gunicorn", "-b", "0.0.0.0:7860", "--
|
|
|
|
| 21 |
|
| 22 |
EXPOSE 7860
|
| 23 |
|
| 24 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "--workers", "2", "--threads", "4", "--timeout", "120", "app:app"]
|
app.py
CHANGED
|
@@ -71,6 +71,7 @@ def generate_certificates():
|
|
| 71 |
bg_file = request.files["background"]
|
| 72 |
font_file = request.files.get("font")
|
| 73 |
data_file = request.files.get("data_file")
|
|
|
|
| 74 |
|
| 75 |
config_str = request.form.get("config", "[]")
|
| 76 |
data_str = request.form.get("data", "[]")
|
|
@@ -78,6 +79,9 @@ def generate_certificates():
|
|
| 78 |
export_scale = float(request.form.get("export_scale", "1"))
|
| 79 |
if export_scale <= 0:
|
| 80 |
export_scale = 1
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
try:
|
| 83 |
config = json.loads(config_str)
|
|
@@ -140,6 +144,15 @@ def generate_certificates():
|
|
| 140 |
draw.text((x, y), text_wrapped, fill=color, font=font, anchor=anchor,
|
| 141 |
stroke_width=stroke_width if stroke_color else 0, stroke_fill=stroke_color or None)
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
out = io.BytesIO()
|
| 144 |
img.save(out, format="PNG")
|
| 145 |
|
|
|
|
| 71 |
bg_file = request.files["background"]
|
| 72 |
font_file = request.files.get("font")
|
| 73 |
data_file = request.files.get("data_file")
|
| 74 |
+
logo_file = request.files.get("logo")
|
| 75 |
|
| 76 |
config_str = request.form.get("config", "[]")
|
| 77 |
data_str = request.form.get("data", "[]")
|
|
|
|
| 79 |
export_scale = float(request.form.get("export_scale", "1"))
|
| 80 |
if export_scale <= 0:
|
| 81 |
export_scale = 1
|
| 82 |
+
logo_x = int(float(request.form.get("logo_x", "0")))
|
| 83 |
+
logo_y = int(float(request.form.get("logo_y", "0")))
|
| 84 |
+
logo_scale = float(request.form.get("logo_scale", "0"))
|
| 85 |
|
| 86 |
try:
|
| 87 |
config = json.loads(config_str)
|
|
|
|
| 144 |
draw.text((x, y), text_wrapped, fill=color, font=font, anchor=anchor,
|
| 145 |
stroke_width=stroke_width if stroke_color else 0, stroke_fill=stroke_color or None)
|
| 146 |
|
| 147 |
+
if logo_file and logo_scale > 0:
|
| 148 |
+
logo_file.stream.seek(0)
|
| 149 |
+
logo = Image.open(logo_file).convert("RGBA")
|
| 150 |
+
lw, lh = logo.size
|
| 151 |
+
target_w = max(1, int(lw * logo_scale))
|
| 152 |
+
target_h = max(1, int(lh * logo_scale))
|
| 153 |
+
logo_resized = logo.resize((target_w, target_h), Image.LANCZOS)
|
| 154 |
+
img.paste(logo_resized, (int(logo_x * export_scale), int(logo_y * export_scale)), logo_resized)
|
| 155 |
+
|
| 156 |
out = io.BytesIO()
|
| 157 |
img.save(out, format="PNG")
|
| 158 |
|
templates/index.html
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="zh-CN">
|
| 3 |
<head>
|
|
@@ -120,6 +121,37 @@
|
|
| 120 |
</div>
|
| 121 |
</div>
|
| 122 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</div>
|
| 124 |
|
| 125 |
<!-- 2. Data Input -->
|
|
@@ -292,6 +324,11 @@
|
|
| 292 |
const fontFile = ref(null);
|
| 293 |
const customFontFamily = ref('Inter'); // Default
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
const fields = ref([]);
|
| 296 |
const activeFieldId = ref(null);
|
| 297 |
|
|
@@ -376,6 +413,12 @@
|
|
| 376 |
}
|
| 377 |
};
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
const addField = () => {
|
| 380 |
if (!bgImage.value) {
|
| 381 |
alert("请先上传背景图片");
|
|
@@ -529,6 +572,12 @@
|
|
| 529 |
if (csvFile.value) {
|
| 530 |
formData.append('data_file', csvFile.value);
|
| 531 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
try {
|
| 534 |
const res = await fetch('/api/generate', {
|
|
@@ -572,10 +621,12 @@
|
|
| 572 |
getPreviewText, getFieldStyle,
|
| 573 |
startDrag,
|
| 574 |
zoomIn, zoomOut,
|
| 575 |
-
generateCertificates, loadDemoData, handleCsvUpload
|
|
|
|
| 576 |
};
|
| 577 |
}
|
| 578 |
}).mount('#app');
|
| 579 |
</script>
|
| 580 |
</body>
|
| 581 |
</html>
|
|
|
|
|
|
| 1 |
+
{% raw %}
|
| 2 |
<!DOCTYPE html>
|
| 3 |
<html lang="zh-CN">
|
| 4 |
<head>
|
|
|
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
+
|
| 125 |
+
<!-- Logo Upload -->
|
| 126 |
+
<div class="mt-4">
|
| 127 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">徽章/Logo (可选)</label>
|
| 128 |
+
<div class="relative border border-gray-300 rounded-lg p-3 flex items-center gap-3 hover:bg-gray-50 cursor-pointer" @click="$refs.logoInput.click()">
|
| 129 |
+
<input type="file" ref="logoInput" class="hidden" accept="image/*" @change="handleLogoUpload">
|
| 130 |
+
<div class="bg-gray-100 p-2 rounded text-gray-500">
|
| 131 |
+
<i class="fas fa-stamp"></i>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="flex-1 min-w-0">
|
| 134 |
+
<div class="text-sm font-medium text-gray-900 truncate">
|
| 135 |
+
{{ logoFile ? logoFile.name : '未选择' }}
|
| 136 |
+
</div>
|
| 137 |
+
<div class="text-xs text-gray-500">支持 PNG,带透明背景最佳</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
<div v-if="logoFile" class="grid grid-cols-3 gap-2 mt-2">
|
| 141 |
+
<div>
|
| 142 |
+
<label class="block text-xs text-gray-500 mb-1">Logo X</label>
|
| 143 |
+
<input type="number" v-model.number="logoX" class="w-full p-2 border border-gray-300 rounded text-sm">
|
| 144 |
+
</div>
|
| 145 |
+
<div>
|
| 146 |
+
<label class="block text-xs text-gray-500 mb-1">Logo Y</label>
|
| 147 |
+
<input type="number" v-model.number="logoY" class="w-full p-2 border border-gray-300 rounded text-sm">
|
| 148 |
+
</div>
|
| 149 |
+
<div>
|
| 150 |
+
<label class="block text-xs text-gray-500 mb-1">Logo 缩放</label>
|
| 151 |
+
<input type="number" v-model.number="logoScale" step="0.1" min="0" class="w-full p-2 border border-gray-300 rounded text-sm">
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
<!-- 2. Data Input -->
|
|
|
|
| 324 |
const fontFile = ref(null);
|
| 325 |
const customFontFamily = ref('Inter'); // Default
|
| 326 |
|
| 327 |
+
const logoFile = ref(null);
|
| 328 |
+
const logoX = ref(0);
|
| 329 |
+
const logoY = ref(0);
|
| 330 |
+
const logoScale = ref(0);
|
| 331 |
+
|
| 332 |
const fields = ref([]);
|
| 333 |
const activeFieldId = ref(null);
|
| 334 |
|
|
|
|
| 413 |
}
|
| 414 |
};
|
| 415 |
|
| 416 |
+
const handleLogoUpload = async (event) => {
|
| 417 |
+
const file = event.target.files[0];
|
| 418 |
+
if (!file) return;
|
| 419 |
+
logoFile.value = file;
|
| 420 |
+
};
|
| 421 |
+
|
| 422 |
const addField = () => {
|
| 423 |
if (!bgImage.value) {
|
| 424 |
alert("请先上传背景图片");
|
|
|
|
| 572 |
if (csvFile.value) {
|
| 573 |
formData.append('data_file', csvFile.value);
|
| 574 |
}
|
| 575 |
+
if (logoFile.value) {
|
| 576 |
+
formData.append('logo', logoFile.value);
|
| 577 |
+
formData.append('logo_x', String(logoX.value || 0));
|
| 578 |
+
formData.append('logo_y', String(logoY.value || 0));
|
| 579 |
+
formData.append('logo_scale', String(logoScale.value || 0));
|
| 580 |
+
}
|
| 581 |
|
| 582 |
try {
|
| 583 |
const res = await fetch('/api/generate', {
|
|
|
|
| 621 |
getPreviewText, getFieldStyle,
|
| 622 |
startDrag,
|
| 623 |
zoomIn, zoomOut,
|
| 624 |
+
generateCertificates, loadDemoData, handleCsvUpload,
|
| 625 |
+
logoFile, logoX, logoY, logoScale, handleLogoUpload
|
| 626 |
};
|
| 627 |
}
|
| 628 |
}).mount('#app');
|
| 629 |
</script>
|
| 630 |
</body>
|
| 631 |
</html>
|
| 632 |
+
{% endraw %}
|