自己做了一个PDF加水印的工具

查看 9|回复 0
作者:makejon   
1、我使用 PySide6 开发了一个 PDF 加水印工具,界面基于 QtWidgets。现在尝试将其打包成 Windows 的 EXE 可执行文件,但使用 PyInstaller打包时,总是提示缺少 QtWidgets 模块。请问有人知道如何解决吗?或者有其他可靠的打包方案可以推荐吗?
或者将下面代码帮忙转换成EXE文件;
[Python] 纯文本查看 复制代码import sys
import os
import json
import re
import fitz  # PyMuPDF
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QListWidget, QLabel, QLineEdit, QSlider,
    QProgressBar, QTextEdit, QFileDialog, QSpinBox, QRadioButton,
    QButtonGroup, QMessageBox, QCheckBox, QDialog, QTextBrowser
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon, QPixmap
CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".pdf_watermark_config.json")
HELP_DOC = """
PDF批量水印工具 使用说明
一、基本功能
1. 批量添加水印:支持对多个PDF文件同时添加文字水印
2. 自定义水印参数:可调整透明度、字体大小、旋转角度
3. 自动过滤已处理文件:避免重复操作
二、操作步骤
1. 选择PDF文件夹:点击"选择PDF文件夹"按钮,选择包含PDF文件的目录
2. 设置水印内容:在"水印文字"输入框中输入需要添加的水印文字
3. 调整水印效果:
   - 透明度:0-100之间调整,值越小越透明
   - 字体大小:8-72之间调整
   - 旋转角度:可选择0°、90°、180°、270°
4. 开始处理:点击"开始处理"按钮开始添加水印
5. 查看结果:处理完成后,点击"打开输出文件夹"查看处理后的文件
三、注意事项
1. 处理后的文件将保存在原PDF所在目录下的子文件夹中
2. 子文件夹名称为水印文字的前20个字符(特殊字符会被替换为下划线)
3. 可通过"过滤文件名包含"功能跳过已处理的文件
4. 处理过程中可点击"取消处理"停止操作
四、常见问题
1. 若遇到"无法打开PDF文件"错误,可能是文件已损坏、被其他程序占用或PDF加密
2. 水印效果不理想时,可尝试调整透明度和字体大小
3. 如需处理大量文件,建议关闭其他占用资源的程序
"""
def sanitize_filename(text: str) -> str:
    return re.sub(r'[\\/:*?"|#丨]', '_', text)[:20]  # 限制前20字符并处理特殊字符
class WatermarkWorker(QThread):
    progress = Signal(int)
    result = Signal(dict)
    error = Signal(str)
    def __init__(self, files, text, opacity, font_size, rotate):
        super().__init__()
        self.files = files
        self.text = text
        self.opacity = opacity / 100
        self.font_size = font_size
        self.rotate = rotate
        self._stopped = False
        self.output_dir = None  # 存储最终输出目录(基于首个文件路径)
    def run(self):
        success_count = 0
        fail_list = []
        total_files = len(self.files)
        if not total_files:
            self.result.emit({'success': 0, 'fail': []})
            return
        # 确定输出目录(基于首个文件的父目录和水印文字)
        first_file_dir = os.path.dirname(self.files[0]) if self.files else ""
        safe_text = sanitize_filename(self.text)
        self.output_dir = os.path.join(first_file_dir, safe_text) if first_file_dir else ""
        os.makedirs(self.output_dir, exist_ok=True)
        for idx, file_path in enumerate(self.files):
            if self._stopped:
                break
            try:
                self.add_watermark(file_path, self.output_dir)
                success_count += 1
            except Exception as e:
                fail_list.append((file_path, str(e)))
            self.progress.emit(int((idx + 1) / total_files * 100))
        self.result.emit({'success': success_count, 'fail': fail_list})
    def stop(self):
        self._stopped = True
    def add_watermark(self, file_path, output_dir):
        file_path = os.path.normpath(file_path)
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")
        try:
            doc = fitz.open(file_path)
        except Exception as e:
            raise RuntimeError(f"无法打开PDF文件: {file_path}\n原因: {str(e)}")
        for page in doc:
            rect = page.rect
            page.insert_textbox(
                rect,
                self.text,
                fontsize=self.font_size,
                rotate=self.rotate,
                color=(0, 0, 0),
                fill_opacity=self.opacity,
                align=1  # 居中对齐
            )
        # 生成唯一文件名
        base_name = os.path.basename(file_path)
        name_wo_ext = os.path.splitext(base_name)[0]
        output_path = os.path.join(output_dir, f"{name_wo_ext}_已增加水印.pdf")
        counter = 1
        while os.path.exists(output_path):
            output_path = os.path.join(output_dir, f"{name_wo_ext}_已增加水印({counter}).pdf")
            counter += 1
        doc.save(output_path)
        doc.close()
class HelpDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("使用说明")
        self.resize(600, 500)
        layout = QVBoxLayout()
        text_browser = QTextBrowser()
        text_browser.setMarkdown(HELP_DOC)
        text_browser.setReadOnly(True)
        layout.addWidget(text_browser)
        self.setLayout(layout)
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("TOMILO_PDF批量水印大师V1.0_By:郭玉斌")
        self.resize(900, 600)
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        # 顶部操作栏
        top_bar_layout = QHBoxLayout()
        file_layout = QHBoxLayout()
        self.btn_select_folder = QPushButton("选择PDF文件夹")
        self.btn_clear_list = QPushButton("清空列表")
        self.checkbox_filter = QCheckBox("过滤文件名包含:")
        self.edit_filter_text = QLineEdit("已增加水印")
        self.edit_filter_text.setFixedWidth(100)
        file_layout.addWidget(self.btn_select_folder)
        file_layout.addWidget(self.btn_clear_list)
        file_layout.addWidget(self.checkbox_filter)
        file_layout.addWidget(self.edit_filter_text)
        self.btn_open_output = QPushButton("打开输出文件夹")
        self.btn_open_output.setEnabled(False)
        self.btn_help = QPushButton("使用说明")
        top_bar_layout.addLayout(file_layout)
        top_bar_layout.addWidget(self.btn_open_output)
        top_bar_layout.addWidget(self.btn_help)
        main_layout.addLayout(top_bar_layout)
        # 文件列表
        self.list_pdf_files = QListWidget()
        main_layout.addWidget(self.list_pdf_files)
        # 参数设置区域
        param_layout = QHBoxLayout()
        param_layout.addWidget(QLabel("水印文字:"))
        self.edit_text = QLineEdit()
        param_layout.addWidget(self.edit_text)
        param_layout.addWidget(QLabel("透明度:"))
        self.slider_opacity = QSlider(Qt.Horizontal)
        self.slider_opacity.setRange(0, 100)
        self.slider_opacity.setValue(50)
        self.spin_opacity = QSpinBox()
        self.spin_opacity.setRange(0, 100)
        self.spin_opacity.setValue(50)
        param_layout.addWidget(self.slider_opacity)
        param_layout.addWidget(self.spin_opacity)
        param_layout.addWidget(QLabel("字体大小:"))
        self.slider_fontsize = QSlider(Qt.Horizontal)
        self.slider_fontsize.setRange(8, 72)
        self.slider_fontsize.setValue(12)
        self.spin_fontsize = QSpinBox()
        self.spin_fontsize.setRange(8, 72)
        self.spin_fontsize.setValue(12)
        param_layout.addWidget(self.slider_fontsize)
        param_layout.addWidget(self.spin_fontsize)
        param_layout.addWidget(QLabel("旋转角度:"))
        self.rotate_group = QButtonGroup()
        for angle in [0, 90, 180, 270]:
            rb = QRadioButton(f"{angle}°")
            self.rotate_group.addButton(rb, angle)
            param_layout.addWidget(rb)
        self.rotate_group.buttons()[0].setChecked(True)
        main_layout.addLayout(param_layout)
        # 底部区域
        bottom_layout = QVBoxLayout()
        self.progress_bar = QProgressBar()
        bottom_layout.addWidget(self.progress_bar)
        self.text_result = QTextEdit()
        self.text_result.setReadOnly(True)
        bottom_layout.addWidget(self.text_result)
        btn_layout = QHBoxLayout()
        self.btn_start = QPushButton("开始处理")
        self.btn_cancel = QPushButton("取消处理")
        self.btn_cancel.setEnabled(False)
        btn_layout.addWidget(self.btn_start)
        btn_layout.addWidget(self.btn_cancel)
        bottom_layout.addLayout(btn_layout)
        main_layout.addLayout(bottom_layout)
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
        # 信号连接
        self.btn_select_folder.clicked.connect(self.select_folder)
        self.btn_clear_list.clicked.connect(self.list_pdf_files.clear)
        self.slider_opacity.valueChanged.connect(self.spin_opacity.setValue)
        self.spin_opacity.valueChanged.connect(self.slider_opacity.setValue)
        self.slider_fontsize.valueChanged.connect(self.spin_fontsize.setValue)
        self.spin_fontsize.valueChanged.connect(self.slider_fontsize.setValue)
        self.btn_start.clicked.connect(self.start_process)
        self.btn_cancel.clicked.connect(self.cancel_process)
        self.btn_help.clicked.connect(self.show_help)
        self.btn_open_output.clicked.connect(self.open_output_folder)  # 直接连接打开方法
        self.load_ui_config()
        self.worker = None
        self.current_output_dir = None  # 存储当前输出目录
    def select_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "选择PDF文件夹")
        if folder:
            self.list_pdf_files.clear()
            filter_enabled = self.checkbox_filter.isChecked()
            filter_text = self.edit_filter_text.text().strip()
            for root, _, files in os.walk(folder):
                for f in files:
                    if f.lower().endswith('.pdf'):
                        if filter_enabled and filter_text and filter_text in f:
                            continue
                        full_path = os.path.normpath(os.path.join(root, f)).replace("\\", "/")
                        self.list_pdf_files.addItem(full_path)
    def start_process(self):
        text = self.edit_text.text().strip()
        opacity = self.spin_opacity.value()
        fontsize = self.spin_fontsize.value()
        rotate = self.rotate_group.checkedId()
        files = [self.list_pdf_files.item(i).text() for i in range(self.list_pdf_files.count())]
        if not files:
            QMessageBox.warning(self, "警告", "请先选择PDF文件!")
            return
        if not text:
            QMessageBox.warning(self, "警告", "请输入水印文字!")
            return
        self.btn_start.setEnabled(False)
        self.btn_cancel.setEnabled(True)
        self.btn_open_output.setEnabled(False)
        self.text_result.clear()
        self.progress_bar.setValue(0)
        self.text_result.append("开始处理...")
        # 生成输出目录(首个文件路径 + 水印文字处理后名称)
        if files:
            first_file_dir = os.path.dirname(files[0])
            safe_text = sanitize_filename(text)
            self.current_output_dir = os.path.join(first_file_dir, safe_text)
            os.makedirs(self.current_output_dir, exist_ok=True)
        self.worker = WatermarkWorker(files, text, opacity, font_size=fontsize, rotate=rotate)
        self.worker.progress.connect(self.progress_bar.setValue)
        self.worker.result.connect(self.process_finished)
        self.worker.error.connect(self.display_error)
        self.worker.start()
    def cancel_process(self):
        if self.worker and self.worker.isRunning():
            self.worker.stop()
            self.text_result.append("取消处理中...")
    def process_finished(self, result):
        self.btn_start.setEnabled(True)
        self.btn_cancel.setEnabled(False)
        succ = result['success']
        fails = result['fail']
        self.text_result.append(f"\n处理完成!成功:{succ},失败:{len(fails)}")
        for f, reason in fails:
            self.text_result.append(f"失败文件: {f}\n原因: {reason}\n")
        self.progress_bar.setValue(100)
        self.save_ui_config()
        if succ > 0 and self.current_output_dir and os.path.exists(self.current_output_dir):
            self.btn_open_output.setEnabled(True)
    def display_error(self, message):
        QMessageBox.critical(self, "错误", message)
    def save_ui_config(self):
        data = {
            'text': self.edit_text.text(),
            'opacity': self.spin_opacity.value(),
            'fontsize': self.spin_fontsize.value(),
            'rotate': self.rotate_group.checkedId(),
            'filter_enabled': self.checkbox_filter.isChecked(),
            'filter_text': self.edit_filter_text.text(),
        }
        try:
            with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
                json.dump(data, f)
        except Exception as e:
            self.text_result.append(f"保存配置失败: {str(e)}")
    def load_ui_config(self):
        if os.path.exists(CONFIG_PATH):
            try:
                with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                self.edit_text.setText(data.get('text', ''))
                self.spin_opacity.setValue(data.get('opacity', 50))
                self.spin_fontsize.setValue(data.get('fontsize', 12))
                rotate = data.get('rotate', 0)
                btn = self.rotate_group.button(rotate)
                if btn:
                    btn.setChecked(True)
                self.checkbox_filter.setChecked(data.get('filter_enabled', True))
                self.edit_filter_text.setText(data.get('filter_text', '已增加水印'))
            except Exception:
                pass
    def open_output_folder(self):
        if self.current_output_dir and os.path.exists(self.current_output_dir):
            if sys.platform.startswith('win'):
                try:
                    os.startfile(self.current_output_dir)
                except Exception as e:
                    QMessageBox.critical(self, "错误", f"打开文件夹失败: {str(e)}")
            elif sys.platform.startswith('darwin'):  # macOS
                os.system(f'open "{self.current_output_dir}"')
            else:  # Linux
                os.system(f'xdg-open "{self.current_output_dir}"')
        else:
            QMessageBox.warning(self, "警告", "输出文件夹不存在或未处理任何文件!")
    def show_help(self):
        help_dialog = HelpDialog(self)
        help_dialog.exec()
def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
if __name__ == "__main__":
    main()
代码没有问题,运行代码效果:


水印, 文件

您需要登录后才可以回帖 登录 | 立即注册

返回顶部