Python+PyQt5開發(fā)一個全屏水印批量添加工具
概述
在數(shù)字化時代,保護知識產(chǎn)權(quán)和防止內(nèi)容盜用變得尤為重要。水印技術(shù)作為一種有效的內(nèi)容保護手段,被廣泛應用于文檔、圖片和視頻等多媒體內(nèi)容中。今天我們要介紹的是一款基于Python和PyQt5開發(fā)的全屏水印批量添加工具,它能夠為PDF文檔和多種圖片格式快速添加自定義水印。
這款工具不僅支持基本的文字水印功能,還提供了豐富的自定義選項,包括字體選擇、顏色調(diào)整、透明度設(shè)置、旋轉(zhuǎn)角度和密度控制等。通過多線程技術(shù),實現(xiàn)了高效的批量處理能力,大大提升了工作效率。
功能特性
核心功能
- 批量處理:支持同時處理多個PDF或圖片文件
- 多格式支持:PDF、JPG、PNG、BMP、GIF、TIFF、WEBP等格式
- 全屏水印:自動在整個頁面/圖片上平鋪水印內(nèi)容
- 高度自定義:字體、顏色、大小、角度、透明度、密度均可調(diào)整
技術(shù)特點
- 多線程處理:后臺線程處理,UI不卡頓
- 實時進度:顯示處理進度和已處理文件列表
- 實時預覽:實時查看水印效果
- 高效性能:優(yōu)化的算法確保處理速度
展示效果


PDF全屏水印效果:

圖片全屏水印效果:

系統(tǒng)架構(gòu)

軟件步驟說明
1. 環(huán)境準備
首先需要安裝必要的Python庫:
pip install PyPDF2 reportlab pillow PyQt5
2. 使用步驟
1.選擇文件夾:指定輸入和輸出文件夾
2.設(shè)置水印參數(shù):
- 輸入水印文字內(nèi)容
- 選擇字體和顏色
- 調(diào)整大小、角度和透明度
- 設(shè)置水印密度
3.預覽效果:實時查看水印樣式
4.開始處理:選擇處理PDF或圖片文件
5.查看結(jié)果:在輸出文件夾查看處理后的文件
3. 參數(shù)說明
| 參數(shù) | 說明 | 推薦值 |
|---|---|---|
| 字體大小 | 水印文字的大小 | 20-40px |
| 旋轉(zhuǎn)角度 | 水印的旋轉(zhuǎn)角度 | 30-45° |
| 透明度 | 水印的不透明度 | 0.1-0.3 |
| 密度 | 水印的分布密度 | 4-8 |
代碼解析
核心類結(jié)構(gòu)
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):
# 初始化參數(shù)
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文件 - 創(chuàng)建水印:使用
ReportLab在內(nèi)存中生成水印PDF - 合并水印:將水印PDF與原始PDF每一頁合并
- 保存結(jié)果:使用
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)
# 創(chuàng)建水印
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()讀取圖片 - 創(chuàng)建水印層:新建一個透明圖層用于繪制水印
- 繪制水印:根據(jù)密度參數(shù)計算水印位置并繪制
- 合并圖層:將水印層與原始圖片合并
- 保存結(jié)果:根據(jù)原格式保存圖片
def add_watermark_to_image(self, input_path, output_path):
try:
# 打開圖片
image = Image.open(input_path).convert('RGBA')
# 創(chuàng)建水印圖層
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
# 處理文件...
# 發(fā)送進度信號
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"發(fā)生錯誤: {str(e)}")
字體處理策略
為了解決中文字體顯示問題,實現(xiàn)了智能字體選擇機制:
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:
# 嘗試使用系統(tǒng)默認中文字體
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()
性能優(yōu)化策略
1. 內(nèi)存優(yōu)化
- 使用流式處理,避免一次性加載所有文件到內(nèi)存
- 及時釋放不再需要的資源
2. 處理速度優(yōu)化
- 多線程處理,充分利用多核CPU
- 算法優(yōu)化,減少不必要的計算
3. 用戶體驗優(yōu)化
- 實時預覽功能
- 進度反饋機制
- 錯誤處理和恢復機制
擴展功能
已實現(xiàn)功能
- 批量處理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
# 發(fā)送文件處理完成信號
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"發(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)
# 創(chuàng)建水印
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')
# 創(chuàng)建水印圖層
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
# 設(shè)置顏色和透明度
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
# 創(chuàng)建文本圖像并旋轉(zhuǎn)
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)
# 旋轉(zhuǎn)文本
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)
# 保存圖片(根據(jù)原格式保存)
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:
# 嘗試使用系統(tǒng)默認字體
try:
# Windows系統(tǒng)常見中文字體
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):
"""創(chuàng)建水印PDF"""
# 創(chuàng)建內(nèi)存中的PDF
packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=(page_width, page_height))
# 設(shè)置顏色和透明度
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:
# 嘗試使用系統(tǒng)默認中文字體
try:
# Windows系統(tǒng)常見中文字體
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()
# 移動到數(shù)據(jù)開頭
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
# 設(shè)置應用程序樣式
self.setup_styles()
self.init_ui()
def setup_styles(self):
"""設(shè)置應用程序樣式"""
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)
# 水印設(shè)置部分
watermark_group = QGroupBox("?? 水印設(shè)置")
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("請輸入水印內(nèi)容...")
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("?? 默認使用系統(tǒng)字體")
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("數(shù)值越大,水印越密集")
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)
# 預覽和處理區(qū)域
preview_process_layout = QHBoxLayout()
# 預覽區(qū)域
preview_group = QGroupBox("?? 預覽")
preview_layout = QVBoxLayout()
self.preview_label = QLabel("?? 水印預覽區(qū)域")
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)
# 進度區(qū)域
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開發(fā)")
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創(chuàng)建真實的水印效果"""
text = self.watermark_text.text()
if not text:
self.preview_label.setText("?? 請輸入水印文字...")
return
# 創(chuàng)建預覽圖像
pixmap = QPixmap(300, 150)
pixmap.fill(Qt.white)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
# 設(shè)置字體
font = self.font_combo.currentFont()
font.setPointSize(self.font_size.value())
painter.setFont(font)
# 設(shè)置顏色和透明度
color = self.color
color.setAlphaF(self.opacity.value())
painter.setPen(QColor(color))
# 平移和旋轉(zhuǎn)
painter.translate(150, 75) # 中心點
painter.rotate(self.angle.value())
# 繪制文本 - 使用整數(shù)坐標
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()
# 設(shè)置預覽圖像
self.preview_label.setPixmap(pixmap)
self.preview_label.setText("") # 清除文本
def on_opacity_slider_changed(self, value):
"""當透明度滑塊改變時更新數(shù)值"""
self.opacity.setValue(value / 100.0)
self.update_preview()
def on_opacity_spinbox_changed(self, value):
"""當透明度數(shù)值改變時更新滑塊"""
self.opacity_slider.setValue(int(value * 100))
self.update_preview()
def on_density_slider_changed(self, value):
"""當密度滑塊改變時更新數(shù)值"""
self.density.setValue(value)
def on_density_spinbox_changed(self, value):
"""當密度數(shù)值改變時更新滑塊"""
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,
"? 確認",
"?? 輸入和輸出文件夾相同,這可能會覆蓋原始文件。是否繼續(xù)?",
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
# 創(chuàng)建工作線程
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)
# 設(shè)置應用程序圖標和樣式
app.setStyle('Fusion')
window = PDFWatermarkTool()
window.show()
sys.exit(app.exec_())
項目結(jié)構(gòu)
pdf-watermark-tool/
├── main.py # 主程序入口
├── requirements.txt # 依賴庫列表
├── README.md # 項目說明
├── fonts/ # 字體文件目錄
├── examples/ # 示例文件
└── tests/ # 測試文件
使用技巧和注意事項
最佳實踐
- 水印文字選擇:使用簡潔有力的文字,如公司名稱或用戶名
- 透明度設(shè)置:建議設(shè)置在0.1-0.3之間,既不影響內(nèi)容閱讀又能起到保護作用
- 密度控制:根據(jù)文件類型調(diào)整密度,文檔類可以密一些,圖片類可以疏一些
- 字體選擇:選擇清晰易讀的字體,避免使用過于花哨的字體
常見問題解決
- 中文顯示亂碼:確保選擇了支持中文的字體文件
- 處理速度慢:減少同時處理的文件數(shù)量或降低水印密度
- 內(nèi)存不足:分批處理大型文件
總結(jié)
本文詳細介紹了一個基于PyQt5的全屏水印批量添加工具的開發(fā)和實現(xiàn)過程。通過這個項目,我們不僅學習到了:
- PyQt5 GUI開發(fā):如何創(chuàng)建美觀實用的桌面應用程序
- 多線程編程:如何使用QThread實現(xiàn)后臺處理
- PDF處理技術(shù):如何使用PyPDF2和ReportLab處理PDF文件
- 圖像處理技術(shù):如何使用Pillow庫處理圖片水印
- 字體處理:如何解決中文字體顯示問題
這個工具不僅實用性強,而且代碼結(jié)構(gòu)清晰,易于擴展和維護。讀者可以根據(jù)自己的需求進一步擴展功能,如添加圖片水印支持、增加水印模板功能等。
水印技術(shù)是數(shù)字內(nèi)容保護的重要手段,掌握這項技術(shù)對于開發(fā)者來說具有很高的實用價值。希望本文能夠幫助讀者深入理解水印技術(shù)的實現(xiàn)原理,并開發(fā)出更多有趣實用的應用。
以上就是Python+PyQt5開發(fā)一個全屏水印批量添加工具的詳細內(nèi)容,更多關(guān)于Python水印添加的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一分鐘帶你上手Python調(diào)用DeepSeek的API
最近DeepSeek非常火,作為一枚對前言技術(shù)非常關(guān)注的程序員來說,自然都想對接DeepSeek的API來體驗一把,下面小編就來為大家介紹一下Python如何快速上手調(diào)用DeepSeek?API吧2025-02-02
利用Python實現(xiàn)Shp格式向GeoJSON的轉(zhuǎn)換方法
JSON(JavaScript Object Nonation)是利用鍵值對+嵌套來表示數(shù)據(jù)的一種格式,以其輕量、易解析的優(yōu)點,這篇文章主要介紹了利用Python實現(xiàn)Shp格式向GeoJSON的轉(zhuǎn)換,需要的朋友可以參考下2019-07-07
PyQt6中QWidget 和QMainWindow的區(qū)別小結(jié)
QWidget?和?QMainWindow?是 PyQt 中兩個常用的類,它們在功能和用途上有顯著區(qū)別,本文主要介紹了PyQt6中QWidget 和QMainWindow的區(qū)別小結(jié),感興趣的可以了解一下2025-05-05
Python實現(xiàn)JSON數(shù)據(jù)動態(tài)生成思維導圖圖片
這篇文章主要為大家詳細介紹了Python如何實現(xiàn)將JSON格式數(shù)據(jù)動態(tài)生成思維導圖圖片,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下2025-02-02
基于Flask+websocket實現(xiàn)一個在線聊天室
在今天的互聯(lián)網(wǎng)時代,實時通信成為了許多應用和服務的核心特色,在本文中,我們將介紹如何使用 Flask 和 Websockets 通過 Flask-SocketIO 框架創(chuàng)建一個簡單的在線聊天室,感興趣的可以跟隨小編一起了解下2023-09-09

