python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python水印添加

Python+PyQt5开发一个全屏水印批量添加工具

作者:创客白泽

本文详细介绍了如何使用Python和PyQt5开发一个功能强大的全屏水印批量添加工具,支持PDF和多种图片格式,具备完整的GUI界面和批量处理能力,希望对大家有所帮助

概述

在数字化时代,保护知识产权和防止内容盗用变得尤为重要。水印技术作为一种有效的内容保护手段,被广泛应用于文档、图片和视频等多媒体内容中。今天我们要介绍的是一款基于Python和PyQt5开发的全屏水印批量添加工具,它能够为PDF文档和多种图片格式快速添加自定义水印。

这款工具不仅支持基本的文字水印功能,还提供了丰富的自定义选项,包括字体选择、颜色调整、透明度设置、旋转角度和密度控制等。通过多线程技术,实现了高效的批量处理能力,大大提升了工作效率。

功能特性

核心功能

技术特点

展示效果

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水印处理使用了PyPDF2ReportLab两个库:

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库:

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. 处理速度优化

3. 用户体验优化

扩展功能

已实现功能

未来可扩展功能

源码下载

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/              # 测试文件

使用技巧和注意事项

最佳实践

常见问题解决

总结

本文详细介绍了一个基于PyQt5的全屏水印批量添加工具的开发和实现过程。通过这个项目,我们不仅学习到了:

这个工具不仅实用性强,而且代码结构清晰,易于扩展和维护。读者可以根据自己的需求进一步扩展功能,如添加图片水印支持、增加水印模板功能等。

水印技术是数字内容保护的重要手段,掌握这项技术对于开发者来说具有很高的实用价值。希望本文能够帮助读者深入理解水印技术的实现原理,并开发出更多有趣实用的应用。

以上就是Python+PyQt5开发一个全屏水印批量添加工具的详细内容,更多关于Python水印添加的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文