diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d03b6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/v1/build/ +/v1/dist/ +/v1/test/ diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/v1/DocConverter.spec b/v1/DocConverter.spec new file mode 100644 index 0000000..3208461 --- /dev/null +++ b/v1/DocConverter.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='DocConverter', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['app.ico'], +) diff --git a/v1/icon.ico b/v1/icon.ico new file mode 100644 index 0000000..a6c0b58 Binary files /dev/null and b/v1/icon.ico differ diff --git a/v1/main.spec b/v1/main.spec new file mode 100644 index 0000000..2ba8dd9 --- /dev/null +++ b/v1/main.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='main', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/v1/views/pdf_tools_page.py b/v1/views/pdf_tools_page.py new file mode 100644 index 0000000..6e9ddf7 --- /dev/null +++ b/v1/views/pdf_tools_page.py @@ -0,0 +1,1188 @@ +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; + } + """ diff --git a/v1/views/word_to_pdf-v2.py b/v1/views/word_to_pdf-v2.py new file mode 100644 index 0000000..3e20a54 --- /dev/null +++ b/v1/views/word_to_pdf-v2.py @@ -0,0 +1,869 @@ +import os +import shutil +import time +import traceback +import random +import datetime + +import win32com +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QFrame, QListWidget, QListWidgetItem, + QFileDialog, QProgressBar, QMessageBox, QSpacerItem) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer +from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent, QDragLeaveEvent + + +def generate_unique_filename(original_path, output_dir): + """生成唯一的文件名格式:原名称_日期_随机数.pdf""" + base_name = os.path.basename(original_path) + name_without_ext = os.path.splitext(base_name)[0] + + # 获取当前日期 + current_date = datetime.datetime.now().strftime("%Y%m%d") + + # 生成4位随机数 + random_num = random.randint(1000000, 9999999) + + # 组合成新文件名 + new_filename = f"{name_without_ext}_{current_date}_{random_num}.pdf" + return os.path.join(output_dir, new_filename) + + +class ConversionThread(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, files, output_dir): + super().__init__() + self.files = files + self.output_dir = output_dir + self.is_running = True + + def run(self): + print("转换线程启动") + word = None + + try: + # 尝试创建Word应用程序实例 + print("正在创建Word应用程序实例...") + try: + word = win32com.client.Dispatch("Word.Application") + print("Word应用程序实例创建成功") + word.Visible = False + word.DisplayAlerts = False + print("Word应用程序配置完成") + except Exception as e: + print(f"创建Word应用程序失败: {str(e)}") + self.conversion_done.emit("", False, f"无法启动Word: {str(e)}") + return + + total = len(self.files) + print(f"共有 {total} 个文件需要处理") + + # 处理每个文件 + for i, file_path in enumerate(self.files): + if not self.is_running: + print("线程被请求中断") + break + + print(f"开始处理文件 #{i + 1}: {file_path}") + self.progress_updated.emit(i + 1, total, os.path.basename(file_path)) + + # 检查文件是否存在 + if not os.path.exists(file_path): + print(f"文件不存在: {file_path}") + self.conversion_done.emit(file_path, False, "文件不存在") + continue + + try: + # 生成PDF路径 + pdf_path = generate_unique_filename(file_path, self.output_dir) + pdf_path = os.path.abspath(pdf_path) + print(f"PDF路径: {pdf_path}") + + # 打开文档 + print("准备打开Word文档...") + doc = None + try: + # 使用绝对路径 + doc = word.Documents.Open( + os.path.abspath(file_path), + False, # ConfirmConversions + True, # ReadOnly + False # AddToRecent + ) + print("Word文档打开成功") + + # 短暂延迟确保文档加载 + time.sleep(1) + + # 保存为PDF + print("准备保存为PDF...") + doc.SaveAs(pdf_path, FileFormat=17) + print("PDF保存成功") + + self.conversion_done.emit(file_path, True, pdf_path) + except Exception as e: + print(f"文档处理失败: {str(e)}") + error_msg = f"转换失败: {str(e)}" + self.conversion_done.emit(file_path, False, error_msg) + finally: + # 确保文档被安全关闭 + if doc: + try: + print("正在关闭文档...") + doc.Close(False) # 不保存更改 + doc = None + print("文档关闭成功") + except Exception as e: + print(f"文档关闭失败: {str(e)}") + except Exception as e: + print(f"文件处理失败: {str(e)}") + error_msg = f"处理文件时出错: {str(e)}" + self.conversion_done.emit(file_path, False, error_msg) + + print(f"文件 #{i + 1} 处理完成") + + print("所有文件处理完成") + self.finished_all.emit() + except Exception as e: + print(f"转换过程中发生严重错误: {str(e)}") + error_msg = f"转换过程中发生严重错误: {str(e)}" + self.conversion_done.emit("", False, error_msg) + self.finished_all.emit() + finally: + # 清理代码(不使用pythoncom) + try: + if word: + print("正在退出Word应用程序...") + word.Quit() + print("Word应用程序已退出") + word = None + except Exception as e: + print(f"退出Word失败: {str(e)}") + try: + print("尝试强制终止Word进程...") + os.system('taskkill /f /im winword.exe') + print("强制终止命令已执行") + except: + pass + + print("清理完成") + print("转换线程结束") + + def stop(self): + """安全停止转换线程""" + self.is_running = False + + +class WordToPDFPage(QWidget): + def __init__(self): + super().__init__() + self.files = [] + self.converted_files = {} + self.conversion_thread = None + self.output_dir = "" # 添加输出目录变量 + self.init_ui() + self.setStyleSheet(self.get_page_style()) + + def init_ui(self): + self.setObjectName("wordToPdfPage") + self.setAcceptDrops(True) + + layout = QHBoxLayout() + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(20) + + # 左侧面板 - 文件上传 + left_panel = QWidget() + left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(0, 0, 0, 0) + left_layout.setSpacing(15) + + # 上传区域(增强拖拽效果) + upload_area = QFrame() + upload_area.setObjectName("uploadArea") + upload_area.setFixedHeight(200) + upload_area.setAcceptDrops(True) + + upload_layout = QVBoxLayout(upload_area) + upload_layout.setAlignment(Qt.AlignCenter) + + upload_icon = QLabel() + upload_icon.setPixmap(QPixmap(":/icons/upload.png").scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + upload_icon.setAlignment(Qt.AlignCenter) + + self.upload_text = QLabel("拖拽文件或文件夹到此处\n或点击上传") + self.upload_text.setAlignment(Qt.AlignCenter) + self.upload_text.setWordWrap(True) + + upload_btn = QPushButton("选择文件或文件夹") + upload_btn.setObjectName("uploadBtn") + upload_btn.clicked.connect(self.handle_upload) + + upload_layout.addWidget(upload_icon) + upload_layout.addWidget(self.upload_text) + upload_layout.addWidget(upload_btn) + + # 文件列表 + file_list_frame = QFrame() + file_list_frame.setObjectName("fileListFrame") + file_list_layout = QVBoxLayout(file_list_frame) + file_list_layout.setContentsMargins(0, 0, 0, 0) + file_list_layout.setSpacing(5) + + file_title = QLabel("待转换文件") + file_title.setObjectName("sectionTitle") + + self.file_list = QListWidget() + self.file_list.setObjectName("fileList") + self.file_list.setSelectionMode(QListWidget.ExtendedSelection) + + file_list_layout.addWidget(file_title) + file_list_layout.addWidget(self.file_list) + + # 在操作按钮上方添加输出目录选择 + output_layout = QHBoxLayout() + output_layout.setSpacing(10) + + self.output_label = QLabel("输出目录: 未选择") + self.output_label.setObjectName("outputLabel") + self.output_label.setStyleSheet("color: #cbd5e0; font-size: 13px;") + self.output_label.setFixedWidth(300) + + output_btn = QPushButton("选择目录") + output_btn.setObjectName("outputBtn") + output_btn.clicked.connect(self.select_output_directory) + + output_layout.addWidget(self.output_label) + output_layout.addWidget(output_btn) + output_layout.addStretch() + + # 操作按钮 + btn_layout = QHBoxLayout() + btn_layout.setSpacing(10) + + self.convert_btn = QPushButton("开始转换") + self.convert_btn.setObjectName("convertBtn") + self.convert_btn.clicked.connect(self.start_conversion) + + delete_btn = QPushButton("删除选中") + delete_btn.setObjectName("deleteBtn") + delete_btn.clicked.connect(self.delete_selected) + + btn_layout.addWidget(self.convert_btn) + btn_layout.addWidget(delete_btn) + + # 组装左侧面板 + left_layout.addWidget(upload_area) + left_layout.addLayout(output_layout) # 添加输出目录选择 + left_layout.addWidget(file_list_frame) + left_layout.addLayout(btn_layout) + + # 右侧面板 - 转换结果 + right_panel = QWidget() + right_layout = QVBoxLayout(right_panel) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(15) + + # 进度区域 + progress_frame = QFrame() + progress_frame.setObjectName("progressFrame") + progress_layout = QVBoxLayout(progress_frame) + progress_layout.setContentsMargins(15, 15, 15, 15) + progress_layout.setSpacing(10) + + progress_title = QLabel("转换进度") + progress_title.setObjectName("sectionTitle") + + # 总体进度 + total_layout = QHBoxLayout() + total_layout.setSpacing(10) + + total_label = QLabel("总体进度:") + total_label.setObjectName("progressLabel") + + self.total_progress = QProgressBar() + self.total_progress.setObjectName("totalProgress") + self.total_progress.setTextVisible(False) + + total_layout.addWidget(total_label) + total_layout.addWidget(self.total_progress) + + # 文件进度 + file_layout = QHBoxLayout() + file_layout.setSpacing(10) + + file_label = QLabel("当前文件:") + file_label.setObjectName("progressLabel") + + self.file_progress = QProgressBar() + self.file_progress.setObjectName("fileProgress") + self.file_progress.setTextVisible(False) + + file_layout.addWidget(file_label) + file_layout.addWidget(self.file_progress) + + # 状态标签 + self.status_label = QLabel("就绪") + self.status_label.setObjectName("statusLabel") + self.status_label.setAlignment(Qt.AlignCenter) + + progress_layout.addWidget(progress_title) + progress_layout.addLayout(total_layout) + progress_layout.addLayout(file_layout) + progress_layout.addWidget(self.status_label) + + # 结果列表 + result_frame = QFrame() + result_frame.setObjectName("resultFrame") + result_layout = QVBoxLayout(result_frame) + result_layout.setContentsMargins(0, 0, 0, 0) + result_layout.setSpacing(5) + + result_title = QLabel("转换结果") + result_title.setObjectName("sectionTitle") + + self.result_list = QListWidget() + self.result_list.setObjectName("resultList") + self.result_list.setSelectionMode(QListWidget.ExtendedSelection) + + result_layout.addWidget(result_title) + result_layout.addWidget(self.result_list) + + # 下载按钮改为另存为 + self.save_as_btn = QPushButton("另存为...") + self.save_as_btn.setObjectName("saveAsBtn") + self.save_as_btn.setEnabled(False) + self.save_as_btn.clicked.connect(self.save_as) + + # 日志区域 + log_frame = QFrame() + log_frame.setObjectName("logFrame") + log_layout = QVBoxLayout(log_frame) + log_layout.setContentsMargins(0, 0, 0, 0) + log_layout.setSpacing(5) + + log_title = QLabel("转换日志") + log_title.setObjectName("sectionTitle") + + self.log_content = QListWidget() + self.log_content.setObjectName("logContent") + self.log_content.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + + log_layout.addWidget(log_title) + log_layout.addWidget(self.log_content) + + # 组装右侧面板 + right_layout.addWidget(progress_frame) + right_layout.addWidget(result_frame) + right_layout.addWidget(self.save_as_btn) + right_layout.addWidget(log_frame) + + # 主布局 + layout.addWidget(left_panel) + layout.addWidget(right_panel) + + self.setLayout(layout) + + def get_page_style(self): + return """ + /* ===== Word转PDF页面样式 ===== */ + #wordToPdfPage { + background-color: #1a202c; + } + + /* 卡片样式 */ + #uploadArea, + #fileListFrame, + #progressFrame, + #resultFrame, + #logFrame { + background-color: #2d3748; + border: 1px solid #4a5568; + border-radius: 8px; + padding: 15px; + } + + /* 上传区域 */ + #uploadArea { + border: 2px dashed #4a5568; + background-color: rgba(45, 55, 72, 0.5); + 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); + } + + /* 标题样式 */ + #sectionTitle { + font-size: 16px; + font-weight: 600; + color: #e2e8f0; + margin-bottom: 10px; + } + + /* 文件列表 */ + #fileList, #resultList, #logContent { + background-color: rgba(26, 32, 44, 0.3); + border: 1px solid #4a5568; + border-radius: 6px; + min-height: 200px; + max-height: 250px; + font-size: 13px; + } + + #fileList::item, #resultList::item, #logContent::item { + padding: 8px 12px; + border-bottom: 1px solid rgba(74, 85, 104, 0.5); + } + + #fileList::item:selected, + #resultList::item:selected, + #logContent::item:selected { + background-color: rgba(129, 140, 248, 0.15); + } + + /* 进度条 */ + #totalProgress, #fileProgress { + height: 8px; + border-radius: 4px; + background-color: #2d3748; + } + + #totalProgress::chunk { + background-color: #818cf8; + border-radius: 4px; + } + + #fileProgress::chunk { + background-color: #4ade80; + 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); + } + + /* 按钮样式 */ + #outputBtn, #uploadBtn, #convertBtn, #deleteBtn, #saveAsBtn { + min-height: 36px; + font-weight: 500; + border-radius: 6px; + padding: 8px 16px; + } + + #uploadBtn { + min-width: 180px; + } + + #outputBtn { + background-color: #4a5568; + color: #e2e8f0; + } + + #outputBtn:hover { + background-color: #2d3748; + } + + #convertBtn { + background-color: #4f46e5; + } + + #convertBtn:disabled { + background-color: #4a5568; + } + + #saveAsBtn { + background-color: #2563eb; + } + + #saveAsBtn:hover { + background-color: #1d4ed8; + } + + #saveAsBtn:disabled { + background-color: #4a5568; + } + + #deleteBtn { + background-color: #ef4444; + } + + #deleteBtn:hover { + background-color: #dc2626; + } + + /* 日志样式 */ + #logContent::item[type="info"] { + color: #a0aec0; + } + + #logContent::item[type="success"] { + color: #34d399; + } + + #logContent::item[type="warning"] { + color: #fbbf24; + } + + #logContent::item[type="error"] { + color: #f87171; + } + """ + + def dragEnterEvent(self, event: QDragEnterEvent): + if event.mimeData().hasUrls(): + event.acceptProposedAction() + # 高亮显示拖拽区域 + self.findChild(QFrame, "uploadArea").setProperty("dragActive", True) + self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) + self.upload_text.setText("释放文件以添加") + + def dragLeaveEvent(self, event: QDragLeaveEvent): + # 恢复拖拽区域样式 + self.findChild(QFrame, "uploadArea").setProperty("dragActive", False) + self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) + self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传") + + def dropEvent(self, event: QDropEvent): + # 恢复拖拽区域样式 + self.findChild(QFrame, "uploadArea").setProperty("dragActive", False) + self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) + self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传") + + urls = event.mimeData().urls() + files = [] + added_count = 0 + + for url in urls: + path = url.toLocalFile() + if os.path.isdir(path): + # 处理文件夹 + for root, _, filenames in os.walk(path): + for f in filenames: + if f.lower().endswith(('.doc', '.docx')): + files.append(os.path.join(root, f)) + elif os.path.isfile(path) and path.lower().endswith(('.doc', '.docx')): + files.append(path) + + if files: + added_count = self.add_files(files) + self.add_log(f"添加了 {added_count} 个文件", "success") + else: + self.add_log("未找到有效的Word文件", "warning") + + def handle_upload(self): + """处理文件或文件夹上传""" + options = QFileDialog.Options() + files, _ = QFileDialog.getOpenFileNames( + self, "选择Word文件", "", "Word Files (*.doc *.docx);;All Files (*)", options=options + ) + + if files: + added_count = self.add_files(files) + self.add_log(f"添加了 {added_count} 个文件", "success") + else: + folder = QFileDialog.getExistingDirectory(self, "选择包含Word文件的文件夹") + if folder: + files = [] + for root, _, filenames in os.walk(folder): + for f in filenames: + if f.lower().endswith(('.doc', '.docx')): + files.append(os.path.join(root, f)) + + if files: + added_count = self.add_files(files) + self.add_log(f"从文件夹添加了 {added_count} 个文件", "success") + else: + self.add_log("文件夹中没有找到Word文件", "warning") + + def add_files(self, files): + """添加文件到列表,避免重复""" + added_count = 0 + for file in files: + if file not in self.files and os.path.exists(file): + self.files.append(file) + item = QListWidgetItem(os.path.basename(file)) + item.setData(Qt.UserRole, file) + self.file_list.addItem(item) + added_count += 1 + return added_count + + def delete_selected(self): + """删除选中的文件""" + selected_items = self.file_list.selectedItems() + if not selected_items: + self.add_log("请先选择要删除的文件", "warning") + return + + for item in selected_items: + self.files.remove(item.data(Qt.UserRole)) + self.file_list.takeItem(self.file_list.row(item)) + + self.add_log(f"删除了 {len(selected_items)} 个文件", "info") + + def select_output_directory(self): + """选择输出目录""" + folder = QFileDialog.getExistingDirectory(self, "选择PDF保存位置") + if folder: + self.output_dir = folder + self.output_label.setText(f"输出目录: {folder}") + self.output_label.setToolTip(folder) + self.add_log(f"设置输出目录: {folder}", "info") + return True + return False + + def start_conversion(self): + """开始转换文件""" + + if not self.files: + self.add_log("请先添加要转换的文件!", "warning") + return + + # 检查输出目录 + if not self.output_dir: + if not self.select_output_directory(): + self.add_log("未选择输出目录,转换取消", "warning") + return + + # 确保目录存在 + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + self.add_log(f"创建输出目录: {self.output_dir}", "info") + + if not self.files: + self.add_log("请先添加要转换的文件!", "warning") + return + + # 重置状态 + self.converted_files = {} + self.result_list.clear() + self.log_content.clear() + self.download_btn.setEnabled(False) + self.convert_btn.setEnabled(False) + + # 重置进度条 + self.total_progress.setValue(0) + self.file_progress.setValue(0) + self.status_label.setText("正在准备转换...") + + # 添加开始日志 + self.add_log("开始转换任务", "info") + self.add_log(f"共 {len(self.files)} 个文件待处理", "info") + + # 创建并启动转换线程 + self.conversion_thread = ConversionThread(self.files.copy(), self.output_dir) + self.conversion_thread.progress_updated.connect(self.update_progress) + self.conversion_thread.conversion_done.connect(self.handle_conversion_done) + self.conversion_thread.finished_all.connect(self.handle_finished_all) + self.conversion_thread.start() + + def update_progress(self, current, total, filename): + """更新进度条""" + self.total_progress.setMaximum(total) + self.total_progress.setValue(current) + + # 重置文件进度为0 + self.file_progress.setValue(0) + self.file_progress.setMaximum(100) + + # 更新状态 + self.status_label.setText(f"正在转换: {os.path.basename(filename)}") + self.add_log(f"开始转换: {os.path.basename(filename)} ({current}/{total})", "info") + + # 模拟文件进度动画 + self.animate_file_progress() + + def animate_file_progress(self): + """模拟文件转换进度动画(实际应用中应由实际进度驱动)""" + self.file_progress_value = 0 + + def update_progress(): + if self.file_progress_value < 100: + self.file_progress_value += 2 + self.file_progress.setValue(self.file_progress_value) + QTimer.singleShot(50, update_progress) + + update_progress() + + def handle_conversion_done(self, file_path, success, message): + """处理单个文件转换完成""" + basename = os.path.basename(file_path) + if success: + # 成功时,message包含新的PDF路径 + pdf_path = message + self.converted_files[file_path] = pdf_path + + # 从待转换列表移除 + for i in range(self.file_list.count()): + item = self.file_list.item(i) + if item.data(Qt.UserRole) == file_path: + self.file_list.takeItem(i) + break + + # 添加到结果列表 + item = QListWidgetItem(f"{basename} → {os.path.basename(pdf_path)}") + item.setData(Qt.UserRole, pdf_path) + self.result_list.addItem(item) + + self.add_log(f"✅ 转换成功: {basename} -> {os.path.basename(pdf_path)}", "success") + else: + # 失败时显示错误信息 + self.add_log(f"❌❌ 转换失败: {basename} - {message}", "error") + + def handle_finished_all(self): + """所有文件转换完成""" + success_count = len(self.converted_files) + fail_count = len(self.files) - success_count + + # 更新状态 + self.status_label.setText(f"转换完成! 成功: {success_count}, 失败: {fail_count}") + self.total_progress.setValue(self.total_progress.maximum()) + self.file_progress.setValue(100) + self.convert_btn.setEnabled(True) + self.save_as_btn.setEnabled(bool(self.converted_files)) + + # 添加完成日志 + if success_count: + self.add_log(f"🎉🎉 全部转换完成! 成功转换 {success_count} 个文件", "success") + self.add_log(f"文件已保存到: {self.output_dir}", "info") + if fail_count: + self.add_log(f"⚠️ 有 {fail_count} 个文件转换失败", "warning") + + def save_as(self): + """另存为单个或选中的文件""" + if not self.converted_files: + self.add_log("没有可保存的文件", "warning") + return + + selected_items = self.result_list.selectedItems() + files_to_save = [] + + if selected_items: + # 保存选中的文件 + for item in selected_items: + file_path = item.data(Qt.UserRole) + files_to_save.append(file_path) + else: + # 保存所有文件 + files_to_save = list(self.converted_files.values()) + + if not files_to_save: + self.add_log("没有选择要保存的文件", "warning") + return + + # 如果只保存一个文件,使用另存为对话框 + if len(files_to_save) == 1: + default_name = os.path.basename(files_to_save[0]) + file_path, _ = QFileDialog.getSaveFileName( + self, "保存PDF文件", default_name, "PDF Files (*.pdf)" + ) + if file_path: + try: + shutil.copy2(files_to_save[0], file_path) + self.add_log(f"已保存: {os.path.basename(file_path)}", "success") + except Exception as e: + self.add_log(f"保存失败: {os.path.basename(file_path)} - {str(e)}", "error") + else: + # 保存多个文件,选择目录 + folder = QFileDialog.getExistingDirectory(self, "选择保存位置") + if folder: + success = 0 + errors = [] + + for src_path in files_to_save: + try: + file_name = os.path.basename(src_path) + dest_path = os.path.join(folder, file_name) + shutil.copy2(src_path, dest_path) + success += 1 + except Exception as e: + errors.append(f"{file_name}: {str(e)}") + + if success: + self.add_log(f"已保存 {success} 个文件到 {folder}", "success") + if errors: + self.add_log(f"保存失败 {len(errors)} 个文件", "error") + for error in errors: + self.add_log(f" {error}", "error") + + def download_all(self): + """下载所有转换后的文件""" + if not self.converted_files: + self.add_log("没有可下载的文件", "warning") + return + + folder = QFileDialog.getExistingDirectory(self, "选择保存位置") + if not folder: + self.add_log("下载已取消", "info") + return + + success = 0 + errors = [] + + # 开始下载 + self.status_label.setText("正在下载文件...") + self.add_log(f"开始下载 {len(self.converted_files)} 个文件到 {folder}", "info") + + for pdf_path in self.converted_files.values(): + try: + file_name = os.path.basename(pdf_path) + destination = os.path.join(folder, file_name) + + # 复制文件 + with open(pdf_path, 'rb') as src_file: + with open(destination, 'wb') as dest_file: + dest_file.write(src_file.read()) + success += 1 + self.add_log(f"已下载: {file_name}", "success") + except Exception as e: + errors.append(f"{os.path.basename(pdf_path)}: {str(e)}") + self.add_log(f"下载失败: {os.path.basename(pdf_path)} - {str(e)}", "error") + + # 更新状态 + self.status_label.setText(f"下载完成! 成功: {success}, 失败: {len(errors)}") + + # 显示结果 + if errors: + self.add_log(f"⚠️ 部分文件下载失败 ({len(errors)} 个)", "warning") + else: + self.add_log(f"✅ 全部文件下载成功!", "success") + + self.add_log(f"文件已保存到: {folder}", "info") + + def add_log(self, message, log_type="info"): + """添加日志项""" + item = QListWidgetItem(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {message}") + item.setData(Qt.UserRole + 1, log_type) # 存储日志类型用于样式 + self.log_content.addItem(item) + self.log_content.scrollToBottom() + + def paintEvent(self, event): + """为拖拽区域添加悬停效果""" + if self.underMouse(): + self.findChild(QFrame, "uploadArea").setProperty("hover", True) + self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) + else: + self.findChild(QFrame, "uploadArea").setProperty("hover", False) + self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) + super().paintEvent(event) \ No newline at end of file diff --git a/v1/奶酪云工具箱.spec b/v1/奶酪云工具箱.spec new file mode 100644 index 0000000..7f5b1fe --- /dev/null +++ b/v1/奶酪云工具箱.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='奶酪云工具箱', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icon.ico'], +)