Files
nl-utils/v1/views/pdf_tools_page.py
2025-06-08 23:35:27 +08:00

1189 lines
42 KiB
Python

import os
import re
import random
import datetime
import shutil
import fitz
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QListWidget, QListWidgetItem,
QFileDialog, QProgressBar, QTabWidget, QLineEdit,
QGroupBox, QRadioButton, QAbstractItemView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent, QDragLeaveEvent
# 常量定义 - 默认保存位置
DEFAULT_SAVE_PATH = os.path.join(os.path.expanduser("~"), "Documents", "PDFTools")
if not os.path.exists(DEFAULT_SAVE_PATH):
os.makedirs(DEFAULT_SAVE_PATH)
def generate_unique_filename(original_path, prefix="", suffix="", ext="pdf", save_dir=DEFAULT_SAVE_PATH):
"""生成唯一的文件名并保存到指定目录"""
base_name = os.path.basename(original_path)
name_without_ext = os.path.splitext(base_name)[0]
if prefix:
name_without_ext = f"{prefix}_{name_without_ext}"
if suffix:
name_without_ext = f"{name_without_ext}_{suffix}"
# 获取当前日期
current_date = datetime.datetime.now().strftime("%Y%m%d")
# 生成4位随机数
random_num = random.randint(1000, 9999)
# 组合成新文件名
new_filename = f"{name_without_ext}_{current_date}_{random_num}.{ext}"
return os.path.join(save_dir, new_filename)
class PDFSplitterThread(QThread):
progress_updated = pyqtSignal(int, int, str) # current, total, filename
conversion_done = pyqtSignal(str, bool, str) # filename, success, message
finished_all = pyqtSignal()
def __init__(self, pdf_path, split_mode, pages_range="", save_dir=DEFAULT_SAVE_PATH):
super().__init__()
self.pdf_path = pdf_path
self.split_mode = split_mode # "single", "range", "bookmarks"
self.pages_range = pages_range
self.save_dir = save_dir
self.output_files = []
def run(self):
try:
doc = fitz.open(self.pdf_path)
total_pages = len(doc)
if self.split_mode == "single":
self.progress_updated.emit(0, total_pages, os.path.basename(self.pdf_path))
for i in range(total_pages):
new_doc = fitz.open()
new_doc.insert_pdf(doc, from_page=i, to_page=i)
output_path = generate_unique_filename(
self.pdf_path,
suffix=f"page_{i + 1}",
save_dir=self.save_dir
)
new_doc.save(output_path)
new_doc.close()
self.output_files.append(output_path)
self.conversion_done.emit(self.pdf_path, True, output_path)
self.progress_updated.emit(i + 1, total_pages, os.path.basename(output_path))
self.finished_all.emit()
elif self.split_mode == "range":
if not self.pages_range:
self.conversion_done.emit(self.pdf_path, False, "请提供有效的页码范围")
return
ranges = self.parse_range(self.pages_range, total_pages)
if not ranges:
self.conversion_done.emit(self.pdf_path, False, "无效的页码范围")
return
self.progress_updated.emit(0, len(ranges), os.path.basename(self.pdf_path))
for idx, (start, end) in enumerate(ranges):
new_doc = fitz.open()
new_doc.insert_pdf(doc, from_page=start - 1, to_page=end - 1)
output_path = generate_unique_filename(
self.pdf_path,
suffix=f"pages_{start}-{end}",
save_dir=self.save_dir
)
new_doc.save(output_path)
new_doc.close()
self.output_files.append(output_path)
self.conversion_done.emit(self.pdf_path, True, output_path)
self.progress_updated.emit(idx + 1, len(ranges), os.path.basename(output_path))
self.finished_all.emit()
elif self.split_mode == "bookmarks":
toc = doc.get_toc()
if not toc:
self.conversion_done.emit(self.pdf_path, False, "该PDF文件没有书签")
return
bookmarks = {}
for item in toc:
if item[0] == 1:
bookmarks[item[1]] = item[2]
self.progress_updated.emit(0, len(bookmarks), os.path.basename(self.pdf_path))
bookmark_names = list(bookmarks.keys())
for idx, (name, page) in enumerate(bookmarks.items()):
end_page = bookmarks[bookmark_names[idx + 1]] - 1 if idx < len(bookmarks) - 1 else total_pages
new_doc = fitz.open()
new_doc.insert_pdf(doc, from_page=page - 1, to_page=end_page - 1)
clean_name = re.sub(r'[\\/*?:"<>|]', "", name)
output_path = generate_unique_filename(
self.pdf_path,
prefix=clean_name,
save_dir=self.save_dir
)
new_doc.save(output_path)
new_doc.close()
self.output_files.append(output_path)
self.conversion_done.emit(self.pdf_path, True, output_path)
self.progress_updated.emit(idx + 1, len(bookmarks), os.path.basename(output_path))
self.finished_all.emit()
doc.close()
except Exception as e:
self.conversion_done.emit(self.pdf_path, False, str(e))
def parse_range(self, range_str, total_pages):
ranges = []
parts = range_str.split(',')
for part in parts:
if '-' in part:
start_end = part.split('-')
if len(start_end) != 2:
return []
try:
start = int(start_end[0])
end = int(start_end[1])
if 1 <= start <= end <= total_pages:
ranges.append((start, end))
except ValueError:
return []
else:
try:
page = int(part)
if 1 <= page <= total_pages:
ranges.append((page, page))
except ValueError:
return []
return ranges
class PDFMergerThread(QThread):
progress_updated = pyqtSignal(int, int, str) # current, total, filename
conversion_done = pyqtSignal(bool, str) # success, message
finished_all = pyqtSignal()
def __init__(self, pdf_files, save_dir=DEFAULT_SAVE_PATH):
super().__init__()
self.pdf_files = pdf_files
self.save_dir = save_dir
self.output_file = ""
def run(self):
try:
if not self.pdf_files:
self.conversion_done.emit(False, "没有要合并的文件")
return
output = fitz.open()
self.progress_updated.emit(0, len(self.pdf_files), "")
for i, file_path in enumerate(self.pdf_files):
doc = fitz.open(file_path)
output.insert_pdf(doc)
doc.close()
self.progress_updated.emit(i + 1, len(self.pdf_files), os.path.basename(file_path))
base_file = os.path.basename(self.pdf_files[0])
self.output_file = generate_unique_filename(
base_file,
prefix="merged",
save_dir=self.save_dir
)
output.save(self.output_file)
output.close()
self.conversion_done.emit(True, self.output_file)
self.finished_all.emit()
except Exception as e:
self.conversion_done.emit(False, str(e))
class PDFToolsPage(QWidget):
def __init__(self):
super().__init__()
self.split_save_dir = DEFAULT_SAVE_PATH
self.merge_save_dir = DEFAULT_SAVE_PATH
self.init_ui()
self.setStyleSheet(self.get_page_style())
def init_ui(self):
self.setObjectName("pdfToolsPage")
self.setAcceptDrops(True)
# 创建选项卡
self.tab_widget = QTabWidget()
self.tab_widget.setObjectName("toolTabs")
self.tab_widget.setTabPosition(QTabWidget.North)
# 创建拆分工具页
self.splitter_page = QWidget()
self.setup_splitter_page()
# 创建合并工具页
self.merger_page = QWidget()
self.setup_merger_page()
# 添加选项卡
self.tab_widget.addTab(self.splitter_page, "PDF拆分")
self.tab_widget.addTab(self.merger_page, "PDF合并")
# 主布局
layout = QVBoxLayout()
layout.addWidget(self.tab_widget)
self.setLayout(layout)
# 当前线程
self.current_thread = None
self.merger_files = []
self.split_output_files = []
self.merger_output_file = ""
self.current_pdf = ""
def setup_splitter_page(self):
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15) # 减少间距使布局更紧凑
# 文件选择区域
file_group = QGroupBox("选择PDF文件")
file_group.setObjectName("fileGroup")
file_layout = QVBoxLayout(file_group)
# 上传区域
self.upload_area = QFrame()
self.upload_area.setObjectName("uploadArea")
self.upload_area.setFixedHeight(120)
self.upload_area.setAcceptDrops(True)
upload_layout = QVBoxLayout(self.upload_area)
upload_layout.setAlignment(Qt.AlignCenter)
self.upload_icon = QLabel()
self.upload_icon.setPixmap(
QPixmap(":/icons/pdf.png").scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.upload_icon.setAlignment(Qt.AlignCenter)
self.upload_text = QLabel("拖拽PDF文件到此处\n或点击上传")
self.upload_text.setAlignment(Qt.AlignCenter)
self.upload_text.setWordWrap(True)
upload_btn = QPushButton("选择PDF文件")
upload_btn.setObjectName("uploadBtn")
upload_btn.clicked.connect(self.handle_split_upload)
upload_layout.addWidget(self.upload_icon)
upload_layout.addWidget(self.upload_text)
upload_layout.addWidget(upload_btn)
file_layout.addWidget(self.upload_area)
# 文件信息
file_info_layout = QHBoxLayout()
file_info_layout.setSpacing(10)
self.file_name_label = QLabel("未选择文件")
self.file_name_label.setObjectName("fileInfo")
self.page_count_label = QLabel("")
self.page_count_label.setObjectName("fileInfo")
file_info_layout.addWidget(QLabel("文件:"))
file_info_layout.addWidget(self.file_name_label, 1)
file_info_layout.addSpacing(20)
file_info_layout.addWidget(QLabel("页数:"))
file_info_layout.addWidget(self.page_count_label)
file_info_layout.addStretch()
file_layout.addLayout(file_info_layout)
# 保存位置设置
save_layout = QHBoxLayout()
save_layout.setSpacing(10)
save_layout.addWidget(QLabel("保存位置:"))
self.save_path_label = QLabel(self.split_save_dir)
self.save_path_label.setObjectName("savePathLabel")
self.save_path_label.setMinimumWidth(200)
browse_btn = QPushButton("浏览")
browse_btn.setObjectName("browseBtn")
browse_btn.setFixedWidth(80)
browse_btn.clicked.connect(self.browse_split_save_dir)
save_layout.addWidget(self.save_path_label, 1)
save_layout.addWidget(browse_btn)
file_layout.addLayout(save_layout)
# 拆分选项
options_group = QGroupBox("拆分选项")
options_group.setObjectName("optionsGroup")
options_layout = QVBoxLayout(options_group)
# 拆分模式选择
mode_group = QGroupBox("拆分模式")
mode_layout = QVBoxLayout(mode_group)
self.mode_single = QRadioButton("每页拆分为单独文件")
self.mode_single.setChecked(True)
self.mode_range = QRadioButton("按页码范围拆分")
self.mode_bookmarks = QRadioButton("按书签/章节拆分")
mode_layout.addWidget(self.mode_single)
mode_layout.addWidget(self.mode_range)
mode_layout.addWidget(self.mode_bookmarks)
# 页码范围输入
range_layout = QHBoxLayout()
range_layout.addWidget(QLabel("页码范围 (如 1-3,5,7-9):"))
self.range_input = QLineEdit()
self.range_input.setPlaceholderText("1-3,5,7-9")
self.range_input.setEnabled(False)
range_layout.addWidget(self.range_input, 1)
self.mode_single.toggled.connect(lambda: self.range_input.setEnabled(False))
self.mode_range.toggled.connect(lambda: self.range_input.setEnabled(True))
self.mode_bookmarks.toggled.connect(lambda: self.range_input.setEnabled(False))
options_layout.addWidget(mode_group)
options_layout.addLayout(range_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.split_btn = QPushButton("开始拆分")
self.split_btn.setObjectName("actionBtn")
self.split_btn.clicked.connect(self.start_split)
self.split_btn.setEnabled(False)
self.download_split_btn = QPushButton("下载所有拆分文件")
self.download_split_btn.setObjectName("downloadBtn")
self.download_split_btn.clicked.connect(self.download_split_files)
self.download_split_btn.setEnabled(False)
btn_layout.addWidget(self.split_btn)
btn_layout.addWidget(self.download_split_btn)
# 进度区域
progress_group = QGroupBox("进度")
progress_group.setObjectName("progressGroup")
progress_layout = QVBoxLayout(progress_group)
# 总体进度
total_layout = QHBoxLayout()
total_label = QLabel("总体进度:")
total_label.setObjectName("progressLabel")
self.total_progress_split = QProgressBar()
self.total_progress_split.setObjectName("totalProgress")
self.total_progress_split.setTextVisible(False)
self.total_progress_split.setMaximum(100)
total_layout.addWidget(total_label)
total_layout.addWidget(self.total_progress_split, 1)
# 状态标签
self.status_label_split = QLabel("就绪")
self.status_label_split.setObjectName("statusLabel")
self.status_label_split.setAlignment(Qt.AlignCenter)
progress_layout.addLayout(total_layout)
progress_layout.addWidget(self.status_label_split)
# 结果列表
result_group = QGroupBox("拆分结果")
result_group.setObjectName("resultGroup")
result_layout = QVBoxLayout(result_group)
self.result_list_split = QListWidget()
self.result_list_split.setObjectName("resultList")
self.result_list_split.setSelectionMode(QListWidget.ExtendedSelection)
result_layout.addWidget(self.result_list_split)
# 组装布局
layout.addWidget(file_group)
layout.addWidget(options_group)
layout.addLayout(btn_layout)
layout.addWidget(progress_group)
layout.addWidget(result_group)
self.splitter_page.setLayout(layout)
def setup_merger_page(self):
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15) # 减少间距使布局更紧凑
# 文件选择区域
file_group = QGroupBox("选择要合并的PDF文件")
file_group.setObjectName("fileGroup")
file_layout = QVBoxLayout(file_group)
# 上传区域
self.merger_upload_area = QFrame()
self.merger_upload_area.setObjectName("uploadArea")
self.merger_upload_area.setFixedHeight(120)
self.merger_upload_area.setAcceptDrops(True)
upload_layout = QVBoxLayout(self.merger_upload_area)
upload_layout.setAlignment(Qt.AlignCenter)
self.merger_upload_icon = QLabel()
self.merger_upload_icon.setPixmap(
QPixmap(":/icons/merge.png").scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.merger_upload_icon.setAlignment(Qt.AlignCenter)
self.merger_upload_text = QLabel("拖拽PDF文件到此处\n或点击上传")
self.merger_upload_text.setAlignment(Qt.AlignCenter)
self.merger_upload_text.setWordWrap(True)
upload_btn = QPushButton("选择PDF文件")
upload_btn.setObjectName("uploadBtn")
upload_btn.clicked.connect(self.handle_merger_upload)
upload_layout.addWidget(self.merger_upload_icon)
upload_layout.addWidget(self.merger_upload_text)
upload_layout.addWidget(upload_btn)
file_layout.addWidget(self.merger_upload_area)
# 保存位置设置
save_layout = QHBoxLayout()
save_layout.setSpacing(10)
save_layout.addWidget(QLabel("保存位置:"))
self.merger_save_path_label = QLabel(self.merge_save_dir)
self.merger_save_path_label.setObjectName("savePathLabel")
self.merger_save_path_label.setMinimumWidth(200)
browse_btn = QPushButton("浏览")
browse_btn.setObjectName("browseBtn")
browse_btn.setFixedWidth(80)
browse_btn.clicked.connect(self.browse_merge_save_dir)
save_layout.addWidget(self.merger_save_path_label, 1)
save_layout.addWidget(browse_btn)
file_layout.addLayout(save_layout)
# 文件列表
file_list_layout = QHBoxLayout()
self.merger_file_list = QListWidget()
self.merger_file_list.setObjectName("fileList")
self.merger_file_list.setSelectionMode(QListWidget.ExtendedSelection)
self.merger_file_list.setDragDropMode(QAbstractItemView.InternalMove)
# 操作按钮
btn_layout = QVBoxLayout()
btn_layout.setSpacing(10)
self.up_btn = QPushButton("上移")
self.up_btn.setObjectName("smallBtn")
self.up_btn.clicked.connect(self.move_file_up)
self.down_btn = QPushButton("下移")
self.down_btn.setObjectName("smallBtn")
self.down_btn.clicked.connect(self.move_file_down)
self.delete_btn = QPushButton("删除选中")
self.delete_btn.setObjectName("smallBtn")
self.delete_btn.clicked.connect(self.delete_selected_merger_files)
self.clear_btn = QPushButton("清空列表")
self.clear_btn.setObjectName("smallBtn")
self.clear_btn.clicked.connect(self.clear_merger_files)
btn_layout.addWidget(self.up_btn)
btn_layout.addWidget(self.down_btn)
btn_layout.addWidget(self.delete_btn)
btn_layout.addWidget(self.clear_btn)
btn_layout.addStretch()
file_list_layout.addWidget(self.merger_file_list, 1)
file_list_layout.addLayout(btn_layout)
file_layout.addLayout(file_list_layout)
# 操作按钮
action_layout = QHBoxLayout()
self.merge_btn = QPushButton("开始合并")
self.merge_btn.setObjectName("actionBtn")
self.merge_btn.clicked.connect(self.start_merge)
self.merge_btn.setEnabled(False)
self.download_merge_btn = QPushButton("下载合并文件")
self.download_merge_btn.setObjectName("downloadBtn")
self.download_merge_btn.clicked.connect(self.download_merge_file)
self.download_merge_btn.setEnabled(False)
action_layout.addWidget(self.merge_btn)
action_layout.addWidget(self.download_merge_btn)
# 进度区域
progress_group = QGroupBox("进度")
progress_group.setObjectName("progressGroup")
progress_layout = QVBoxLayout(progress_group)
# 总体进度
total_layout = QHBoxLayout()
total_label = QLabel("总体进度:")
total_label.setObjectName("progressLabel")
self.total_progress_merge = QProgressBar()
self.total_progress_merge.setObjectName("totalProgress")
self.total_progress_merge.setTextVisible(False)
self.total_progress_merge.setMaximum(100)
total_layout.addWidget(total_label)
total_layout.addWidget(self.total_progress_merge, 1)
# 状态标签
self.status_label_merge = QLabel("就绪")
self.status_label_merge.setObjectName("statusLabel")
self.status_label_merge.setAlignment(Qt.AlignCenter)
progress_layout.addLayout(total_layout)
progress_layout.addWidget(self.status_label_merge)
# 结果区域
result_group = QGroupBox("合并结果")
result_group.setObjectName("resultGroup")
result_layout = QVBoxLayout(result_group)
self.result_label_merge = QLabel("尚未合并任何文件")
self.result_label_merge.setObjectName("resultLabel")
self.result_label_merge.setWordWrap(True)
self.result_label_merge.setAlignment(Qt.AlignCenter)
result_layout.addWidget(self.result_label_merge)
# 组装布局
layout.addWidget(file_group)
layout.addLayout(action_layout)
layout.addWidget(progress_group)
layout.addWidget(result_group)
self.merger_page.setLayout(layout)
# 拖拽事件处理 - 拆分页面
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
# 根据当前标签页决定使用哪个上传区域
if self.tab_widget.currentIndex() == 0: # 拆分页面
target_area = self.upload_area
else: # 合并页面
target_area = self.merger_upload_area
target_area.setProperty("dragActive", True)
target_area.style().polish(target_area)
event.acceptProposedAction()
def dragLeaveEvent(self, event: QDragLeaveEvent):
if self.tab_widget.currentIndex() == 0: # 拆分页面
self.upload_area.setProperty("dragActive", False)
self.upload_area.style().polish(self.upload_area)
else: # 合并页面
self.merger_upload_area.setProperty("dragActive", False)
self.merger_upload_area.style().polish(self.merger_upload_area)
def dropEvent(self, event: QDropEvent):
if self.tab_widget.currentIndex() == 0: # 拆分页面
self.upload_area.setProperty("dragActive", False)
self.upload_area.style().polish(self.upload_area)
self.handle_split_drop(event)
else: # 合并页面
self.merger_upload_area.setProperty("dragActive", False)
self.merger_upload_area.style().polish(self.merger_upload_area)
self.handle_merge_drop(event)
def handle_split_drop(self, event):
"""处理拆分页面的文件拖放"""
urls = event.mimeData().urls()
if not urls:
return
# 只取第一个有效的PDF文件
for url in urls:
file_path = url.toLocalFile()
if os.path.isfile(file_path) and file_path.lower().endswith('.pdf'):
self.load_pdf_file(file_path)
return # 只处理一个文件
self.status_label_split.setText("请拖放有效的PDF文件")
def handle_merge_drop(self, event):
"""处理合并页面的文件拖放"""
urls = event.mimeData().urls()
files = []
for url in urls:
path = url.toLocalFile()
if os.path.isdir(path):
# 扫描文件夹中的PDF文件
for root, _, filenames in os.walk(path):
for f in filenames:
if f.lower().endswith('.pdf'):
files.append(os.path.join(root, f))
elif os.path.isfile(path) and path.lower().endswith('.pdf'):
files.append(path)
if files:
self.add_merger_files(files)
self.status_label_merge.setText(f"添加了 {len(files)} 个文件")
else:
self.status_label_merge.setText("未找到有效的PDF文件")
def browse_split_save_dir(self):
"""选择拆分文件的保存位置"""
directory = QFileDialog.getExistingDirectory(
self,
"选择保存位置",
self.split_save_dir
)
if directory:
self.split_save_dir = directory
self.save_path_label.setText(directory)
self.status_label_split.setText(f"保存位置已更新: {directory}")
def browse_merge_save_dir(self):
"""选择合并文件的保存位置"""
directory = QFileDialog.getExistingDirectory(
self,
"选择保存位置",
self.merge_save_dir
)
if directory:
self.merge_save_dir = directory
self.merger_save_path_label.setText(directory)
self.status_label_merge.setText(f"保存位置已更新: {directory}")
def handle_split_upload(self):
options = QFileDialog.Options()
file, _ = QFileDialog.getOpenFileName(
self, "选择PDF文件", "", "PDF Files (*.pdf);;All Files (*)", options=options
)
if file and file.lower().endswith('.pdf'):
self.load_pdf_file(file)
def load_pdf_file(self, file_path):
"""加载PDF文件并显示信息"""
self.current_pdf = file_path
self.file_name_label.setText(os.path.basename(file_path))
try:
# 获取PDF页数
doc = fitz.open(file_path)
page_count = len(doc)
doc.close()
self.page_count_label.setText(str(page_count))
self.split_btn.setEnabled(True)
self.status_label_split.setText("文件已加载,请选择拆分方式")
self.upload_icon.setPixmap(
QPixmap(":/icons/pdf_ready.png").scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.upload_text.setText(f"已选择: {os.path.basename(file_path)}")
except Exception as e:
self.status_label_split.setText(f"文件加载失败: {str(e)}")
self.page_count_label.setText("0")
self.split_btn.setEnabled(False)
def start_split(self):
"""开始拆分PDF"""
if not self.current_pdf:
self.status_label_split.setText("请先选择PDF文件")
return
# 确定拆分模式
if self.mode_single.isChecked():
split_mode = "single"
elif self.mode_range.isChecked():
split_mode = "range"
if not self.range_input.text().strip():
self.status_label_split.setText("请输入有效的页码范围")
return
else: # bookmarks
split_mode = "bookmarks"
# 重置状态
self.result_list_split.clear()
self.split_output_files = []
self.download_split_btn.setEnabled(False)
self.split_btn.setEnabled(False)
self.status_label_split.setText("正在拆分文件...")
self.total_progress_split.setValue(0)
# 创建并启动拆分线程
self.current_thread = PDFSplitterThread(
self.current_pdf,
split_mode,
self.range_input.text() if split_mode == "range" else "",
self.split_save_dir # 传递保存位置
)
self.current_thread.progress_updated.connect(self.update_split_progress)
self.current_thread.conversion_done.connect(self.handle_split_done)
self.current_thread.finished_all.connect(self.handle_split_finished)
self.current_thread.start()
def update_split_progress(self, current, total, filename):
"""更新拆分进度"""
self.total_progress_split.setMaximum(total)
self.total_progress_split.setValue(current)
self.status_label_split.setText(f"正在拆分: {filename} ({current}/{total})")
def handle_split_done(self, file_path, success, output_path):
"""处理单个拆分完成"""
if success:
self.split_output_files.append(output_path)
item = QListWidgetItem(os.path.basename(output_path))
item.setData(Qt.UserRole, output_path)
self.result_list_split.addItem(item)
self.status_label_split.setText(f"已拆分: {os.path.basename(output_path)}")
def handle_split_finished(self):
"""所有拆分完成"""
self.split_btn.setEnabled(True)
self.download_split_btn.setEnabled(bool(self.split_output_files))
self.status_label_split.setText(f"拆分完成! 共生成 {len(self.split_output_files)} 个文件")
self.total_progress_split.setValue(self.total_progress_split.maximum())
# 添加提示
self.status_label_split.setText(
f"拆分完成! 共生成 {len(self.split_output_files)} 个文件\n"
f"文件保存在: {self.split_save_dir}"
)
def download_split_files(self):
"""下载所有拆分文件"""
if not self.split_output_files:
self.status_label_split.setText("没有可下载的文件")
return
folder = QFileDialog.getExistingDirectory(self, "选择保存位置")
if not folder:
self.status_label_split.setText("下载已取消")
return
success = 0
for file_path in self.split_output_files:
try:
file_name = os.path.basename(file_path)
destination = os.path.join(folder, file_name)
shutil.copy2(file_path, destination)
success += 1
except Exception as e:
self.status_label_split.setText(f"下载失败: {file_name} - {str(e)}")
self.status_label_split.setText(f"下载完成! 成功: {success}/{len(self.split_output_files)}")
def handle_merger_upload(self):
"""处理合并页面的文件上传"""
options = QFileDialog.Options()
files, _ = QFileDialog.getOpenFileNames(
self, "选择PDF文件", "", "PDF Files (*.pdf);;All Files (*)", options=options
)
if files:
self.add_merger_files(files)
def add_merger_files(self, files):
"""添加合并文件"""
added = 0
for file in files:
if os.path.isfile(file) and file.lower().endswith('.pdf') and file not in self.merger_files:
self.merger_files.append(file)
item = QListWidgetItem(os.path.basename(file))
item.setData(Qt.UserRole, file)
self.merger_file_list.addItem(item)
added += 1
if added:
self.merge_btn.setEnabled(True)
self.status_label_merge.setText(f"添加了 {added} 个文件")
else:
self.status_label_merge.setText("未添加新文件")
def delete_selected_merger_files(self):
"""删除选中的合并文件"""
selected_items = self.merger_file_list.selectedItems()
if not selected_items:
self.status_label_merge.setText("请选择要删除的文件")
return
for item in selected_items:
row = self.merger_file_list.row(item)
self.merger_files.pop(row)
self.merger_file_list.takeItem(row)
self.status_label_merge.setText(f"删除了 {len(selected_items)} 个文件")
if not self.merger_files:
self.merge_btn.setEnabled(False)
def clear_merger_files(self):
"""清空合并文件列表"""
self.merger_files = []
self.merger_file_list.clear()
self.merge_btn.setEnabled(False)
self.status_label_merge.setText("文件列表已清空")
def move_file_up(self):
"""上移选中的文件"""
selected_items = self.merger_file_list.selectedItems()
if not selected_items:
return
for item in selected_items:
row = self.merger_file_list.row(item)
if row > 0:
# 交换文件顺序
self.merger_files[row], self.merger_files[row - 1] = self.merger_files[row - 1], self.merger_files[row]
# 交换列表项
current_item = self.merger_file_list.takeItem(row)
self.merger_file_list.insertItem(row - 1, current_item)
current_item.setSelected(True)
# 更新显示
self.merger_file_list.setCurrentRow(row - 1)
def move_file_down(self):
"""下移选中的文件"""
selected_items = self.merger_file_list.selectedItems()
if not selected_items:
return
# 从下往上处理,避免索引变化
for item in reversed(selected_items):
row = self.merger_file_list.row(item)
if row < self.merger_file_list.count() - 1:
# 交换文件顺序
self.merger_files[row], self.merger_files[row + 1] = self.merger_files[row + 1], self.merger_files[row]
# 交换列表项
current_item = self.merger_file_list.takeItem(row)
self.merger_file_list.insertItem(row + 1, current_item)
current_item.setSelected(True)
# 更新显示
self.merger_file_list.setCurrentRow(row + 1)
def start_merge(self):
"""开始合并PDF"""
if not self.merger_files:
self.status_label_merge.setText("请添加要合并的文件")
return
if len(self.merger_files) < 2:
self.status_label_merge.setText("至少需要2个文件才能合并")
return
# 重置状态
self.merger_output_file = ""
self.download_merge_btn.setEnabled(False)
self.merge_btn.setEnabled(False)
self.status_label_merge.setText("正在合并文件...")
self.total_progress_merge.setValue(0)
self.result_label_merge.setText("处理中...")
# 创建并启动合并线程
self.current_thread = PDFMergerThread(
self.merger_files.copy(),
self.merge_save_dir # 传递保存位置
)
self.current_thread.progress_updated.connect(self.update_merge_progress)
self.current_thread.conversion_done.connect(self.handle_merge_done)
self.current_thread.finished_all.connect(self.handle_merge_finished)
self.current_thread.start()
def update_merge_progress(self, current, total, filename):
"""更新合并进度"""
self.total_progress_merge.setMaximum(total)
self.total_progress_merge.setValue(current)
self.status_label_merge.setText(f"正在处理: {filename} ({current}/{total})")
def handle_merge_done(self, success, output_path):
"""处理合并完成"""
if success:
self.merger_output_file = output_path
self.result_label_merge.setText(
f"合并成功!\n输出文件: {os.path.basename(output_path)}\n"
f"保存位置: {self.merge_save_dir}"
)
else:
self.result_label_merge.setText(f"合并失败: {output_path}")
def handle_merge_finished(self):
"""合并线程完成"""
self.merge_btn.setEnabled(True)
self.download_merge_btn.setEnabled(bool(self.merger_output_file))
self.status_label_merge.setText("合并完成!")
self.total_progress_merge.setValue(self.total_progress_merge.maximum())
def download_merge_file(self):
"""下载合并后的PDF文件"""
if not self.merger_output_file or not os.path.exists(self.merger_output_file):
self.status_label_merge.setText("没有可下载的文件")
return
options = QFileDialog.Options()
file_name, _ = QFileDialog.getSaveFileName(
self, "保存合并的PDF文件",
os.path.basename(self.merger_output_file),
"PDF Files (*.pdf)",
options=options
)
if file_name:
try:
shutil.copy2(self.merger_output_file, file_name)
self.status_label_merge.setText(f"文件已保存到: {os.path.basename(file_name)}")
except Exception as e:
self.status_label_merge.setText(f"保存失败: {str(e)}")
def get_page_style(self):
return """
/* ===== PDF工具页面样式 ===== */
#pdfToolsPage {
background-color: #1a202c;
font-family: 'Microsoft YaHei', Arial, sans-serif;
}
/* 选项卡样式 */
#toolTabs::pane {
border: none;
background: transparent;
}
#toolTabs > QTabBar::tab {
background-color: #2d3748;
color: #cbd5e0;
border: 1px solid #4a5568;
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 8px 16px;
margin-right: 4px;
font-size: 14px;
}
#toolTabs > QTabBar::tab:selected {
background-color: #4f46e5;
color: #ffffff;
font-weight: bold;
}
#toolTabs > QTabBar::tab:hover {
background-color: #4a5568;
}
/* 分组框样式 */
#fileGroup, #optionsGroup, #progressGroup, #resultGroup {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
background-color: #4f46e5;
color: white;
border-radius: 4px;
margin-top: -10px;
}
/* 上传区域 */
#uploadArea {
border: 2px dashed #4a5568;
background-color: rgba(45, 55, 72, 0.5);
border-radius: 8px;
transition: all 0.3s ease;
}
#uploadArea:hover {
border-color: #818cf8;
background-color: rgba(129, 140, 248, 0.1);
}
#uploadArea[dragActive="true"] {
border-color: #4ade80;
background-color: rgba(74, 222, 128, 0.1);
}
/* 文件信息 */
#fileInfo {
background-color: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 5px 10px;
color: #e2e8f0;
font-size: 13px;
}
/* 保存位置标签 */
#savePathLabel {
background-color: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 5px 10px;
color: #a0aec0;
font-size: 13px;
}
/* 按钮样式 */
#uploadBtn, #actionBtn, #downloadBtn, #smallBtn, #browseBtn {
min-height: 36px;
font-weight: 500;
border-radius: 6px;
padding: 5px 15px;
}
#uploadBtn {
min-width: 150px;
background-color: #4f46e5;
color: white;
}
#uploadBtn:hover {
background-color: #4338ca;
}
#actionBtn {
background-color: #10b981;
color: white;
}
#actionBtn:hover {
background-color: #059669;
}
#actionBtn:disabled {
background-color: #4a5568;
color: #a0aec0;
}
#downloadBtn {
background-color: #f59e0b;
color: white;
}
#downloadBtn:hover {
background-color: #d97706;
}
#downloadBtn:disabled {
background-color: #4a5568;
color: #a0aec0;
}
#smallBtn {
min-height: 30px;
background-color: #4a5568;
color: #e2e8f0;
font-size: 12px;
}
#smallBtn:hover {
background-color: #2d3748;
}
#browseBtn {
background-color: #4a5568;
color: #e2e8f0;
}
#browseBtn:hover {
background-color: #2d3748;
}
/* 进度条 */
#totalProgress {
height: 8px;
border-radius: 4px;
background-color: #1a202c;
}
#totalProgress::chunk {
background-color: #818cf8;
border-radius: 4px;
}
#progressLabel {
color: #a0aec0;
font-size: 13px;
min-width: 70px;
}
#statusLabel {
font-size: 14px;
color: #e2e8f0;
margin-top: 10px;
padding: 8px;
border-radius: 6px;
background-color: rgba(26, 32, 44, 0.3);
}
/* 文件列表 */
#fileList, #resultList {
background-color: rgba(26, 32, 44, 0.3);
border: 1px solid #4a5568;
border-radius: 6px;
min-height: 200px;
font-size: 13px;
}
#fileList::item, #resultList::item {
padding: 8px 12px;
border-bottom: 1px solid rgba(74, 85, 104, 0.5);
color: #e2e8f0;
}
#fileList::item:selected,
#resultList::item:selected {
background-color: rgba(129, 140, 248, 0.15);
}
/* 结果标签 */
#resultLabel {
font-size: 14px;
color: #cbd5e0;
padding: 20px;
background-color: rgba(26, 32, 44, 0.3);
border-radius: 8px;
border: 1px dashed #4a5568;
}
/* 输入框 */
QLineEdit {
background-color: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 5px 10px;
color: #e2e8f0;
font-size: 14px;
}
QLineEdit:focus {
border-color: #818cf8;
}
"""