Trae Assistant
修改部署方式
1310213
import os
import json
import io
import zipfile
import csv
import tempfile
import gradio as gr
from PIL import Image, ImageDraw, ImageFont
DEFAULT_FONTS = [
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/System/Library/Fonts/PingFang.ttc",
"simhei.ttf",
"arial.ttf"
]
def load_font(font_file, size):
if font_file:
return ImageFont.truetype(font_file, size)
for font_path in DEFAULT_FONTS:
try:
return ImageFont.truetype(font_path, size)
except OSError:
continue
return ImageFont.load_default()
def sanitize_filename(name):
safe = "".join([c for c in str(name) if c.isalnum() or c in (" ", "-", "_")]).strip()
return safe or "certificate"
def parse_rows_from_inputs(data_str, csv_bytes):
if csv_bytes:
content = csv_bytes.decode("utf-8", errors="ignore")
reader = csv.DictReader(io.StringIO(content))
return [row for row in reader]
if data_str:
try:
data = json.loads(data_str or "[]")
return data if isinstance(data, list) else []
except json.JSONDecodeError:
return []
return []
def wrap_text(text, font, max_width, draw):
if not max_width or max_width <= 0:
return text
lines = []
current = ""
for ch in text:
trial = current + ch
w = draw.textlength(trial, font=font)
if w <= max_width or not current:
current = trial
else:
lines.append(current)
current = ch
if current:
lines.append(current)
return "\n".join(lines)
def generate_certificates_gradio(
bg_image,
font_bytes,
data_csv_bytes,
data_json_str,
config_str,
filename_tpl,
export_scale,
logo_image,
logo_x,
logo_y,
logo_scale,
):
if bg_image is None:
raise gr.Error("请先上传背景图片")
try:
config = json.loads(config_str or "[]")
except json.JSONDecodeError:
raise gr.Error("字段配置 JSON 无效,请检查格式")
data_list = parse_rows_from_inputs(data_json_str, data_csv_bytes)
if not data_list:
raise gr.Error("没有有效的数据行,请检查 CSV 或 JSON 输入")
try:
export_scale_val = float(export_scale or 1)
except ValueError:
export_scale_val = 1
if export_scale_val <= 0:
export_scale_val = 1
try:
logo_x_val = int(logo_x or 0)
logo_y_val = int(logo_y or 0)
logo_scale_val = float(logo_scale or 0)
except ValueError:
logo_x_val = 0
logo_y_val = 0
logo_scale_val = 0
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
base_image = bg_image.convert("RGB")
if export_scale_val != 1:
w, h = base_image.size
base_image = base_image.resize(
(int(w * export_scale_val), int(h * export_scale_val)),
Image.LANCZOS,
)
font_stream = None
if font_bytes:
font_stream = io.BytesIO(font_bytes)
for idx, row in enumerate(data_list):
img = base_image.copy()
draw = ImageDraw.Draw(img)
for field in config:
text_tpl = field.get("template", "")
x = int(field.get("x", 0)) * export_scale_val
y = int(field.get("y", 0)) * export_scale_val
size = int(field.get("size", 30)) * export_scale_val
color = field.get("color", "#000000")
align = field.get("align", "left")
stroke_width = int(field.get("stroke_width", 0))
stroke_color = field.get("stroke_color", None)
shadow_dx = int(field.get("shadow_dx", 0))
shadow_dy = int(field.get("shadow_dy", 0))
shadow_color = field.get("shadow_color", None)
max_width = int(field.get("max_width", 0)) * export_scale_val
try:
text = text_tpl.format(**row)
except Exception:
text = text_tpl
if font_stream:
font_stream.seek(0)
font = ImageFont.truetype(font_stream, int(size))
else:
font = load_font(None, int(size))
anchor = "la"
if align == "center":
anchor = "ma"
elif align == "right":
anchor = "ra"
text_wrapped = wrap_text(text, font, max_width, draw)
if shadow_color and (shadow_dx or shadow_dy):
draw.text(
(x + shadow_dx, y + shadow_dy),
text_wrapped,
fill=shadow_color,
font=font,
anchor=anchor,
stroke_width=stroke_width if stroke_color else 0,
stroke_fill=stroke_color or None,
)
draw.text(
(x, y),
text_wrapped,
fill=color,
font=font,
anchor=anchor,
stroke_width=stroke_width if stroke_color else 0,
stroke_fill=stroke_color or None,
)
if logo_image is not None and logo_scale_val > 0:
logo = logo_image.convert("RGBA")
lw, lh = logo.size
target_w = max(1, int(lw * logo_scale_val))
target_h = max(1, int(lh * logo_scale_val))
logo_resized = logo.resize((target_w, target_h), Image.LANCZOS)
img.paste(
logo_resized,
(int(logo_x_val * export_scale_val), int(logo_y_val * export_scale_val)),
logo_resized,
)
out = io.BytesIO()
img.save(out, format="PNG")
try:
name = (filename_tpl or "{index}.png").format(index=idx + 1, **row)
except Exception:
name = f"certificate_{idx + 1}.png"
name = sanitize_filename(os.path.splitext(name)[0]) + ".png"
zip_file.writestr(name, out.getvalue())
zip_buffer.seek(0)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
tmp.write(zip_buffer.getvalue())
tmp.flush()
tmp.close()
return tmp.name
default_config = json.dumps(
[
{
"template": "{name}",
"x": 800,
"y": 600,
"size": 60,
"color": "#000000",
"align": "center",
"stroke_width": 0,
"stroke_color": "#000000",
"shadow_dx": 0,
"shadow_dy": 0,
"shadow_color": "#000000",
"max_width": 0,
}
],
ensure_ascii=False,
indent=2,
)
with gr.Blocks(title="证书批量生成大师 | Certificate Master") as demo:
gr.Markdown("# 🎓 证书批量生成大师\n批量生成带个性化信息的证书图片,并打包为 ZIP 下载。")
with gr.Row():
with gr.Column():
bg_image = gr.Image(label="背景图片(必选)", type="pil")
font_file = gr.File(label="字体文件(可选,ttf/otf)", type="bytes")
with gr.Row():
data_csv = gr.File(label="学员数据 CSV(可选)", type="bytes")
data_json = gr.Textbox(
label="或 JSON 数据(可选)",
lines=4,
placeholder='[{"name": "张三", "course": "Python 进阶", "date": "2024-01-01"}]',
)
config_box = gr.Textbox(
label="字段配置 JSON(高级)",
value=default_config,
lines=10,
)
filename_tpl = gr.Textbox(
label="文件名模板",
value="{name}.png",
placeholder="{name}.png 或 certificate_{index}.png",
)
export_scale = gr.Number(
label="导出缩放倍数",
value=1,
)
with gr.Row():
logo_image = gr.Image(label="徽章 / Logo(可选)", type="pil")
with gr.Row():
logo_x = gr.Number(label="Logo X 坐标", value=0)
logo_y = gr.Number(label="Logo Y 坐标", value=0)
logo_scale = gr.Number(label="Logo 缩放倍数", value=0.0)
generate_button = gr.Button("生成并下载 ZIP", variant="primary")
with gr.Column():
result_zip = gr.File(label="生成结果 ZIP")
gr.Markdown(
"提示:\n"
"- CSV 第一行应为表头,如 name,course,date\n"
"- 字段配置 JSON 中的 template 支持使用 {name}、{course} 等占位符\n"
"- 坐标和字号单位与背景图像像素一致"
)
generate_button.click(
fn=generate_certificates_gradio,
inputs=[
bg_image,
font_file,
data_csv,
data_json,
config_box,
filename_tpl,
export_scale,
logo_image,
logo_x,
logo_y,
logo_scale,
],
outputs=result_zip,
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)