diff --git a/app/__pycache__/config.cpython-313.pyc b/app/__pycache__/config.cpython-313.pyc index 1150462..5db5fd5 100644 Binary files a/app/__pycache__/config.cpython-313.pyc and b/app/__pycache__/config.cpython-313.pyc differ diff --git a/app/config.py b/app/config.py index edf7f58..93a52ed 100644 --- a/app/config.py +++ b/app/config.py @@ -1,4 +1,4 @@ -version = '0.1.3' +version = '0.1.4' contact = '' github = '' website = 'https://memotrace.cn/tools/' diff --git a/app/ui/__pycache__/Icon.cpython-313.pyc b/app/ui/__pycache__/Icon.cpython-313.pyc index c004d4b..072199e 100644 Binary files a/app/ui/__pycache__/Icon.cpython-313.pyc and b/app/ui/__pycache__/Icon.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc index 617ffb2..7715147 100644 Binary files a/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc and b/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/__pycache__/pdf_tool.cpython-313.pyc b/app/ui/pdf_tools/__pycache__/pdf_tool.cpython-313.pyc index ee0ebec..c89f621 100644 Binary files a/app/ui/pdf_tools/__pycache__/pdf_tool.cpython-313.pyc and b/app/ui/pdf_tools/__pycache__/pdf_tool.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/__pycache__/pdf_tool_ui.cpython-313.pyc b/app/ui/pdf_tools/__pycache__/pdf_tool_ui.cpython-313.pyc index c3f08b1..7f932a2 100644 Binary files a/app/ui/pdf_tools/__pycache__/pdf_tool_ui.cpython-313.pyc and b/app/ui/pdf_tools/__pycache__/pdf_tool_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/blank_pages/blank_pages.py b/app/ui/pdf_tools/blank_pages/blank_pages.py new file mode 100644 index 0000000..3e87ec0 --- /dev/null +++ b/app/ui/pdf_tools/blank_pages/blank_pages.py @@ -0,0 +1,786 @@ +import os.path +import shutil +import tempfile +from typing import List, Dict, Tuple, Set + +import fitz +import numpy as np +from PIL import Image +from PySide6.QtCore import Signal, QThread, QUrl, Qt, QFile, QIODevice, QTextStream +from PySide6.QtGui import QDesktopServices, QPixmap, QIcon, QFont, QFontMetrics, QImage +from PySide6.QtWidgets import QWidget, QMessageBox, QFileDialog, QListWidgetItem, QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout + +from app.model import PdfFile +from app.ui.components.QCursorGif import QCursorGif +from app.ui.Icon import Icon +from app.util import common +from app.ui.pdf_tools.blank_pages.blank_pages_ui import Ui_blank_pages_view +from app.ui.components.router import Router +from app.log import logger + + +def open_file_explorer(path): + # 使用QDesktopServices打开文件管理器 + if os.path.isfile(path): + path = os.path.dirname(path) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + + +class BlankPagesControl(QWidget, Ui_blank_pages_view, QCursorGif): + okSignal = Signal(bool) + childRouterSignal = Signal(str) + + def __init__(self, router: Router, parent=None): + super().__init__(parent) + self.input_file_path = "" + self.output_dir = "" + self.output_suffix = "_无空白页" + self.batch_files = [] # 批量处理文件列表 + self.blank_pages_dict = {} # 存储每个文件的空白页信息 {file_path: [page_nums]} + self.router = router + self.router_path = (self.parent().router_path if self.parent() else '') + '/删除空白页' + self.child_routes = {} + self.worker = None + self.running_flag = False + self.setupUi(self) + # 设置忙碌光标图片数组 + self.initCursor([':/icons/icons/Cursors/%d.png' % + i for i in range(8)], self) + self.setCursorTimeout(100) + self.init_ui() + + # 按钮连接 + self.btn_choose_file.clicked.connect(self.open_file_dialog) + self.btn_choose_file.setIcon(Icon.Add_Icon) + self.btn_process.clicked.connect(self.process_files) + self.btn_preview.clicked.connect(self.preview_blank_page) + + # 批量文件操作 + self.btn_add_files.clicked.connect(self.add_files) + self.btn_remove_file.clicked.connect(self.remove_file) + self.btn_clear_files.clicked.connect(self.clear_files) + self.listWidget_files.itemClicked.connect(self.select_file) + + # 输出选项连接 + self.lineEdit_suffix.textChanged.connect(self.change_output_suffix) + self.comboBox_output_dir.activated.connect(self.select_output_dir) + self.btn_choose_output_dir.clicked.connect(self.set_output_dir) + + # 初始界面设置 + self.label_output_dir.setVisible(False) + self.btn_choose_output_dir.setVisible(False) + self.btn_process.setEnabled(False) + self.btn_preview.setEnabled(False) + + def init_ui(self): + self.btn_process.setObjectName('border') + if not self.parent(): + pixmap = QPixmap(Icon.logo_ico_path) + icon = QIcon(pixmap) + self.setWindowIcon(icon) + self.setWindowTitle('删除PDF空白页') + style_qss_file = QFile(":/data/resources/QSS/style.qss") + if style_qss_file.open(QIODevice.ReadOnly | QIODevice.Text): + stream = QTextStream(style_qss_file) + style_content = stream.readAll() + self.setStyleSheet(style_content) + style_qss_file.close() + self.lineEdit_suffix.setText(self.output_suffix) + + def change_output_suffix(self): + self.output_suffix = common.correct_filename(self.lineEdit_suffix.text()) + + def select_output_dir(self): + text = self.comboBox_output_dir.currentText() + if text == 'PDF相同目录': + self.label_output_dir.setVisible(False) + self.btn_choose_output_dir.setVisible(False) + elif text == '自定义目录': + self.label_output_dir.setVisible(True) + self.btn_choose_output_dir.setVisible(True) + + def set_output_dir(self): + folder = QFileDialog.getExistingDirectory(self, "选择输出目录") + if folder: + self.output_dir = folder + font_metrics = QFontMetrics(self.label_output_dir.font()) + # 使用 elidedText 根据按钮宽度生成省略文字 + elided_text = font_metrics.elidedText(self.output_dir, Qt.ElideRight, self.label_output_dir.width() - 10) + self.label_output_dir.setText(elided_text) + self.label_output_dir.setToolTip(self.output_dir) + + def open_file_dialog(self): + # 打开文件对话框,选择PDF文件 + file_path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF Files (*.pdf);;All Files (*)") + if file_path: + self.input_file_path = file_path + self.lineEdit_pdf_path.setText(file_path) + self.add_file_to_batch(file_path) + + # 如果未设置输出目录,默认使用与PDF相同的目录 + if not self.output_dir: + self.output_dir = os.path.dirname(file_path) + font_metrics = QFontMetrics(self.label_output_dir.font()) + elided_text = font_metrics.elidedText(self.output_dir, Qt.ElideRight, self.label_output_dir.width() - 10) + self.label_output_dir.setText(elided_text) + self.label_output_dir.setToolTip(self.output_dir) + + # 检测空白页 + self.detect_blank_pages(file_path) + + def add_files(self): + # 打开文件对话框,选择多个PDF文件 + file_paths, _ = QFileDialog.getOpenFileNames(self, "选择PDF文件", "", "PDF Files (*.pdf);;All Files (*)") + if file_paths: + for file_path in file_paths: + self.add_file_to_batch(file_path) + + # 如果未设置输出目录,默认使用第一个PDF的目录 + if not self.output_dir and file_paths: + self.output_dir = os.path.dirname(file_paths[0]) + font_metrics = QFontMetrics(self.label_output_dir.font()) + elided_text = font_metrics.elidedText(self.output_dir, Qt.ElideRight, self.label_output_dir.width() - 10) + self.label_output_dir.setText(elided_text) + self.label_output_dir.setToolTip(self.output_dir) + + # 检测第一个文件的空白页 + if file_paths: + self.detect_blank_pages(file_paths[0]) + + def add_file_to_batch(self, file_path): + # 添加文件到批量处理列表 + if file_path not in self.batch_files: + self.batch_files.append(file_path) + filename = os.path.basename(file_path) + item = QListWidgetItem(filename) + item.setToolTip(file_path) + self.listWidget_files.addItem(item) + self.btn_process.setEnabled(True) + + def remove_file(self): + # 从批量处理列表中移除选中的文件 + selected_items = self.listWidget_files.selectedItems() + if selected_items: + for item in selected_items: + file_path = item.toolTip() + if file_path in self.batch_files: + self.batch_files.remove(file_path) + # 如果有空白页信息,也一并移除 + if file_path in self.blank_pages_dict: + del self.blank_pages_dict[file_path] + row = self.listWidget_files.row(item) + self.listWidget_files.takeItem(row) + + # 如果列表为空,禁用处理按钮 + if not self.batch_files: + self.btn_process.setEnabled(False) + self.clear_blank_pages_list() + # 如果还有文件,选择第一个进行显示 + elif self.listWidget_files.count() > 0: + self.listWidget_files.setCurrentRow(0) + self.select_file(self.listWidget_files.item(0)) + + def clear_files(self): + # 清空批量处理列表 + self.batch_files = [] + self.blank_pages_dict = {} + self.listWidget_files.clear() + self.clear_blank_pages_list() + self.btn_process.setEnabled(False) + + def select_file(self, item): + # 当选择文件列表中的一个文件时 + if item: + file_path = item.toolTip() + self.input_file_path = file_path + self.lineEdit_pdf_path.setText(file_path) + self.detect_blank_pages(file_path) + + def clear_blank_pages_list(self): + # 清空空白页列表 + self.listWidget_blank_pages.clear() + self.btn_preview.setEnabled(False) + + def detect_blank_pages(self, file_path): + # 检测文件中的空白页 + self.clear_blank_pages_list() + + # 如果已经检测过,直接显示结果 + if file_path in self.blank_pages_dict: + self.display_blank_pages(file_path, self.blank_pages_dict[file_path]) + return + + # 否则启动检测线程 + self.startBusy() + self.progressBar.setValue(0) + + self.worker = BlankPagesDetectThread(file_path) + self.worker.progressSignal.connect(self.update_progress) + self.worker.resultSignal.connect(self.detection_complete) + self.worker.start() + + def display_blank_pages(self, file_path, blank_pages): + # 显示空白页列表 + self.listWidget_blank_pages.clear() + + if blank_pages: + for page_num in blank_pages: + item = QListWidgetItem(f"第 {page_num + 1} 页") + item.setData(Qt.UserRole, page_num) + self.listWidget_blank_pages.addItem(item) + self.btn_preview.setEnabled(True) + else: + self.listWidget_blank_pages.addItem("未检测到空白页") + self.btn_preview.setEnabled(False) + + def detection_complete(self, result): + # 空白页检测完成 + self.stopBusy() + self.progressBar.setValue(100) + + file_path, blank_pages = result + + # 保存检测结果 + self.blank_pages_dict[file_path] = blank_pages + + # 显示检测结果 + self.display_blank_pages(file_path, blank_pages) + + def preview_blank_page(self): + # 预览选中的空白页 + selected_items = self.listWidget_blank_pages.selectedItems() + if not selected_items: + # 如果没有选择任何项,则预览所有空白页 + self.preview_all_blank_pages() + return + + # 获取当前选中的文件和页面 + file_path = self.input_file_path + page_num = selected_items[0].data(Qt.UserRole) + + # 显示预览对话框 + preview_dialog = BlankPagePreviewDialog(file_path, page_num, self) + preview_dialog.exec_() + + def preview_all_blank_pages(self): + # 预览当前文件所有检测到的空白页 + file_path = self.input_file_path + if not file_path or file_path not in self.blank_pages_dict: + QMessageBox.information(self, "提示", "请先选择一个PDF文件并检测空白页") + return + + blank_pages = self.blank_pages_dict[file_path] + if not blank_pages: + QMessageBox.information(self, "提示", "当前文件未检测到空白页") + return + + # 显示所有空白页预览对话框 + preview_dialog = AllBlankPagesPreviewDialog(file_path, blank_pages, self) + preview_dialog.exec_() + + def process_files(self): + # 处理所有文件,删除空白页 + if not self.batch_files: + QMessageBox.warning(self, "警告", "请先添加要处理的PDF文件") + return + + # 检查是否有需要处理的文件(有空白页的文件) + files_to_process = [] + for file_path in self.batch_files: + # 如果还未检测,先检测空白页 + if file_path not in self.blank_pages_dict: + QMessageBox.information(self, "提示", f"需要先检测 {os.path.basename(file_path)} 中的空白页") + return + + # 有空白页的文件才需要处理 + if self.blank_pages_dict[file_path]: + files_to_process.append(file_path) + + if not files_to_process: + QMessageBox.information(self, "提示", "所有文件中都没有空白页,无需处理") + return + + # 获取输出目录 + if self.comboBox_output_dir.currentText() == '自定义目录' and self.output_dir: + output_directory = self.output_dir + else: + # 如果使用PDF相同目录,每个文件使用自己的目录 + output_directory = "" + + # 如果是自定义目录且不存在,创建它 + if output_directory and not os.path.exists(output_directory): + try: + os.makedirs(output_directory) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建输出目录: {str(e)}") + return + + # 确认是否创建备份 + create_backup = self.checkBox_backup.isChecked() + + # 启动处理线程 + self.startBusy() + self.progressBar.setValue(0) + self.btn_process.setEnabled(False) + + self.worker = BlankPagesRemoveThread( + files_to_process, + self.blank_pages_dict, + output_directory, + self.output_suffix, + create_backup + ) + self.worker.progressSignal.connect(self.update_progress) + self.worker.resultSignal.connect(self.process_complete) + self.worker.start() + + def update_progress(self, value): + self.progressBar.setValue(value) + + def process_complete(self, result): + # 处理完成 + self.stopBusy() + self.progressBar.setValue(100) + self.btn_process.setEnabled(True) + + success, output_dir, processed_files = result + + if success: + reply = QMessageBox(self) + reply.setIcon(QMessageBox.Information) + reply.setWindowTitle('完成') + + # 生成详细信息 + details = "" + total_blank_pages = 0 + for file_path, blank_pages in processed_files.items(): + filename = os.path.basename(file_path) + blank_count = len(blank_pages) + total_blank_pages += blank_count + details += f"{filename}: 删除了 {blank_count} 个空白页\n" + + reply.setText(f"空白页删除完成\n共处理 {len(processed_files)} 个文件,删除 {total_blank_pages} 个空白页") + reply.setDetailedText(details) + + btn = reply.addButton('打开输出文件夹', QMessageBox.ActionRole) + btn.clicked.connect(lambda: open_file_explorer(output_dir)) + reply.addButton("确认", QMessageBox.AcceptRole) + reply.exec_() + else: + # output_dir 在这种情况下包含错误信息 + QMessageBox.critical(self, "错误", f"处理失败: {output_dir}") + + def closeEvent(self, event): + super().closeEvent(event) + self.okSignal.emit(True) + + +class BlankPagePreviewDialog(QDialog): + def __init__(self, file_path, page_num, parent=None): + super().__init__(parent) + self.file_path = file_path + self.page_num = page_num + self.is_thumbnail = True # 默认显示缩略图 + self.setWindowTitle(f"空白页预览 - 第 {page_num + 1} 页") + self.setMinimumSize(600, 800) + + layout = QVBoxLayout() + + # 添加预览图像 + self.preview_label = QLabel() + self.preview_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.preview_label) + + # 添加查看模式切换按钮 + btn_layout = QHBoxLayout() + self.btn_toggle_view = QPushButton("查看原始尺寸") + self.btn_toggle_view.clicked.connect(self.toggle_view_mode) + btn_layout.addWidget(self.btn_toggle_view) + + # 关闭按钮 + close_button = QPushButton("关闭") + close_button.clicked.connect(self.accept) + btn_layout.addWidget(close_button) + + layout.addLayout(btn_layout) + + self.setLayout(layout) + + # 加载预览图像 + self.load_preview() + + def toggle_view_mode(self): + self.is_thumbnail = not self.is_thumbnail + if self.is_thumbnail: + self.btn_toggle_view.setText("查看原始尺寸") + else: + self.btn_toggle_view.setText("查看缩略图") + self.load_preview() + + def load_preview(self): + try: + # 打开PDF文件 + with fitz.open(self.file_path) as pdf: + if 0 <= self.page_num < len(pdf): + # 获取页面 + page = pdf[self.page_num] + + # 根据模式选择缩放比例 + if self.is_thumbnail: + # 缩略图模式 + scale_factor = 0.5 + else: + # 原始尺寸模式 + scale_factor = 2.0 + + # 生成图像 + pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor)) + + # 转换为QPixmap + img_data = pix.samples + + # 使用PIL转换 + image = Image.frombytes("RGB", [pix.width, pix.height], img_data) + img_data = image.tobytes("raw", "RGB") + + qimg = QImage(img_data, pix.width, pix.height, QImage.Format_RGB888) + qpixmap = QPixmap.fromImage(qimg) + + # 如果是缩略图模式,调整图像大小以适应窗口 + if self.is_thumbnail: + qpixmap = qpixmap.scaled(self.width() - 40, self.height() - 80, + Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 显示图像 + self.preview_label.setPixmap(qpixmap) + + # 如果是原始尺寸模式,可能需要调整窗口大小 + if not self.is_thumbnail: + # 确保窗口足够大以显示原始尺寸图像 + self.resize(max(600, pix.width + 80), max(800, pix.height + 120)) + except Exception as e: + logger.error(f"加载预览时出错: {str(e)}") + self.preview_label.setText(f"无法加载预览: {str(e)}") + + +class BlankPagesDetectThread(QThread): + progressSignal = Signal(int) + resultSignal = Signal(tuple) # (file_path, blank_pages) + + def __init__(self, file_path): + super().__init__() + self.file_path = file_path + + def run(self): + try: + blank_pages = [] + + # 打开PDF文件 + with fitz.open(self.file_path) as pdf: + total_pages = len(pdf) + + # 遍历所有页面 + for page_num in range(total_pages): + # 更新进度 + progress = int((page_num + 1) / total_pages * 90) + self.progressSignal.emit(progress) + + # 检查是否为空白页 + is_blank = self.is_blank_page(pdf, page_num) + if is_blank: + blank_pages.append(page_num) + + # 发送结果信号 + self.resultSignal.emit((self.file_path, blank_pages)) + + except Exception as e: + logger.error(f"检测空白页出错: {str(e)}") + self.resultSignal.emit((self.file_path, [])) + + def is_blank_page(self, pdf, page_num): + """检查页面是否为空白页""" + try: + # 获取页面 + page = pdf[page_num] + + # 获取页面文本 + text = page.get_text().strip() + if text: + return False # 有文本,不是空白页 + + # 渲染页面为图像 + pix = page.get_pixmap() + + # 计算非白色像素的比例 + img_data = pix.samples + arr = np.frombuffer(img_data, dtype=np.uint8) + + # 重塑为(height, width, 3)形状的数组 (RGB图像) + img_array = arr.reshape(pix.height, pix.width, 3 if pix.n == 3 else 4) + + # 计算非白色像素的数量 + # 我们将白色定义为RGB值都大于240的像素 + non_white_pixels = np.sum(np.any(img_array < 240, axis=2)) + total_pixels = pix.width * pix.height + + # 计算非白色像素的比例 + non_white_ratio = non_white_pixels / total_pixels + + # 如果非白色像素比例小于阈值,认为是空白页 + # 这里我们使用一个很小的阈值,例如0.5% + return non_white_ratio < 0.005 + + except Exception as e: + logger.error(f"判断空白页出错: {str(e)}") + return False + + +class BlankPagesRemoveThread(QThread): + progressSignal = Signal(int) + resultSignal = Signal(tuple) # (success, output_dir, processed_files) + + def __init__(self, files, blank_pages_dict, output_dir, output_suffix, create_backup): + super().__init__() + self.files = files + self.blank_pages_dict = blank_pages_dict + self.output_dir = output_dir + self.output_suffix = output_suffix + self.create_backup = create_backup + + def run(self): + try: + processed_files = {} + total_files = len(self.files) + + for i, file_path in enumerate(self.files): + # 更新进度 + progress = int((i / total_files) * 90) + self.progressSignal.emit(progress) + + # 获取要删除的空白页 + blank_pages = self.blank_pages_dict.get(file_path, []) + + if not blank_pages: + # 没有空白页,跳过处理 + continue + + # 处理单个文件 + success = self.process_single_file(file_path, blank_pages) + + if success: + processed_files[file_path] = blank_pages + + # 检查是否处理成功 + if processed_files: + # 使用第一个文件的目录作为输出目录(如果未指定) + output_dir = self.output_dir + if not output_dir and self.files: + output_dir = os.path.dirname(self.files[0]) + + self.progressSignal.emit(100) + self.resultSignal.emit((True, output_dir, processed_files)) + else: + self.resultSignal.emit((False, "没有文件被处理", {})) + + except Exception as e: + logger.error(f"删除空白页出错: {str(e)}") + self.resultSignal.emit((False, str(e), {})) + + def process_single_file(self, file_path, blank_pages): + """处理单个文件的空白页删除""" + try: + # 获取输出文件路径 + file_dir = os.path.dirname(file_path) + file_name = os.path.basename(file_path) + name, ext = os.path.splitext(file_name) + + # 确定输出目录 + output_dir = self.output_dir if self.output_dir else file_dir + + # 生成输出文件名 + output_file = os.path.join(output_dir, f"{name}{self.output_suffix}{ext}") + + # 确保输出文件路径可用 + output_file = common.usable_filepath(output_file) + + # 如果创建备份且源文件和目标文件不同 + if self.create_backup and file_path != output_file: + backup_file = os.path.join(output_dir, f"{name}_备份{ext}") + backup_file = common.usable_filepath(backup_file) + shutil.copy2(file_path, backup_file) + + # 打开源文件 + with fitz.open(file_path) as src_pdf: + # 创建新PDF + dst_pdf = fitz.open() + + # 获取所有页面,排除空白页 + for page_num in range(len(src_pdf)): + if page_num not in blank_pages: + # 复制非空白页 + dst_pdf.insert_pdf(src_pdf, from_page=page_num, to_page=page_num) + + # 保存新PDF + dst_pdf.save(output_file) + dst_pdf.close() + + return True + + except Exception as e: + logger.error(f"处理文件 {file_path} 出错: {str(e)}") + return False + + +class AllBlankPagesPreviewDialog(QDialog): + def __init__(self, file_path, blank_pages, parent=None): + super().__init__(parent) + self.file_path = file_path + self.blank_pages = blank_pages + self.current_index = 0 + self.is_thumbnail = True # 默认显示缩略图 + self.setWindowTitle(f"空白页预览 - 全部 ({len(blank_pages)} 页)") + self.setMinimumSize(700, 800) + + layout = QVBoxLayout() + + # 预览标题 + self.title_label = QLabel() + self.title_label.setAlignment(Qt.AlignCenter) + font = QFont() + font.setPointSize(12) + font.setBold(True) + self.title_label.setFont(font) + layout.addWidget(self.title_label) + + # 预览图像 + self.preview_label = QLabel() + self.preview_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.preview_label) + + # 导航按钮 + nav_layout = QHBoxLayout() + + self.btn_prev = QPushButton("上一页") + self.btn_prev.clicked.connect(self.show_prev_page) + nav_layout.addWidget(self.btn_prev) + + self.page_label = QLabel() + self.page_label.setAlignment(Qt.AlignCenter) + nav_layout.addWidget(self.page_label) + + self.btn_next = QPushButton("下一页") + self.btn_next.clicked.connect(self.show_next_page) + nav_layout.addWidget(self.btn_next) + + layout.addLayout(nav_layout) + + # 切换视图模式和关闭按钮 + btn_layout = QHBoxLayout() + + self.btn_toggle_view = QPushButton("查看原始尺寸") + self.btn_toggle_view.clicked.connect(self.toggle_view_mode) + btn_layout.addWidget(self.btn_toggle_view) + + # 关闭按钮 + close_button = QPushButton("关闭") + close_button.clicked.connect(self.accept) + btn_layout.addWidget(close_button) + + layout.addLayout(btn_layout) + + self.setLayout(layout) + + # 加载第一页 + self.update_page() + + def toggle_view_mode(self): + self.is_thumbnail = not self.is_thumbnail + if self.is_thumbnail: + self.btn_toggle_view.setText("查看原始尺寸") + else: + self.btn_toggle_view.setText("查看缩略图") + self.update_page() + + def update_page(self): + if not self.blank_pages: + return + + page_num = self.blank_pages[self.current_index] + self.title_label.setText(f"第 {page_num + 1} 页 (空白页 {self.current_index + 1}/{len(self.blank_pages)})") + self.page_label.setText(f"{self.current_index + 1} / {len(self.blank_pages)}") + + # 更新导航按钮状态 + self.btn_prev.setEnabled(self.current_index > 0) + self.btn_next.setEnabled(self.current_index < len(self.blank_pages) - 1) + + # 加载预览图像 + self.load_preview(page_num) + + def show_prev_page(self): + if self.current_index > 0: + self.current_index -= 1 + self.update_page() + + def show_next_page(self): + if self.current_index < len(self.blank_pages) - 1: + self.current_index += 1 + self.update_page() + + def load_preview(self, page_num): + try: + # 打开PDF文件 + with fitz.open(self.file_path) as pdf: + if 0 <= page_num < len(pdf): + # 获取页面 + page = pdf[page_num] + + # 根据模式选择缩放比例 + if self.is_thumbnail: + # 缩略图模式 + scale_factor = 0.5 + else: + # 原始尺寸模式 + scale_factor = 2.0 + + # 生成图像 + pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor)) + + # 转换为QPixmap + img_data = pix.samples + + # 使用PIL转换 + image = Image.frombytes("RGB", [pix.width, pix.height], img_data) + img_data = image.tobytes("raw", "RGB") + + qimg = QImage(img_data, pix.width, pix.height, QImage.Format_RGB888) + qpixmap = QPixmap.fromImage(qimg) + + # 如果是缩略图模式,调整图像大小以适应窗口 + if self.is_thumbnail: + qpixmap = qpixmap.scaled(self.width() - 40, self.height() - 120, + Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 显示图像 + self.preview_label.setPixmap(qpixmap) + + # 如果是原始尺寸模式,可能需要调整窗口大小 + if not self.is_thumbnail: + # 确保窗口足够大以显示原始尺寸图像 + self.resize(max(700, pix.width + 80), max(800, pix.height + 160)) + except Exception as e: + logger.error(f"加载预览时出错: {str(e)}") + self.preview_label.setText(f"无法加载预览: {str(e)}") + + +if __name__ == '__main__': + from PySide6.QtWidgets import QApplication + import sys + from PySide6.QtGui import QFont + + app = QApplication(sys.argv) + font = QFont('微软雅黑', 10) + app.setFont(font) + router = Router(None) + view = BlankPagesControl(router) + view.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/app/ui/pdf_tools/blank_pages/blank_pages_ui.py b/app/ui/pdf_tools/blank_pages/blank_pages_ui.py new file mode 100644 index 0000000..2a9bd7e --- /dev/null +++ b/app/ui/pdf_tools/blank_pages/blank_pages_ui.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'blank_pages_ui.ui' +## +## Created by: Qt User Interface Compiler version 6.8.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFrame, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QListWidget, QListWidgetItem, QProgressBar, + QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout, + QWidget) +import resource_rc + + +class Ui_blank_pages_view(object): + def setupUi(self, blank_pages_view): + if not blank_pages_view.objectName(): + blank_pages_view.setObjectName(u"blank_pages_view") + blank_pages_view.resize(800, 600) + self.verticalLayout = QVBoxLayout(blank_pages_view) + self.verticalLayout.setObjectName(u"verticalLayout") + + # 文件选择区域 + self.horizontalLayout_file = QHBoxLayout() + self.horizontalLayout_file.setObjectName(u"horizontalLayout_file") + self.label_file = QLabel(blank_pages_view) + self.label_file.setObjectName(u"label_file") + self.horizontalLayout_file.addWidget(self.label_file) + self.lineEdit_pdf_path = QLineEdit(blank_pages_view) + self.lineEdit_pdf_path.setObjectName(u"lineEdit_pdf_path") + self.lineEdit_pdf_path.setReadOnly(True) + self.horizontalLayout_file.addWidget(self.lineEdit_pdf_path) + self.btn_choose_file = QPushButton(blank_pages_view) + self.btn_choose_file.setObjectName(u"btn_choose_file") + self.horizontalLayout_file.addWidget(self.btn_choose_file) + self.verticalLayout.addLayout(self.horizontalLayout_file) + + # 批量处理区域 + self.groupBox_batch = QGroupBox(blank_pages_view) + self.groupBox_batch.setObjectName(u"groupBox_batch") + self.verticalLayout_batch = QVBoxLayout(self.groupBox_batch) + self.verticalLayout_batch.setObjectName(u"verticalLayout_batch") + + # 文件列表 + self.listWidget_files = QListWidget(self.groupBox_batch) + self.listWidget_files.setObjectName(u"listWidget_files") + self.verticalLayout_batch.addWidget(self.listWidget_files) + + # 文件操作按钮 + self.horizontalLayout_batch_btns = QHBoxLayout() + self.horizontalLayout_batch_btns.setObjectName(u"horizontalLayout_batch_btns") + + self.btn_add_files = QPushButton(self.groupBox_batch) + self.btn_add_files.setObjectName(u"btn_add_files") + self.horizontalLayout_batch_btns.addWidget(self.btn_add_files) + + self.btn_remove_file = QPushButton(self.groupBox_batch) + self.btn_remove_file.setObjectName(u"btn_remove_file") + self.horizontalLayout_batch_btns.addWidget(self.btn_remove_file) + + self.btn_clear_files = QPushButton(self.groupBox_batch) + self.btn_clear_files.setObjectName(u"btn_clear_files") + self.horizontalLayout_batch_btns.addWidget(self.btn_clear_files) + + self.verticalLayout_batch.addLayout(self.horizontalLayout_batch_btns) + self.verticalLayout.addWidget(self.groupBox_batch) + + # 预览区域 + self.groupBox_preview = QGroupBox(blank_pages_view) + self.groupBox_preview.setObjectName(u"groupBox_preview") + self.verticalLayout_preview = QVBoxLayout(self.groupBox_preview) + self.verticalLayout_preview.setObjectName(u"verticalLayout_preview") + + # 空白页列表 + self.listWidget_blank_pages = QListWidget(self.groupBox_preview) + self.listWidget_blank_pages.setObjectName(u"listWidget_blank_pages") + self.verticalLayout_preview.addWidget(self.listWidget_blank_pages) + + # 预览按钮 + self.horizontalLayout_preview_btn = QHBoxLayout() + self.horizontalLayout_preview_btn.setObjectName(u"horizontalLayout_preview_btn") + + self.horizontalSpacer_preview = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalLayout_preview_btn.addItem(self.horizontalSpacer_preview) + + self.btn_preview = QPushButton(self.groupBox_preview) + self.btn_preview.setObjectName(u"btn_preview") + self.horizontalLayout_preview_btn.addWidget(self.btn_preview) + + self.verticalLayout_preview.addLayout(self.horizontalLayout_preview_btn) + self.verticalLayout.addWidget(self.groupBox_preview) + + # 输出选项区域 + self.groupBox_output = QGroupBox(blank_pages_view) + self.groupBox_output.setObjectName(u"groupBox_output") + self.verticalLayout_output = QVBoxLayout(self.groupBox_output) + self.verticalLayout_output.setObjectName(u"verticalLayout_output") + + # 输出目录选择 + self.horizontalLayout_output_dir = QHBoxLayout() + self.horizontalLayout_output_dir.setObjectName(u"horizontalLayout_output_dir") + + self.label_output_dir_type = QLabel(self.groupBox_output) + self.label_output_dir_type.setObjectName(u"label_output_dir_type") + self.horizontalLayout_output_dir.addWidget(self.label_output_dir_type) + + self.comboBox_output_dir = QComboBox(self.groupBox_output) + self.comboBox_output_dir.addItem("") + self.comboBox_output_dir.addItem("") + self.comboBox_output_dir.setObjectName(u"comboBox_output_dir") + self.horizontalLayout_output_dir.addWidget(self.comboBox_output_dir) + + self.horizontalSpacer_output = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalLayout_output_dir.addItem(self.horizontalSpacer_output) + + self.label_output_dir = QLabel(self.groupBox_output) + self.label_output_dir.setObjectName(u"label_output_dir") + self.horizontalLayout_output_dir.addWidget(self.label_output_dir) + + self.btn_choose_output_dir = QPushButton(self.groupBox_output) + self.btn_choose_output_dir.setObjectName(u"btn_choose_output_dir") + self.horizontalLayout_output_dir.addWidget(self.btn_choose_output_dir) + + self.verticalLayout_output.addLayout(self.horizontalLayout_output_dir) + + # 输出文件名后缀 + self.horizontalLayout_suffix = QHBoxLayout() + self.horizontalLayout_suffix.setObjectName(u"horizontalLayout_suffix") + + self.label_suffix = QLabel(self.groupBox_output) + self.label_suffix.setObjectName(u"label_suffix") + self.horizontalLayout_suffix.addWidget(self.label_suffix) + + self.lineEdit_suffix = QLineEdit(self.groupBox_output) + self.lineEdit_suffix.setObjectName(u"lineEdit_suffix") + self.horizontalLayout_suffix.addWidget(self.lineEdit_suffix) + + self.checkBox_backup = QCheckBox(self.groupBox_output) + self.checkBox_backup.setObjectName(u"checkBox_backup") + self.horizontalLayout_suffix.addWidget(self.checkBox_backup) + + self.verticalLayout_output.addLayout(self.horizontalLayout_suffix) + self.verticalLayout.addWidget(self.groupBox_output) + + # 进度条和处理按钮 + self.progressBar = QProgressBar(blank_pages_view) + self.progressBar.setObjectName(u"progressBar") + self.progressBar.setValue(0) + self.verticalLayout.addWidget(self.progressBar) + + self.horizontalLayout_process = QHBoxLayout() + self.horizontalLayout_process.setObjectName(u"horizontalLayout_process") + + self.horizontalSpacer_process = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalLayout_process.addItem(self.horizontalSpacer_process) + + self.btn_process = QPushButton(blank_pages_view) + self.btn_process.setObjectName(u"btn_process") + self.btn_process.setMinimumSize(QSize(150, 40)) + self.horizontalLayout_process.addWidget(self.btn_process) + + self.verticalLayout.addLayout(self.horizontalLayout_process) + + self.retranslateUi(blank_pages_view) + + QMetaObject.connectSlotsByName(blank_pages_view) + # setupUi + + def retranslateUi(self, blank_pages_view): + blank_pages_view.setWindowTitle(QCoreApplication.translate("blank_pages_view", u"删除空白页", None)) + self.label_file.setText(QCoreApplication.translate("blank_pages_view", u"PDF文件:", None)) + self.btn_choose_file.setText(QCoreApplication.translate("blank_pages_view", u"选择PDF", None)) + self.groupBox_batch.setTitle(QCoreApplication.translate("blank_pages_view", u"批量处理", None)) + self.btn_add_files.setText(QCoreApplication.translate("blank_pages_view", u"添加文件", None)) + self.btn_remove_file.setText(QCoreApplication.translate("blank_pages_view", u"移除文件", None)) + self.btn_clear_files.setText(QCoreApplication.translate("blank_pages_view", u"清空列表", None)) + self.groupBox_preview.setTitle(QCoreApplication.translate("blank_pages_view", u"检测到的空白页", None)) + self.btn_preview.setText(QCoreApplication.translate("blank_pages_view", u"预览所有空白页", None)) + self.groupBox_output.setTitle(QCoreApplication.translate("blank_pages_view", u"输出选项", None)) + self.label_output_dir_type.setText(QCoreApplication.translate("blank_pages_view", u"输出位置:", None)) + self.comboBox_output_dir.setItemText(0, QCoreApplication.translate("blank_pages_view", u"PDF相同目录", None)) + self.comboBox_output_dir.setItemText(1, QCoreApplication.translate("blank_pages_view", u"自定义目录", None)) + self.label_output_dir.setText(QCoreApplication.translate("blank_pages_view", u"指定目录", None)) + self.btn_choose_output_dir.setText(QCoreApplication.translate("blank_pages_view", u"选择", None)) + self.label_suffix.setText(QCoreApplication.translate("blank_pages_view", u"文件名后缀:", None)) + self.checkBox_backup.setText(QCoreApplication.translate("blank_pages_view", u"创建备份", None)) + self.btn_process.setText(QCoreApplication.translate("blank_pages_view", u"删除空白页", None)) + # retranslateUi \ No newline at end of file diff --git a/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc index f0a21ec..bd2e432 100644 Binary files a/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc and b/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/__pycache__/merge_ui.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/merge_ui.cpython-313.pyc index dbcfa10..905ac2e 100644 Binary files a/app/ui/pdf_tools/merge/__pycache__/merge_ui.cpython-313.pyc and b/app/ui/pdf_tools/merge/__pycache__/merge_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/pdf_tool.py b/app/ui/pdf_tools/pdf_tool.py index f073aa9..79867aa 100644 --- a/app/ui/pdf_tools/pdf_tool.py +++ b/app/ui/pdf_tools/pdf_tool.py @@ -7,6 +7,7 @@ from app.ui.pdf_tools.merge import MergeControl from app.ui.pdf_tools.pdf_tool_ui import Ui_Form from app.ui.components.router import Router +from app.ui.pdf_tools.blank_pages.blank_pages import BlankPagesControl class PDFToolControl(QWidget, Ui_Form, QCursorGif): @@ -34,7 +35,7 @@ def __init__(self, router: Router, parent=None): self.commandLinkButton_split_pdf.clicked.connect(self.split_pdf) self.commandLinkButton_encrypt.clicked.connect(self.encrypt_pdf) self.commandLinkButton_decrypt.clicked.connect(self.decrypt_pdf) - self.commandLinkButton_delete_blank_pages.clicked.connect(globalSignals.not_support) + self.commandLinkButton_delete_blank_pages.clicked.connect(self.delete_blank_pages) self.commandLinkButton_add_watermark.clicked.connect(globalSignals.not_support) def init_ui(self): @@ -98,17 +99,46 @@ def decrypt_pdf(self): else: self.router.navigate(self.decrypt_view.router_path) - def encrypt_finish(self): - self.encrypt_view = None - - def decrypt_finish(self): - self.decrypt_view = None - - def split_finish(self): - self.split_view = None - - def merge_finish(self): - self.merge_view = None + def delete_blank_pages(self): + if not hasattr(self, 'blank_pages_view') or not self.blank_pages_view: + self.blank_pages_view = BlankPagesControl(router=self.router, parent=self if self.parent() else None) + self.blank_pages_view.okSignal.connect(self.blank_pages_finish) + self.router.add_route(self.blank_pages_view.router_path, self.blank_pages_view) + self.child_routes[self.blank_pages_view.router_path] = 0 + self.childRouterSignal.emit(self.blank_pages_view.router_path) + self.router.navigate(self.blank_pages_view.router_path) + else: + self.router.navigate(self.blank_pages_view.router_path) + + def merge_finish(self, s=None): + if self.merge_view and self.merge_view.router_path in self.child_routes: + self.child_routes.pop(self.merge_view.router_path) + self.router.remove_route(self.merge_view.router_path) + self.merge_view = None + + def split_finish(self, s=None): + if hasattr(self, 'split_view') and self.split_view and self.split_view.router_path in self.child_routes: + self.child_routes.pop(self.split_view.router_path) + self.router.remove_route(self.split_view.router_path) + self.split_view = None + + def encrypt_finish(self, s=None): + if hasattr(self, 'encrypt_view') and self.encrypt_view and self.encrypt_view.router_path in self.child_routes: + self.child_routes.pop(self.encrypt_view.router_path) + self.router.remove_route(self.encrypt_view.router_path) + self.encrypt_view = None + + def decrypt_finish(self, s=None): + if hasattr(self, 'decrypt_view') and self.decrypt_view and self.decrypt_view.router_path in self.child_routes: + self.child_routes.pop(self.decrypt_view.router_path) + self.router.remove_route(self.decrypt_view.router_path) + self.decrypt_view = None + + def blank_pages_finish(self, s=None): + if hasattr(self, 'blank_pages_view') and self.blank_pages_view and self.blank_pages_view.router_path in self.child_routes: + self.child_routes.pop(self.blank_pages_view.router_path) + self.router.remove_route(self.blank_pages_view.router_path) + self.blank_pages_view = None def __del__(self): self.merge_view = None diff --git a/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc index 8b5a6c0..6939ba3 100644 Binary files a/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc and b/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/security/__pycache__/encrypt_ui.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/encrypt_ui.cpython-313.pyc index 048be64..e788176 100644 Binary files a/app/ui/pdf_tools/security/__pycache__/encrypt_ui.cpython-313.pyc and b/app/ui/pdf_tools/security/__pycache__/encrypt_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/security/decrypt.py b/app/ui/pdf_tools/security/decrypt.py index f549db3..7a19428 100644 --- a/app/ui/pdf_tools/security/decrypt.py +++ b/app/ui/pdf_tools/security/decrypt.py @@ -8,6 +8,8 @@ import concurrent.futures import queue import math +import sys +import re import fitz from PyPDF2 import PdfReader, PdfWriter @@ -16,7 +18,8 @@ from PySide6.QtWidgets import (QWidget, QMessageBox, QFileDialog, QRadioButton, QButtonGroup, QHBoxLayout, QLabel, QCheckBox, QProgressDialog, QDialog, QVBoxLayout, QPushButton, - QLineEdit, QComboBox, QSpinBox) + QLineEdit, QComboBox, QSpinBox, QGroupBox, QListWidget, + QListWidgetItem, QGridLayout) from app.model import PdfFile from app.ui.components.QCursorGif import QCursorGif @@ -130,9 +133,18 @@ def __init__(self, router: Router, parent=None): "max_length": 8, # 最大密码长度 "charset": "digits", # 'digits', 'lowercase', 'uppercase', 'all' "timeout": 0, # 超时时间(秒),0表示不限制 - "threads": self.get_recommended_threads() # 线程数,默认为推荐值 + "threads": self.get_recommended_threads(), # 线程数,默认为推荐值 + "use_gpu": False, # 是否使用GPU加速 + "selected_gpus": [], # 选择的GPU设备ID列表 + "hashcat_path": "", # hashcat安装目录 + "gpu_threads": 8, # GPU线程数,默认为8 + "gpu_accel": 64, # GPU加速因子,默认为64 + "workload": 3 # GPU工作负载配置(1-4),默认为3(高负载) } + # 检测GPU + self.available_gpus = [] # 初始为空列表,等选择了hashcat路径后再检测 + def init_ui(self): self.btn_decrypt.setObjectName('border') if not self.parent(): @@ -249,7 +261,8 @@ def show_crack_settings(self): """显示密码破解设置对话框""" dialog = QDialog(self) dialog.setWindowTitle("密码破解设置") - dialog.setMinimumWidth(400) + dialog.setMinimumWidth(500) + dialog.setMinimumHeight(550) # 增加高度以容纳新的GPU设置 layout = QVBoxLayout() @@ -322,14 +335,18 @@ def show_crack_settings(self): charset_layout.addWidget(charset_combo) layout.addLayout(charset_layout) + # CPU线程设置组 + cpu_group = QGroupBox("CPU设置") + cpu_layout = QVBoxLayout() + # 添加线程数设置 threads_layout = QHBoxLayout() - threads_label = QLabel("线程数:") + threads_label = QLabel("CPU线程数:") threads_layout.addWidget(threads_label) threads_spinbox = QSpinBox() threads_spinbox.setMinimum(1) - threads_spinbox.setMaximum(32) + threads_spinbox.setMaximum(1000) # 移除32线程限制 threads_spinbox.setValue(self.crack_settings.get("threads", self.get_recommended_threads())) threads_spinbox.setToolTip(f"推荐线程数: {self.get_recommended_threads()} (基于CPU核心数)") threads_layout.addWidget(threads_spinbox) @@ -339,7 +356,118 @@ def show_crack_settings(self): recommend_btn.clicked.connect(lambda: threads_spinbox.setValue(self.get_recommended_threads())) threads_layout.addWidget(recommend_btn) - layout.addLayout(threads_layout) + cpu_layout.addLayout(threads_layout) + cpu_group.setLayout(cpu_layout) + layout.addWidget(cpu_group) + + # 添加GPU加速设置 + gpu_group = QGroupBox("GPU加速") + gpu_layout = QVBoxLayout() + + # GPU启用开关 + use_gpu_checkbox = QCheckBox("启用GPU加速") + use_gpu_checkbox.setChecked(self.crack_settings.get("use_gpu", False)) + gpu_layout.addWidget(use_gpu_checkbox) + + # Hashcat路径选择 + hashcat_layout = QHBoxLayout() + hashcat_label = QLabel("Hashcat目录:") + hashcat_layout.addWidget(hashcat_label) + + hashcat_path = QLineEdit() + hashcat_path.setText(self.crack_settings.get("hashcat_path", "")) + hashcat_path.setReadOnly(True) + hashcat_layout.addWidget(hashcat_path) + + hashcat_btn = QPushButton("选择") + hashcat_btn.clicked.connect(lambda: self.select_hashcat_dir(hashcat_path, gpu_list)) + hashcat_layout.addWidget(hashcat_btn) + gpu_layout.addLayout(hashcat_layout) + + # 检测Hashcat是否可用的状态标签 + hashcat_status_label = QLabel() + hashcat_status_label.setText("请先选择Hashcat目录") + gpu_layout.addWidget(hashcat_status_label) + + # 安装指南按钮 + install_hashcat_btn = QPushButton("查看Hashcat安装指南") + install_hashcat_btn.clicked.connect(self.show_hashcat_installation_guide) + gpu_layout.addWidget(install_hashcat_btn) + + # 添加GPU线程数设置 + gpu_threads_layout = QHBoxLayout() + gpu_threads_label = QLabel("GPU线程数(-n):") + gpu_threads_layout.addWidget(gpu_threads_label) + + gpu_threads_spinbox = QSpinBox() + gpu_threads_spinbox.setMinimum(1) + gpu_threads_spinbox.setMaximum(1024) + gpu_threads_spinbox.setValue(self.crack_settings.get("gpu_threads", 8)) + gpu_threads_spinbox.setToolTip("设置GPU线程数,影响并行计算能力") + gpu_threads_layout.addWidget(gpu_threads_spinbox) + gpu_layout.addLayout(gpu_threads_layout) + + # 添加GPU加速因子设置 + gpu_accel_layout = QHBoxLayout() + gpu_accel_label = QLabel("GPU加速因子(-u):") + gpu_accel_layout.addWidget(gpu_accel_label) + + gpu_accel_spinbox = QSpinBox() + gpu_accel_spinbox.setMinimum(1) + gpu_accel_spinbox.setMaximum(1024) + gpu_accel_spinbox.setValue(self.crack_settings.get("gpu_accel", 64)) + gpu_accel_spinbox.setToolTip("设置GPU加速因子,增大此值可提高GPU利用率") + gpu_accel_layout.addWidget(gpu_accel_spinbox) + gpu_layout.addLayout(gpu_accel_layout) + + # 添加工作负载配置 + workload_layout = QHBoxLayout() + workload_label = QLabel("工作负载(-w):") + workload_layout.addWidget(workload_label) + + workload_combo = QComboBox() + workload_combo.addItem("1 - 低") + workload_combo.addItem("2 - 默认") + workload_combo.addItem("3 - 高") + workload_combo.addItem("4 - 最高") + workload_combo.setCurrentIndex(self.crack_settings.get("workload", 3) - 1) + workload_combo.setToolTip("设置GPU工作负载,值越高利用率越高,但可能影响系统响应") + workload_layout.addWidget(workload_combo) + gpu_layout.addLayout(workload_layout) + + # GPU设备选择 + gpu_devices_label = QLabel("选择GPU设备:") + gpu_layout.addWidget(gpu_devices_label) + + # GPU列表 + gpu_list = QListWidget() + gpu_list.setSelectionMode(QListWidget.MultiSelection) + + # 最初的状态提示 + if not hashcat_path.text(): + item = QListWidgetItem("请先选择Hashcat目录") + gpu_list.addItem(item) + gpu_list.setEnabled(False) + elif self.available_gpus: + for i, gpu in enumerate(self.available_gpus): + item = QListWidgetItem(f"{i}: {gpu}") + gpu_list.addItem(item) + # 如果之前选择了该GPU,设置为选中状态 + if i in self.crack_settings.get("selected_gpus", []): + item.setSelected(True) + else: + gpu_list.addItem("未检测到GPU设备") + gpu_list.setEnabled(False) + + gpu_layout.addWidget(gpu_list) + + # 刷新GPU列表按钮 + refresh_gpu_btn = QPushButton("刷新GPU列表") + refresh_gpu_btn.clicked.connect(lambda: self.refresh_gpu_list(gpu_list, hashcat_path.text())) + gpu_layout.addWidget(refresh_gpu_btn) + + gpu_group.setLayout(gpu_layout) + layout.addWidget(gpu_group) # 按钮区域 btn_layout = QHBoxLayout() @@ -352,6 +480,27 @@ def show_crack_settings(self): dialog.setLayout(layout) + # 检查hashcat状态并更新UI + def check_hashcat_status(): + path = hashcat_path.text() + if not path: + hashcat_status_label.setText("请先选择Hashcat目录") + hashcat_status_label.setStyleSheet("color: black;") + return False + + is_valid = self.check_hashcat_in_dir(path) + if is_valid: + hashcat_status_label.setText("Hashcat可用 ✓") + hashcat_status_label.setStyleSheet("color: green;") + return True + else: + hashcat_status_label.setText("所选目录中未找到有效的Hashcat ✗") + hashcat_status_label.setStyleSheet("color: red;") + return False + + # 初始检查 + hashcat_valid = check_hashcat_status() + # 连接按钮事件 ok_btn.clicked.connect(lambda: self.save_crack_settings( mode_combo.currentIndex() == 0, @@ -360,7 +509,14 @@ def show_crack_settings(self): max_length.text(), charset_combo.currentIndex(), "0", # 总是传递0作为超时时间 - threads_spinbox.value(), # 传递线程数 + threads_spinbox.value(), + use_gpu_checkbox.isChecked(), + [i for i in range(gpu_list.count()) if gpu_list.item(i).isSelected() and not gpu_list.item(i).text().startswith("请先") + and not gpu_list.item(i).text().startswith("未检测到")], + hashcat_path.text(), + gpu_threads_spinbox.value(), + gpu_accel_spinbox.value(), + workload_combo.currentIndex() + 1, dialog )) cancel_btn.clicked.connect(dialog.reject) @@ -374,19 +530,203 @@ def update_ui(): min_length.setEnabled(not is_dict_mode) max_length.setEnabled(not is_dict_mode) charset_combo.setEnabled(not is_dict_mode) + + # GPU设置只在启用GPU并且有hashcat时可用 + gpu_enabled = use_gpu_checkbox.isChecked() and hashcat_valid + gpu_list.setEnabled(gpu_enabled) + gpu_threads_spinbox.setEnabled(gpu_enabled) + gpu_accel_spinbox.setEnabled(gpu_enabled) + workload_combo.setEnabled(gpu_enabled) update_ui() mode_combo.currentIndexChanged.connect(update_ui) + use_gpu_checkbox.toggled.connect(update_ui) dialog.exec_() - def select_dict_file(self, line_edit): - """选择字典文件""" - file_path, _ = QFileDialog.getOpenFileName(self, "选择密码字典文件", "", "文本文件 (*.txt);;所有文件 (*)") - if file_path: - line_edit.setText(file_path) + def select_hashcat_dir(self, line_edit, gpu_list=None): + """选择Hashcat安装目录""" + dir_path = QFileDialog.getExistingDirectory(self, "选择Hashcat安装目录") + if dir_path: + # 检查目录是否包含hashcat可执行文件 + is_valid = self.check_hashcat_in_dir(dir_path) + + if is_valid: + line_edit.setText(dir_path) + # 如果提供了GPU列表,刷新它 + if gpu_list: + self.refresh_gpu_list(gpu_list, dir_path) + else: + QMessageBox.warning( + self, + "无效的Hashcat目录", + "在所选目录中未找到有效的Hashcat可执行文件。请确保选择正确的Hashcat安装目录。" + ) - def save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, charset_index, timeout, threads, dialog): + def check_hashcat_in_dir(self, dir_path): + """检查指定目录中是否存在有效的hashcat可执行文件""" + if not dir_path: + return False + + # 根据操作系统确定可执行文件名 + if os.name == 'nt': # Windows + hashcat_exe = os.path.join(dir_path, "hashcat.exe") + else: # Linux/macOS + hashcat_exe = os.path.join(dir_path, "hashcat") + + # 检查是否存在 + if not os.path.isfile(hashcat_exe): + return False + + # 检查是否可执行 + try: + # 使用绝对路径运行hashcat --version命令 + result = subprocess.run( + [hashcat_exe, "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + + # 检查命令是否执行成功 + return result.returncode == 0 + except Exception as e: + logger.error(f"检查hashcat出错: {str(e)}") + return False + + def refresh_gpu_list(self, gpu_list, hashcat_dir=""): + """刷新GPU列表""" + # 保存当前选择的项目 + selected_indexes = [i for i in range(gpu_list.count()) + if gpu_list.item(i).isSelected() and not gpu_list.item(i).text().startswith("请先") + and not gpu_list.item(i).text().startswith("未检测到")] + + # 清空列表 + gpu_list.clear() + + # 如果未提供hashcat目录,使用保存的路径 + if not hashcat_dir: + hashcat_dir = self.crack_settings.get("hashcat_path", "") + + # 检查hashcat目录是否有效 + if not hashcat_dir or not self.check_hashcat_in_dir(hashcat_dir): + gpu_list.addItem("请先选择有效的Hashcat目录") + gpu_list.setEnabled(False) + self.available_gpus = [] + return + + # 重新检测GPU + self.available_gpus = self.detect_gpu_with_hashcat(hashcat_dir) + + # 重新填充列表 + if self.available_gpus: + for i, gpu in enumerate(self.available_gpus): + item = QListWidgetItem(f"{i}: {gpu}") + gpu_list.addItem(item) + # 如果之前选择了该索引,并且索引仍然有效,则重新选中 + if i in selected_indexes and i < len(self.available_gpus): + item.setSelected(True) + else: + gpu_list.addItem("未检测到GPU设备") + gpu_list.setEnabled(False) + + def detect_gpu_with_hashcat(self, hashcat_dir): + """使用指定目录中的hashcat检测GPU设备""" + gpu_list = [] + + # 确定hashcat可执行文件路径 + if os.name == 'nt': # Windows + hashcat_exe = os.path.join(hashcat_dir, "hashcat.exe") + else: # Linux/macOS + hashcat_exe = os.path.join(hashcat_dir, "hashcat") + + try: + # 检查是否存在hashcat + if not os.path.isfile(hashcat_exe): + logger.error(f"未找到hashcat可执行文件: {hashcat_exe}") + return gpu_list + + # 执行hashcat --listdevices命令 + result = subprocess.run( + [hashcat_exe, "--listdevices"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + + if result.returncode == 0: + # 解析输出中的GPU信息 + output = result.stdout + # 查找形如 * Device #1: ... 的行 + device_lines = re.findall(r'Device #\d+:.*', output) + + for line in device_lines: + # 提取设备名称 + if "CPU" not in line and ("GPU" in line or "NVIDIA" in line or "AMD" in line): + name_match = re.search(r'Device #\d+: (.*)', line) + if name_match: + gpu_name = name_match.group(1).strip() + gpu_list.append(gpu_name) + + # 如果hashcat未检测到GPU,尝试其他方法 + if not gpu_list: + # 这里可以添加备用检测方法,保持与之前相同 + if os.name == 'nt': # Windows + # 使用WMIC查询GPU信息 + result = subprocess.run( + ["wmic", "path", "win32_VideoController", "get", "Name"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode == 0: + lines = result.stdout.strip().split('\n')[1:] # 跳过标题行 + for line in lines: + if line.strip(): + gpu_list.append(line.strip()) + + elif sys.platform == 'darwin': # macOS + result = subprocess.run( + ["system_profiler", "SPDisplaysDataType"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if "Chipset Model:" in line: + gpu_name = line.split(':', 1)[1].strip() + gpu_list.append(gpu_name) + + else: # Linux + # 尝试使用lspci + result = subprocess.run( + ["lspci", "-v"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if "VGA" in line or "3D" in line: + parts = line.split(':', 2) + if len(parts) >= 3: + gpu_name = parts[2].strip() + gpu_list.append(gpu_name) + + except Exception as e: + logger.error(f"使用hashcat检测GPU出错: {str(e)}") + + return gpu_list + + def save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, charset_index, + timeout, threads, use_gpu, selected_gpus, hashcat_path, + gpu_threads=8, gpu_accel=64, workload=3, dialog=None): """保存破解设置""" # 验证输入 if is_dict_mode and not os.path.exists(dict_path): @@ -398,8 +738,12 @@ def save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, c max_len = int(max_length) # 忽略超时参数,始终设置为0表示无限制 timeout_val = 0 - # 验证线程数 - thread_count = min(max(1, threads), 32) # 限制在1-32之间 + # 线程数不再限制最大值 + thread_count = max(1, threads) + # GPU相关参数验证 + gpu_threads_val = max(1, min(gpu_threads, 1024)) + gpu_accel_val = max(1, min(gpu_accel, 1024)) + workload_val = max(1, min(workload, 4)) if min_len < 1 or max_len < min_len: raise ValueError("无效的参数值") @@ -407,6 +751,25 @@ def save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, c QMessageBox.warning(self, "警告", "请输入有效的数字") return + # 验证GPU选择 + if use_gpu: + # 验证hashcat路径 + if not hashcat_path or not self.check_hashcat_in_dir(hashcat_path): + QMessageBox.warning(self, "警告", "请选择有效的Hashcat目录") + return + + # 验证是否选择了GPU + if not selected_gpus: + reply = QMessageBox.question( + self, + "未选择GPU", + "您启用了GPU加速但未选择任何GPU设备。是否继续?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.No: + return + # 保存设置 charset_map = ["digits", "lowercase", "uppercase", "alphanumeric", "all"] self.crack_settings = { @@ -415,11 +778,18 @@ def save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, c "min_length": min_len, "max_length": max_len, "charset": charset_map[charset_index], - "timeout": timeout_val, # 始终为0,表示不限制时间 - "threads": thread_count # 保存线程数设置 + "timeout": timeout_val, + "threads": thread_count, + "use_gpu": use_gpu, + "selected_gpus": selected_gpus, + "hashcat_path": hashcat_path, + "gpu_threads": gpu_threads_val, + "gpu_accel": gpu_accel_val, + "workload": workload_val } - dialog.accept() + if dialog: + dialog.accept() def decrypt_pdf(self): if not os.path.exists(self.input_file_path): @@ -780,6 +1150,64 @@ def get_recommended_threads(self): # 推荐使用核心数-1的线程数,最少为2 return max(2, cpu_count - 1) + def show_hashcat_installation_guide(self): + """显示Hashcat安装指南""" + dialog = QDialog(self) + dialog.setWindowTitle("Hashcat安装指南") + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout() + + # 添加安装说明 + info_label = QLabel() + info_label.setWordWrap(True) + info_label.setText( + "
1. Windows系统:
" + "2. Linux系统:
" + "sudo apt-get install hashcat(Ubuntu/Debian) 或相应命令3. macOS系统:
" + "brew install hashcat验证安装:
" + "注意事项:
" + "