From 23a4222ede6384a8bbb46ea080a66fd422fdf2c4 Mon Sep 17 00:00:00 2001 From: vistaminc Date: Mon, 5 May 2025 15:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9F=BA=E4=BA=8Ehashcat?= =?UTF-8?q?=E7=9A=84GPU=E5=8A=A0=E9=80=9F=E6=9A=B4=E5=8A=9B=E7=A0=B4?= =?UTF-8?q?=E8=A7=A3=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=89=B9=E9=87=8F=E5=88=A0?= =?UTF-8?q?=E5=A4=84=E7=A9=BA=E7=99=BD=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__pycache__/config.cpython-313.pyc | Bin 818 -> 816 bytes app/config.py | 2 +- app/ui/__pycache__/Icon.cpython-313.pyc | Bin 1672 -> 1670 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 170 -> 168 bytes .../__pycache__/pdf_tool.cpython-313.pyc | Bin 10260 -> 14048 bytes .../__pycache__/pdf_tool_ui.cpython-313.pyc | Bin 20066 -> 20064 bytes app/ui/pdf_tools/blank_pages/blank_pages.py | 786 ++++++++++++++++++ .../pdf_tools/blank_pages/blank_pages_ui.py | 199 +++++ .../__pycache__/__init__.cpython-313.pyc | Bin 440 -> 438 bytes .../__pycache__/merge_ui.cpython-313.pyc | Bin 25346 -> 25344 bytes app/ui/pdf_tools/pdf_tool.py | 54 +- .../__pycache__/decrypt.cpython-313.pyc | Bin 65582 -> 98290 bytes .../__pycache__/encrypt_ui.cpython-313.pyc | Bin 18257 -> 18257 bytes app/ui/pdf_tools/security/decrypt.py | 783 ++++++++++++++++- .../__pycache__/__init__.cpython-313.pyc | Bin 217 -> 217 bytes .../__pycache__/split_ui.cpython-313.pyc | Bin 16061 -> 16061 bytes 16 files changed, 1792 insertions(+), 32 deletions(-) create mode 100644 app/ui/pdf_tools/blank_pages/blank_pages.py create mode 100644 app/ui/pdf_tools/blank_pages/blank_pages_ui.py diff --git a/app/__pycache__/config.cpython-313.pyc b/app/__pycache__/config.cpython-313.pyc index 1150462bb1911ca817097c9d247b2fe7e1a416cc..5db5fd5d8dd12b5fcea806860da2a7001c77f018 100644 GIT binary patch delta 58 zcmdnQwtn&Ma40Jr6q}ZX^DC1F)sP# Pc{%xsDaDhcn63c;KG_ua 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 c004d4ba0ed6d1806d2b648b216839a5f303c9a6..072199ef04ae7fc76f11ed2c1bed6689bac85195 100644 GIT binary patch delta 54 zcmeC+ZR6$s%*)Hg00ear5*xXHv&eig19DQVVnT~ki;80kit^Ko5_4mo&24(JbK_ol3$*elb@JU Kyjh7gg%JScOcG!K diff --git a/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/__pycache__/__init__.cpython-313.pyc index 617ffb2e29360fa04dc0330aa4c7fe341b930818..77151474ded671e3e1ab40bdb4cd76abc094d8b3 100644 GIT binary patch delta 51 zcmZ3*xPp=UGcPX}0}y&_}?`)v4JE|LZAVXG>N8( zAWdffq*NUZ?2L)Esu~`qQfVr5d7fD{GD^Y^PTU0|IRtT9RBpQ?txaj5z%LAtm)LK^OCNK`@^N?vxO*%)`iXe z7Hn~&CgefUjW!h3#A;*47{6N$6A;3Uv8E{Bc?6-vs|e8<+Hg&OEw1giVyhe4y08t~ z=&wG!so#$6{SNHtcVZ_SGlWsU3%lGXX7!+$Ez-k8mVq2KXpdkaW~VFHM~%A`@&NY4 zgvh}%(^tPS&u&g`QF$ghM^|oRZ>*8Z_g31zn2+4x>{JQ5=-^i($`oZ5B8SNitBrib zY1FzYU7&&7_j?Nun<0v;kA& zTGd+P@#&fAd_3N;^!K_$oaVMy>Kzk%$Chiy$$m{6w__=-2`M-`d0+bm*G>j>A*JLqCbJKyP-|4O>cgQOPGk8Cs#V(+=0F|LqZRX|Z^wdlujs4`D(Xqh+!%p&+ zF;L%5$sB}*8?z+?T&2M#`$P1A2aN%hU0!iS+>UKgkcKonqvLO_8*4-ArX@Ez_|ZMIQu z99%DXukqhZjjc* z|BV{(GMZRTb%aJylq;78a11;NBlJfU@#ftkxBI zs*Dj<4y$_*ifGBA+RW)7?Z_pwi6%R3n;$pZ$T$^b-0V;&N6B;Mrghj1^;QzJHIO&z z8`t4jU5vbGv#iTuCq0|gpwc&U6{b)?HCQrKC4T(Obm|oh6;RGEo$Tc_bQl007kS2N z0Z$VDc$A9DaRrL2fxnZIC86wRxs0|xT9;US{Fm~cM|bs>i}|9XP15fW^*e5=q`ime zTiko-u6Os%ky{fVOcckCN@GcJEGdnh6zO|-w3yC{-r4!kGB2#~2C2XcA}>h%R*~N- z@z03-GdJ3$UH#&&{$)N)85Y8?h9$M zc!$V4t|TSrF44J5a(0XK<$GWW-XroJiEkG9=4&d+e?asfko+%*{uh?{{*{UH5^j-q zFPbEwLlinBp;r`oKjwpJIV>jftieM5V=%EE`;jP}2hkuuQwaXv zJ(j_H=cU4^H6?yOC z$UE_?amm*s`g)f6=T@Aa`Ot--`!&ct%*}VNF=HqDC@>1M<0RPe+X~BDmBM1PCti~q zOw1aHsF?ZN%nci4^OK*Otz6x@^+E;KH?LcB9-H_u$NmH6pGq;4{X^4SF~~NqK#iQw zY!j>a3WMF{%*H_FD6H{LjQrl?F1!NW=P1zR(-<$dU$IM?Euv=2wf5`X*Sl}#Zxuc$ zNZsRN_jvKxap_n_JeIlBop~Cr>fy57wQzS)vt^CHs458kzq)IZep}gHKvx|mzRg|7 zNsq@wih?fqLpt%nPbrjL=9DykQRBZhBDHkU_jbD!91(*fQgBiXP8O4=q~x5KoVyd8 ztGJEax0?#fteVO7P);hz*CF~kBww%S>s{u9D{k+6=wseN^D4xh;fgI?MUDR{ISV0j zQQ!1E&n`FzmEFb8L{FYw)6`v_;h%tyA-kTE)#y>@dr^_-rLp>WRb7!_A1T|6oiLu< zW~b@YN6rh69z*C2w19G#DD!Fp6s_AN&2~|<{f0@}*+<{Mv`gVhF+3@S(_%PX%;vvD zXqM|${7e3MfY1PKRkESH-&7w5mif@vb{xsqDf&7kU!Umfvn}%nSG<164uNI|NGtB_ zD{8jWI~smS)(D@qP;Qimf=DpW`X{`JdkV#~-iXd-G@aKD=}fcE><~eCGS&M9H7X zW?Sc`Tj?Dxo~KvjTx(So8i; z0WT1~nckSmwq~bu0EXc5SgPHBI+L7Br@HWOXcKnb+%+=Qyt(NU)c0^hP{Fw_zIbJ7 T@!UfM^!))9dAoU51@r$0*iF2U delta 1830 zcmb7EO>A355Z;ZQ`1d)9e`3e+&x;c&ag;VcP134OQAwjpI3x{zg+lU*Ym#Tj#Ic=u zPH2%(kpQVZAhp_7YI{IjI7Dhug-;xjkhpL`0)ZYEq+XyBC`yVrAQg7jj&Upw;KS#c zotbaG-JQ2TZQNdT?MsrK@ZXtf@0z{qmMcIX-5R)>36L03Y(b*fRjcY&os+GYLVBWvmwbWK zd)TiION6Hfl8HvDs?AHH$@zn{j)Q8MWTmVWce9`A9sf#WnYrfdKVmyA7ic?s)^h5& zJxIWI~Cr7U$ar7xJ=z5u)rr_@<@eaeaID76wv6w$wER>;`|Ha8}ntJJ8)wfC-Kqq@*iBM0)W<5{c;N%2`qfjiT zc(hc6GRxa~+vBZrj{ETWVKXjH8aI# zJdbIj@>{@ZV&ncGeUZ)k`>(x%MV7U+o-UUGE^_am<}i=$0A@J#Ql6hehVMXMgY&pv zRf5(c!b=EA4)GR1Wq`~&?o6)f8{&4eKDO(()92Zr{yiaW$i!vQ-rfPNumV?0Ww?Y) zuOYmSP(PQNG?or|kN+n%NOVZeFnbV+()r5QAsZFFB@=rrheiMRwbgoG?ce(HHN&;4 z361~jP>}bKjH5^uY`U~nUyYjIo7nstC?dgr92*Jk(1kdyo`!;C4FhnQeK8#Kq2PUd zx!xr|gyiey@2uUTbTs@XU*xr7N6ocaUhC~Xyrngs4|xP^U9T5I(pZ7f_du=s{q80rEVV7`#geYL~i6d<%&^*c137HPC zcP4&!3L?0I%kQy=lY#a3(X6JS^#KRNoQCYSVM>=uhB?DuP{X2Y<#YIy;^qzWc20YX zKXq$Gj@p%68Btr=;dhD@_;483>5by*R$hA!?sFFhO8`|;aP P3EI0!6I-9!G-L7K6c~^U 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 c3f08b14f975c574d3602948a20bdd60fa8ccfa0..7f932a2384746d2a8ebf576a484cda62e10c1b89 100644 GIT binary patch delta 56 zcmaDfhw;H2M()qNyj%=G@XcRhBllNhnJ?}@PKs4bXmM&$aZEu`etJ=2Zp^c}O;2`i K+$?9}A_V|2*cB-N delta 58 zcmaDbhw;%IM()qNyj%=Ga9x0RBllNhxm!LiRxzQ)sYS&xfu$vhd1;Ax=`k+(<#{>z Mi7CaKWlUV8093aWkN^Mx 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 f0a21ec4d0e1609ce952b81af848c8c5167135ad..bd2e43204670735d1f91a6e161af0a8d499e588b 100644 GIT binary patch delta 53 zcmdnNyp5UrGcPX}0}vcFme|NG$0+l~3&=^aiU}=FEh>&FD9TSSO3aOUHn-`?&W)4Z G84CfJ;}SOj delta 55 zcmdnSyn~thGcPX}0}xyn;N8eA$0&Ep-^D5>v^ce>I3}>PBrz{7F)uyFCBHl`CqFTz Jc(N;FAppcJ5$gZ| 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 dbcfa10d8f7b39ea2a7aa8b33885548f68d75e81..905ac2eb52ff9d8fea3f3b44291b3f1c211c5a99 100644 GIT binary patch delta 56 zcmZoV#@KL-k^3_*FBbz4blFL4k^3_*FBbz4To>To$erpacgx?!DkiizwWv5Iu(TvGFD)@IJ;o)!JTE6d MF{OBOk7J1<04wJdSpWb4 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 8b5a6c018fa48a1a152fd4ea7ffbe2e1ac10aeec..6939ba3997acf95ad51cb6d1e5a12d9b1240b8ea 100644 GIT binary patch delta 35403 zcmbrn30zyp@i?v%gpfcW0Rn^&Hv+_sv9SSTu)!Fx&66BE*d`ccOap>ELF_n<<+M#o zPUCDG<2b2tlEyffNof-r=Wu#_;~q(+NQo5H-_&i5k2Y|3f7r^sdM!V~Ei|}AYbu-Cs$+GnX>3}np4GRe zv+4Akblt%}RykUW+2Ym`wxqR`Ep07h%Ua9X^41Erf+mSrSJ_&{R?%?ex+SgEY&8wb z)-7#a#xA4bsC6~1%h}~LEMK>xwU(`=;plaBt@Ug@4J+2IY;9m0AgoLgu&cC!OaT*< zB4A>-ZpqkMyS2JXitQB$*hczE1)t)!YO2J6*ru(knfRK`0zuDh0&J*youPFNyM`7m zT&G|a5T}8bLhB5It!pv`TNR!6ljomINn~5L>Y=U#Cb3EsC~WQ47P4QYDc!oZO2|b) zo_a_m_;|1FJSac zER9KIp!?V)CJvyMi3ga>r~#%h8i1)5L8XvMfRr*;$0R~b8j}Q2&u9UrGsyrAObWmZ zCY8;!2x^6FR<@wpnC_2hvi7pKjEpu8+eX>p!7KTIsXqc*J8ETn14BhSGzK8Pe=JeV z?u>LrS4&EucNr<8!GS1NS_FelG$DWz0lcl|-}(i1kpceM#5)DXq*=4NP^h@F624x+ z@=hwr?V6@mf8?0m+TA}g=2zOSgVx?rYahPZ{fb>Z_FcU_qunDtqr3dk5Z67ri?#Oj z+5J)Y($m{(9rVj~53@H84)^pK*N`_fD}~F5Ea63A?(CU_&qWRXn4X(^1_pa}4AL6x zMyX!{mF#4fLH+C|0Dj?sKdQUCcd*B9@9wrE;TquC&sJcHPI6hRiztGi{U@{X6}@9DYqgE8we?x| zu=irIBM62Oj3Kxg!7Tv%G0Qp)&|xM<7G+ zGXy_J@Czbbsx={P&^|TFX&3mTu&23MBkcBz0VBWA%Kir?dxNZ5+Q2odm)y5BTlk0B zA1&P?T8ov@IsR0lK(Jk~J#TydcH{Pf?SrlQqpKN^GAcCK`;RG`AVKfb98A#1anEPGPPz!yKSn#olx{&ng!$|mY zY+`c$KoYr{8BPA@ez}5AJXB$>Ag^a75M4Hu%LUI2L`e(+cyZ}9LNlp9kdVkk;v{Bd zbb$dvV4~Iw8990PfFZ_Q!B=4rScGK1OhfL`D@aYf+`w02lZ0Uu%_p}>$;L+$NldM> zAT@xlfX^{hU@quP<6}d=6dU00hz(y3pjtpSrfbNuZUvT@Yc2?V>dN3VGckPZkkM@H z%p_$wYBCh7NhknzFQN;Bj89<@U_mivV<;lfv=wzqvb;VjlSkAR5!N&npWGH{i6E{| zG-SL^ne^4hm7B{u)5wQ_G!d}7RHe9b{pFjP`f2=!M~7@`S$a+}-|Mf+?2 z3v#VN&=towoA2hKJaZnBsWOqz0;5i2$otbkxZFqWS3Vw?<3FaQgg!D+jGRejbRZ$< zi>0<`$X@nQLSl4hAzyFkmkx}MNvHH5#Y;$`vN*(J%z0r^+$=cdR&44*{zD(ez$dUN zx}x}4zI`_3HRf?lSXX5PbH`d@u&XjHv3wMCSjpl7hbqmLByM{`i9D3bT-k}s%%X?M z3{OECwg=`s>HkC%!DN{Qoz+x``(l+kwXze}_(czs&8LZv5imIw;!t@Us=4OMP*m82 z%;jTkDvK&OArlfUl`tRkQ2gQ(06Eb{$5?_>45E~j*}UJV%u;kNUxXCAT9jegh1h2g zG$|WG1Cq~ww#8jzK;pY<`68Jh8yKoGSCNc&l1Re=HTI8|DPRh(K@;&tJK7Q~ z3Bke8Bo|qVfj^3wl11Y|k&dwhr!b61JdQ_`CCGlD9l?c&C-k8bbIG;Z!IXxuf`Xt! za?K^7s4%Jt@)%p9B{7Jon!Ndr9#$b??FzaS0brI%8#OB_=DQhxYs znPIiH1Z&F*iw$aOY}uAfEF;E}#V3UF15w#wQB+D%S;Ck9e8A#wL;6hM`6ZO=w<6}Y z97|4SNHj0CWQE#TmS@Sg6j%x^MHUm}DzlVZDl7s^rKQTUq%wwCx2WaW!JKM|Ilt2k z3aGS-Jc}_{L}{Q1%&uVCX!W_4+)(wnG3ngI3-7T(83N*~IyYZ4KEJbzDyI6|jUD9j z>RQ>rCqSqJxgwIkralSPEPi?4Kre0`d)Ql8pd11FB-ytnQMj5MTC>4KH{L?OiqoWY zV^X^BhTZXMOgjO9?vD3^hJ(a6H>PGo`F7m?eX(CKt??S`T*kV&TRg_KY!w-4PE1<^ zpRSYx7{dkf;v@U#pEz;l&};KAJz2vxlgFDMiD6g42eux-Nh!OMw6~ZFTQEIJj*S~E zm!Es}(ksu=-0*hkrPnS^-*fTMOwE zb!&C8RN?R^h++Q>ppV?VHq(LRMr&x^V41&r>dO9OW3do5KXK~PgVVt{7TLs_G1OqS zaLS}hN1vO2w>;=ZnXFcy#LID+FRVJI)%|9y)1{EI)l@X8~Xe|+rnE3f36b~}tUW06p3nZabZ z@XGyTs9-OL@Rie(m!ErreyYINrzh#BN(ck#!YjK0@xk_n@Chp#@!9|K0)VE#>1tQ$>NC5X8@75k^td+k zxHt5AO8Y2HjzvJKOUIv^f9_!#oF~t&jVFJ-6s4$yGI)G(?T)vBau_ETK7CmNfmzIH zi^ROPNYXDhOGsQ6Y~|+_DvOX$+`}hMAX6i-OXf0w8c5O~fiJ&HBva*bKK`UU+R-mI zMs=_sLq&U)y~BgUY>i>ZU{CLj%hyn9;-|*}hw%k01Q~Kgv`(Z#i zj&Wh`SscE*UWKu{_{d!s4~<`X_Y=v!EgcSw`TO4cNPH?in2Dqb$O&;Zxa1dq&6X-BcILv2G*=&`t+fhksG2 zTzdb?CX(4)=vY`I_w53XE9xL&Tz%?vuR7bM&UUMF$D7_)C;3#_UR9AxRpd0acvNfY zYq?8RK2z^eHG<;%d{U}6snnHJI-_wXEg5flKQ=teN{^}`Jj)H3C-r=&%r1{=YbZf2 zCQs5)8d^Z_IFBg3e+dk>E(UmT^fh^~9En}V2y0-m0V4RLSwjr1QNpNTNEQsyc_Cp$ zW(7G|pp%%Dol!`TijX41EG0co9iIMZ)Pha0M3%rmkopdR#3z7{Af%A<5;=MOQ5^vh zGzw5_2gO&|m*!#31F|S0%hnM6K%Be>Q*8gb6jLKnnL*;llmx^-!3I(LVNUXcBNKr+ zGKhZ!qAh}{L4RWCEN7(1s?DM*T-@Nv280dp&nAGgmWU228u*2!pW^hSOUC%83J8Yt zAofX!Fv|UEd(X|*?p_w=M>nj%qXV{nJ9`4YQ6lzBfi%ag{-^2lRnzRj@ow*jP5eVvIn6czp{5%4+~{?_YM#381_f>4fG5S z_fwg~9~V5#>9&sy*mex>VL!q=YK#w@_yoVub-NksXHhco%ZG;h0Cy~v)Bpb+V^x78 zrEWS0phSNRe@w)swIkQXc@*|6T8;mSQ>6?5OqHooZ(hACuimFf^eVDkiYzzTvTl__ z=T+po6uE9i{yA;hSGTWszvBi=2yy0+YURUo)cxLWkNKybZqjp&|@ zKCR)H@`!S($F0rxYE3S!$*nE%Y8^E$ZOvSrSKIozSP&EI(w2Nk>pic~dKGCdMVecY z?o*_D6*(?Nj$4uEQ=|p3SLFB z3Hhs+YJV;)L%0OtFA5_72Iec7zZ9!@4!OL7o259ZnyHIzkC8vj2#f+pP|dV#(DA zL298lrP7sB=}uYVO=+2{9be~DXuOJamm+;C+M_6<<5%EP6ihGoD5~iQ=DQU6)2SXs z6&*ty+o>DhQIu0o+F>kaQxL5q0Q|8q3Dqx#vvbyf z-_r@xp>zBVgZCl$1pt2(pvN7dvq*mQNG`y0{OR^x!@I-QPv9|wgFUzr>h2#K=(DmJ zm;)Qg@gexoj$<6tJ~lWy5RBv2Nq_-DB!P3C0CF`6=JP{RI%YPaK!76 z4Kl_+iV&yd3ZdopS#KWbweq5f3@M%Rbukp@j|~?u5j{Bo>;__8uXX$n_H`2Gz!}CW z5F{X=8`6a{jN2mpH|9vhB#UMk@^*fj!91#l*BcD~$3sr9kBMCwcsLe+6Y1HKK#cA2 zatTyoAA*hQ|30vxAjT()@ygO&vh*pjN0vQXu;FDe-a@` z$vK)f9&=8spL75lCZBYcNGd(De3Hl9>xlQtbS{~0a=S-XSU`&mPZTM zx@6kPWRENhin&I~x*TWD5_f8~Q>OLFv|d@3OO`d&S z$U1zoKwZu>lSfv};pN~EbEcQLwWZ@qpDfWUGq_}isWgwQ_PAnXc_Q}*->o_#u&F=EZ zwmL$oQ#~?M5J%b{fTAJy=pLs`=acEYvTTh zwZtQL!oONGqXlqjcb)NeiAF}Lk(gFC0g zBU>L#?#$cdkzE(6!jZGoBU=`RScykg3W(|Qj@H7Uo=Y(}GpgMwOUGmB_-49fnN#&1 zS;Yli_R&^~Y;vIHjg(~gH?v1p;FG}{%~9#iTH?xD;?1gYW!20rKPz%)t@Fs*Ac2E# z>J1)QPp}GS7SLx^7*+v-NzFZK8jn4fobEJKxRWc#VFDz1Wf?A6##AwM=!N9Wqp27P zM5K4g^pm&Hp5ouWO?!%eGg*(!z7U5X(v#glPtcQTS4Z`ndj6%BXw?d{df~yadj?*Zqn>gWQNvX={t&CpEAy? zG{FDoQi4|IU4N6~`RWmjVa$^@?hadD{N)1Vfj;449W!mgkb?y_?_Xky#gZ-}!&TkjQI{ z#qoJWY*gBZ*@9${FN?>O&}UJx2V(95u88CRxjZ3uhT6 z≪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 E0B4?;(*OVf delta 10936 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 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( + "

安装Hashcat的步骤

" + "

1. Windows系统:

" + "
    " + "
  • 访问官方网站下载:https://github.com/hashcat/hashcat/releases
  • " + "
  • 下载Windows版本的.7z文件(如hashcat-6.2.6.7z)
  • " + "
  • 使用7zip解压下载的文件到任意目录
  • " + "
  • 在PDF解密工具中选择刚才解压出来的目录作为Hashcat目录
  • " + "
  • 注意:目录中应当直接包含hashcat.exe文件
  • " + "
" + "

2. Linux系统:

" + "
    " + "
  • 从发行版仓库安装: sudo apt-get install hashcat(Ubuntu/Debian) 或相应命令
  • " + "
  • 或从官网下载Linux版本并解压到任意目录
  • " + "
  • 在PDF解密工具中选择hashcat的安装目录
  • " + "
" + "

3. macOS系统:

" + "
    " + "
  • 使用Homebrew安装: brew install hashcat
  • " + "
  • 找到安装路径(通常是/usr/local/bin/或/opt/homebrew/bin/)
  • " + "
  • 在PDF解密工具中选择该目录
  • " + "
" + "

验证安装:

" + "
    " + "
  • 在PDF解密工具中选择好Hashcat目录后,可以看到\"Hashcat可用 ✓\"的提示
  • " + "
  • 如果看到错误提示,请确认选择的目录中直接包含hashcat可执行文件
  • " + "
" + "

注意事项:

" + "
    " + "
  • 确保已安装GPU驱动程序(NVIDIA或AMD)
  • " + "
  • 例如:NVIDIA需要安装CUDA
  • " + "
  • 使用Hashcat进行GPU加速需要OpenCL支持
  • " + "
  • 某些集成显卡可能不支持或性能较差
  • " + "
  • 如果不确定Hashcat的具体目录,请在系统中找到hashcat可执行文件,然后选择其所在目录
  • " + "
" + ) + + layout.addWidget(info_label) + + # 关闭按钮 + close_btn = QPushButton("关闭") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec_() + class DecryptThread(QThread): okSignal = Signal(tuple) # (success, message) @@ -892,7 +1320,14 @@ def run_crack_decrypt(self): except Exception as e: logger.error(f"尝试密码'{pwd}'失败: {str(e)}") - # 如果是字典模式,直接使用字典文件尝试密码 + # 检查是否需要使用GPU加速 + use_gpu = self.crack_settings.get("use_gpu", False) + + # 如果启用GPU且Hashcat可用,使用Hashcat进行GPU加速破解 + if use_gpu and self.check_tool_installed("hashcat"): + return self.run_gpu_crack() + + # 根据模式选择破解方法 if self.crack_settings.get("mode") == "dictionary": return self.run_dict_crack() @@ -909,6 +1344,316 @@ def run_crack_decrypt(self): self.okSignal.emit((False, f"PDF破解失败: {str(e)}")) return + def run_gpu_crack(self): + """使用GPU加速进行密码破解""" + try: + # 获取设置 + is_dict_mode = self.crack_settings.get("mode") == "dictionary" + selected_gpus = self.crack_settings.get("selected_gpus", []) + hashcat_dir = self.crack_settings.get("hashcat_path", "") + gpu_threads = self.crack_settings.get("gpu_threads", 8) + gpu_accel = self.crack_settings.get("gpu_accel", 64) + workload = self.crack_settings.get("workload", 3) + + # 检查hashcat路径 + if not hashcat_dir: + self.okSignal.emit((False, "未指定Hashcat目录")) + return + + # 确定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") + + # 检查是否存在hashcat + if not os.path.isfile(hashcat_exe): + self.okSignal.emit((False, f"未找到Hashcat可执行文件: {hashcat_exe}")) + return + + # 创建临时目录 + temp_dir = tempfile.mkdtemp() + hash_file = os.path.join(temp_dir, "pdf_hash.txt") + + # 提取PDF密码哈希 + self.current_pwd_signal.emit("正在提取PDF密码哈希...") + + # 使用pdf2john提取哈希 + try: + # 尝试直接使用pdf2john + pdf2john_cmd = ["pdf2john", self.input_file.file_path] + result = subprocess.run( + pdf2john_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + + if result.returncode != 0: + # 尝试使用pdf2john.py + pdf2john_cmd = ["pdf2john.py", self.input_file.file_path] + result = subprocess.run( + pdf2john_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + + if result.returncode != 0: + self.okSignal.emit((False, "提取PDF密码哈希失败,无法使用GPU加速")) + return + + # 保存哈希到文件 + hash_content = result.stdout.strip() + with open(hash_file, 'w') as f: + f.write(hash_content) + + logger.info(f"成功提取PDF密码哈希: {hash_content[:50]}...") + + except Exception as e: + logger.error(f"提取PDF密码哈希失败: {str(e)}") + self.okSignal.emit((False, f"提取PDF密码哈希失败: {str(e)}")) + return + + # 准备hashcat命令 + hashcat_cmd = [hashcat_exe] # 使用指定目录的hashcat可执行文件 + + # 添加设备选项 + if selected_gpus: + devices_str = ",".join(str(gpu) for gpu in selected_gpus) + hashcat_cmd.extend(["-d", devices_str]) + + # 添加哈希类型 (PDF的哈希类型为10400/10500/10600,取决于PDF版本) + hashcat_cmd.extend(["-m", "10500"]) # 尝试使用PDF 1.4-1.6格式 + + # 添加GPU优化参数 + hashcat_cmd.extend(["-n", str(gpu_threads)]) # GPU线程数 + hashcat_cmd.extend(["-u", str(gpu_accel)]) # GPU加速因子 + hashcat_cmd.extend(["-w", str(workload)]) # 工作负载 + + # 优化参数 + hashcat_cmd.extend(["--opencl-device-types=1,2,3"]) # 使用所有类型的OpenCL设备 + hashcat_cmd.extend(["--force"]) # 忽略警告 + hashcat_cmd.extend(["--optimized-kernel-enable"]) # 启用优化内核 + + # 添加哈希文件 + hashcat_cmd.append(hash_file) + + # 根据模式添加字典或掩码 + if is_dict_mode: + # 字典模式 + dict_path = self.crack_settings.get("dict_path", "") + if not os.path.exists(dict_path): + self.okSignal.emit((False, "字典文件不存在")) + return + + hashcat_cmd.append(dict_path) + + self.current_pwd_signal.emit(f"正在使用GPU加速进行字典破解 (线程数:{gpu_threads}, 加速因子:{gpu_accel})...") + logger.info(f"执行GPU字典破解命令: {' '.join(hashcat_cmd)}") + + else: + # 暴力破解模式 + min_len = self.crack_settings.get("min_length", 4) + max_len = self.crack_settings.get("max_length", 8) + charset = self.crack_settings.get("charset", "digits") + + # 定义字符集掩码 + charset_mask = "" + if charset == "digits": + charset_mask = "?d" # 数字 + elif charset == "lowercase": + charset_mask = "?l" # 小写字母 + elif charset == "uppercase": + charset_mask = "?u" # 大写字母 + elif charset == "alphanumeric": + charset_mask = "?a" # 字母数字 + else: # all + charset_mask = "?a" # 所有字符 + + # 构建掩码 + mask = charset_mask * min_len + + # 添加掩码参数 + hashcat_cmd.append(mask) + + # 如果有长度范围,添加增量模式 + if min_len < max_len: + increment_str = f"--increment --increment-min={min_len} --increment-max={max_len}" + hashcat_cmd.extend(increment_str.split()) + + self.current_pwd_signal.emit( + f"正在使用GPU加速进行暴力破解 (长度: {min_len}-{max_len}, 线程数:{gpu_threads}, 加速因子:{gpu_accel})..." + ) + logger.info(f"执行GPU暴力破解命令: {' '.join(hashcat_cmd)}") + + # 添加其他选项 + hashcat_cmd.extend(["--status", "--potfile-disable"]) + + # 运行hashcat进程 + self.progressSignal.emit(30) + hashcat_process = subprocess.Popen( + hashcat_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + # 创建读取输出的线程,防止阻塞 + def read_output(): + found_password = None + for line in hashcat_process.stdout: + if self.isInterruptionRequested(): + hashcat_process.terminate() + break + + # 解析hashcat输出,更新进度和找到的密码 + if "STATUS" in line: + try: + progress = re.search(r'PROGRESS\s*:\s*(\d+)', line) + if progress: + progress_val = min(int(progress.group(1)), 100) + self.progressSignal.emit(30 + int(progress_val * 0.6)) + except: + pass + + if "Session.Name..." in line: + self.current_pwd_signal.emit("GPU加速初始化完成,开始破解...") + + # 更新进度信息 + if "Speed.Dev" in line: + speed_match = re.search(r'Speed.Dev.*:\s*([\d.]+)\s*([A-Za-z/]+)', line) + if speed_match: + speed = speed_match.group(1) + unit = speed_match.group(2) + self.current_pwd_signal.emit(f"GPU破解速度: {speed} {unit}") + + # 更新GPU利用率信息(如果有) + if "Util:" in line: + util_match = re.search(r'Util:\s*(\d+)%', line) + if util_match: + util = util_match.group(1) + self.current_pwd_signal.emit(f"GPU利用率: {util}%") + + # 检查是否找到密码 + if "Recovered" in line and ":" in line and "0/1 (0.00%)" not in line: + self.current_pwd_signal.emit("已找到密码!正在验证...") + # 尝试从输出中提取密码 + password_match = re.search(r':\s*(.+?)$', line) + if password_match: + found_password = password_match.group(1).strip() + + # 如果找到了密码,结束循环 + if found_password: + break + + # 创建读取stderr的线程,可能包含更多调试信息 + def read_stderr(): + for line in hashcat_process.stderr: + if self.isInterruptionRequested(): + break + + # 记录错误信息 + logger.debug(f"Hashcat stderr: {line.strip()}") + + # 如果包含重要信息,也更新UI + if "CUDA" in line or "OpenCL" in line or "Error" in line: + self.current_pwd_signal.emit(f"GPU信息: {line.strip()}") + + # 启动输出读取线程 + output_thread = threading.Thread(target=read_output) + output_thread.daemon = True + output_thread.start() + + # 启动错误读取线程 + stderr_thread = threading.Thread(target=read_stderr) + stderr_thread.daemon = True + stderr_thread.start() + + # 等待hashcat进程完成或用户中断 + start_time = time.time() + while hashcat_process.poll() is None: + if self.isInterruptionRequested(): + hashcat_process.terminate() + self.okSignal.emit((False, "用户终止了破解过程")) + return + + # 检查是否超时 + if time.time() - start_time > 300: # 5分钟超时 + self.current_pwd_signal.emit("GPU破解超时,尝试读取结果...") + break + + time.sleep(0.5) + + # 等待输出线程结束 + output_thread.join(timeout=2) + stderr_thread.join(timeout=2) + + # 检查破解结果 + return_code = hashcat_process.poll() or 0 + stdout, stderr = hashcat_process.communicate() + + # 查找密码 + password = None + + # 在输出中查找密码 + potfile_path = os.path.join(temp_dir, "hashcat.potfile") + if os.path.exists(potfile_path): + with open(potfile_path, 'r') as f: + potfile_content = f.read() + if ":" in potfile_content: + password = potfile_content.split(":", 1)[1].strip() + + # 如果没有找到密码,尝试从stdout中解析 + if not password and stdout: + password_match = re.search(r'Hash\.Target\s*:\s*.*:(.*?)$', stdout, re.MULTILINE) + if password_match: + password = password_match.group(1).strip() + + if not password: + # 尝试从结果目录查找 + cracked_files = [f for f in os.listdir(temp_dir) if f.endswith('.cracked')] + for cracked_file in cracked_files: + with open(os.path.join(temp_dir, cracked_file), 'r') as f: + content = f.read().strip() + if content: + password_parts = content.split(':') + if len(password_parts) > 1: + password = password_parts[-1].strip() + break + + # 如果找到了密码,验证并保存解密后的PDF + if password: + try: + # 验证密码 + self.current_pwd_signal.emit(f"验证密码: {password}") + with fitz.open(self.input_file.file_path) as pdf: + if pdf.authenticate(password): + # 保存解密后的PDF + pdf.save(self.output_path) + self.progressSignal.emit(100) + success_message = f"{os.path.dirname(self.output_path)}\n成功破解密码: {password}" + self.okSignal.emit((True, success_message)) + return + except Exception as e: + logger.error(f"验证密码失败: {str(e)}") + + # 如果GPU破解失败,尝试回退到CPU方法 + self.current_pwd_signal.emit("GPU破解未找到密码,切换到CPU方法...") + + if self.crack_settings.get("mode") == "dictionary": + return self.run_dict_crack() + else: + return self.run_bruteforce_crack() + + except Exception as e: + logger.error(f"GPU破解出错: {str(e)}") + self.okSignal.emit((False, f"GPU破解失败: {str(e)}")) + return + def run_dict_crack(self): """使用字典破解密码 - 多线程版本""" dict_path = self.crack_settings.get("dict_path", "") diff --git a/app/ui/pdf_tools/split/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/split/__pycache__/__init__.cpython-313.pyc index db27cd76bdac8bddf57a87ddc5d89e823e40f3df..5a761f2398890f9189c4301a4defe71bcc8c3cd7 100644 GIT binary patch delta 21 bcmcb~c$1O$GcPX}0}vcBmdG%h$a@9=LAeF+ delta 21 bcmcb~c$1O$GcPX}0}ya&iDXz#GcPX}0}%Xl*~qoo4gf}k27dqm delta 19 Zcmdm6ySJ9>GcPX}0}woQ*vPfn4gf{?24?^O