前情提要

【纯小白,勿喷】刚刚发错区了,重发
由于工作需要,在word中插入大量的手写签名,开始我在网上找在线的网站一个一个转换,但是这样非常麻烦,后来想干脆自己设计一个,现在ai这么强大,你只管提需求,剩下的交ai。
于是我基于自己的需求,通过ai写了一个程序,基于python开发的,我自己一句代码都没写,也不会写,完全是一个小白。有用的上的源码拿走直接用,不保证兼容系统,我自己的windows11正常运行。(当然,手写字体自行下载放入对应的文件夹内)

 

目录结构如下

/SignatureGenerator
│── /fonts          # 存放手写体.ttf文件(用户自备)
│── /output         # 生成的签名图片
│── app.py          # 主程序(PyQt5界面+核心逻辑)
│── config.py       # 配置文件(调整DPI/旋转角度等)
│── requirements.txt # 依赖库清单

源码

app.py

 复制代码 隐藏代码
import os
import random
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import numpy as np

class SignatureGenerator(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("智能签名生成器 v2.1")
        self.setGeometry(300, 300, 1200, 800)
        self.setStyleSheet("""
            QMainWindow { background: #f8f9fa; font-family: 'Microsoft YaHei'; }
            QPushButton { 
                background: #4e73df; color: white; border-radius: 6px;
                padding: 10px 20px; font-size: 14px; min-width: 120px;
            }
            QTextEdit, QComboBox {
                border: 1px solid #d1d9e6; border-radius: 6px; padding: 12px;
                font-size: 14px; background: white;
            }
        """)
        self.init_ui()
        self.load_fonts()
        self.generated_images = []

    def init_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QHBoxLayout(central_widget)

        # 左侧控制面板
        control_panel = QVBoxLayout()
        control_panel.setSpacing(20)

        input_group = QGroupBox("批量输入设置")
        input_layout = QVBoxLayout()
        self.input_field = QTextEdit()
        self.input_field.setPlaceholderText("请输入姓名,用逗号分隔(例:张三,李四·王五)")
        input_layout.addWidget(QLabel("待生成姓名列表:"))
        input_layout.addWidget(self.input_field)
        input_group.setLayout(input_layout)

        self.generate_btn = QPushButton("🎨 随机生成签名")
        self.generate_btn.clicked.connect(self.generate_signatures)

        self.download_btn = QPushButton("💾 下载全部签名")
        self.download_btn.setEnabled(False)
        self.download_btn.clicked.connect(self.download_signatures)

        # 右侧预览区域
        self.preview_area = QScrollArea()
        self.preview_container = QWidget()
        self.preview_layout = QVBoxLayout(self.preview_container)
        self.preview_area.setWidget(self.preview_container)
        self.preview_area.setWidgetResizable(True)

        # 组装布局
        control_panel.addWidget(input_group)
        control_panel.addWidget(self.generate_btn)
        control_panel.addWidget(self.download_btn)
        control_panel.addStretch()
        layout.addLayout(control_panel, 1)
        layout.addWidget(self.preview_area, 2)

    def load_fonts(self):
        """加载字体文件"""
        self.font_files = []
        fonts_dir = os.path.join(os.path.dirname(__file__), "fonts")
        if os.path.exists(fonts_dir):
            for f in os.listdir(fonts_dir):
                if f.lower().endswith(('.ttf', '.otf')):
                    self.font_files.append(os.path.join(fonts_dir, f))
        if not self.font_files:
            QMessageBox.warning(self, "警告", "未检测到字体文件,请将.ttf文件放入/fonts目录")

    def generate_signatures(self):
        """核心生成逻辑"""
        names_text = self.input_field.toPlainText().strip()
        if not names_text:
            QMessageBox.warning(self, "提示", "请输入至少一个姓名")
            return

        names = [n.strip() for n in names_text.replace(",", ",").split(",") if n.strip()]
        if not names:
            QMessageBox.warning(self, "提示", "请输入有效的姓名(用逗号分隔)")
            return

        # 清空预览区
        self.clear_preview()

        self.generated_images = []
        used_fonts = set()

        for name in names:
            if len(name) < 2 or len(name) > 4:
                QMessageBox.warning(self, "格式错误", f"跳过不支持的姓名长度: {name} (需2-4个字符)")
                continue

            # 选择未使用的字体
            font_path = self.select_unique_font(used_fonts)
            if not font_path:
                QMessageBox.critical(self, "错误", "字体资源不足,请添加更多字体文件")
                break

            # 生成签名图片
            img_path = self.create_signature_image(name, font_path)
            if img_path:
                self.generated_images.append((name, img_path))
                self.add_preview_item(name, img_path)

        self.download_btn.setEnabled(bool(self.generated_images))

    def create_signature_image(self, text, font_path, img_size=(800, 300)):
        """生成高清签名图片(修复重叠问题)"""
        try:
            # 创建透明画布(扩大尺寸防裁剪)
            img = Image.new('RGBA', img_size, (255, 255, 255, 0))
            draw = ImageDraw.Draw(img)

            # 动态计算字体大小
            font_size = self.calculate_font_size(len(text))
            font = ImageFont.truetype(font_path, font_size)

            # 计算文本位置(居中)
            text_width = font.getlength(text)
            text_height = font.size
            x = (img_size[0] - text_width) / 2
            y = (img_size[1] - text_height) / 2

            # 添加自然手写效果(单次绘制+后期处理)
            draw.text((x, y), text, font=font, fill=(0, 0, 0, 220))

            # 模拟手写抖动(高斯模糊替代多次绘制)
            img = img.filter(ImageFilter.GaussianBlur(radius=0.8))

            # 随机旋转(中心旋转防裁剪)
            angle = random.uniform(-10, 10)
            rotated_img = img.rotate(
                angle, 
                center=(img_size[0]//2, img_size[1]//2),
                expand=True,
                resample=Image.BICUBIC
            )

            # 裁剪透明边缘
            bbox = rotated_img.getbbox()
            cropped_img = rotated_img.crop(bbox) if bbox else rotated_img

            # 保存高清PNG
            os.makedirs("output", exist_ok=True)
            output_path = os.path.join("output", f"sign_{text}_{random.randint(1000,9999)}.png")
            cropped_img.save(output_path, dpi=(300, 300), quality=100)
            return output_path

        except Exception as e:
            print(f"生成失败: {str(e)}")
            return None

    def select_unique_font(self, used_fonts):
        """选择未使用的字体"""
        available_fonts = [f for f in self.font_files if f not in used_fonts]
        if not available_fonts:
            available_fonts = self.font_files  # 字体不足时复用
        return random.choice(available_fonts) if available_fonts else None

    def calculate_font_size(self, name_length):
        """根据姓名长度动态计算字体大小"""
        base_size = 80
        size_step = -10
        return max(50, base_size + (name_length - 2) * size_step)

    def clear_preview(self):
        """清空预览区域"""
        for i in reversed(range(self.preview_layout.count())): 
            self.preview_layout.itemAt(i).widget().deleteLater()

    def add_preview_item(self, name, img_path):
        """添加预览项"""
        pixmap = QPixmap(img_path)
        label = QLabel()
        label.setPixmap(pixmap.scaled(300, 100, Qt.KeepAspectRatio))

        name_label = QLabel(f"签名:{name}")
        name_label.setStyleSheet("font-weight: bold; margin-top: 10px;")

        self.preview_layout.addWidget(name_label)
        self.preview_layout.addWidget(label)

    def download_signatures(self):
        """下载所有生成的签名图片"""
        if not hasattr(self, 'generated_images') or not self.generated_images:
            QMessageBox.warning(self, "提示", "没有可下载的签名")
            return

        download_dir = QFileDialog.getExistingDirectory(
            self, 
            "选择保存目录",
            options=QFileDialog.ShowDirsOnly
        )
        if not download_dir:
            return

        success_count = 0
        for name, img_path in self.generated_images:
            try:
                target_path = os.path.join(download_dir, f"{name}_签名.png")
                with open(img_path, 'rb') as src, open(target_path, 'wb') as dst:
                    dst.write(src.read())
                success_count += 1
            except Exception as e:
                print(f"保存 {name} 的签名失败: {e}")

        QMessageBox.information(
            self, 
            "完成", 
            f"成功保存 {success_count}/{len(self.generated_images)} 个签名到:\n{download_dir}"
        )

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SignatureGenerator()
    window.show()
    sys.exit(app.exec_())

config.py

 复制代码 隐藏代码
# 图像生成设置
IMAGE_CONFIG = {
    "default_size": (800, 300),  # 默认画布尺寸
    "dpi": 300,                  # 输出分辨率
    "blur_radius": 0.8,          # 手写效果模糊度
    "max_rotate_angle": 10,      # 最大旋转角度
    "opacity": 220,              # 文字透明度(0-255)
    "min_font_size": 50,         # 最小字体大小
    "base_font_size": 80         # 基准字体大小
}

# 字体设置
FONT_CONFIG = {
    "supported_length": [2, 3, 4]  # 支持的姓名长度
}

requirements.txt

 复制代码 隐藏代码
PyQt5>=5.15.7  # 测试支持 Python 3.13 的版本
numpy>=2.0.0    # 明确使用 NumPy 2.x
requests==2.31.0

很多人要成品,我不知道发成品是否属违规,既然有需求,我发出来。
下载:https://alinwei.lanzouu.com/iXVjE30q7w6f 密码:8zu1

2025.7.14更新优化
前端界面增加字体大小滑块,增加字符间距滑块、增加噪点程度滑块、增加凌乱度滑块。

 

核心源码文件

app.py

 复制代码 隐藏代码
import os
import random
import sys
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt, QSize, QPoint, QRect
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import numpy as np
import math
import io

class SignatureGenerator(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("智能随机签名生成器优化增强版 v2.4")
        self.setGeometry(300, 200, 1200, 800)
        app_font = QFont("Microsoft YaHei", 14)
        QApplication.setFont(app_font)
        self.setStyleSheet("""
            QMainWindow { background: #f0f2f5; font-family: 'Microsoft YaHei'; }
            QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5e87db, stop:1 #3a66cc); color: white; border-radius: 6px; padding: 12px 24px; font-size: 14px; min-width: 140px; font-weight: bold; border: none; }
            QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6b93e1, stop:1 #4470d2); }
            QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3a66cc, stop:1 #5e87db); }
            QPushButton:disabled { background: #d0d0d0; color: #a0a0a0; }
            QTextEdit { border: 1px solid #c0cfed; border-radius: 8px; padding: 12px; font-size: 14px; background: white; selection-background-color: #c0cfed; }
            QTextEdit:focus { border: 2px solid #5e87db; }
            QGroupBox { font-weight: bold; font-size: 15px; padding-top: 16px; border: 2px solid #c0cfed; border-radius: 10px; margin-top: 15px; color: #2e4374; background: white; }
            QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background: white; }
            QLabel { font-size: 14px; color: #333333; padding: 3px; }
            QScrollArea { border: 1px solid #c0cfed; border-radius: 10px; background: white; }
            QSlider { height: 30px; }
            QSlider::groove:horizontal { border: 1px solid #c0cfed; height: 8px; background: #e6ecf7; margin: 2px 0; border-radius: 4px; }
            QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #5e87db, stop:1 #3a66cc); border: 1px solid #5e87db; width: 18px; margin: -8px 0; border-radius: 9px; }
            QSlider::handle:horizontal:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #6b93e1, stop:1 #4470d2); }
            QSlider::add-page:horizontal { background: #e6ecf7; border: 1px solid #c0cfed; border-radius: 4px; }
            QSlider::sub-page:horizontal { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #add5fc, stop:1 #5e87db); border: 1px solid #c0cfed; border-radius: 4px; }
        """)
        self.init_ui()
        self.load_fonts()
        self.generated_signatures = []

    def init_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QHBoxLayout(central_widget)
        layout.setSpacing(25)
        layout.setContentsMargins(20, 20, 20, 20)

        control_panel = QVBoxLayout()
        control_panel.setSpacing(25)

        title_label = QLabel("智能签名生成器")
        title_label.setStyleSheet("""
            font-size: 22px; 
            font-weight: bold; 
            color: #2e4374;
            padding: 10px 0;
        """)
        title_label.setAlignment(Qt.AlignCenter)

        desc_label = QLabel("生成专业、自然的手写签名效果")
        desc_label.setStyleSheet("font-size: 14px; color: #666666; padding-bottom: 10px;")
        desc_label.setAlignment(Qt.AlignCenter)

        input_group = QGroupBox("批量输入设置")
        input_layout = QVBoxLayout()
        input_layout.setContentsMargins(15, 25, 15, 15)
        input_layout.setSpacing(12)

        name_label = QLabel("待生成姓名列表:")
        name_label.setStyleSheet("font-weight: bold;")

        self.input_field = QTextEdit()
        self.input_field.setPlaceholderText("请输入姓名,用逗号分隔(例:张三,李四,王五)")
        self.input_field.setMaximumHeight(100)

        input_layout.addWidget(name_label)
        input_layout.addWidget(self.input_field)
        input_group.setLayout(input_layout)

        style_group = QGroupBox("样式调整设置")
        style_layout = QVBoxLayout()
        style_layout.setContentsMargins(15, 25, 15, 15)
        style_layout.setSpacing(15)

        font_size_layout = QVBoxLayout()
        font_size_label = QLabel("字体大小:")
        font_size_label.setStyleSheet("font-weight: bold;")

        font_size_slider_layout = QHBoxLayout()
        self.font_size_slider = QSlider(Qt.Horizontal)
        self.font_size_slider.setRange(30, 150)
        self.font_size_slider.setValue(80)
        self.font_size_slider.valueChanged.connect(self.update_font_size_label)

        self.font_size_label = QLabel("80")
        self.font_size_label.setFixedWidth(35)
        self.font_size_label.setAlignment(Qt.AlignCenter)
        self.font_size_label.setStyleSheet("""
            background: #5e87db; 
            color: white; 
            border-radius: 4px; 
            padding: 4px;
            font-weight: bold;
        """)

        font_size_slider_layout.addWidget(self.font_size_slider)
        font_size_slider_layout.addWidget(self.font_size_label)

        font_size_layout.addWidget(font_size_label)
        font_size_layout.addLayout(font_size_slider_layout)

        spacing_layout = QVBoxLayout()
        spacing_label = QLabel("字符间距:")
        spacing_label.setStyleSheet("font-weight: bold;")

        spacing_slider_layout = QHBoxLayout()
        self.spacing_slider = QSlider(Qt.Horizontal)
        self.spacing_slider.setRange(-20, 50)
        self.spacing_slider.setValue(0)
        self.spacing_slider.valueChanged.connect(self.update_spacing_label)

        self.spacing_label = QLabel("0")
        self.spacing_label.setFixedWidth(35)
        self.spacing_label.setAlignment(Qt.AlignCenter)
        self.spacing_label.setStyleSheet("""
            background: #5e87db; 
            color: white; 
            border-radius: 4px; 
            padding: 4px;
            font-weight: bold;
        """)

        spacing_slider_layout.addWidget(self.spacing_slider)
        spacing_slider_layout.addWidget(self.spacing_label)

        spacing_layout.addWidget(spacing_label)
        spacing_layout.addLayout(spacing_slider_layout)

        noise_layout = QVBoxLayout()
        noise_label = QLabel("噪点程度:")
        noise_label.setStyleSheet("font-weight: bold;")

        noise_slider_layout = QHBoxLayout()
        self.noise_slider = QSlider(Qt.Horizontal)
        self.noise_slider.setRange(0, 100)
        self.noise_slider.setValue(20)
        self.noise_slider.valueChanged.connect(self.update_noise_label)

        self.noise_label = QLabel("20")
        self.noise_label.setFixedWidth(35)
        self.noise_label.setAlignment(Qt.AlignCenter)
        self.noise_label.setStyleSheet("""
            background: #5e87db; 
            color: white; 
            border-radius: 4px; 
            padding: 4px;
            font-weight: bold;
        """)

        noise_slider_layout.addWidget(self.noise_slider)
        noise_slider_layout.addWidget(self.noise_label)

        noise_layout.addWidget(noise_label)
        noise_layout.addLayout(noise_slider_layout)

        mess_layout = QVBoxLayout()
        mess_label = QLabel("凌乱程度:")
        mess_label.setStyleSheet("font-weight: bold;")

        mess_slider_layout = QHBoxLayout()
        self.mess_slider = QSlider(Qt.Horizontal)
        self.mess_slider.setRange(0, 100)
        self.mess_slider.setValue(50)
        self.mess_slider.valueChanged.connect(self.update_mess_label)

        self.mess_label = QLabel("50")
        self.mess_label.setFixedWidth(35)
        self.mess_label.setAlignment(Qt.AlignCenter)
        self.mess_label.setStyleSheet("""
            background: #5e87db; 
            color: white; 
            border-radius: 4px; 
            padding: 4px;
            font-weight: bold;
        """)

        mess_slider_layout.addWidget(self.mess_slider)
        mess_slider_layout.addWidget(self.mess_label)

        mess_layout.addWidget(mess_label)
        mess_layout.addLayout(mess_slider_layout)

        style_layout.addLayout(font_size_layout)
        style_layout.addLayout(spacing_layout)
        style_layout.addLayout(noise_layout)
        style_layout.addLayout(mess_layout)

        reset_btn = QPushButton("🔄 重置默认设置")
        reset_btn.clicked.connect(self.reset_style_settings)
        style_layout.addWidget(reset_btn)

        style_group.setLayout(style_layout)

        button_layout = QHBoxLayout()
        button_layout.setSpacing(15)

        self.generate_btn = QPushButton("🎨 随机生成签名")
        self.generate_btn.clicked.connect(self.generate_signatures)

        self.download_btn = QPushButton("💾 下载全部签名")
        self.download_btn.setEnabled(False)
        self.download_btn.clicked.connect(self.download_signatures)

        button_layout.addWidget(self.generate_btn)
        button_layout.addWidget(self.download_btn)

        preview_title = QLabel("签名预览区")
        preview_title.setStyleSheet("""
            font-size: 16px; 
            font-weight: bold; 
            color: #2e4374;
            padding: 10px;
            background: white;
            border-radius: 8px;
            border: 1px solid #c0cfed;
            margin-bottom: 10px;
        """)
        preview_title.setAlignment(Qt.AlignCenter)

        control_panel.addWidget(title_label)
        control_panel.addWidget(desc_label)
        control_panel.addWidget(input_group)
        control_panel.addWidget(style_group)
        control_panel.addLayout(button_layout)
        control_panel.addStretch(1)

        preview_panel = QVBoxLayout()
        preview_panel.addWidget(preview_title)

        self.preview_scroll = QScrollArea()
        self.preview_scroll.setWidgetResizable(True)
        self.preview_content = QWidget()
        self.preview_layout = QVBoxLayout(self.preview_content)
        self.preview_layout.setAlignment(Qt.AlignTop)
        self.preview_layout.setSpacing(20)
        self.preview_layout.setContentsMargins(20, 20, 20, 20)

        self.preview_scroll.setWidget(self.preview_content)
        preview_panel.addWidget(self.preview_scroll)

        layout.addLayout(control_panel, 1)
        layout.addLayout(preview_panel, 2)

    def load_fonts(self):
        self.fonts = []
        fonts_dir = "./fonts/"
        if not os.path.exists(fonts_dir):
            os.makedirs(fonts_dir)
            print("创建了fonts目录,请添加字体文件")
            QMessageBox.warning(self, "字体缺失", "字体目录为空,请在fonts目录添加.ttf或.otf字体文件")
            return
        for file in os.listdir(fonts_dir):
            if file.lower().endswith(('.ttf', '.otf')):
                font_path = os.path.join(fonts_dir, file)
                try:
                    ImageFont.truetype(font_path, 40)
                    self.fonts.append(font_path)
                except Exception as e:
                    print(f"加载字体 {font_path} 失败: {e}")
        if not self.fonts:
            QMessageBox.warning(self, "字体缺失", "没有找到可用字体,请在fonts目录添加.ttf或.otf字体文件")

    def update_font_size_label(self, value):
        self.font_size_label.setText(str(value))

    def update_spacing_label(self, value):
        self.spacing_label.setText(str(value))

    def update_noise_label(self, value):
        self.noise_label.setText(str(value))

    def update_mess_label(self, value):
        self.mess_label.setText(str(value))

    def reset_style_settings(self):
        self.font_size_slider.setValue(80)
        self.spacing_slider.setValue(0)
        self.noise_slider.setValue(20)
        self.mess_slider.setValue(50)

    def generate_signatures(self):
        if not self.fonts:
            QMessageBox.warning(self, "字体缺失", "没有找到可用字体,请在fonts目录添加.ttf或.otf字体文件")
            return
        for i in reversed(range(self.preview_layout.count())): 
            self.preview_layout.itemAt(i).widget().deleteLater()
        self.generated_signatures = []
        input_text = self.input_field.toPlainText().strip()
        if not input_text:
            QMessageBox.warning(self, "输入缺失", "请输入至少一个姓名")
            return
        names = [name.strip() for name in input_text.split(',') if name.strip()]
        if not names:
            QMessageBox.warning(self, "输入缺失", "请输入有效的姓名列表,用逗号分隔")
            return
        font_size = self.font_size_slider.value()
        spacing = self.spacing_slider.value()
        noise_level = self.noise_slider.value() / 100.0
        mess_level = self.mess_slider.value() / 100.0
        for name in names:
            font_path = random.choice(self.fonts)
            try:
                signature = self.create_signature(name, font_path, font_size, spacing, noise_level, mess_level)
                if signature:
                    self.generated_signatures.append((name, signature))
                    preview_item = self.create_preview_item(name, signature)
                    self.preview_layout.addWidget(preview_item)
            except Exception as e:
                print(f"生成签名失败: {e}")
        if self.generated_signatures:
            self.download_btn.setEnabled(True)
        else:
            QMessageBox.warning(self, "生成失败", "没有成功生成签名,请重试")

    def create_signature(self, name, font_path, font_size, spacing, noise_level, mess_level):
        try:
            font = ImageFont.truetype(font_path, font_size)
            text_width, text_height = self.get_text_dimensions(name, font, spacing)
            padding = 50
            img_width = text_width + padding * 2
            img_height = text_height + padding * 2
            image = Image.new('RGBA', (img_width, img_height), (255, 255, 255, 0))
            draw = ImageDraw.Draw(image)
            x = padding
            y = padding
            if mess_level > 0:
                x += random.randint(-int(10 * mess_level), int(10 * mess_level))
                y += random.randint(-int(10 * mess_level), int(10 * mess_level))
                rotation = random.uniform(-5 * mess_level, 5 * mess_level)
                image = image.rotate(rotation, resample=Image.BICUBIC, expand=False, fillcolor=(255, 255, 255, 0))
                draw = ImageDraw.Draw(image)
            for char in name:
                char_x = x
                char_y = y
                if mess_level > 0:
                    char_x += random.randint(-int(5 * mess_level), int(5 * mess_level))
                    char_y += random.randint(-int(5 * mess_level), int(5 * mess_level))
                draw.text((char_x, char_y), char, fill=(0, 0, 0, 255), font=font)
                bbox = font.getbbox(char)
                char_width = bbox[2] - bbox[0]
                x += char_width + spacing
            if noise_level > 0:
                image = self.apply_noise(image, noise_level)
            return image
        except Exception as e:
            print(f"创建签名失败: {e}")
            return None

    def get_text_dimensions(self, text, font, spacing):
        total_width = 0
        max_height = 0
        for char in text:
            bbox = font.getbbox(char)
            width = bbox[2] - bbox[0]
            height = bbox[3] - bbox[1]
            total_width += width + spacing
            max_height = max(max_height, height)
        if text:
            total_width -= spacing
        return total_width, max_height

    def apply_noise(self, image, level):
        noisy_image = image.copy()
        width, height = image.size
        pixels = noisy_image.load()
        for y in range(height):
            for x in range(width):
                r, g, b, a = pixels[x, y]
                if a > 0:
                    if random.random() < level * 0.05:
                        noise = random.randint(-30, 30)
                        r = max(0, min(255, r + noise))
                        g = max(0, min(255, g + noise))
                        b = max(0, min(255, b + noise))
                        pixels[x, y] = (r, g, b, a)
                    if random.random() < level * 0.01:
                        pixels[x, y] = (r, g, b, max(0, a - random.randint(50, 200)))
        if level > 0.5:
            noisy_image = noisy_image.filter(ImageFilter.GaussianBlur(radius=0.3))
        return noisy_image

    def create_preview_item(self, name, signature):
        container = QWidget()
        layout = QVBoxLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        container.setStyleSheet("""
            QWidget {
                background-color: white;
                border: 1px solid #c0cfed;
                border-radius: 10px;
            }
        """)
        preview_label = QLabel()
        preview_label.setAlignment(Qt.AlignCenter)
        preview_label.setStyleSheet("border: none; padding: 10px;")
        img_byte_arr = io.BytesIO()
        signature.save(img_byte_arr, format='PNG')
        img_byte_arr.seek(0)
        img_data = img_byte_arr.read()
        pixmap = QPixmap()
        pixmap.loadFromData(img_data)
        preview_label.setPixmap(pixmap)
        name_label = QLabel(f"姓名: {name}")
        name_label.setStyleSheet("""
            font-size: 14px; 
            font-weight: bold; 
            color: #2e4374;
            padding: 10px;
            border-top: 1px solid #c0cfed;
        """)
        name_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(preview_label)
        layout.addWidget(name_label)
        return container

    def download_signatures(self):
        if not self.generated_signatures:
            return
        save_dir = QFileDialog.getExistingDirectory(self, "选择保存目录")
        if not save_dir:
            return
        date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
        save_path = os.path.join(save_dir, f"签名_{date_str}")
        try:
            if not os.path.exists(save_path):
                os.makedirs(save_path)
            for i, (name, signature) in enumerate(self.generated_signatures):
                file_name = f"{name}_{i+1}.png"
                file_path = os.path.join(save_path, file_name)
                signature.save(file_path, "PNG")
            QMessageBox.information(self, "下载成功", f"所有签名已保存至: {save_path}")
        except Exception as e:
            QMessageBox.critical(self, "保存失败", f"保存签名时出错: {str(e)}")

if __name__ == "__main__":
    fonts_dir = os.path.abspath("fonts")
    if not os.path.exists(fonts_dir):
        os.makedirs(fonts_dir)
        app = QApplication(sys.argv)
        QMessageBox.information(None, "首次使用提示", "已自动创建 fonts 文件夹,请将手写体字体(.ttf/.otf)文件放入该文件夹后重新启动程序。")
        sys.exit(0)
    font_files = [f for f in os.listdir(fonts_dir) if f.lower().endswith(('.ttf', '.otf'))]
    if not font_files:
        app = QApplication(sys.argv)
        QMessageBox.warning(None, "字体缺失", "fonts 文件夹为空,请将手写体字体(.ttf/.otf)文件放入该文件夹后重新启动程序。")
        sys.exit(0)
    app = QApplication(sys.argv)
    window = SignatureGenerator()
    window.show()
    sys.exit(app.exec_())

扫码免费获取资源: