1189 lines
42 KiB
Python
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;
|
|
}
|
|
"""
|