或者将下面代码帮忙转换成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()
代码没有问题,运行代码效果:
