Python+PyQt5實(shí)現(xiàn)PDF批量水印工具
PyQt5 的 PDF 批量水印工具:
本文圍繞 PDF添加水印.py,完整講解如何將單文件水印腳本重構(gòu)為可交互的 PyQt5 桌面應(yīng)用,支持拖拽導(dǎo)入、實(shí)時(shí)預(yù)覽、子線程處理與輸出管理,并給出關(guān)鍵實(shí)現(xiàn)細(xì)節(jié)與結(jié)構(gòu)說明。

一、功能概覽
這款工具面向“批量給 PDF 添加文字水印”的場景,核心功能如下:
- 支持拖拽或選擇多個(gè) PDF 文件
- 參數(shù)可配置:字體、字號、行間距、旋轉(zhuǎn)角度、透明度
- 實(shí)時(shí)預(yù)覽旋轉(zhuǎn)與透明度效果
- 使用子線程處理,避免界面卡頓
- 輸出目錄統(tǒng)一為
./output/并自動(dòng)重命名 - 常見錯(cuò)誤(空列表、字體缺失、權(quán)限不足)彈窗提示
二、整體架構(gòu)與模塊拆分
應(yīng)用主要分為三層:
UI 層(WatermarkApp)
負(fù)責(zé)界面布局、參數(shù)采集、按鈕交互、預(yù)覽觸發(fā)、狀態(tài)展示等。
處理層(WatermarkWorker / PreviewWorker)
使用 QThread 執(zhí)行批量水印與預(yù)覽生成,避免阻塞主線程。
工具層(字體解析 / 輸出重名 / 水印生成)
封裝公共邏輯,確保主流程清晰且復(fù)用性強(qiáng)。
三、界面布局設(shè)計(jì)
界面布局遵循“左列表、右參數(shù)、底部操作區(qū)”的結(jié)構(gòu)。
1. 左側(cè):PDF 文件列表 + 添加/刪除
- 列表控件支持拖拽(繼承
QListWidget重寫拖拽事件) - 添加按鈕打開文件對話框
- 刪除按鈕移除選中項(xiàng)
2. 右側(cè):參數(shù)面板(按順序分組)
- 水印文字:
QLineEdit - 字體選擇:
QComboBox(系統(tǒng)字體列表) - 行間距:
QSpinBox(0–200%) - 字號:
QSpinBox(8–200pt) - 角度:
QSlider + QSpinBox(-90–90°) - 透明度:
QSlider + QSpinBox(0–100%) - 預(yù)覽:
QLabel顯示動(dòng)態(tài)渲染圖
3. 底部:操作按鈕 + 進(jìn)度/狀態(tài)
- “開始處理”
- “預(yù)覽效果”
- “打開輸出目錄”
- 進(jìn)度條 + 狀態(tài)提示文本
四、拖拽導(dǎo)入實(shí)現(xiàn)
自定義 PdfListWidget,重寫拖拽相關(guān)事件:
dragEnterEvent/dragMoveEvent:校驗(yàn) URLdropEvent:過濾.pdf文件并添加到列表
這樣用戶可以直接把 PDF 拖進(jìn)列表,極大提升效率。
五、實(shí)時(shí)預(yù)覽機(jī)制
預(yù)覽的核心思想是:
- 使用當(dāng)前 UI 參數(shù)生成一個(gè)水印 PDF
- 將水印合成到原 PDF 的第一頁
- 保存為臨時(shí)預(yù)覽文件并自動(dòng)打開
實(shí)現(xiàn)上通過 PreviewWorker(QThread) 執(zhí)行生成邏輯,以避免 UI 卡頓。
預(yù)覽按鈕會(huì)優(yōu)先使用“選中項(xiàng)”,若未選中則默認(rèn)使用第一個(gè) PDF。
六、子線程批量處理
處理邏輯放在 WatermarkWorker(QThread) 中完成:
- 讀取每個(gè) PDF
- 對每一頁合成水印
- 保存到
./output目錄 - 通過信號更新進(jìn)度條與狀態(tài)文本
七、水印生成邏輯
水印生成使用 ReportLab,在內(nèi)存中創(chuàng)建一頁 PDF:
- 讀取當(dāng)前頁面大?。ù_保適配不同尺寸 PDF)
- 在中心點(diǎn)平移 + 旋轉(zhuǎn)
- 按固定網(wǎng)格繪制水印文本
- 輸出為
BytesIO內(nèi)存對象
關(guān)鍵點(diǎn):
- 行間距與透明度全部來自 UI 參數(shù)
- 字體通過系統(tǒng)字體注冊,缺失時(shí)拋錯(cuò)
八、字體選擇與注冊
字體下拉框基于系統(tǒng)字體列表:
QFontDatabase().families()獲取可用字體- 默認(rèn)選“宋體”
字體注冊則通過 Windows 注冊表查詢真實(shí)字體文件路徑,然后用 ReportLab 注冊:
- 在注冊表中查找字體名稱
- 拼接字體文件路徑
pdfmetrics.registerFont(TTFont(...))
這樣既能保證預(yù)覽字體一致,也能保證 PDF 輸出字體可用。
九、輸出策略與重名機(jī)制
輸出統(tǒng)一保存到:
./output/原文件名_watermarked.pdf
如果文件已存在,會(huì)自動(dòng)追加序號:
xxx_watermarked_1.pdf xxx_watermarked_2.pdf
這一邏輯由 unique_output_path 負(fù)責(zé),避免覆蓋歷史結(jié)果。
十、錯(cuò)誤處理與彈窗提示
以下情況會(huì)觸發(fā)彈窗警告:
- PDF 列表為空
- 字體文件缺失
- 沒有讀寫權(quán)限
- 缺少 PyPDF2 依賴
這樣可以避免用戶在無感知情況下操作失敗。
十一、運(yùn)行方式
直接運(yùn)行腳本即可啟動(dòng) GUI:
python d:\測試\公眾號水文\11-PDF添加水印\PDF添加水印.py
如需打包為 EXE,可使用 PyInstaller(另行說明)。
十二、總結(jié)與擴(kuò)展建議
當(dāng)前版本已經(jīng)具備完整的批量水印能力,并具備以下優(yōu)勢:
- 參數(shù)全可配置
- UI 友好
- 處理不卡頓
- 輸出安全可靠
后續(xù)可考慮的擴(kuò)展方向:
- 增加顏色選擇
- 自定義橫向/縱向密度
- 多行預(yù)覽與縮略圖預(yù)覽
- 輸出目錄可自定義
如需進(jìn)一步增強(qiáng)功能或增加更多參數(shù),我可以繼續(xù)擴(kuò)展優(yōu)化。
完整代碼
import io
import os
import sys
import winreg
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont, QFontDatabase, QPainter, QPixmap
from PyQt5.QtWidgets import (
QApplication,
QFileDialog,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QProgressBar,
QSlider,
QSpinBox,
QDoubleSpinBox,
QVBoxLayout,
QWidget,
QComboBox,
QAbstractItemView,
QSizePolicy,
)
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
try:
from PyPDF2 import PdfReader, PdfWriter
except Exception:
PdfReader = None
PdfWriter = None
def find_font_file(font_family):
fonts_dir = os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts")
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
) as key:
index = 0
while True:
try:
name, data, _ = winreg.EnumValue(key, index)
index += 1
except OSError:
break
if font_family.lower() in name.lower():
if os.path.isabs(data):
return data
return os.path.join(fonts_dir, data)
except OSError:
pass
return None
def unique_output_path(output_dir, base_name):
output_path = os.path.join(output_dir, base_name)
if not os.path.exists(output_path):
return output_path
name, ext = os.path.splitext(base_name)
counter = 1
while True:
candidate = os.path.join(output_dir, f"{name}_{counter}{ext}")
if not os.path.exists(candidate):
return candidate
counter += 1
def get_reportlab_font(font_family, font_cache):
if font_family in font_cache:
return font_cache[font_family]
font_path = find_font_file(font_family)
if not font_path or not os.path.exists(font_path):
raise FileNotFoundError(f"字體文件缺失:{font_family}")
font_key = f"font_{len(font_cache)}"
pdfmetrics.registerFont(TTFont(font_key, font_path))
font_cache[font_family] = font_key
return font_key
def create_watermark_pdf_buffer(width, height, options, font_cache):
buffer = io.BytesIO()
watermark_canvas = canvas.Canvas(buffer, pagesize=(width, height))
font_name = get_reportlab_font(options["font_family"], font_cache)
watermark_canvas.setFont(font_name, options["font_size"])
watermark_canvas.setFillAlpha(options["opacity"] / 100)
watermark_canvas.translate(width / 2, height / 2)
watermark_canvas.rotate(options["angle"])
x_step = width / 5
y_step = height / 10 * (options["line_spacing"] / 100)
for i in range(5):
for j in range(10):
a = (i - 2) * x_step
b = (j - 4) * y_step
watermark_canvas.drawString(a, b, options["text"])
watermark_canvas.save()
buffer.seek(0)
return buffer
class PdfListWidget(QListWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
if not event.mimeData().hasUrls():
return
for url in event.mimeData().urls():
path = url.toLocalFile()
if path.lower().endswith(".pdf"):
self.add_pdf_item(path)
def add_pdf_item(self, path):
for i in range(self.count()):
if self.item(i).text() == path:
return
item = QListWidgetItem(path)
self.addItem(item)
def selected_paths(self):
return [item.text() for item in self.selectedItems()]
def all_paths(self):
return [self.item(i).text() for i in range(self.count())]
class WatermarkWorker(QThread):
progress_changed = pyqtSignal(int)
status_changed = pyqtSignal(str)
error_occurred = pyqtSignal(str)
finished_success = pyqtSignal(str)
def __init__(self, files, options, parent=None):
super().__init__(parent)
self.files = files
self.options = options
self._font_cache = {}
def run(self):
if PdfReader is None or PdfWriter is None:
self.error_occurred.emit("缺少 PyPDF2 庫,請先安裝 PyPDF2。")
return
try:
output_dir = os.path.join(os.getcwd(), "output")
os.makedirs(output_dir, exist_ok=True)
except Exception as exc:
self.error_occurred.emit(f"無法創(chuàng)建輸出目錄:{exc}")
return
total = len(self.files)
for index, file_path in enumerate(self.files, start=1):
try:
self.status_changed.emit(f"處理中:{os.path.basename(file_path)}")
output_name = f"{os.path.splitext(os.path.basename(file_path))[0]}_watermarked.pdf"
output_path = unique_output_path(output_dir, output_name)
reader = PdfReader(file_path)
writer = PdfWriter()
for page in reader.pages:
width = float(page.mediabox.width)
height = float(page.mediabox.height)
watermark_pdf = create_watermark_pdf_buffer(
width,
height,
self.options,
self._font_cache,
)
watermark_reader = PdfReader(watermark_pdf)
watermark_page = watermark_reader.pages[0]
page.merge_page(watermark_page)
writer.add_page(page)
with open(output_path, "wb") as output_file:
writer.write(output_file)
progress = int(index / total * 100)
self.progress_changed.emit(progress)
except PermissionError:
self.error_occurred.emit("文件權(quán)限不足,無法讀寫 PDF。")
return
except Exception as exc:
self.error_occurred.emit(f"處理失?。簕exc}")
return
self.progress_changed.emit(100)
self.status_changed.emit("處理完成")
self.finished_success.emit(output_dir)
class PreviewWorker(QThread):
status_changed = pyqtSignal(str)
error_occurred = pyqtSignal(str)
preview_ready = pyqtSignal(str)
def __init__(self, file_path, options, parent=None):
super().__init__(parent)
self.file_path = file_path
self.options = options
self._font_cache = {}
def run(self):
if PdfReader is None or PdfWriter is None:
self.error_occurred.emit("缺少 PyPDF2 庫,請先安裝 PyPDF2。")
return
try:
output_dir = os.path.join(os.getcwd(), "output")
os.makedirs(output_dir, exist_ok=True)
except Exception as exc:
self.error_occurred.emit(f"無法創(chuàng)建輸出目錄:{exc}")
return
try:
self.status_changed.emit("生成預(yù)覽中")
reader = PdfReader(self.file_path)
if not reader.pages:
self.error_occurred.emit("PDF 內(nèi)容為空,無法預(yù)覽。")
return
writer = PdfWriter()
page = reader.pages[0]
width = float(page.mediabox.width)
height = float(page.mediabox.height)
watermark_pdf = create_watermark_pdf_buffer(
width,
height,
self.options,
self._font_cache,
)
watermark_reader = PdfReader(watermark_pdf)
watermark_page = watermark_reader.pages[0]
page.merge_page(watermark_page)
writer.add_page(page)
base_name = os.path.splitext(os.path.basename(self.file_path))[0]
output_name = f"{base_name}_preview.pdf"
output_path = unique_output_path(output_dir, output_name)
with open(output_path, "wb") as output_file:
writer.write(output_file)
self.preview_ready.emit(output_path)
except PermissionError:
self.error_occurred.emit("文件權(quán)限不足,無法讀寫 PDF。")
except Exception as exc:
self.error_occurred.emit(f"預(yù)覽失?。簕exc}")
class WatermarkApp(QWidget):
def __init__(self):
super().__init__()
self.worker = None
self.preview_worker = None
self.init_ui()
def init_ui(self):
self.setWindowTitle("PDF 水印工具")
self.resize(1100, 650)
main_layout = QVBoxLayout(self)
content_layout = QHBoxLayout()
main_layout.addLayout(content_layout)
left_layout = QVBoxLayout()
content_layout.addLayout(left_layout, 3)
self.pdf_list = PdfListWidget()
self.pdf_list.setToolTip("拖拽或通過按鈕添加 PDF 文件")
left_layout.addWidget(self.pdf_list)
list_button_layout = QHBoxLayout()
left_layout.addLayout(list_button_layout)
self.add_button = QPushButton("添加 PDF")
self.add_button.setToolTip("選擇并添加 PDF 文件")
self.add_button.clicked.connect(self.add_files)
list_button_layout.addWidget(self.add_button)
self.remove_button = QPushButton("刪除選中")
self.remove_button.setToolTip("刪除列表中選中的 PDF 文件")
self.remove_button.clicked.connect(self.remove_selected)
list_button_layout.addWidget(self.remove_button)
right_layout = QVBoxLayout()
content_layout.addLayout(right_layout, 4)
self.text_group = QGroupBox("水印文字")
self.text_group.setToolTip("設(shè)置水印顯示的文字內(nèi)容")
text_layout = QVBoxLayout()
self.text_input = QLineEdit("python-小莊辦公")
self.text_input.setToolTip("輸入水印文字內(nèi)容")
self.text_input.textChanged.connect(self.update_preview)
text_layout.addWidget(self.text_input)
self.text_group.setLayout(text_layout)
right_layout.addWidget(self.text_group)
self.font_group = QGroupBox("字體選擇器")
self.font_group.setToolTip("選擇系統(tǒng)已安裝字體")
font_layout = QVBoxLayout()
self.font_combo = QComboBox()
self.font_combo.setToolTip("從系統(tǒng)字體列表中選擇字體")
font_layout.addWidget(self.font_combo)
self.font_group.setLayout(font_layout)
right_layout.addWidget(self.font_group)
self.spacing_group = QGroupBox("水印行間距")
self.spacing_group.setToolTip("調(diào)整水印縱向間距")
spacing_layout = QHBoxLayout()
self.spacing_spin = QSpinBox()
self.spacing_spin.setToolTip("設(shè)置水印行間距百分比")
self.spacing_spin.setRange(0, 200)
self.spacing_spin.setSingleStep(1)
self.spacing_spin.setSuffix(" %")
self.spacing_spin.setValue(100)
self.spacing_spin.valueChanged.connect(self.update_preview)
spacing_layout.addWidget(self.spacing_spin)
self.spacing_group.setLayout(spacing_layout)
right_layout.addWidget(self.spacing_group)
self.size_group = QGroupBox("水印字體大小")
self.size_group.setToolTip("設(shè)置水印字體大小")
size_layout = QHBoxLayout()
self.size_spin = QSpinBox()
self.size_spin.setToolTip("設(shè)置水印字體大小(pt)")
self.size_spin.setRange(8, 200)
self.size_spin.setSingleStep(1)
self.size_spin.setSuffix(" pt")
self.size_spin.setValue(36)
self.size_spin.valueChanged.connect(self.update_preview)
size_layout.addWidget(self.size_spin)
self.size_group.setLayout(size_layout)
right_layout.addWidget(self.size_group)
self.angle_group = QGroupBox("旋轉(zhuǎn)角度")
self.angle_group.setToolTip("調(diào)整水印旋轉(zhuǎn)角度")
angle_layout = QHBoxLayout()
self.angle_slider = QSlider(Qt.Horizontal)
self.angle_slider.setToolTip("拖動(dòng)調(diào)整旋轉(zhuǎn)角度")
self.angle_slider.setRange(-90, 90)
self.angle_slider.setSingleStep(1)
self.angle_slider.setValue(0)
self.angle_spin = QSpinBox()
self.angle_spin.setToolTip("輸入旋轉(zhuǎn)角度")
self.angle_spin.setRange(-90, 90)
self.angle_spin.setSingleStep(1)
self.angle_spin.setValue(0)
angle_layout.addWidget(self.angle_slider)
angle_layout.addWidget(self.angle_spin)
self.angle_group.setLayout(angle_layout)
right_layout.addWidget(self.angle_group)
self.opacity_group = QGroupBox("透明度")
self.opacity_group.setToolTip("調(diào)整水印透明度")
opacity_layout = QHBoxLayout()
self.opacity_slider = QSlider(Qt.Horizontal)
self.opacity_slider.setToolTip("拖動(dòng)調(diào)整透明度")
self.opacity_slider.setRange(0, 100)
self.opacity_slider.setSingleStep(1)
self.opacity_slider.setValue(30)
self.opacity_spin = QSpinBox()
self.opacity_spin.setToolTip("輸入透明度百分比")
self.opacity_spin.setRange(0, 100)
self.opacity_spin.setSingleStep(1)
self.opacity_spin.setValue(30)
opacity_layout.addWidget(self.opacity_slider)
opacity_layout.addWidget(self.opacity_spin)
self.opacity_group.setLayout(opacity_layout)
right_layout.addWidget(self.opacity_group)
self.preview_group = QGroupBox("預(yù)覽")
self.preview_group.setToolTip("實(shí)時(shí)預(yù)覽水印效果")
preview_layout = QVBoxLayout()
self.preview_label = QLabel()
self.preview_label.setToolTip("顯示水印旋轉(zhuǎn)與透明度預(yù)覽")
self.preview_label.setMinimumHeight(140)
self.preview_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.preview_label.setAlignment(Qt.AlignCenter)
preview_layout.addWidget(self.preview_label)
self.preview_group.setLayout(preview_layout)
right_layout.addWidget(self.preview_group)
right_layout.addStretch()
bottom_layout = QHBoxLayout()
main_layout.addLayout(bottom_layout)
self.start_button = QPushButton("開始處理")
self.start_button.setToolTip("開始給列表中的 PDF 添加水印")
self.start_button.clicked.connect(self.start_processing)
bottom_layout.addWidget(self.start_button)
self.preview_button = QPushButton("預(yù)覽效果")
self.preview_button.setToolTip("生成當(dāng)前參數(shù)下的水印預(yù)覽文件")
self.preview_button.clicked.connect(self.start_preview)
bottom_layout.addWidget(self.preview_button)
self.open_output_button = QPushButton("打開輸出目錄")
self.open_output_button.setToolTip("打開輸出文件夾")
self.open_output_button.clicked.connect(self.open_output_dir)
bottom_layout.addWidget(self.open_output_button)
self.progress_bar = QProgressBar()
self.progress_bar.setToolTip("顯示處理進(jìn)度")
self.progress_bar.setValue(0)
bottom_layout.addWidget(self.progress_bar, 3)
self.status_label = QLabel("就緒")
self.status_label.setToolTip("顯示當(dāng)前處理狀態(tài)")
bottom_layout.addWidget(self.status_label, 2)
self.load_fonts()
self.bind_signals()
self.update_preview()
def load_fonts(self):
db = QFontDatabase()
families = db.families()
self.font_combo.addItems(families)
default_font = "宋體"
if default_font in families:
self.font_combo.setCurrentText(default_font)
elif families:
self.font_combo.setCurrentIndex(0)
self.font_combo.currentTextChanged.connect(self.update_preview)
def bind_signals(self):
self.angle_slider.valueChanged.connect(self.angle_spin.setValue)
self.angle_spin.valueChanged.connect(self.angle_slider.setValue)
self.angle_slider.valueChanged.connect(self.update_preview)
self.angle_spin.valueChanged.connect(self.update_preview)
self.opacity_slider.valueChanged.connect(self.opacity_spin.setValue)
self.opacity_spin.valueChanged.connect(self.opacity_slider.setValue)
self.opacity_slider.valueChanged.connect(self.update_preview)
self.opacity_spin.valueChanged.connect(self.update_preview)
def update_preview(self):
width = max(self.preview_label.width(), 300)
height = max(self.preview_label.height(), 140)
pixmap = QPixmap(width, height)
pixmap.fill(Qt.white)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
font = QFont(self.font_combo.currentText(), self.size_spin.value())
painter.setFont(font)
painter.setOpacity(self.opacity_spin.value() / 100)
painter.translate(width / 2, height / 2)
painter.rotate(self.angle_spin.value())
painter.drawText(-width // 4, 0, self.text_input.text())
painter.end()
self.preview_label.setPixmap(pixmap)
def add_files(self):
files, _ = QFileDialog.getOpenFileNames(
self,
"選擇 PDF 文件",
"",
"PDF 文件 (*.pdf)",
)
for file_path in files:
self.pdf_list.add_pdf_item(file_path)
def remove_selected(self):
for item in self.pdf_list.selectedItems():
row = self.pdf_list.row(item)
self.pdf_list.takeItem(row)
def start_processing(self):
files = self.pdf_list.all_paths()
if not files:
QMessageBox.warning(self, "提示", "請先添加需要處理的 PDF 文件。")
return
if PdfReader is None or PdfWriter is None:
QMessageBox.warning(self, "提示", "未安裝 PyPDF2,無法處理 PDF。")
return
font_path = find_font_file(self.font_combo.currentText())
if not font_path or not os.path.exists(font_path):
QMessageBox.warning(self, "提示", "所選字體文件缺失,請更換字體。")
return
options = {
"text": self.text_input.text().strip() or " ",
"font_family": self.font_combo.currentText(),
"line_spacing": self.spacing_spin.value(),
"font_size": self.size_spin.value(),
"angle": self.angle_spin.value(),
"opacity": self.opacity_spin.value(),
}
self.progress_bar.setValue(0)
self.status_label.setText("準(zhǔn)備處理")
self.start_button.setEnabled(False)
self.preview_button.setEnabled(False)
self.worker = WatermarkWorker(files, options)
self.worker.progress_changed.connect(self.progress_bar.setValue)
self.worker.status_changed.connect(self.status_label.setText)
self.worker.error_occurred.connect(self.handle_error)
self.worker.finished_success.connect(self.handle_success)
self.worker.start()
def handle_error(self, message):
self.start_button.setEnabled(True)
self.preview_button.setEnabled(True)
self.status_label.setText("處理失敗")
QMessageBox.warning(self, "錯(cuò)誤", message)
def handle_success(self, output_dir):
self.start_button.setEnabled(True)
self.preview_button.setEnabled(True)
self.status_label.setText("處理完成")
QMessageBox.information(self, "完成", f"水印處理完成,輸出目錄:{output_dir}")
def start_preview(self):
files = self.pdf_list.selected_paths() or self.pdf_list.all_paths()
if not files:
QMessageBox.warning(self, "提示", "請先添加需要預(yù)覽的 PDF 文件。")
return
if PdfReader is None or PdfWriter is None:
QMessageBox.warning(self, "提示", "未安裝 PyPDF2,無法預(yù)覽。")
return
font_path = find_font_file(self.font_combo.currentText())
if not font_path or not os.path.exists(font_path):
QMessageBox.warning(self, "提示", "所選字體文件缺失,請更換字體。")
return
options = {
"text": self.text_input.text().strip() or " ",
"font_family": self.font_combo.currentText(),
"line_spacing": self.spacing_spin.value(),
"font_size": self.size_spin.value(),
"angle": self.angle_spin.value(),
"opacity": self.opacity_spin.value(),
}
self.status_label.setText("生成預(yù)覽")
self.preview_button.setEnabled(False)
self.start_button.setEnabled(False)
self.preview_worker = PreviewWorker(files[0], options)
self.preview_worker.status_changed.connect(self.status_label.setText)
self.preview_worker.error_occurred.connect(self.handle_error)
self.preview_worker.preview_ready.connect(self.handle_preview_ready)
self.preview_worker.start()
def handle_preview_ready(self, preview_path):
self.start_button.setEnabled(True)
self.preview_button.setEnabled(True)
self.status_label.setText("預(yù)覽完成")
try:
os.startfile(preview_path)
except Exception as exc:
QMessageBox.warning(self, "提示", f"無法打開預(yù)覽文件:{exc}")
def open_output_dir(self):
output_dir = os.path.join(os.getcwd(), "output")
if not os.path.exists(output_dir):
QMessageBox.warning(self, "提示", "輸出目錄不存在,請先處理文件。")
return
try:
os.startfile(output_dir)
except Exception as exc:
QMessageBox.warning(self, "提示", f"無法打開輸出目錄:{exc}")
def main():
app = QApplication(sys.argv)
window = WatermarkApp()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
以上就是Python+PyQt5實(shí)現(xiàn)PDF批量水印工具的詳細(xì)內(nèi)容,更多關(guān)于Python PDF批量水印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python在多玩圖片上下載妹子圖的實(shí)現(xiàn)代碼
學(xué)python的第二天,想寫個(gè)東西出來玩玩,于是就寫了這個(gè),供那些才學(xué)一天的參考參考也行2013-08-08
pyqt5使用按鈕進(jìn)行界面的跳轉(zhuǎn)方法
今天小編就為大家分享一篇pyqt5使用按鈕進(jìn)行界面的跳轉(zhuǎn)方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06
python生成xml時(shí)規(guī)定dtd實(shí)例方法
在本篇文章里小編給大家整理的是關(guān)于python生成xml時(shí)規(guī)定dtd實(shí)例方法,需要的朋友們學(xué)習(xí)參考下。2020-09-09
淺談keras中的目標(biāo)函數(shù)和優(yōu)化函數(shù)MSE用法
這篇文章主要介紹了淺談keras中的目標(biāo)函數(shù)和優(yōu)化函數(shù)MSE用法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06

