Python+PyQt5开发一个全屏水印批量添加工具
作者:创客白泽
概述
在数字化时代,保护知识产权和防止内容盗用变得尤为重要。水印技术作为一种有效的内容保护手段,被广泛应用于文档、图片和视频等多媒体内容中。今天我们要介绍的是一款基于Python和PyQt5开发的全屏水印批量添加工具,它能够为PDF文档和多种图片格式快速添加自定义水印。
这款工具不仅支持基本的文字水印功能,还提供了丰富的自定义选项,包括字体选择、颜色调整、透明度设置、旋转角度和密度控制等。通过多线程技术,实现了高效的批量处理能力,大大提升了工作效率。
功能特性
核心功能
- 批量处理:支持同时处理多个PDF或图片文件
- 多格式支持:PDF、JPG、PNG、BMP、GIF、TIFF、WEBP等格式
- 全屏水印:自动在整个页面/图片上平铺水印内容
- 高度自定义:字体、颜色、大小、角度、透明度、密度均可调整
技术特点
- 多线程处理:后台线程处理,UI不卡顿
- 实时进度:显示处理进度和已处理文件列表
- 实时预览:实时查看水印效果
- 高效性能:优化的算法确保处理速度
展示效果
PDF全屏水印效果:
图片全屏水印效果:
系统架构
软件步骤说明
1. 环境准备
首先需要安装必要的Python库:
pip install PyPDF2 reportlab pillow PyQt5
2. 使用步骤
1.选择文件夹:指定输入和输出文件夹
2.设置水印参数:
- 输入水印文字内容
- 选择字体和颜色
- 调整大小、角度和透明度
- 设置水印密度
3.预览效果:实时查看水印样式
4.开始处理:选择处理PDF或图片文件
5.查看结果:在输出文件夹查看处理后的文件
3. 参数说明
参数 | 说明 | 推荐值 |
---|---|---|
字体大小 | 水印文字的大小 | 20-40px |
旋转角度 | 水印的旋转角度 | 30-45° |
透明度 | 水印的不透明度 | 0.1-0.3 |
密度 | 水印的分布密度 | 4-8 |
代码解析
核心类结构
class WatermarkWorker(QThread): """处理PDF和图片水印的工作线程""" progress = pyqtSignal(int) finished = pyqtSignal(bool, str) file_processed = pyqtSignal(str) def __init__(self, input_folder, output_folder, watermark_text, font_size, font_path, color, angle, opacity, density, file_type): # 初始化参数 super().__init__() def run(self): # 主处理逻辑 pass def add_watermark_to_pdf(self, input_path, output_path): # PDF水印处理 pass def add_watermark_to_image(self, input_path, output_path): # 图片水印处理 pass
PDF水印处理原理
PDF水印处理使用了PyPDF2
和ReportLab
两个库:
- 读取PDF:使用
PdfReader
读取原始PDF文件 - 创建水印:使用
ReportLab
在内存中生成水印PDF - 合并水印:将水印PDF与原始PDF每一页合并
- 保存结果:使用
PdfWriter
写入新的PDF文件
def add_watermark_to_pdf(self, input_path, output_path): try: # 读取PDF reader = PdfReader(input_path) writer = PdfWriter() # 获取PDF页面尺寸 first_page = reader.pages[0] page_width = float(first_page.mediabox.width) page_height = float(first_page.mediabox.height) # 创建水印 watermark_pdf = self.create_watermark_pdf(page_width, page_height) # 为每一页添加水印 for page in reader.pages: page.merge_page(watermark_pdf.pages[0]) writer.add_page(page) # 保存PDF with open(output_path, 'wb') as output_file: writer.write(output_file) return True except Exception as e: print(f"Error processing PDF {input_path}: {str(e)}") return False
图片水印处理原理
图片水印处理使用Pillow
库:
- 打开图片:使用
Image.open()
读取图片 - 创建水印层:新建一个透明图层用于绘制水印
- 绘制水印:根据密度参数计算水印位置并绘制
- 合并图层:将水印层与原始图片合并
- 保存结果:根据原格式保存图片
def add_watermark_to_image(self, input_path, output_path): try: # 打开图片 image = Image.open(input_path).convert('RGBA') # 创建水印图层 watermark_layer = Image.new('RGBA', image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(watermark_layer) # 获取字体 font = self.get_pil_font() # 计算水印间距和位置 spacing_x = image.width / self.density spacing_y = image.height / self.density # 绘制水印 for x in range(0, int(image.width // spacing_x) + 1): for y in range(0, int(image.height // spacing_y) + 1): # 计算位置并绘制水印 pass # 合并水印到原图 watermarked_image = Image.alpha_composite(image, watermark_layer) watermarked_image.save(output_path) return True except Exception as e: print(f"Error processing image {input_path}: {str(e)}") return False
多线程处理机制
为了避免界面卡顿,使用了QThread
进行后台处理:
class WatermarkWorker(QThread): """处理PDF和图片水印的工作线程""" progress = pyqtSignal(int) # 进度信号 finished = pyqtSignal(bool, str) # 完成信号 file_processed = pyqtSignal(str) # 文件处理完成信号 def run(self): try: # 获取文件列表 if self.file_type == 'pdf': files = [f for f in os.listdir(self.input_folder) if f.lower().endswith('.pdf')] else: # 获取图片文件 pass # 处理每个文件 for i, filename in enumerate(files): if self.canceled: break # 处理文件... # 发送进度信号 progress = int((i + 1) / total_files * 100) self.progress.emit(progress) self.finished.emit(not self.canceled, "处理完成") except Exception as e: self.finished.emit(False, f"发生错误: {str(e)}")
字体处理策略
为了解决中文字体显示问题,实现了智能字体选择机制:
def get_pil_font(self): """获取PIL字体对象""" try: if self.font_path and os.path.exists(self.font_path): return ImageFont.truetype(self.font_path, self.font_size) else: # 尝试使用系统默认中文字体 windows_fonts = [ "C:/Windows/Fonts/simhei.ttf", # 黑体 "C:/Windows/Fonts/simsun.ttc", # 宋体 "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 ] for font_path in windows_fonts: if os.path.exists(font_path): return ImageFont.truetype(font_path, self.font_size) # 如果找不到中文字体,使用默认字体 return ImageFont.load_default() except: return ImageFont.load_default()
性能优化策略
1. 内存优化
- 使用流式处理,避免一次性加载所有文件到内存
- 及时释放不再需要的资源
2. 处理速度优化
- 多线程处理,充分利用多核CPU
- 算法优化,减少不必要的计算
3. 用户体验优化
- 实时预览功能
- 进度反馈机制
- 错误处理和恢复机制
扩展功能
已实现功能
- 批量处理PDF和图片文件
- 自定义水印文字和样式
- 实时预览效果
- 多线程处理
- 进度显示和文件列表
未来可扩展功能
- 图片水印支持(上传Logo图片)
- 水印模板保存和加载
- 命令行界面支持
- 云端处理功能
- 批量重命名功能
- 更多文件格式支持
源码下载
import os import sys import math import io from PyPDF2 import PdfReader, PdfWriter from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.lib.colors import HexColor, Color from reportlab.lib.utils import ImageReader from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from PIL import Image, ImageDraw, ImageFont, ImageOps from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QDoubleSpinBox, QColorDialog, QFileDialog, QComboBox, QSlider, QGroupBox, QProgressBar, QMessageBox, QCheckBox, QFrame, QTextEdit, QFontComboBox, QListWidget, QListWidgetItem, QGridLayout) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QRectF, QPointF from PyQt5.QtGui import QColor, QFont, QPalette, QIcon, QPixmap, QFontDatabase, QPainter class WatermarkWorker(QThread): """处理PDF和图片水印的工作线程""" progress = pyqtSignal(int) finished = pyqtSignal(bool, str) file_processed = pyqtSignal(str) def __init__(self, input_folder, output_folder, watermark_text, font_size, font_path, color, angle, opacity, density, file_type): super().__init__() self.input_folder = input_folder self.output_folder = output_folder self.watermark_text = watermark_text self.font_size = font_size self.font_path = font_path self.color = color self.angle = angle self.opacity = opacity self.density = density self.file_type = file_type # 'pdf' 或 'image' self.canceled = False def run(self): try: if self.file_type == 'pdf': # 获取所有PDF文件 files = [f for f in os.listdir(self.input_folder) if f.lower().endswith('.pdf')] else: # 获取所有图片文件 image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp', '.tif'] files = [f for f in os.listdir(self.input_folder) if any(f.lower().endswith(ext) for ext in image_extensions)] total_files = len(files) if total_files == 0: self.finished.emit(False, f"未找到{self.file_type.upper()}文件") return # 处理每个文件 for i, filename in enumerate(files): if self.canceled: break input_path = os.path.join(self.input_folder, filename) output_path = os.path.join(self.output_folder, filename) # 添加水印 if self.file_type == 'pdf': success = self.add_watermark_to_pdf(input_path, output_path) else: success = self.add_watermark_to_image(input_path, output_path) if not success: self.finished.emit(False, f"处理文件 {filename} 时出错") return # 发送文件处理完成信号 self.file_processed.emit(filename) # 更新进度 progress = int((i + 1) / total_files * 100) self.progress.emit(progress) self.finished.emit(not self.canceled, "处理完成" if not self.canceled else "已取消") except Exception as e: self.finished.emit(False, f"发生错误: {str(e)}") def cancel(self): self.canceled = True def add_watermark_to_pdf(self, input_path, output_path): try: # 读取PDF reader = PdfReader(input_path) writer = PdfWriter() # 获取PDF页面尺寸 first_page = reader.pages[0] page_width = float(first_page.mediabox.width) page_height = float(first_page.mediabox.height) # 创建水印 watermark_pdf = self.create_watermark_pdf(page_width, page_height) # 为每一页添加水印 for page in reader.pages: # 合并水印 page.merge_page(watermark_pdf.pages[0]) writer.add_page(page) # 保存PDF with open(output_path, 'wb') as output_file: writer.write(output_file) return True except Exception as e: print(f"Error processing PDF {input_path}: {str(e)}") return False def add_watermark_to_image(self, input_path, output_path): try: # 打开图片 image = Image.open(input_path).convert('RGBA') # 创建水印图层 watermark_layer = Image.new('RGBA', image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(watermark_layer) # 获取字体 font = self.get_pil_font() # 计算水印间距 spacing_x = image.width / self.density spacing_y = image.height / self.density # 计算偏移量,使水印居中分布 offset_x = spacing_x / 2 offset_y = spacing_y / 2 # 设置颜色和透明度 rgba_color = (self.color.red(), self.color.green(), self.color.blue(), int(self.opacity * 255)) # 在整个图片上绘制水印 for x in range(0, int(image.width // spacing_x) + 1): for y in range(0, int(image.height // spacing_y) + 1): pos_x = offset_x + x * spacing_x pos_y = offset_y + y * spacing_y # 创建文本图像并旋转 text_image = Image.new('RGBA', (image.width, image.height), (0, 0, 0, 0)) text_draw = ImageDraw.Draw(text_image) # 获取文本尺寸 bbox = draw.textbbox((0, 0), self.watermark_text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # 绘制文本 text_draw.text((pos_x - text_width/2, pos_y - text_height/2), self.watermark_text, font=font, fill=rgba_color) # 旋转文本 rotated_text = text_image.rotate(self.angle, expand=False, center=(pos_x, pos_y)) # 合并到水印图层 watermark_layer = Image.alpha_composite(watermark_layer, rotated_text) # 合并水印到原图 watermarked_image = Image.alpha_composite(image, watermark_layer) # 保存图片(根据原格式保存) if image.mode != 'RGB' and output_path.lower().endswith(('.jpg', '.jpeg')): watermarked_image = watermarked_image.convert('RGB') watermarked_image.save(output_path) return True except Exception as e: print(f"Error processing image {input_path}: {str(e)}") return False def get_pil_font(self): """获取PIL字体对象""" try: if self.font_path and os.path.exists(self.font_path): return ImageFont.truetype(self.font_path, self.font_size) else: # 尝试使用系统默认字体 try: # Windows系统常见中文字体 windows_fonts = [ "C:/Windows/Fonts/simhei.ttf", # 黑体 "C:/Windows/Fonts/simsun.ttc", # 宋体 "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 ] for font_path in windows_fonts: if os.path.exists(font_path): return ImageFont.truetype(font_path, self.font_size) # 如果找不到中文字体,使用默认字体 return ImageFont.load_default() except: return ImageFont.load_default() except: return ImageFont.load_default() def create_watermark_pdf(self, page_width, page_height): """创建水印PDF""" # 创建内存中的PDF packet = io.BytesIO() can = canvas.Canvas(packet, pagesize=(page_width, page_height)) # 设置颜色和透明度 r, g, b = self.color.red()/255, self.color.green()/255, self.color.blue()/255 can.setFillColor(Color(r, g, b, alpha=self.opacity)) # 计算水印间距 spacing_x = page_width / self.density spacing_y = page_height / self.density # 计算偏移量,使水印居中分布 offset_x = spacing_x / 2 offset_y = spacing_y / 2 # 处理文本水印 # 尝试注册字体 try: if self.font_path and os.path.exists(self.font_path): # 注册字体 font_name = os.path.basename(self.font_path).split('.')[0] pdfmetrics.registerFont(TTFont(font_name, self.font_path)) can.setFont(font_name, self.font_size) else: # 尝试使用系统默认中文字体 try: # Windows系统常见中文字体 windows_fonts = [ "C:/Windows/Fonts/simhei.ttf", # 黑体 "C:/Windows/Fonts/simsun.ttc", # 宋体 "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 ] for font_path in windows_fonts: if os.path.exists(font_path): font_name = os.path.basename(font_path).split('.')[0] pdfmetrics.registerFont(TTFont(font_name, font_path)) can.setFont(font_name, self.font_size) break else: # 如果没有找到中文字体,使用默认字体 can.setFont("Helvetica", self.font_size) except: can.setFont("Helvetica", self.font_size) except Exception as e: print(f"字体注册失败: {e}") can.setFont("Helvetica", self.font_size) # 在整个页面上绘制文本水印 for x in range(0, int(page_width // spacing_x) + 1): for y in range(0, int(page_height // spacing_y) + 1): pos_x = offset_x + x * spacing_x pos_y = offset_y + y * spacing_y can.saveState() can.translate(pos_x, pos_y) can.rotate(self.angle) can.drawCentredString(0, 0, self.watermark_text) can.restoreState() can.save() # 移动到数据开头 packet.seek(0) return PdfReader(packet) class PDFWatermarkTool(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PDF和图片全屏水印批量添加工具") self.setGeometry(100, 100, 1000, 800) # 初始化变量 self.input_folder = "" self.output_folder = "" self.color = QColor(74, 144, 226) # 默认蓝色 self.selected_font_path = "" # 存储选定的字体文件路径 self.worker = None # 设置应用程序样式 self.setup_styles() self.init_ui() def setup_styles(self): """设置应用程序样式""" self.setStyleSheet(""" QMainWindow { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f8f9fa, stop: 1 #e9ecef); font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; } QGroupBox { font-weight: bold; font-size: 12px; border: 2px solid #dee2e6; border-radius: 8px; margin-top: 1ex; padding-top: 10px; background: white; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 0 8px; background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6c757d, stop: 1 #495057); color: white; border-radius: 4px; } QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6c757d, stop: 1 #495057); color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5a6268, stop: 1 #3d4348); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #495057, stop: 1 #343a40); } QPushButton:disabled { background: #adb5bd; color: #6c757d; } QLineEdit, QSpinBox, QDoubleSpinBox, QFontComboBox { border: 2px solid #ced4da; border-radius: 6px; padding: 6px; background: white; selection-background-color: #4a90e2; } QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QFontComboBox:focus { border-color: #4a90e2; } QSlider::groove:horizontal { border: 1px solid #ced4da; height: 8px; background: #e9ecef; border-radius: 4px; } QSlider::handle:horizontal { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); border: 1px solid #5c7cfa; width: 18px; margin: -2px 0; border-radius: 9px; } QProgressBar { border: 2px solid #dee2e6; border-radius: 6px; text-align: center; background: white; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #20c997, stop: 1 #198754); border-radius: 4px; } QTextEdit { border: 2px solid #ced4da; border-radius: 6px; background: white; font-family: 'Consolas', 'Courier New', monospace; } QLabel { color: #2b2d42; } """) def init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) main_layout.setContentsMargins(20, 20, 20, 20) # 添加标题和说明 title_label = QLabel("🖼️ PDF和图片全屏水印批量添加工具") title_label.setStyleSheet(""" font-size: 18px; font-weight: bold; color: #2b2d42; padding: 10px; background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); border-radius: 8px; color: white; """) title_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(title_label) # 文件夹选择部分 folder_group = QGroupBox("📁 文件夹选择") folder_layout = QGridLayout() folder_layout.setSpacing(10) self.input_label = QLabel("📂 未选择输入文件夹") self.input_label.setStyleSheet(""" padding: 10px; background: #e9ecef; border-radius: 6px; border: 1px solid #ced4da; color: #495057; """) folder_layout.addWidget(self.input_label, 0, 0, 1, 2) self.input_btn = QPushButton("📁 选择输入文件夹") self.input_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); padding: 10px; } """) self.input_btn.clicked.connect(self.select_input_folder) folder_layout.addWidget(self.input_btn, 0, 2) self.output_label = QLabel("📂 未选择输出文件夹") self.output_label.setStyleSheet(""" padding: 10px; background: #e9ecef; border-radius: 6px; border: 1px solid #ced4da; color: #495057; """) folder_layout.addWidget(self.output_label, 1, 0, 1, 2) self.output_btn = QPushButton("📁 选择输出文件夹") self.output_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); padding: 10px; } """) self.output_btn.clicked.connect(self.select_output_folder) folder_layout.addWidget(self.output_btn, 1, 2) folder_group.setLayout(folder_layout) main_layout.addWidget(folder_group) # 水印设置部分 watermark_group = QGroupBox("🎨 水印设置") watermark_layout = QGridLayout() watermark_layout.setSpacing(12) watermark_layout.setContentsMargins(15, 20, 15, 15) # 第一行:水印文字 watermark_layout.addWidget(QLabel("💬 水印文字:"), 0, 0) self.watermark_text = QLineEdit("水印文字") self.watermark_text.setPlaceholderText("请输入水印内容...") watermark_layout.addWidget(self.watermark_text, 0, 1, 1, 2) # 第二行:字体选择 watermark_layout.addWidget(QLabel("🔤 字体选择:"), 1, 0) font_select_layout = QHBoxLayout() self.font_combo = QFontComboBox() font_select_layout.addWidget(self.font_combo) self.font_select_btn = QPushButton("📝 选择字体文件") self.font_select_btn.setStyleSheet("padding: 6px;") self.font_select_btn.clicked.connect(self.select_font_file) font_select_layout.addWidget(self.font_select_btn) watermark_layout.addLayout(font_select_layout, 1, 1, 1, 2) # 字体路径显示 self.font_path_label = QLabel("💾 默认使用系统字体") self.font_path_label.setStyleSheet(""" padding: 8px; background: #f8f9fa; border-radius: 4px; border: 1px dashed #ced4da; color: #6c757d; font-size: 10px; """) watermark_layout.addWidget(self.font_path_label, 2, 0, 1, 3) # 第三行:字体大小和颜色 watermark_layout.addWidget(QLabel("📏 字体大小:"), 3, 0) self.font_size = QSpinBox() self.font_size.setRange(10, 200) self.font_size.setValue(30) self.font_size.setSuffix(" px") watermark_layout.addWidget(self.font_size, 3, 1) watermark_layout.addWidget(QLabel("🎨 颜色:"), 4, 0) self.color_btn = QPushButton() self.color_btn.setFixedSize(80, 30) self.color_btn.clicked.connect(self.select_color) self.update_color_button() watermark_layout.addWidget(self.color_btn, 4, 1) # 第四行:角度和透明度 watermark_layout.addWidget(QLabel("🔄 角度:"), 5, 0) self.angle = QSpinBox() self.angle.setRange(0, 359) self.angle.setValue(45) self.angle.setSuffix("°") watermark_layout.addWidget(self.angle, 5, 1) watermark_layout.addWidget(QLabel("🌫️ 透明度:"), 6, 0) opacity_layout = QHBoxLayout() self.opacity = QDoubleSpinBox() self.opacity.setRange(0.01, 1.0) self.opacity.setSingleStep(0.01) self.opacity.setValue(0.15) opacity_layout.addWidget(self.opacity) self.opacity_slider = QSlider(Qt.Horizontal) self.opacity_slider.setRange(1, 100) self.opacity_slider.setValue(15) self.opacity_slider.valueChanged.connect(self.on_opacity_slider_changed) self.opacity.valueChanged.connect(self.on_opacity_spinbox_changed) opacity_layout.addWidget(self.opacity_slider) watermark_layout.addLayout(opacity_layout, 6, 1) # 第五行:密度 watermark_layout.addWidget(QLabel("🔢 密度:"), 7, 0) density_layout = QHBoxLayout() self.density = QSpinBox() self.density.setRange(3, 20) self.density.setValue(5) self.density.setToolTip("数值越大,水印越密集") density_layout.addWidget(self.density) self.density_slider = QSlider(Qt.Horizontal) self.density_slider.setRange(3, 20) self.density_slider.setValue(5) self.density_slider.valueChanged.connect(self.on_density_slider_changed) self.density.valueChanged.connect(self.on_density_spinbox_changed) density_layout.addWidget(self.density_slider) watermark_layout.addLayout(density_layout, 7, 1) watermark_group.setLayout(watermark_layout) main_layout.addWidget(watermark_group) # 预览和处理区域 preview_process_layout = QHBoxLayout() # 预览区域 preview_group = QGroupBox("👀 预览") preview_layout = QVBoxLayout() self.preview_label = QLabel("💧 水印预览区域") self.preview_label.setAlignment(Qt.AlignCenter) self.preview_label.setStyleSheet(""" padding: 30px; background: white; border: 2px dashed #4a90e2; border-radius: 8px; min-height: 120px; min-width: 200px; font-size: 16px; color: #495057; """) self.preview_label.setWordWrap(True) preview_layout.addWidget(self.preview_label) preview_group.setLayout(preview_layout) preview_process_layout.addWidget(preview_group, 1) # 进度区域 progress_group = QGroupBox("📊 进度") progress_layout = QVBoxLayout() self.progress_bar = QProgressBar() self.progress_bar.setValue(0) self.progress_bar.setFormat("🔄 处理中: %p%") progress_layout.addWidget(self.progress_bar) self.status_label = QLabel("✅ 准备就绪") self.status_label.setStyleSheet(""" padding: 12px; background: #e9ecef; border-radius: 6px; border: 1px solid #ced4da; color: #495057; font-weight: bold; """) progress_layout.addWidget(self.status_label) self.processed_files = QTextEdit() self.processed_files.setMaximumHeight(120) self.processed_files.setPlaceholderText("📋 已处理文件列表将显示在这里...") progress_layout.addWidget(self.processed_files) progress_group.setLayout(progress_layout) preview_process_layout.addWidget(progress_group, 1) main_layout.addLayout(preview_process_layout) # 按钮部分 button_layout = QHBoxLayout() button_layout.setSpacing(15) self.start_pdf_btn = QPushButton("🚀 开始添加PDF水印") self.start_pdf_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #20c997, stop: 1 #198754); padding: 12px; font-size: 14px; } """) self.start_pdf_btn.clicked.connect(lambda: self.start_processing('pdf')) button_layout.addWidget(self.start_pdf_btn) self.start_image_btn = QPushButton("🚀 开始添加图片水印") self.start_image_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); padding: 12px; font-size: 14px; } """) self.start_image_btn.clicked.connect(lambda: self.start_processing('image')) button_layout.addWidget(self.start_image_btn) self.cancel_btn = QPushButton("❌ 取消") self.cancel_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #dc3545, stop: 1 #c82333); padding: 12px; font-size: 14px; } """) self.cancel_btn.clicked.connect(self.cancel_processing) self.cancel_btn.setEnabled(False) button_layout.addWidget(self.cancel_btn) main_layout.addLayout(button_layout) # 底部信息栏 footer_label = QLabel("💡 提示: 支持PDF、JPG、PNG、BMP、GIF、TIFF、WEBP等格式 | 🛠️ 由白泽基于PyQt5和Python开发") footer_label.setStyleSheet(""" padding: 10px; background: #495057; color: white; border-radius: 6px; font-size: 11px; """) footer_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(footer_label) # 连接信号以自动更新预览 self.watermark_text.textChanged.connect(self.update_preview) self.font_combo.currentFontChanged.connect(self.update_preview) self.font_size.valueChanged.connect(self.update_preview) self.color_btn.clicked.connect(self.update_preview) self.angle.valueChanged.connect(self.update_preview) self.opacity.valueChanged.connect(self.update_preview) # 初始化预览 self.update_preview() def select_font_file(self): """选择字体文件""" font_file, _ = QFileDialog.getOpenFileName( self, "选择字体文件", "", "字体文件 (*.ttf *.ttc *.otf)" ) if font_file: self.selected_font_path = font_file self.font_path_label.setText(f"💾 已选择: {os.path.basename(font_file)}") self.update_preview() def select_input_folder(self): folder = QFileDialog.getExistingDirectory(self, "选择输入文件夹") if folder: self.input_folder = folder self.input_label.setText(f"📂 输入文件夹: {folder}") def select_output_folder(self): folder = QFileDialog.getExistingDirectory(self, "选择输出文件夹") if folder: self.output_folder = folder self.output_label.setText(f"📂 输出文件夹: {folder}") def select_color(self): color = QColorDialog.getColor(self.color, self, "选择水印颜色") if color.isValid(): self.color = color self.update_color_button() self.update_preview() def update_color_button(self): # 更新颜色按钮的背景色 palette = self.color_btn.palette() palette.setColor(QPalette.Button, self.color) self.color_btn.setPalette(palette) self.color_btn.setAutoFillBackground(True) self.color_btn.setFlat(True) self.color_btn.setText("🎨 " + self.color.name()) def update_preview(self): """更新水印预览 - 使用QPixmap创建真实的水印效果""" text = self.watermark_text.text() if not text: self.preview_label.setText("💬 请输入水印文字...") return # 创建预览图像 pixmap = QPixmap(300, 150) pixmap.fill(Qt.white) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) # 设置字体 font = self.font_combo.currentFont() font.setPointSize(self.font_size.value()) painter.setFont(font) # 设置颜色和透明度 color = self.color color.setAlphaF(self.opacity.value()) painter.setPen(QColor(color)) # 平移和旋转 painter.translate(150, 75) # 中心点 painter.rotate(self.angle.value()) # 绘制文本 - 使用整数坐标 text_rect = painter.fontMetrics().boundingRect(text) x_pos = int(-text_rect.width() / 2) y_pos = int(-text_rect.height() / 2) # 使用QRectF来绘制文本 text_rect = QRectF(x_pos, y_pos, text_rect.width(), text_rect.height()) painter.drawText(text_rect, Qt.AlignCenter, text) painter.end() # 设置预览图像 self.preview_label.setPixmap(pixmap) self.preview_label.setText("") # 清除文本 def on_opacity_slider_changed(self, value): """当透明度滑块改变时更新数值""" self.opacity.setValue(value / 100.0) self.update_preview() def on_opacity_spinbox_changed(self, value): """当透明度数值改变时更新滑块""" self.opacity_slider.setValue(int(value * 100)) self.update_preview() def on_density_slider_changed(self, value): """当密度滑块改变时更新数值""" self.density.setValue(value) def on_density_spinbox_changed(self, value): """当密度数值改变时更新滑块""" self.density_slider.setValue(value) def start_processing(self, file_type): # 验证输入 if not self.input_folder or not self.output_folder: QMessageBox.warning(self, "⚠️ 警告", "请先选择输入和输出文件夹") return if not self.watermark_text.text().strip(): QMessageBox.warning(self, "⚠️ 警告", "请输入水印文字") return # 检查输出文件夹是否与输入文件夹相同 if self.input_folder == self.output_folder: reply = QMessageBox.question( self, "❓ 确认", "⚠️ 输入和输出文件夹相同,这可能会覆盖原始文件。是否继续?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.No: return # 禁用开始按钮,启用取消按钮 self.start_pdf_btn.setEnabled(False) self.start_image_btn.setEnabled(False) self.cancel_btn.setEnabled(True) self.progress_bar.setValue(0) self.status_label.setText(f"🔄 正在处理{file_type.upper()}文件...") self.processed_files.clear() # 获取字体路径 font_path = self.selected_font_path # 创建工作线程 self.worker = WatermarkWorker( self.input_folder, self.output_folder, self.watermark_text.text(), self.font_size.value(), font_path, self.color, self.angle.value(), self.opacity.value(), self.density.value(), file_type ) # 连接信号 self.worker.progress.connect(self.update_progress) self.worker.finished.connect(self.processing_finished) self.worker.file_processed.connect(self.add_processed_file) # 启动线程 self.worker.start() def add_processed_file(self, filename): """添加已处理文件到列表""" current_text = self.processed_files.toPlainText() if current_text: current_text += "\n" current_text += f"✅ {filename}" self.processed_files.setPlainText(current_text) # 滚动到底部 self.processed_files.verticalScrollBar().setValue( self.processed_files.verticalScrollBar().maximum() ) def cancel_processing(self): if self.worker and self.worker.isRunning(): self.worker.cancel() self.status_label.setText("⏹️ 正在取消...") self.cancel_btn.setEnabled(False) def update_progress(self, value): self.progress_bar.setValue(value) if value == 100: self.progress_bar.setFormat("✅ 完成: %p%") else: self.progress_bar.setFormat(f"🔄 处理中: {value}%") def processing_finished(self, success, message): self.start_pdf_btn.setEnabled(True) self.start_image_btn.setEnabled(True) self.cancel_btn.setEnabled(False) if success: self.status_label.setText(f"✅ {message}") self.progress_bar.setFormat("✅ 完成: 100%") else: self.status_label.setText(f"❌ {message}") if not success: QMessageBox.warning(self, "❌ 处理完成", message) else: QMessageBox.information(self, "✅ 处理完成", message) if __name__ == "__main__": app = QApplication(sys.argv) # 设置应用程序图标和样式 app.setStyle('Fusion') window = PDFWatermarkTool() window.show() sys.exit(app.exec_())
项目结构
pdf-watermark-tool/
├── main.py # 主程序入口
├── requirements.txt # 依赖库列表
├── README.md # 项目说明
├── fonts/ # 字体文件目录
├── examples/ # 示例文件
└── tests/ # 测试文件
使用技巧和注意事项
最佳实践
- 水印文字选择:使用简洁有力的文字,如公司名称或用户名
- 透明度设置:建议设置在0.1-0.3之间,既不影响内容阅读又能起到保护作用
- 密度控制:根据文件类型调整密度,文档类可以密一些,图片类可以疏一些
- 字体选择:选择清晰易读的字体,避免使用过于花哨的字体
常见问题解决
- 中文显示乱码:确保选择了支持中文的字体文件
- 处理速度慢:减少同时处理的文件数量或降低水印密度
- 内存不足:分批处理大型文件
总结
本文详细介绍了一个基于PyQt5的全屏水印批量添加工具的开发和实现过程。通过这个项目,我们不仅学习到了:
- PyQt5 GUI开发:如何创建美观实用的桌面应用程序
- 多线程编程:如何使用QThread实现后台处理
- PDF处理技术:如何使用PyPDF2和ReportLab处理PDF文件
- 图像处理技术:如何使用Pillow库处理图片水印
- 字体处理:如何解决中文字体显示问题
这个工具不仅实用性强,而且代码结构清晰,易于扩展和维护。读者可以根据自己的需求进一步扩展功能,如添加图片水印支持、增加水印模板功能等。
水印技术是数字内容保护的重要手段,掌握这项技术对于开发者来说具有很高的实用价值。希望本文能够帮助读者深入理解水印技术的实现原理,并开发出更多有趣实用的应用。
以上就是Python+PyQt5开发一个全屏水印批量添加工具的详细内容,更多关于Python水印添加的资料请关注脚本之家其它相关文章!