diff --git a/.gitignore b/.gitignore index 4eec840..01c9398 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,6 @@ build *test* *.log *.c -*.pyd \ No newline at end of file +*.pyd +__pycache__/ +*.pyc \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..73ef729 Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/__pycache__/config.cpython-313.pyc b/app/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..1150462 Binary files /dev/null and b/app/__pycache__/config.cpython-313.pyc differ diff --git a/app/log/__pycache__/__init__.cpython-313.pyc b/app/log/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..38aa441 Binary files /dev/null and b/app/log/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/log/__pycache__/exception_handling.cpython-313.pyc b/app/log/__pycache__/exception_handling.cpython-313.pyc new file mode 100644 index 0000000..3711321 Binary files /dev/null and b/app/log/__pycache__/exception_handling.cpython-313.pyc differ diff --git a/app/log/__pycache__/logger.cpython-313.pyc b/app/log/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000..e1f02ed Binary files /dev/null and b/app/log/__pycache__/logger.cpython-313.pyc differ diff --git a/app/model/__pycache__/__init__.cpython-313.pyc b/app/model/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..f3cd27b Binary files /dev/null and b/app/model/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/model/__pycache__/doc_cov_model.cpython-313.pyc b/app/model/__pycache__/doc_cov_model.cpython-313.pyc new file mode 100644 index 0000000..60e0312 Binary files /dev/null and b/app/model/__pycache__/doc_cov_model.cpython-313.pyc differ diff --git a/app/model/__pycache__/file_model.cpython-313.pyc b/app/model/__pycache__/file_model.cpython-313.pyc new file mode 100644 index 0000000..1a22ecf Binary files /dev/null and b/app/model/__pycache__/file_model.cpython-313.pyc differ diff --git a/app/ui/__pycache__/Icon.cpython-313.pyc b/app/ui/__pycache__/Icon.cpython-313.pyc new file mode 100644 index 0000000..c004d4b Binary files /dev/null and b/app/ui/__pycache__/Icon.cpython-313.pyc differ diff --git a/app/ui/__pycache__/__init__.cpython-313.pyc b/app/ui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b5f5ee0 Binary files /dev/null and b/app/ui/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/__pycache__/global_signal.cpython-313.pyc b/app/ui/__pycache__/global_signal.cpython-313.pyc new file mode 100644 index 0000000..71cd536 Binary files /dev/null and b/app/ui/__pycache__/global_signal.cpython-313.pyc differ diff --git a/app/ui/__pycache__/mainview.cpython-313.pyc b/app/ui/__pycache__/mainview.cpython-313.pyc new file mode 100644 index 0000000..cd04ede Binary files /dev/null and b/app/ui/__pycache__/mainview.cpython-313.pyc differ diff --git a/app/ui/__pycache__/mainwindow.cpython-313.pyc b/app/ui/__pycache__/mainwindow.cpython-313.pyc new file mode 100644 index 0000000..96f5e86 Binary files /dev/null and b/app/ui/__pycache__/mainwindow.cpython-313.pyc differ diff --git a/app/ui/components/__pycache__/QCursorGif.cpython-313.pyc b/app/ui/components/__pycache__/QCursorGif.cpython-313.pyc new file mode 100644 index 0000000..8884f42 Binary files /dev/null and b/app/ui/components/__pycache__/QCursorGif.cpython-313.pyc differ diff --git a/app/ui/components/__pycache__/__init__.cpython-313.pyc b/app/ui/components/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..61fb385 Binary files /dev/null and b/app/ui/components/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/components/__pycache__/flowlayout.cpython-313.pyc b/app/ui/components/__pycache__/flowlayout.cpython-313.pyc new file mode 100644 index 0000000..9da849b Binary files /dev/null and b/app/ui/components/__pycache__/flowlayout.cpython-313.pyc differ diff --git a/app/ui/components/__pycache__/router.cpython-313.pyc b/app/ui/components/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000..ad6f030 Binary files /dev/null and b/app/ui/components/__pycache__/router.cpython-313.pyc differ diff --git a/app/ui/components/__pycache__/scroll_bar.cpython-313.pyc b/app/ui/components/__pycache__/scroll_bar.cpython-313.pyc new file mode 100644 index 0000000..ab64718 Binary files /dev/null and b/app/ui/components/__pycache__/scroll_bar.cpython-313.pyc differ diff --git a/app/ui/components/file_list/__pycache__/__init__.cpython-313.pyc b/app/ui/components/file_list/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..ec44a1c Binary files /dev/null and b/app/ui/components/file_list/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/components/file_list/__pycache__/file_item_ui.cpython-313.pyc b/app/ui/components/file_list/__pycache__/file_item_ui.cpython-313.pyc new file mode 100644 index 0000000..49d21b5 Binary files /dev/null and b/app/ui/components/file_list/__pycache__/file_item_ui.cpython-313.pyc differ diff --git a/app/ui/components/sidebar/__pycache__/__init__.cpython-313.pyc b/app/ui/components/sidebar/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..95539fe Binary files /dev/null and b/app/ui/components/sidebar/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/components/sidebar/__pycache__/sidebar.cpython-313.pyc b/app/ui/components/sidebar/__pycache__/sidebar.cpython-313.pyc new file mode 100644 index 0000000..c1f40fe Binary files /dev/null and b/app/ui/components/sidebar/__pycache__/sidebar.cpython-313.pyc differ diff --git a/app/ui/components/sidebar/__pycache__/sidebar_ui.cpython-313.pyc b/app/ui/components/sidebar/__pycache__/sidebar_ui.cpython-313.pyc new file mode 100644 index 0000000..e9ba14b Binary files /dev/null and b/app/ui/components/sidebar/__pycache__/sidebar_ui.cpython-313.pyc differ diff --git a/app/ui/doc_convert/__pycache__/__init__.cpython-313.pyc b/app/ui/doc_convert/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bd77b10 Binary files /dev/null and b/app/ui/doc_convert/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/doc_convert/__pycache__/doc_convert.cpython-313.pyc b/app/ui/doc_convert/__pycache__/doc_convert.cpython-313.pyc new file mode 100644 index 0000000..c191ca5 Binary files /dev/null and b/app/ui/doc_convert/__pycache__/doc_convert.cpython-313.pyc differ diff --git a/app/ui/doc_convert/__pycache__/doc_convert_ui.cpython-313.pyc b/app/ui/doc_convert/__pycache__/doc_convert_ui.cpython-313.pyc new file mode 100644 index 0000000..f0cbe0b Binary files /dev/null and b/app/ui/doc_convert/__pycache__/doc_convert_ui.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2image/__pycache__/__init__.cpython-313.pyc b/app/ui/doc_convert/pdf2image/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bbbf375 Binary files /dev/null and b/app/ui/doc_convert/pdf2image/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2image/__pycache__/pdf2image.cpython-313.pyc b/app/ui/doc_convert/pdf2image/__pycache__/pdf2image.cpython-313.pyc new file mode 100644 index 0000000..4e9c50c Binary files /dev/null and b/app/ui/doc_convert/pdf2image/__pycache__/pdf2image.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2image/__pycache__/pdf2image_ui.cpython-313.pyc b/app/ui/doc_convert/pdf2image/__pycache__/pdf2image_ui.cpython-313.pyc new file mode 100644 index 0000000..43a540a Binary files /dev/null and b/app/ui/doc_convert/pdf2image/__pycache__/pdf2image_ui.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2wordui/__pycache__/__init__.cpython-313.pyc b/app/ui/doc_convert/pdf2wordui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4ebd45d Binary files /dev/null and b/app/ui/doc_convert/pdf2wordui/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word.cpython-313.pyc b/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word.cpython-313.pyc new file mode 100644 index 0000000..32c6a13 Binary files /dev/null and b/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word.cpython-313.pyc differ diff --git a/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word_ui.cpython-313.pyc b/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word_ui.cpython-313.pyc new file mode 100644 index 0000000..a21e11f Binary files /dev/null and b/app/ui/doc_convert/pdf2wordui/__pycache__/pdf2word_ui.cpython-313.pyc differ diff --git a/app/ui/doc_convert/web2pdf/__pycache__/__init__.cpython-313.pyc b/app/ui/doc_convert/web2pdf/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..00d737d Binary files /dev/null and b/app/ui/doc_convert/web2pdf/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/doc_convert/web2pdf/__pycache__/web2pdf.cpython-313.pyc b/app/ui/doc_convert/web2pdf/__pycache__/web2pdf.cpython-313.pyc new file mode 100644 index 0000000..2ad5b14 Binary files /dev/null and b/app/ui/doc_convert/web2pdf/__pycache__/web2pdf.cpython-313.pyc differ diff --git a/app/ui/doc_convert/web2pdf/__pycache__/web2pdf_ui.cpython-313.pyc b/app/ui/doc_convert/web2pdf/__pycache__/web2pdf_ui.cpython-313.pyc new file mode 100644 index 0000000..5a849d5 Binary files /dev/null and b/app/ui/doc_convert/web2pdf/__pycache__/web2pdf_ui.cpython-313.pyc differ diff --git a/app/ui/image_tools/__pycache__/image_tool.cpython-313.pyc b/app/ui/image_tools/__pycache__/image_tool.cpython-313.pyc new file mode 100644 index 0000000..dec8938 Binary files /dev/null and b/app/ui/image_tools/__pycache__/image_tool.cpython-313.pyc differ diff --git a/app/ui/image_tools/__pycache__/image_tool_ui.cpython-313.pyc b/app/ui/image_tools/__pycache__/image_tool_ui.cpython-313.pyc new file mode 100644 index 0000000..d7be17f Binary files /dev/null and b/app/ui/image_tools/__pycache__/image_tool_ui.cpython-313.pyc differ diff --git a/app/ui/image_tools/modify_date/__pycache__/modify_date.cpython-313.pyc b/app/ui/image_tools/modify_date/__pycache__/modify_date.cpython-313.pyc new file mode 100644 index 0000000..0834b3a Binary files /dev/null and b/app/ui/image_tools/modify_date/__pycache__/modify_date.cpython-313.pyc differ diff --git a/app/ui/image_tools/modify_date/__pycache__/modify_date_ui.cpython-313.pyc b/app/ui/image_tools/modify_date/__pycache__/modify_date_ui.cpython-313.pyc new file mode 100644 index 0000000..119b9c6 Binary files /dev/null and b/app/ui/image_tools/modify_date/__pycache__/modify_date_ui.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/__pycache__/__init__.cpython-313.pyc b/app/ui/memotrace_enhance/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..63624d9 Binary files /dev/null and b/app/ui/memotrace_enhance/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/__pycache__/enhance.cpython-313.pyc b/app/ui/memotrace_enhance/__pycache__/enhance.cpython-313.pyc new file mode 100644 index 0000000..2a2ba3f Binary files /dev/null and b/app/ui/memotrace_enhance/__pycache__/enhance.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/__pycache__/enhance_ui.cpython-313.pyc b/app/ui/memotrace_enhance/__pycache__/enhance_ui.cpython-313.pyc new file mode 100644 index 0000000..83f92e6 Binary files /dev/null and b/app/ui/memotrace_enhance/__pycache__/enhance_ui.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/toc/__pycache__/__init__.cpython-313.pyc b/app/ui/memotrace_enhance/toc/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..3a4bbf2 Binary files /dev/null and b/app/ui/memotrace_enhance/toc/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/toc/__pycache__/toc.cpython-313.pyc b/app/ui/memotrace_enhance/toc/__pycache__/toc.cpython-313.pyc new file mode 100644 index 0000000..16ec5b9 Binary files /dev/null and b/app/ui/memotrace_enhance/toc/__pycache__/toc.cpython-313.pyc differ diff --git a/app/ui/memotrace_enhance/toc/__pycache__/toc_ui.cpython-313.pyc b/app/ui/memotrace_enhance/toc/__pycache__/toc_ui.cpython-313.pyc new file mode 100644 index 0000000..7d33503 Binary files /dev/null and b/app/ui/memotrace_enhance/toc/__pycache__/toc_ui.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 new file mode 100644 index 0000000..617ffb2 Binary files /dev/null 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 new file mode 100644 index 0000000..ee0ebec Binary files /dev/null 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 new file mode 100644 index 0000000..c3f08b1 Binary files /dev/null and b/app/ui/pdf_tools/__pycache__/pdf_tool_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..f0a21ec Binary files /dev/null and b/app/ui/pdf_tools/merge/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog.cpython-313.pyc new file mode 100644 index 0000000..e299afd Binary files /dev/null and b/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog_ui.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog_ui.cpython-313.pyc new file mode 100644 index 0000000..b50d3f6 Binary files /dev/null and b/app/ui/pdf_tools/merge/__pycache__/encrypt_dialog_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/__pycache__/merge.cpython-313.pyc b/app/ui/pdf_tools/merge/__pycache__/merge.cpython-313.pyc new file mode 100644 index 0000000..41b928f Binary files /dev/null and b/app/ui/pdf_tools/merge/__pycache__/merge.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 new file mode 100644 index 0000000..dbcfa10 Binary files /dev/null and b/app/ui/pdf_tools/merge/__pycache__/merge_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/merge/merge.py b/app/ui/pdf_tools/merge/merge.py index b2d525f..ceab111 100644 --- a/app/ui/pdf_tools/merge/merge.py +++ b/app/ui/pdf_tools/merge/merge.py @@ -144,6 +144,15 @@ def open_file_dialog(self): 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 files: + first_file = files[0] + filename = os.path.basename(first_file) + filename_without_ext = os.path.splitext(filename)[0] + new_filename = f"{filename_without_ext}_合并文件" + self.output_filename = new_filename + self.lineEdit_filename.setText(new_filename) def merge(self): input_files = self.list_view.get_data() diff --git a/app/ui/pdf_tools/pdf_tool.py b/app/ui/pdf_tools/pdf_tool.py index acacc42..f073aa9 100644 --- a/app/ui/pdf_tools/pdf_tool.py +++ b/app/ui/pdf_tools/pdf_tool.py @@ -31,9 +31,9 @@ def __init__(self, router: Router, parent=None): self.setCursorTimeout(100) self.commandLinkButton_merge_pdf.clicked.connect(self.merge_pdf) - self.commandLinkButton_split_pdf.clicked.connect(globalSignals.not_support) - self.commandLinkButton_encrypt.clicked.connect(globalSignals.not_support) - self.commandLinkButton_decrypt.clicked.connect(globalSignals.not_support) + 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_add_watermark.clicked.connect(globalSignals.not_support) @@ -62,6 +62,51 @@ def merge_pdf(self): else: self.router.navigate(self.merge_view.router_path) + def split_pdf(self): + from app.ui.pdf_tools.split.split import SplitControl + if not hasattr(self, 'split_view') or not self.split_view: + self.split_view = SplitControl(router=self.router, parent=self if self.parent() else None) + self.split_view.okSignal.connect(self.split_finish) + self.router.add_route(self.split_view.router_path, self.split_view) + self.child_routes[self.split_view.router_path] = 0 + self.childRouterSignal.emit(self.split_view.router_path) + self.router.navigate(self.split_view.router_path) + else: + self.router.navigate(self.split_view.router_path) + + def encrypt_pdf(self): + from app.ui.pdf_tools.security.encrypt import EncryptControl + if not hasattr(self, 'encrypt_view') or not self.encrypt_view: + self.encrypt_view = EncryptControl(router=self.router, parent=self if self.parent() else None) + self.encrypt_view.okSignal.connect(self.encrypt_finish) + self.router.add_route(self.encrypt_view.router_path, self.encrypt_view) + self.child_routes[self.encrypt_view.router_path] = 0 + self.childRouterSignal.emit(self.encrypt_view.router_path) + self.router.navigate(self.encrypt_view.router_path) + else: + self.router.navigate(self.encrypt_view.router_path) + + def decrypt_pdf(self): + from app.ui.pdf_tools.security.decrypt import DecryptControl + if not hasattr(self, 'decrypt_view') or not self.decrypt_view: + self.decrypt_view = DecryptControl(router=self.router, parent=self if self.parent() else None) + self.decrypt_view.okSignal.connect(self.decrypt_finish) + self.router.add_route(self.decrypt_view.router_path, self.decrypt_view) + self.child_routes[self.decrypt_view.router_path] = 0 + self.childRouterSignal.emit(self.decrypt_view.router_path) + self.router.navigate(self.decrypt_view.router_path) + 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 diff --git a/app/ui/pdf_tools/security/__init__.py b/app/ui/pdf_tools/security/__init__.py new file mode 100644 index 0000000..cca4f20 --- /dev/null +++ b/app/ui/pdf_tools/security/__init__.py @@ -0,0 +1,4 @@ +# PDF加密解密功能模块 +""" +包含PDF加密和解密相关功能 +""" \ No newline at end of file diff --git a/app/ui/pdf_tools/security/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..f48ae8a Binary files /dev/null and b/app/ui/pdf_tools/security/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc new file mode 100644 index 0000000..8b5a6c0 Binary files /dev/null and b/app/ui/pdf_tools/security/__pycache__/decrypt.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/security/__pycache__/decrypt_ui.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/decrypt_ui.cpython-313.pyc new file mode 100644 index 0000000..c174c21 Binary files /dev/null and b/app/ui/pdf_tools/security/__pycache__/decrypt_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/security/__pycache__/encrypt.cpython-313.pyc b/app/ui/pdf_tools/security/__pycache__/encrypt.cpython-313.pyc new file mode 100644 index 0000000..97e2059 Binary files /dev/null and b/app/ui/pdf_tools/security/__pycache__/encrypt.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 new file mode 100644 index 0000000..048be64 Binary files /dev/null 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 new file mode 100644 index 0000000..f549db3 --- /dev/null +++ b/app/ui/pdf_tools/security/decrypt.py @@ -0,0 +1,1261 @@ +import os.path +import io +import subprocess +import tempfile +import threading +import time +import multiprocessing +import concurrent.futures +import queue +import math + +import fitz +from PyPDF2 import PdfReader, PdfWriter +from PySide6.QtCore import Signal, QThread, QUrl, Qt, QFile, QIODevice, QTextStream +from PySide6.QtGui import QDesktopServices, QPixmap, QIcon, QFont, QFontMetrics +from PySide6.QtWidgets import (QWidget, QMessageBox, QFileDialog, QRadioButton, + QButtonGroup, QHBoxLayout, QLabel, QCheckBox, + QProgressDialog, QDialog, QVBoxLayout, QPushButton, + QLineEdit, QComboBox, QSpinBox) + +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.security.decrypt_ui import Ui_decrypt_pdf_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 DecryptControl(QWidget, Ui_decrypt_pdf_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_filename = "解密文件" + self.router = router + self.router_path = (self.parent().router_path if self.parent() else '') + '/解密PDF' + self.child_routes = {} + self.worker = None + self.cracking_canceled = 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_decrypt.clicked.connect(self.decrypt_pdf) + + # 添加解密方式选项 + self.horizontal_decrypt_method = QHBoxLayout() + self.label_decrypt_method = QLabel(self.groupBox_decrypt_options) + self.label_decrypt_method.setText("解密方式:") + self.horizontal_decrypt_method.addWidget(self.label_decrypt_method) + + # 创建解密方式下拉框替代单选按钮 + self.combo_decrypt_method = QComboBox(self.groupBox_decrypt_options) + self.combo_decrypt_method.addItem("常规解密(需要密码)") + self.combo_decrypt_method.addItem("密码破解(自动尝试破解密码)") + self.horizontal_decrypt_method.addWidget(self.combo_decrypt_method) + + # 密码破解设置按钮 + self.btn_crack_settings = QPushButton(self.groupBox_decrypt_options) + self.btn_crack_settings.setText("破解设置") + self.btn_crack_settings.setVisible(False) + self.btn_crack_settings.clicked.connect(self.show_crack_settings) + + # 在密码输入前添加解密方式选择 + self.verticalLayout_options.insertLayout(0, self.horizontal_decrypt_method) + self.verticalLayout_options.addWidget(self.btn_crack_settings) + + # 添加当前尝试密码显示 + self.current_pwd_layout = QHBoxLayout() + self.label_current_pwd = QLabel("当前尝试密码:") + self.current_pwd_layout.addWidget(self.label_current_pwd) + + self.lineEdit_current_pwd = QLineEdit() + self.lineEdit_current_pwd.setReadOnly(True) + self.current_pwd_layout.addWidget(self.lineEdit_current_pwd) + self.verticalLayout_options.addLayout(self.current_pwd_layout) + + # 添加终止按钮 + self.btn_stop_crack = QPushButton("终止破解") + self.btn_stop_crack.setStyleSheet("background-color: #ff4d4d; color: white;") + self.btn_stop_crack.clicked.connect(self.stop_cracking) + self.verticalLayout_options.addWidget(self.btn_stop_crack) + + # 初始隐藏密码显示和终止按钮 + self.label_current_pwd.setVisible(False) + self.lineEdit_current_pwd.setVisible(False) + self.btn_stop_crack.setVisible(False) + + # 解密方式变更事件 + self.combo_decrypt_method.currentIndexChanged.connect(self.update_decrypt_ui) + + # 输出选项连接 + self.lineEdit_filename.textChanged.connect(self.change_output_filename) + 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_decrypt.setEnabled(False) + + # 存储密码输入相关控件 + self.password_widgets = [ + self.label_owner_pwd, self.lineEdit_owner_pwd, + self.label_user_pwd, self.lineEdit_user_pwd + ] + + # 破解设置 + self.crack_settings = { + "mode": "bruteforce", # 'dictionary' 或 'bruteforce',默认暴力破解 + "dict_path": "", # 字典文件路径 + "min_length": 4, # 最小密码长度 + "max_length": 8, # 最大密码长度 + "charset": "digits", # 'digits', 'lowercase', 'uppercase', 'all' + "timeout": 0, # 超时时间(秒),0表示不限制 + "threads": self.get_recommended_threads() # 线程数,默认为推荐值 + } + + def init_ui(self): + self.btn_decrypt.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_filename.setText(self.output_filename) + + def update_decrypt_ui(self): + """更新解密选项UI""" + current_method = self.combo_decrypt_method.currentIndex() + is_normal_decrypt = current_method == 0 + is_crack_decrypt = current_method == 1 + + # 根据解密方式显示/隐藏密码输入框 + for widget in self.password_widgets: + widget.setVisible(is_normal_decrypt) + + # 显示或隐藏破解设置按钮 + self.btn_crack_settings.setVisible(is_crack_decrypt) + + # 显示或隐藏当前密码和终止按钮 + self.label_current_pwd.setVisible(False) + self.lineEdit_current_pwd.setVisible(False) + self.btn_stop_crack.setVisible(False) + + # 说明文本 + if is_normal_decrypt: + self.label_pwd_info.setText("注:解密PDF文件需要所有者密码,如果没有所有者密码可以尝试使用用户密码") + elif is_crack_decrypt: + self.label_pwd_info.setText("注:密码破解功能将尝试自动破解PDF密码。破解速度取决于密码复杂度和计算机性能。\n破解过程可能需要较长时间,请耐心等待。") + + # 如果选择了密码破解模式,检查是否安装了必要工具 + if not self.check_cracking_tools_installed(): + reply = QMessageBox.question( + self, + "缺少必要工具", + "使用密码破解功能需要安装John the Ripper,是否查看安装指南?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + if reply == QMessageBox.Yes: + self.show_installation_guide() + else: + self.label_pwd_info.setText("注:无密码解密功能适用于部分加密保护较弱的PDF文件,将尝试直接移除加密。\n如无法解密,请尝试常规解密方式。") + + def change_output_filename(self): + self.output_filename = common.correct_filename(self.lineEdit_filename.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) + + # 获取PDF信息 + try: + with fitz.open(file_path) as pdf: + # 检查PDF是否已加密 + if not pdf.is_encrypted: + QMessageBox.information(self, "提示", "选择的PDF文件未加密,无需解密。") + self.btn_decrypt.setEnabled(False) + return + self.btn_decrypt.setEnabled(True) + except Exception as e: + logger.error(f"读取PDF文件错误: {str(e)}") + QMessageBox.critical(self, "错误", f"无法读取PDF文件: {str(e)}") + self.btn_decrypt.setEnabled(False) + return + + # 如果未设置输出目录,默认使用与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) + + # 输出文件名格式:原文件名+解密+文件 + filename = os.path.basename(file_path) + filename_without_ext = os.path.splitext(filename)[0] + new_filename = f"{filename_without_ext}_解密文件" + self.output_filename = new_filename + self.lineEdit_filename.setText(new_filename) + + def show_crack_settings(self): + """显示密码破解设置对话框""" + dialog = QDialog(self) + dialog.setWindowTitle("密码破解设置") + dialog.setMinimumWidth(400) + + layout = QVBoxLayout() + + # 破解模式选择 + mode_layout = QHBoxLayout() + mode_label = QLabel("破解模式:") + mode_layout.addWidget(mode_label) + + mode_combo = QComboBox() + mode_combo.addItem("字典破解(推荐)") + mode_combo.addItem("暴力破解") + mode_combo.setCurrentIndex(0 if self.crack_settings["mode"] == "dictionary" else 1) + mode_layout.addWidget(mode_combo) + layout.addLayout(mode_layout) + + # 字典文件选择 + dict_layout = QHBoxLayout() + dict_label = QLabel("字典文件:") + dict_layout.addWidget(dict_label) + + dict_path = QLineEdit() + dict_path.setText(self.crack_settings["dict_path"]) + dict_path.setReadOnly(True) + dict_layout.addWidget(dict_path) + + dict_btn = QPushButton("选择") + dict_btn.clicked.connect(lambda: self.select_dict_file(dict_path)) + dict_layout.addWidget(dict_btn) + layout.addLayout(dict_layout) + + # 密码长度设置 + length_layout = QHBoxLayout() + min_label = QLabel("最小长度:") + length_layout.addWidget(min_label) + + min_length = QLineEdit() + min_length.setText(str(self.crack_settings["min_length"])) + min_length.setMaximumWidth(50) + length_layout.addWidget(min_length) + + max_label = QLabel("最大长度:") + length_layout.addWidget(max_label) + + max_length = QLineEdit() + max_length.setText(str(self.crack_settings["max_length"])) + max_length.setMaximumWidth(50) + length_layout.addWidget(max_length) + layout.addLayout(length_layout) + + # 字符集选择 + charset_layout = QHBoxLayout() + charset_label = QLabel("字符集:") + charset_layout.addWidget(charset_label) + + charset_combo = QComboBox() + charset_combo.addItem("数字 (0-9)") + charset_combo.addItem("小写字母 (a-z)") + charset_combo.addItem("大写字母 (A-Z)") + charset_combo.addItem("字母和数字 (a-zA-Z0-9)") + charset_combo.addItem("所有字符 (包含特殊符号)") + + charset_map = { + "digits": 0, + "lowercase": 1, + "uppercase": 2, + "alphanumeric": 3, + "all": 4 + } + charset_combo.setCurrentIndex(charset_map.get(self.crack_settings["charset"], 0)) + charset_layout.addWidget(charset_combo) + layout.addLayout(charset_layout) + + # 添加线程数设置 + threads_layout = QHBoxLayout() + threads_label = QLabel("线程数:") + threads_layout.addWidget(threads_label) + + threads_spinbox = QSpinBox() + threads_spinbox.setMinimum(1) + threads_spinbox.setMaximum(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) + + # 添加自动推荐按钮 + recommend_btn = QPushButton("推荐值") + recommend_btn.clicked.connect(lambda: threads_spinbox.setValue(self.get_recommended_threads())) + threads_layout.addWidget(recommend_btn) + + layout.addLayout(threads_layout) + + # 按钮区域 + btn_layout = QHBoxLayout() + ok_btn = QPushButton("确定") + cancel_btn = QPushButton("取消") + + btn_layout.addWidget(ok_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + dialog.setLayout(layout) + + # 连接按钮事件 + ok_btn.clicked.connect(lambda: self.save_crack_settings( + mode_combo.currentIndex() == 0, + dict_path.text(), + min_length.text(), + max_length.text(), + charset_combo.currentIndex(), + "0", # 总是传递0作为超时时间 + threads_spinbox.value(), # 传递线程数 + dialog + )) + cancel_btn.clicked.connect(dialog.reject) + + # 根据模式启用/禁用控件 + def update_ui(): + is_dict_mode = mode_combo.currentIndex() == 0 + dict_path.setEnabled(is_dict_mode) + dict_btn.setEnabled(is_dict_mode) + + min_length.setEnabled(not is_dict_mode) + max_length.setEnabled(not is_dict_mode) + charset_combo.setEnabled(not is_dict_mode) + + update_ui() + mode_combo.currentIndexChanged.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 save_crack_settings(self, is_dict_mode, dict_path, min_length, max_length, charset_index, timeout, threads, dialog): + """保存破解设置""" + # 验证输入 + if is_dict_mode and not os.path.exists(dict_path): + QMessageBox.warning(self, "警告", "请选择有效的字典文件") + return + + try: + min_len = int(min_length) + max_len = int(max_length) + # 忽略超时参数,始终设置为0表示无限制 + timeout_val = 0 + # 验证线程数 + thread_count = min(max(1, threads), 32) # 限制在1-32之间 + + if min_len < 1 or max_len < min_len: + raise ValueError("无效的参数值") + except ValueError: + QMessageBox.warning(self, "警告", "请输入有效的数字") + return + + # 保存设置 + charset_map = ["digits", "lowercase", "uppercase", "alphanumeric", "all"] + self.crack_settings = { + "mode": "dictionary" if is_dict_mode else "bruteforce", + "dict_path": dict_path, + "min_length": min_len, + "max_length": max_len, + "charset": charset_map[charset_index], + "timeout": timeout_val, # 始终为0,表示不限制时间 + "threads": thread_count # 保存线程数设置 + } + + dialog.accept() + + def decrypt_pdf(self): + if not os.path.exists(self.input_file_path): + QMessageBox.critical(self, "错误", "请先选择PDF文件") + return + + # 获取解密方式 + decrypt_method = self.combo_decrypt_method.currentIndex() + use_crack_decrypt = decrypt_method == 1 + + # 准备密码参数 + if decrypt_method == 0: # 常规解密 + owner_password = self.lineEdit_owner_pwd.text() + user_password = self.lineEdit_user_pwd.text() + + if not owner_password and not user_password: + QMessageBox.warning(self, "警告", "请输入所有者密码或用户密码") + return + else: + owner_password = "" + user_password = "" + + # 获取输出目录 + if self.comboBox_output_dir.currentText() == '自定义目录' and self.output_dir: + output_directory = self.output_dir + else: + output_directory = os.path.dirname(self.input_file_path) + + # 如果输出目录不存在,创建它 + if not os.path.exists(output_directory): + try: + os.makedirs(output_directory) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建输出目录: {str(e)}") + return + + # 生成输出文件路径 + output_path = os.path.join(output_directory, f"{self.output_filename}.pdf") + output_path = common.usable_filepath(output_path) + + # 禁用按钮,开始任务 + self.btn_decrypt.setEnabled(False) + self.cracking_canceled = False + self.startBusy() + + # 如果是暴力破解模式,显示当前密码标签和终止按钮 + if use_crack_decrypt: + self.label_current_pwd.setVisible(True) + self.lineEdit_current_pwd.setVisible(True) + self.btn_stop_crack.setVisible(True) + self.lineEdit_current_pwd.setText("准备开始...") + + # 创建并启动工作线程 + input_file = PdfFile(self.input_file_path) + self.worker = DecryptThread( + input_file=input_file, + output_path=output_path, + owner_password=owner_password, + user_password=user_password, + use_force_decrypt=False, + use_crack_decrypt=use_crack_decrypt, + crack_settings=self.crack_settings + ) + + # 连接信号 + self.worker.progressSignal.connect(self.update_progress) + self.worker.okSignal.connect(self.decrypt_finish) + if use_crack_decrypt: + self.worker.current_pwd_signal.connect(self.update_current_pwd) + + self.worker.start() + + def update_current_pwd(self, pwd): + """更新当前尝试的密码""" + self.lineEdit_current_pwd.setText(pwd) + + def stop_cracking(self): + """终止密码破解""" + if self.worker and self.worker.isRunning(): + # 设置标志位,告知线程需要终止 + self.cracking_canceled = True + # 请求中断线程,但不强制终止 + self.worker.requestInterruption() + + # 显示正在终止 + self.lineEdit_current_pwd.setText("正在安全终止...") + + # 给线程一些时间来自行终止 + if not self.worker.wait(1000): # 等待最多1秒 + # 如果线程没有及时终止,通知用户 + logger.warning("线程未能在短时间内终止,可能需要更长时间") + self.lineEdit_current_pwd.setText("终止中,请稍候...") + + # 继续等待线程自行终止,避免强制终止 + if not self.worker.wait(3000): # 再等3秒 + logger.warning("线程仍未终止,考虑安全地结束") + # 不使用terminate(),避免崩溃 + + # 恢复UI状态 + self.btn_decrypt.setEnabled(True) + self.progressBar.setValue(0) + # 停止忙碌光标 + self.stopBusy() + + # 简单提示已终止 + self.lineEdit_current_pwd.setText("已终止破解") + # 不将worker设为None,避免线程对象被垃圾回收而引起问题 + # 而是让线程自然结束 + + logger.info("用户手动终止破解进程") + + def update_progress(self, value): + self.progressBar.setValue(value) + + def decrypt_finish(self, result_data): + self.stopBusy() + success, message = result_data + + # 隐藏当前密码标签和终止按钮 + self.label_current_pwd.setVisible(False) + self.lineEdit_current_pwd.setVisible(False) + self.btn_stop_crack.setVisible(False) + + if success: + # 处理可能存在的多行消息(包含密码信息等) + folder_path = message.split('\n')[0] if '\n' in message else message + + # 确保路径是有效的目录路径 + if not os.path.isdir(folder_path): + folder_path = os.path.dirname(folder_path) + + reply = QMessageBox(self) + reply.setIcon(QMessageBox.Information) + reply.setWindowTitle('完成') + reply.setText(f"PDF解密成功\n{message}") + btn = reply.addButton('打开文件夹', QMessageBox.ActionRole) + # 使用更可靠的方法打开文件夹 + btn.clicked.connect(lambda: open_file_explorer(folder_path)) + reply.addButton("确认", QMessageBox.AcceptRole) + reply.exec_() + else: + if self.cracking_canceled: + # 如果是用户终止的,不显示任何消息框,状态已在stop_cracking()中恢复 + pass + else: + QMessageBox.critical(self, "错误", f"PDF解密失败: {message}") + + # 恢复UI状态 + self.btn_decrypt.setEnabled(True) + self.progressBar.setValue(0) + self.worker = None + self.cracking_canceled = False + + def closeEvent(self, event): + super().closeEvent(event) + self.okSignal.emit(True) + + def check_cracking_tools_installed(self): + """检查是否安装了密码破解工具""" + # 全局变量,用于调试 + global johnOutput + johnOutput = "" + + try: + # 检查John the Ripper + logger.info("开始检查John the Ripper是否安装...") + + # 尝试方法1:直接运行john命令 + john_process = subprocess.Popen( + ["john"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + stdout, stderr = john_process.communicate() + + # 记录输出到全局变量和日志 + john_stdout = stdout.decode('utf-8', errors='ignore') + john_stderr = stderr.decode('utf-8', errors='ignore') + johnOutput = f"STDOUT: {john_stdout}\nSTDERR: {john_stderr}" + + logger.info(f"John命令输出: {johnOutput}") + + # 宽松检查:只要命令成功运行就认为已安装 + if john_process.returncode == 0 or "john" in john_stdout.lower() or "john" in john_stderr.lower(): + logger.info("检测到John the Ripper已安装(命令成功或包含关键字)") + return True + + logger.info("第一种方法未检测到John the Ripper") + + # 尝试方法2:使用shell执行 + try: + if os.name == 'nt': # Windows系统 + result = subprocess.run("where john", shell=True, capture_output=True, text=True) + if result.returncode == 0: + logger.info("通过where命令找到john路径") + return True + else: # Unix系统 + result = subprocess.run("which john", shell=True, capture_output=True, text=True) + if result.returncode == 0: + logger.info("通过which命令找到john路径") + return True + except Exception as e: + logger.error(f"尝试检测john路径时出错: {str(e)}") + + logger.info("未能检测到John the Ripper的安装") + return False + + except Exception as e: + logger.error(f"检查John the Ripper安装出错: {str(e)}") + return False + + def verify_installation(self): + """验证工具安装状态""" + # 检查John the Ripper + john_installed = False + pdf2john_installed = False + + # 全局变量,用于调试 + global johnOutput + + try: + logger.info("验证John the Ripper安装...") + john_process = subprocess.Popen( + ["john"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + stdout, stderr = john_process.communicate() + + # 记录输出到全局变量 + john_stdout = stdout.decode('utf-8', errors='ignore') + john_stderr = stderr.decode('utf-8', errors='ignore') + johnOutput = f"STDOUT: {john_stdout}\nSTDERR: {john_stderr}" + + logger.info(f"验证时John命令输出: {johnOutput}") + + # 宽松判断是否已安装 + john_installed = john_process.returncode == 0 or "john" in john_stdout.lower() or "john" in john_stderr.lower() + + # 判断是否包含版本信息 + john_version = "已安装" + + # 优先使用stderr,因为john的版本信息常常显示在stderr + if "john" in john_stderr.lower(): + version_lines = john_stderr.split('\n') + if version_lines: + john_version = version_lines[0].strip() + elif "john" in john_stdout.lower(): + version_lines = john_stdout.split('\n') + if version_lines: + john_version = version_lines[0].strip() + + # 如果没有安装,查看全局输出 + if not john_installed: + john_version = f"未安装 - 命令输出: {johnOutput}" + + except Exception as e: + logger.error(f"验证John the Ripper安装出错: {str(e)}") + john_version = f"未安装 - 错误: {str(e)}" + + try: + pdf2john_process = subprocess.Popen( + ["where", "pdf2john"] if os.name == "nt" else ["which", "pdf2john"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + pdf2john_process.communicate() + pdf2john_installed = pdf2john_process.returncode == 0 + except: + pass + + if not pdf2john_installed: + try: + pdf2john_process = subprocess.Popen( + ["where", "pdf2john.py"] if os.name == "nt" else ["which", "pdf2john.py"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + pdf2john_process.communicate() + pdf2john_installed = pdf2john_process.returncode == 0 + except: + pass + + # 显示安装状态 + status_message = f"John the Ripper: {'已安装 - ' + john_version if john_installed else john_version}\n" + status_message += f"pdf2john: {'已安装' if pdf2john_installed else '未安装'}\n\n" + + if john_installed and pdf2john_installed: + status_message += "所有必要工具已安装,可以使用密码破解功能。" + else: + status_message += "部分工具未安装,密码破解功能可能无法正常工作。请按照安装指南进行安装。" + + QMessageBox.information(self, "安装状态", status_message) + + def show_installation_guide(self): + """显示工具安装指南""" + dialog = QDialog(self) + dialog.setWindowTitle("密码破解工具安装指南") + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout() + + # 添加安装说明 + info_label = QLabel() + info_label.setWordWrap(True) + info_label.setText( + "

安装John the Ripper的步骤

" + "

1. Windows系统:

" + "" + "

2. Linux系统:

" + "" + "

3. macOS系统:

" + "" + "

验证安装:

" + "" + "

注意事项:

" + "" + ) + + layout.addWidget(info_label) + + # 检查安装状态按钮 + check_btn = QPushButton("检查安装状态") + check_btn.clicked.connect(self.verify_installation) + layout.addWidget(check_btn) + + # 关闭按钮 + close_btn = QPushButton("关闭") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec_() + + def get_recommended_threads(self): + """根据系统CPU核心数推荐合适的线程数""" + # 获取CPU核心数 + cpu_count = multiprocessing.cpu_count() + # 推荐使用核心数-1的线程数,最少为2 + return max(2, cpu_count - 1) + + +class DecryptThread(QThread): + okSignal = Signal(tuple) # (success, message) + progressSignal = Signal(int) + current_pwd_signal = Signal(str) # 新增信号,用于传递当前尝试的密码 + + def __init__(self, input_file, output_path, owner_password="", user_password="", + use_force_decrypt=False, use_crack_decrypt=False, crack_settings=None): + super().__init__() + self.input_file = input_file + self.output_path = output_path + self.owner_password = owner_password + self.user_password = user_password + self.use_force_decrypt = use_force_decrypt + self.use_crack_decrypt = use_crack_decrypt + self.crack_settings = crack_settings or {} + self.should_stop = False # 添加停止标记 + + def check_tool_installed(self, tool_name): + """检查是否安装了指定工具""" + try: + if tool_name in ["pdf2john.py", "pdf2john"]: + # 特殊处理pdf2john + process = subprocess.Popen( + ["where", tool_name] if os.name == "nt" else ["which", tool_name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + process.communicate() + return process.returncode == 0 + elif tool_name == "john": + # 特殊处理john + john_process = subprocess.Popen( + ["john"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + stdout, stderr = john_process.communicate() + + # 记录输出用于调试 + john_stdout = stdout.decode('utf-8', errors='ignore') + john_stderr = stderr.decode('utf-8', errors='ignore') + + # 宽松检查 + return john_process.returncode == 0 or "john" in john_stdout.lower() or "john" in john_stderr.lower() + else: + # 一般工具检查 + subprocess.run( + [tool_name, "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False + ) + return True + except Exception as e: + logger.error(f"检查{tool_name}工具安装出错: {str(e)}") + return False + + def run(self): + if self.use_crack_decrypt: + # 使用密码破解工具尝试破解密码 + return self.run_crack_decrypt() + else: + # 使用PyMuPDF带密码解密 + return self.run_normal_decrypt() + + def run_crack_decrypt(self): + """使用密码破解工具尝试破解PDF密码""" + try: + self.progressSignal.emit(10) + logger.info("开始尝试破解PDF密码") + + # 确认PDF是否加密 + try: + with fitz.open(self.input_file.file_path) as pdf: + if not pdf.is_encrypted: + self.okSignal.emit((False, "PDF文件未加密,无需解密")) + return + except Exception as e: + logger.error(f"检查PDF加密状态失败: {str(e)}") + + # 准备临时文件 + temp_dir = tempfile.mkdtemp() + + # 直接尝试常见密码 + self.progressSignal.emit(20) + common_passwords = ['', '1234', '0000', '1111', '9999', '123456', 'password', 'admin', 'qwerty'] + logger.info("尝试一些常见密码...") + + for pwd in common_passwords: + if self.isInterruptionRequested(): + logger.info("破解过程被用户终止") + self.okSignal.emit((False, "用户终止了破解过程")) + return + + self.current_pwd_signal.emit(f"尝试常见密码: {pwd}") + try: + with fitz.open(self.input_file.file_path) as pdf: + if pdf.authenticate(pwd): + # 成功找到密码 + logger.info(f"使用常见密码'{pwd}'成功解密") + + # 保存解密后的PDF + pdf.save(self.output_path) + self.progressSignal.emit(100) + success_message = f"{os.path.dirname(self.output_path)}\n成功使用密码: {pwd}" + self.okSignal.emit((True, success_message)) + return + except Exception as e: + logger.error(f"尝试密码'{pwd}'失败: {str(e)}") + + # 如果是字典模式,直接使用字典文件尝试密码 + if self.crack_settings.get("mode") == "dictionary": + return self.run_dict_crack() + + # 暴力破解模式 - 使用排列组合方式,多线程处理 + if self.crack_settings.get("mode") == "bruteforce": + return self.run_bruteforce_crack() + + # 如果所有尝试都失败,返回未成功消息 + logger.info("所有密码尝试均失败") + self.okSignal.emit((False, "未能破解密码,请尝试其他解密方法")) + + except Exception as e: + logger.error(f"PDF破解错误: {str(e)}") + self.okSignal.emit((False, f"PDF破解失败: {str(e)}")) + return + + def run_dict_crack(self): + """使用字典破解密码 - 多线程版本""" + dict_path = self.crack_settings.get("dict_path", "") + if not os.path.exists(dict_path): + self.okSignal.emit((False, "字典文件不存在")) + return + + try: + logger.info(f"使用字典文件多线程破解: {dict_path}") + # 获取线程数 + thread_count = self.crack_settings.get("threads", 4) + logger.info(f"使用 {thread_count} 个线程进行字典破解") + + # 读取字典文件中的密码列表 + passwords = [] + with open(dict_path, 'r', encoding='utf-8', errors='ignore') as f: + for line in f: + pwd = line.strip() + if pwd: # 跳过空行 + passwords.append(pwd) + + if not passwords: + self.okSignal.emit((False, "字典文件为空或格式不正确")) + return + + total_passwords = len(passwords) + logger.info(f"字典中共有 {total_passwords} 个密码") + self.current_pwd_signal.emit(f"正在使用 {thread_count} 个线程破解字典中的 {total_passwords} 个密码...") + + # 创建一个用于状态同步的对象 + class CrackState: + def __init__(self): + self.success = False + self.found_password = None + self.processed_count = 0 + self.lock = threading.Lock() + + crack_state = CrackState() + + # 定义测试密码的函数 + def test_password(pwd): + if crack_state.success: + return # 如果已经成功,就不再测试 + + try: + with fitz.open(self.input_file.file_path) as pdf: + if pdf.authenticate(pwd): + with crack_state.lock: + if not crack_state.success: # 双重检查 + crack_state.success = True + crack_state.found_password = pwd + logger.info(f"字典破解成功,密码: {pwd}") + except Exception as e: + logger.debug(f"尝试密码 '{pwd}' 失败: {str(e)}") + + # 更新处理计数 + with crack_state.lock: + crack_state.processed_count += 1 + progress = int(crack_state.processed_count / total_passwords * 60) + 30 + self.progressSignal.emit(min(progress, 90)) + + # 每处理100个密码更新一次UI + if crack_state.processed_count % 100 == 0 or crack_state.processed_count == total_passwords: + percentage = int(crack_state.processed_count / total_passwords * 100) + self.current_pwd_signal.emit(f"字典破解: 已尝试 {crack_state.processed_count}/{total_passwords} 个密码 ({percentage}%)") + + # 创建线程池 + with concurrent.futures.ThreadPoolExecutor(max_workers=thread_count) as executor: + # 提交任务 + futures = [] + for pwd in passwords: + if self.isInterruptionRequested(): + break + futures.append(executor.submit(test_password, pwd)) + + # 等待任务完成 + for future in concurrent.futures.as_completed(futures): + if self.isInterruptionRequested() or crack_state.success: + # 如果破解成功或用户请求终止,取消所有未完成的任务 + for f in futures: + f.cancel() + break + + # 获取任务结果(可能产生异常) + try: + future.result() + except Exception as e: + logger.error(f"字典破解线程出错: {str(e)}") + + # 检查是否成功 + if crack_state.success and crack_state.found_password: + # 保存解密后的PDF + with fitz.open(self.input_file.file_path) as pdf: + pdf.authenticate(crack_state.found_password) + pdf.save(self.output_path) + + self.progressSignal.emit(100) + success_message = f"{os.path.dirname(self.output_path)}\n成功使用密码: {crack_state.found_password}" + self.okSignal.emit((True, success_message)) + return + + # 如果被终止 + if self.isInterruptionRequested(): + logger.info("字典破解过程被用户终止") + self.okSignal.emit((False, "用户终止了破解过程")) + return + + # 所有密码都尝试过了,但没有成功 + logger.info("字典中所有密码尝试均失败") + self.okSignal.emit((False, "字典中所有密码尝试均失败,请尝试其他解密方法")) + + except Exception as e: + logger.error(f"字典破解出错: {str(e)}") + self.okSignal.emit((False, f"字典破解失败: {str(e)}")) + + def run_bruteforce_crack(self): + """使用暴力破解方式 - 多线程版本""" + 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") + thread_count = self.crack_settings.get("threads", 4) + + logger.info(f"使用 {thread_count} 个线程进行暴力破解") + + # 定义字符集 + charset_chars = "" + if charset == "digits": + charset_chars = "0123456789" + elif charset == "lowercase": + charset_chars = "abcdefghijklmnopqrstuvwxyz" + elif charset == "uppercase": + charset_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + elif charset == "alphanumeric": + charset_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + else: # all + charset_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?/" + + logger.info(f"开始多线程暴力破解,字符集: {charset}, 长度范围: {min_len}-{max_len}") + + import itertools + + # 创建一个用于状态同步的对象 + class CrackState: + def __init__(self): + self.success = False + self.found_password = None + self.current_length = min_len + self.current_count = 0 + self.total_combinations = 0 + self.lock = threading.Lock() + + crack_state = CrackState() + + # 定义密码检查函数 + def check_password(pwd): + if crack_state.success: + return # 如果已经成功,就不再测试 + + try: + with fitz.open(self.input_file.file_path) as pdf: + if pdf.authenticate(pwd): + with crack_state.lock: + if not crack_state.success: # 双重检查 + crack_state.success = True + crack_state.found_password = pwd + logger.info(f"暴力破解成功,密码: {pwd}") + except Exception as e: + logger.debug(f"尝试密码 '{pwd}' 失败: {str(e)}") + + # 更新处理计数 + with crack_state.lock: + crack_state.current_count += 1 + + # 每处理1000个密码或每0.5秒更新一次进度 + if crack_state.current_count % 1000 == 0: + percent = (crack_state.current_count / crack_state.total_combinations * 100) + if crack_state.total_combinations > 0: + length_progress = (crack_state.current_length - min_len) / (max_len - min_len + 1) + total_progress = 30 + min(60 * (length_progress + percent / 100 / (max_len - min_len + 1)), 60) + self.progressSignal.emit(min(int(total_progress), 90)) + + self.current_pwd_signal.emit( + f"暴力破解(长度{crack_state.current_length}): {pwd} " + f"({crack_state.current_count}/{crack_state.total_combinations}, " + f"{thread_count}线程)" + ) + + # 按照密码长度逐一尝试 + for length in range(min_len, max_len + 1): + if self.isInterruptionRequested() or crack_state.success: + break + + crack_state.current_length = length + crack_state.current_count = 0 + crack_state.total_combinations = len(charset_chars) ** length + + logger.info(f"尝试长度为 {length} 的密码,组合总数: {crack_state.total_combinations}") + self.current_pwd_signal.emit(f"准备破解长度: {length},组合总数: {crack_state.total_combinations}...") + + # 如果组合数太多,警告用户 + if crack_state.total_combinations > 10000000: # 1千万 + logger.warning(f"长度 {length} 的组合数超过1千万,可能需要很长时间") + self.current_pwd_signal.emit(f"警告: 长度 {length} 的组合数很大,破解可能需要很长时间!") + + # 分批生成密码并多线程处理 + # 计算合适的批次大小 + batch_size = min(10000, max(1000, crack_state.total_combinations // (thread_count * 10))) + + # 创建密码队列 + password_queue = queue.Queue(maxsize=batch_size * 2) # 队列大小为批次大小的两倍 + + # 生产者线程 - 生成密码并放入队列 + def password_producer(): + for pwd_tuple in itertools.product(charset_chars, repeat=length): + if self.isInterruptionRequested() or crack_state.success: + break + pwd = ''.join(pwd_tuple) + password_queue.put(pwd) + # 添加结束标记 + for _ in range(thread_count): + password_queue.put(None) + + # 启动生产者线程 + producer_thread = threading.Thread(target=password_producer) + producer_thread.daemon = True + producer_thread.start() + + # 消费者函数 - 从队列中获取密码并检查 + def password_consumer(): + while not self.isInterruptionRequested() and not crack_state.success: + pwd = password_queue.get() + if pwd is None: # 结束标记 + break + check_password(pwd) + password_queue.task_done() + + # 启动消费者线程 + consumer_threads = [] + for _ in range(thread_count): + t = threading.Thread(target=password_consumer) + t.daemon = True + t.start() + consumer_threads.append(t) + + # 等待所有密码处理完成或找到密码 + for t in consumer_threads: + t.join() + + # 如果已经找到密码或用户要求终止,就退出循环 + if crack_state.success or self.isInterruptionRequested(): + break + + # 检查最终结果 + if crack_state.success and crack_state.found_password: + # 保存解密后的PDF + with fitz.open(self.input_file.file_path) as pdf: + pdf.authenticate(crack_state.found_password) + pdf.save(self.output_path) + + self.progressSignal.emit(100) + success_message = f"{os.path.dirname(self.output_path)}\n成功破解密码: {crack_state.found_password}" + self.okSignal.emit((True, success_message)) + return + + # 如果被终止 + if self.isInterruptionRequested(): + logger.info("暴力破解过程被用户终止") + self.okSignal.emit((False, "用户终止了破解过程")) + return + + # 所有组合都尝试过了,但没有成功 + logger.info("暴力破解尝试所有可能的密码组合均失败") + self.okSignal.emit((False, "所有可能的密码组合尝试均失败,请使用其他方法")) + + def run_normal_decrypt(self): + try: + # 打开输入PDF + pdf_document = fitz.open(self.input_file.file_path) + + # 如果PDF不是加密的,直接返回错误 + if not pdf_document.is_encrypted: + pdf_document.close() + self.okSignal.emit((False, "PDF文件未加密,无需解密")) + return + + # 设置进度信号 + self.progressSignal.emit(30) + + # 尝试使用所有者密码解密 + auth_success = False + error_message = "解密失败: 密码错误或权限不足" + + if self.owner_password: + try: + auth_success = pdf_document.authenticate(self.owner_password) + except Exception as e: + logger.error(f"使用所有者密码解密失败: {str(e)}") + + # 如果所有者密码解密失败,尝试使用用户密码 + if not auth_success and self.user_password: + try: + auth_success = pdf_document.authenticate(self.user_password) + except Exception as e: + logger.error(f"使用用户密码解密失败: {str(e)}") + + if not auth_success: + pdf_document.close() + self.okSignal.emit((False, error_message)) + return + + # 确认是否已获取足够权限 + if not pdf_document.metadata: + pdf_document.close() + self.okSignal.emit((False, "密码正确,但权限不足,无法解密")) + return + + # 设置进度信号 + self.progressSignal.emit(60) + + # 保存为无加密的副本 + pdf_document.save(self.output_path) + + self.progressSignal.emit(100) + pdf_document.close() + + # 记录成功使用的密码 + password_used = self.owner_password if auth_success else self.user_password + success_message = os.path.dirname(self.output_path) + if password_used: + success_message += f"\n使用密码: {password_used} 成功解密" + + self.okSignal.emit((True, success_message)) + + except Exception as e: + logger.error(f"PDF解密错误: {str(e)}") + self.okSignal.emit((False, 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 = DecryptControl(router) + view.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/app/ui/pdf_tools/security/decrypt_ui.py b/app/ui/pdf_tools/security/decrypt_ui.py new file mode 100644 index 0000000..930adb0 --- /dev/null +++ b/app/ui/pdf_tools/security/decrypt_ui.py @@ -0,0 +1,160 @@ +from PySide6.QtCore import QCoreApplication, QMetaObject, QSize +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import (QLabel, QLineEdit, QProgressBar, QPushButton, + QSizePolicy, QSpacerItem, QVBoxLayout, QHBoxLayout, QWidget, + QGroupBox, QComboBox) + + +class Ui_decrypt_pdf_view(object): + def setupUi(self, decrypt_pdf_view): + if not decrypt_pdf_view.objectName(): + decrypt_pdf_view.setObjectName(u"decrypt_pdf_view") + decrypt_pdf_view.resize(800, 600) + self.verticalLayout = QVBoxLayout(decrypt_pdf_view) + self.verticalLayout.setObjectName(u"verticalLayout") + + # 文件选择区域 + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.label = QLabel(decrypt_pdf_view) + self.label.setObjectName(u"label") + self.horizontalLayout.addWidget(self.label) + + self.lineEdit_pdf_path = QLineEdit(decrypt_pdf_view) + self.lineEdit_pdf_path.setObjectName(u"lineEdit_pdf_path") + self.lineEdit_pdf_path.setReadOnly(True) + self.horizontalLayout.addWidget(self.lineEdit_pdf_path) + + self.btn_choose_file = QPushButton(decrypt_pdf_view) + self.btn_choose_file.setObjectName(u"btn_choose_file") + self.horizontalLayout.addWidget(self.btn_choose_file) + + self.verticalLayout.addLayout(self.horizontalLayout) + + # 解密设置区域 + self.groupBox_decrypt_options = QGroupBox(decrypt_pdf_view) + self.groupBox_decrypt_options.setObjectName(u"groupBox_decrypt_options") + self.verticalLayout_options = QVBoxLayout(self.groupBox_decrypt_options) + self.verticalLayout_options.setObjectName(u"verticalLayout_options") + + # 所有者密码 + self.horizontalLayout_owner_pwd = QHBoxLayout() + self.horizontalLayout_owner_pwd.setObjectName(u"horizontalLayout_owner_pwd") + self.label_owner_pwd = QLabel(self.groupBox_decrypt_options) + self.label_owner_pwd.setObjectName(u"label_owner_pwd") + self.horizontalLayout_owner_pwd.addWidget(self.label_owner_pwd) + + self.lineEdit_owner_pwd = QLineEdit(self.groupBox_decrypt_options) + self.lineEdit_owner_pwd.setObjectName(u"lineEdit_owner_pwd") + self.lineEdit_owner_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_owner_pwd.addWidget(self.lineEdit_owner_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_owner_pwd) + + # 用户密码 + self.horizontalLayout_user_pwd = QHBoxLayout() + self.horizontalLayout_user_pwd.setObjectName(u"horizontalLayout_user_pwd") + self.label_user_pwd = QLabel(self.groupBox_decrypt_options) + self.label_user_pwd.setObjectName(u"label_user_pwd") + self.horizontalLayout_user_pwd.addWidget(self.label_user_pwd) + + self.lineEdit_user_pwd = QLineEdit(self.groupBox_decrypt_options) + self.lineEdit_user_pwd.setObjectName(u"lineEdit_user_pwd") + self.lineEdit_user_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_user_pwd.addWidget(self.lineEdit_user_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_user_pwd) + + # 密码说明 + self.label_pwd_info = QLabel(self.groupBox_decrypt_options) + self.label_pwd_info.setObjectName(u"label_pwd_info") + self.verticalLayout_options.addWidget(self.label_pwd_info) + + self.verticalLayout.addWidget(self.groupBox_decrypt_options) + + # 输出选项区域 + self.groupBox_output = QGroupBox(decrypt_pdf_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 = QLabel(self.groupBox_output) + self.label_output.setObjectName(u"label_output") + self.horizontalLayout_output_dir.addWidget(self.label_output) + + self.comboBox_output_dir = QComboBox(self.groupBox_output) + self.comboBox_output_dir.addItem("PDF相同目录") + self.comboBox_output_dir.addItem("自定义目录") + self.comboBox_output_dir.setObjectName(u"comboBox_output_dir") + self.horizontalLayout_output_dir.addWidget(self.comboBox_output_dir) + + 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_filename = QHBoxLayout() + self.horizontalLayout_filename.setObjectName(u"horizontalLayout_filename") + self.label_filename = QLabel(self.groupBox_output) + self.label_filename.setObjectName(u"label_filename") + self.horizontalLayout_filename.addWidget(self.label_filename) + + self.lineEdit_filename = QLineEdit(self.groupBox_output) + self.lineEdit_filename.setObjectName(u"lineEdit_filename") + self.horizontalLayout_filename.addWidget(self.lineEdit_filename) + + self.verticalLayout_output.addLayout(self.horizontalLayout_filename) + + self.verticalLayout.addWidget(self.groupBox_output) + + # 进度条 + self.progressBar = QProgressBar(decrypt_pdf_view) + self.progressBar.setObjectName(u"progressBar") + self.progressBar.setValue(0) + self.verticalLayout.addWidget(self.progressBar) + + # 解密按钮 + self.horizontalLayout_decrypt = QHBoxLayout() + self.horizontalLayout_decrypt.setObjectName(u"horizontalLayout_decrypt") + self.horizontalLayout_decrypt.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + + self.btn_decrypt = QPushButton(decrypt_pdf_view) + self.btn_decrypt.setObjectName(u"btn_decrypt") + self.btn_decrypt.setMinimumSize(QSize(120, 40)) + self.horizontalLayout_decrypt.addWidget(self.btn_decrypt) + + self.horizontalLayout_decrypt.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout.addLayout(self.horizontalLayout_decrypt) + + self.retranslateUi(decrypt_pdf_view) + + QMetaObject.connectSlotsByName(decrypt_pdf_view) + + def retranslateUi(self, decrypt_pdf_view): + decrypt_pdf_view.setWindowTitle(QCoreApplication.translate("decrypt_pdf_view", u"PDF解密", None)) + self.label.setText(QCoreApplication.translate("decrypt_pdf_view", u"选择PDF文件:", None)) + self.btn_choose_file.setText(QCoreApplication.translate("decrypt_pdf_view", u"选择文件", None)) + + self.groupBox_decrypt_options.setTitle(QCoreApplication.translate("decrypt_pdf_view", u"解密设置", None)) + self.label_owner_pwd.setText(QCoreApplication.translate("decrypt_pdf_view", u"所有者密码:", None)) + self.label_user_pwd.setText(QCoreApplication.translate("decrypt_pdf_view", u"用户密码:", None)) + self.label_pwd_info.setText(QCoreApplication.translate("decrypt_pdf_view", u"注:解密PDF文件需要所有者密码,如果没有所有者密码可以尝试使用用户密码", None)) + + self.groupBox_output.setTitle(QCoreApplication.translate("decrypt_pdf_view", u"输出选项", None)) + self.label_output.setText(QCoreApplication.translate("decrypt_pdf_view", u"输出目录:", None)) + self.label_output_dir.setText(QCoreApplication.translate("decrypt_pdf_view", u"", None)) + self.btn_choose_output_dir.setText(QCoreApplication.translate("decrypt_pdf_view", u"选择目录", None)) + + self.label_filename.setText(QCoreApplication.translate("decrypt_pdf_view", u"文件名:", None)) + self.lineEdit_filename.setText(QCoreApplication.translate("decrypt_pdf_view", u"解密文件", None)) + + self.btn_decrypt.setText(QCoreApplication.translate("decrypt_pdf_view", u"解密PDF", None)) \ No newline at end of file diff --git a/app/ui/pdf_tools/security/encrypt.py b/app/ui/pdf_tools/security/encrypt.py new file mode 100644 index 0000000..43c9cb4 --- /dev/null +++ b/app/ui/pdf_tools/security/encrypt.py @@ -0,0 +1,455 @@ +import os.path +from typing import List + +import fitz +from PySide6.QtCore import Signal, QThread, QUrl, Qt, QFile, QIODevice, QTextStream +from PySide6.QtGui import QDesktopServices, QPixmap, QIcon, QFont, QFontMetrics +from PySide6.QtWidgets import (QWidget, QMessageBox, QFileDialog, QDialog, QPushButton, + QVBoxLayout, QLabel, QDialogButtonBox, QListWidget, + QListWidgetItem, QHBoxLayout, QFrame) + +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.security.encrypt_ui import Ui_encrypt_pdf_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 EncryptControl(QWidget, Ui_encrypt_pdf_view, QCursorGif): + okSignal = Signal(bool) + childRouterSignal = Signal(str) + + def __init__(self, router: Router, parent=None): + super().__init__(parent) + self.input_file_paths = [] # 改为存储多个文件路径 + self.output_dir = "" + self.router = router + self.router_path = (self.parent().router_path if self.parent() else '') + '/加密PDF' + self.child_routes = {} + self.worker = None + self.setupUi(self) + # 设置忙碌光标图片数组 + self.initCursor([':/icons/icons/Cursors/%d.png' % + i for i in range(8)], self) + self.setCursorTimeout(100) + self.init_ui() + + # 创建文件列表组件 + self.list_frame = QFrame(self) + self.list_frame.setFrameShape(QFrame.StyledPanel) + self.list_frame.setFrameShadow(QFrame.Raised) + self.list_frame.setObjectName("list_frame") + + self.list_layout = QVBoxLayout(self.list_frame) + self.list_layout.setContentsMargins(0, 0, 0, 0) + + self.list_label = QLabel("已选择的PDF文件:", self.list_frame) + self.list_layout.addWidget(self.list_label) + + self.file_list = QListWidget(self.list_frame) + self.file_list.setAlternatingRowColors(True) + self.file_list.setSelectionMode(QListWidget.ExtendedSelection) + self.list_layout.addWidget(self.file_list) + + # 文件列表操作按钮 + self.list_buttons_layout = QHBoxLayout() + self.btn_remove_selected = QPushButton("移除选中", self.list_frame) + self.btn_remove_all = QPushButton("清空列表", self.list_frame) + self.btn_remove_selected.clicked.connect(self.remove_selected_files) + self.btn_remove_all.clicked.connect(self.clear_file_list) + + self.list_buttons_layout.addWidget(self.btn_remove_selected) + self.list_buttons_layout.addWidget(self.btn_remove_all) + self.list_layout.addLayout(self.list_buttons_layout) + + # 添加文件列表到主布局 + self.verticalLayout.insertWidget(1, self.list_frame) + + # 按钮连接 + self.btn_choose_file.setText("选择文件") + self.btn_choose_file.clicked.connect(self.open_file_dialog) + self.btn_choose_file.setIcon(Icon.Add_Icon) + self.btn_encrypt.clicked.connect(self.encrypt_pdf) + + # 将说明标签替换为按钮 + self.label_pwd_info.hide() # 隐藏原始标签 + self.btn_help = QPushButton("点击查看说明", self) + self.btn_help.setStyleSheet("text-align: left; border: none; color: blue; text-decoration: underline;") + self.btn_help.setCursor(Qt.PointingHandCursor) + self.btn_help.clicked.connect(self.show_help_dialog) + # 将按钮添加到原标签的位置 + self.verticalLayout.insertWidget(self.verticalLayout.indexOf(self.label_pwd_info), self.btn_help) + + # 帮助文本 + self.base_help_text = "说明:\n1. 用户密码:打开文档时需要输入的密码\n2. 所有者密码:修改文档权限或解密时需要的密码\n\n" + self.config_help_text = "建议:可以只设置一种密码,也可以两种同时设置。如只需限制查看,仅设置用户密码即可;如只需限制编辑,仅设置所有者密码即可。" + + # 密码输入监听 + self.lineEdit_user_pwd.textChanged.connect(self.update_password_status) + self.lineEdit_owner_pwd.textChanged.connect(self.update_password_status) + + # 输出选项连接 + 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_encrypt.setEnabled(False) + + # 隐藏单文件模式下的文件名设置 + self.label_filename.setVisible(False) + self.lineEdit_filename.setVisible(False) + + def init_ui(self): + self.btn_encrypt.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() + + def show_help_dialog(self): + """显示密码设置说明对话框""" + dialog = QDialog(self) + dialog.setWindowTitle("PDF加密设置说明") + dialog.setMinimumWidth(400) + + layout = QVBoxLayout() + + # 添加基本说明 + help_text = self.base_help_text + self.config_help_text + help_label = QLabel(help_text) + help_label.setWordWrap(True) + layout.addWidget(help_label) + + # 添加确定按钮 + buttons = QDialogButtonBox(QDialogButtonBox.Ok) + buttons.accepted.connect(dialog.accept) + layout.addWidget(buttons) + + dialog.setLayout(layout) + dialog.exec_() + + 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 remove_selected_files(self): + """移除选中的文件""" + for item in self.file_list.selectedItems(): + file_path = item.data(Qt.UserRole) + self.file_list.takeItem(self.file_list.row(item)) + self.input_file_paths.remove(file_path) + + # 更新按钮状态 + self.btn_encrypt.setEnabled(len(self.input_file_paths) > 0) + + def clear_file_list(self): + """清空文件列表""" + self.file_list.clear() + self.input_file_paths.clear() + self.btn_encrypt.setEnabled(False) + + def open_file_dialog(self): + # 打开文件对话框,选择多个PDF文件 + files, _ = QFileDialog.getOpenFileNames(self, "选择PDF文件", "", "PDF Files (*.pdf);;All Files (*)") + if not files: + return + + # 遍历所有选择的文件 + for file_path in files: + # 检查文件是否已在列表中 + if file_path in self.input_file_paths: + continue + + # 获取PDF信息 + try: + with fitz.open(file_path) as pdf: + # 如果PDF已加密,提示用户但仍然添加 + if pdf.is_encrypted: + QMessageBox.warning(self, "警告", f"文件 {os.path.basename(file_path)} 已加密,重新加密可能会导致无法打开。") + except Exception as e: + logger.error(f"读取PDF文件错误: {str(e)}") + QMessageBox.warning(self, "警告", f"无法读取文件 {os.path.basename(file_path)}: {str(e)}") + continue + + # 添加到文件列表 + self.input_file_paths.append(file_path) + item = QListWidgetItem(os.path.basename(file_path)) + item.setData(Qt.UserRole, file_path) + self.file_list.addItem(item) + + # 如果添加了文件,启用加密按钮 + if self.input_file_paths: + self.btn_encrypt.setEnabled(True) + + # 如果未设置输出目录,默认使用与第一个PDF相同的目录 + if not self.output_dir and self.input_file_paths: + self.output_dir = os.path.dirname(self.input_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) + + def encrypt_pdf(self): + if not self.input_file_paths: + QMessageBox.critical(self, "错误", "请先选择PDF文件") + return + + # 验证密码 + user_password = self.lineEdit_user_pwd.text() + confirm_user_password = self.lineEdit_confirm_user_pwd.text() + owner_password = self.lineEdit_owner_pwd.text() + confirm_owner_password = self.lineEdit_confirm_owner_pwd.text() + + if user_password and user_password != confirm_user_password: + QMessageBox.warning(self, "警告", "用户密码与确认密码不一致") + return + + if owner_password and owner_password != confirm_owner_password: + QMessageBox.warning(self, "警告", "所有者密码与确认密码不一致") + return + + if not user_password and not owner_password: + reply = QMessageBox.question(self, "提示", + "您没有设置任何密码,是否继续?\n\n" + + "- 如需限制查看文档,请设置用户密码\n" + + "- 如需限制编辑文档,请设置所有者密码", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + return + + # 获取权限设置 + permissions = 0 + if self.checkBox_print.isChecked(): + permissions |= fitz.PDF_PERM_PRINT + if self.checkBox_copy.isChecked(): + permissions |= fitz.PDF_PERM_COPY + if self.checkBox_modify.isChecked(): + permissions |= fitz.PDF_PERM_MODIFY + if self.checkBox_annotate.isChecked(): + permissions |= fitz.PDF_PERM_ANNOTATE + + # 确定加密方法 + encrypt_method = fitz.PDF_ENCRYPT_AES_128 + encrypt_method_text = self.comboBox_enc_level.currentText() + if "128位RC4" in encrypt_method_text: + encrypt_method = fitz.PDF_ENCRYPT_RC4_128 + elif "40位RC4" in encrypt_method_text: + encrypt_method = fitz.PDF_ENCRYPT_RC4_40 + + # 获取输出目录 + if self.comboBox_output_dir.currentText() == '自定义目录' and self.output_dir: + output_directory = self.output_dir + else: + # 使用第一个文件的目录作为默认输出目录 + output_directory = os.path.dirname(self.input_file_paths[0]) + + # 如果输出目录不存在,创建它 + if not os.path.exists(output_directory): + try: + os.makedirs(output_directory) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建输出目录: {str(e)}") + return + + # 禁用按钮,开始任务 + self.btn_encrypt.setEnabled(False) + self.startBusy() + + # 创建批量处理线程 + self.worker = BatchEncryptThread( + input_files=self.input_file_paths, + output_dir=output_directory, + user_password=user_password, + owner_password=owner_password, + permissions=permissions, + encrypt_method=encrypt_method + ) + self.worker.progressSignal.connect(self.update_progress) + self.worker.okSignal.connect(self.encrypt_finish) + self.worker.start() + + def update_progress(self, value): + self.progressBar.setValue(value) + + def encrypt_finish(self, result_data): + self.stopBusy() + success_count, failed_count, output_dir = result_data + + if success_count > 0: + user_pwd = self.lineEdit_user_pwd.text() + owner_pwd = self.lineEdit_owner_pwd.text() + + # 根据密码设置情况提供不同的提示 + success_text = f"成功加密 {success_count} 个文件" + if failed_count > 0: + success_text += f",失败 {failed_count} 个" + + if user_pwd and owner_pwd: + if user_pwd == owner_pwd: + success_text += f"\n\n密码: {user_pwd}\n(用户密码和所有者密码相同)" + else: + success_text += f"\n\n用户密码: {user_pwd} (用于打开文档)\n所有者密码: {owner_pwd} (用于解除保护)" + elif user_pwd: + success_text += f"\n\n用户密码: {user_pwd} (用于打开文档)" + elif owner_pwd: + success_text += f"\n\n所有者密码: {owner_pwd} (用于解除保护和编辑)" + else: + success_text += "\n\n已设置权限控制,但未使用密码保护" + + reply = QMessageBox(self) + reply.setIcon(QMessageBox.Information) + reply.setWindowTitle('完成') + reply.setText(success_text) + btn = reply.addButton('打开文件夹', QMessageBox.ActionRole) + btn.clicked.connect(lambda: open_file_explorer(output_dir)) + reply.addButton("确认", QMessageBox.AcceptRole) + reply.exec_() + else: + QMessageBox.critical(self, "错误", f"PDF加密失败,所有文件均未能成功加密") + + # 恢复UI状态 + self.btn_encrypt.setEnabled(True) + self.progressBar.setValue(0) + self.worker = None + + def closeEvent(self, event): + super().closeEvent(event) + self.okSignal.emit(True) + + def update_password_status(self): + """更新密码设置状态提示""" + user_pwd = self.lineEdit_user_pwd.text() + owner_pwd = self.lineEdit_owner_pwd.text() + + # 根据密码设置情况更新帮助文本 + if user_pwd and owner_pwd: + if user_pwd == owner_pwd: + self.config_help_text = "当前设置:两种密码相同,文档将受到完全保护,但管理不便。\n建议使用不同的密码以便分别控制查看和编辑权限。" + else: + self.config_help_text = "当前设置:两种密码都已设置,文档将受到完全保护。输入用户密码可阅读,输入所有者密码可编辑。" + elif user_pwd: + self.config_help_text = "当前设置:仅设置用户密码,他人需要密码才能打开文档,但有所有者权限的用户可以编辑。" + elif owner_pwd: + self.config_help_text = "当前设置:仅设置所有者密码,任何人都能查看,但需要密码才能编辑文档。" + else: + self.config_help_text = "建议:可以只设置一种密码,也可以两种同时设置。如只需限制查看,仅设置用户密码即可;如只需限制编辑,仅设置所有者密码即可。" + + +class BatchEncryptThread(QThread): + okSignal = Signal(tuple) # (success_count, failed_count, output_dir) + progressSignal = Signal(int) + + def __init__(self, input_files: List[str], output_dir: str, user_password="", owner_password="", + permissions=0, encrypt_method=fitz.PDF_ENCRYPT_AES_128): + super().__init__() + self.input_files = input_files + self.output_dir = output_dir + self.user_password = user_password + self.owner_password = owner_password + self.permissions = permissions + self.encrypt_method = encrypt_method + + def run(self): + success_count = 0 + failed_count = 0 + total_files = len(self.input_files) + + for index, file_path in enumerate(self.input_files): + try: + # 打开输入PDF + pdf_document = fitz.open(file_path) + + # 如果PDF已加密,先尝试解密 + if pdf_document.is_encrypted: + try: + # 尝试用空密码解密,如果之前是无密码文档 + pdf_document.authenticate("") + except: + pass + + # 生成输出文件名:原文件名+加密+文件.pdf + filename = os.path.basename(file_path) + filename_without_ext = os.path.splitext(filename)[0] + output_filename = f"{filename_without_ext}_加密文件.pdf" + output_path = os.path.join(self.output_dir, output_filename) + output_path = common.usable_filepath(output_path) + + # 确保至少有一个密码 + if not self.owner_password and not self.user_password: + # 如果用户确认不使用密码,只使用权限控制 + owner_pw = "" + user_pw = "" + else: + # 将所有者密码设置为用户密码(如果未提供) + owner_pw = self.owner_password if self.owner_password else self.user_password + # 将用户密码设置为所有者密码(如果未提供) + user_pw = self.user_password if self.user_password else "" + + # 设置文档权限并保存 + pdf_document.save( + output_path, + encryption=self.encrypt_method, + owner_pw=owner_pw, + user_pw=user_pw, + permissions=self.permissions + ) + + pdf_document.close() + success_count += 1 + + except Exception as e: + logger.error(f"PDF加密错误 {file_path}: {str(e)}") + failed_count += 1 + + # 更新进度 + progress = int((index + 1) / total_files * 100) + self.progressSignal.emit(progress) + + self.okSignal.emit((success_count, failed_count, self.output_dir)) + + +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 = EncryptControl(router) + view.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/app/ui/pdf_tools/security/encrypt_ui.py b/app/ui/pdf_tools/security/encrypt_ui.py new file mode 100644 index 0000000..6da2516 --- /dev/null +++ b/app/ui/pdf_tools/security/encrypt_ui.py @@ -0,0 +1,244 @@ +from PySide6.QtCore import QCoreApplication, QMetaObject, QSize +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import (QCheckBox, QComboBox, QLabel, QLineEdit, QProgressBar, QPushButton, + QSizePolicy, QSpacerItem, QVBoxLayout, QHBoxLayout, QWidget, + QGroupBox, QSpinBox) + + +class Ui_encrypt_pdf_view(object): + def setupUi(self, encrypt_pdf_view): + if not encrypt_pdf_view.objectName(): + encrypt_pdf_view.setObjectName(u"encrypt_pdf_view") + encrypt_pdf_view.resize(800, 600) + self.verticalLayout = QVBoxLayout(encrypt_pdf_view) + self.verticalLayout.setObjectName(u"verticalLayout") + + # 文件选择区域 + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.label = QLabel(encrypt_pdf_view) + self.label.setObjectName(u"label") + self.horizontalLayout.addWidget(self.label) + + self.lineEdit_pdf_path = QLineEdit(encrypt_pdf_view) + self.lineEdit_pdf_path.setObjectName(u"lineEdit_pdf_path") + self.lineEdit_pdf_path.setReadOnly(True) + self.horizontalLayout.addWidget(self.lineEdit_pdf_path) + + self.btn_choose_file = QPushButton(encrypt_pdf_view) + self.btn_choose_file.setObjectName(u"btn_choose_file") + self.horizontalLayout.addWidget(self.btn_choose_file) + + self.verticalLayout.addLayout(self.horizontalLayout) + + # 加密设置区域 + self.groupBox_encrypt_options = QGroupBox(encrypt_pdf_view) + self.groupBox_encrypt_options.setObjectName(u"groupBox_encrypt_options") + self.verticalLayout_options = QVBoxLayout(self.groupBox_encrypt_options) + self.verticalLayout_options.setObjectName(u"verticalLayout_options") + + # 密码说明 + self.label_pwd_info = QLabel(self.groupBox_encrypt_options) + self.label_pwd_info.setObjectName(u"label_pwd_info") + self.label_pwd_info.setWordWrap(True) + self.verticalLayout_options.addWidget(self.label_pwd_info) + + # 用户密码 + self.horizontalLayout_user_pwd = QHBoxLayout() + self.horizontalLayout_user_pwd.setObjectName(u"horizontalLayout_user_pwd") + self.label_user_pwd = QLabel(self.groupBox_encrypt_options) + self.label_user_pwd.setObjectName(u"label_user_pwd") + self.horizontalLayout_user_pwd.addWidget(self.label_user_pwd) + + self.lineEdit_user_pwd = QLineEdit(self.groupBox_encrypt_options) + self.lineEdit_user_pwd.setObjectName(u"lineEdit_user_pwd") + self.lineEdit_user_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_user_pwd.addWidget(self.lineEdit_user_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_user_pwd) + + # 确认用户密码 + self.horizontalLayout_confirm_user_pwd = QHBoxLayout() + self.horizontalLayout_confirm_user_pwd.setObjectName(u"horizontalLayout_confirm_user_pwd") + self.label_confirm_user_pwd = QLabel(self.groupBox_encrypt_options) + self.label_confirm_user_pwd.setObjectName(u"label_confirm_user_pwd") + self.horizontalLayout_confirm_user_pwd.addWidget(self.label_confirm_user_pwd) + + self.lineEdit_confirm_user_pwd = QLineEdit(self.groupBox_encrypt_options) + self.lineEdit_confirm_user_pwd.setObjectName(u"lineEdit_confirm_user_pwd") + self.lineEdit_confirm_user_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_confirm_user_pwd.addWidget(self.lineEdit_confirm_user_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_confirm_user_pwd) + + # 所有者密码 + self.horizontalLayout_owner_pwd = QHBoxLayout() + self.horizontalLayout_owner_pwd.setObjectName(u"horizontalLayout_owner_pwd") + self.label_owner_pwd = QLabel(self.groupBox_encrypt_options) + self.label_owner_pwd.setObjectName(u"label_owner_pwd") + self.horizontalLayout_owner_pwd.addWidget(self.label_owner_pwd) + + self.lineEdit_owner_pwd = QLineEdit(self.groupBox_encrypt_options) + self.lineEdit_owner_pwd.setObjectName(u"lineEdit_owner_pwd") + self.lineEdit_owner_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_owner_pwd.addWidget(self.lineEdit_owner_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_owner_pwd) + + # 确认所有者密码 + self.horizontalLayout_confirm_owner_pwd = QHBoxLayout() + self.horizontalLayout_confirm_owner_pwd.setObjectName(u"horizontalLayout_confirm_owner_pwd") + self.label_confirm_owner_pwd = QLabel(self.groupBox_encrypt_options) + self.label_confirm_owner_pwd.setObjectName(u"label_confirm_owner_pwd") + self.horizontalLayout_confirm_owner_pwd.addWidget(self.label_confirm_owner_pwd) + + self.lineEdit_confirm_owner_pwd = QLineEdit(self.groupBox_encrypt_options) + self.lineEdit_confirm_owner_pwd.setObjectName(u"lineEdit_confirm_owner_pwd") + self.lineEdit_confirm_owner_pwd.setEchoMode(QLineEdit.Password) + self.horizontalLayout_confirm_owner_pwd.addWidget(self.lineEdit_confirm_owner_pwd) + + self.verticalLayout_options.addLayout(self.horizontalLayout_confirm_owner_pwd) + + # 加密级别 + self.horizontalLayout_enc_level = QHBoxLayout() + self.horizontalLayout_enc_level.setObjectName(u"horizontalLayout_enc_level") + self.label_enc_level = QLabel(self.groupBox_encrypt_options) + self.label_enc_level.setObjectName(u"label_enc_level") + self.horizontalLayout_enc_level.addWidget(self.label_enc_level) + + self.comboBox_enc_level = QComboBox(self.groupBox_encrypt_options) + self.comboBox_enc_level.addItem("128位AES(推荐)") + self.comboBox_enc_level.addItem("128位RC4") + self.comboBox_enc_level.addItem("40位RC4(兼容)") + self.comboBox_enc_level.setObjectName(u"comboBox_enc_level") + self.horizontalLayout_enc_level.addWidget(self.comboBox_enc_level) + + self.horizontalLayout_enc_level.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout_options.addLayout(self.horizontalLayout_enc_level) + + # 权限设置 + self.label_permissions = QLabel(self.groupBox_encrypt_options) + self.label_permissions.setObjectName(u"label_permissions") + self.verticalLayout_options.addWidget(self.label_permissions) + + # 打印权限 + self.checkBox_print = QCheckBox(self.groupBox_encrypt_options) + self.checkBox_print.setObjectName(u"checkBox_print") + self.checkBox_print.setChecked(True) + self.verticalLayout_options.addWidget(self.checkBox_print) + + # 复制权限 + self.checkBox_copy = QCheckBox(self.groupBox_encrypt_options) + self.checkBox_copy.setObjectName(u"checkBox_copy") + self.checkBox_copy.setChecked(True) + self.verticalLayout_options.addWidget(self.checkBox_copy) + + # 修改权限 + self.checkBox_modify = QCheckBox(self.groupBox_encrypt_options) + self.checkBox_modify.setObjectName(u"checkBox_modify") + self.checkBox_modify.setChecked(True) + self.verticalLayout_options.addWidget(self.checkBox_modify) + + # 注释权限 + self.checkBox_annotate = QCheckBox(self.groupBox_encrypt_options) + self.checkBox_annotate.setObjectName(u"checkBox_annotate") + self.checkBox_annotate.setChecked(True) + self.verticalLayout_options.addWidget(self.checkBox_annotate) + + self.verticalLayout.addWidget(self.groupBox_encrypt_options) + + # 输出选项区域 + self.groupBox_output = QGroupBox(encrypt_pdf_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 = QLabel(self.groupBox_output) + self.label_output.setObjectName(u"label_output") + self.horizontalLayout_output_dir.addWidget(self.label_output) + + self.comboBox_output_dir = QComboBox(self.groupBox_output) + self.comboBox_output_dir.addItem("PDF相同目录") + self.comboBox_output_dir.addItem("自定义目录") + self.comboBox_output_dir.setObjectName(u"comboBox_output_dir") + self.horizontalLayout_output_dir.addWidget(self.comboBox_output_dir) + + 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_filename = QHBoxLayout() + self.horizontalLayout_filename.setObjectName(u"horizontalLayout_filename") + self.label_filename = QLabel(self.groupBox_output) + self.label_filename.setObjectName(u"label_filename") + self.horizontalLayout_filename.addWidget(self.label_filename) + + self.lineEdit_filename = QLineEdit(self.groupBox_output) + self.lineEdit_filename.setObjectName(u"lineEdit_filename") + self.horizontalLayout_filename.addWidget(self.lineEdit_filename) + + self.verticalLayout_output.addLayout(self.horizontalLayout_filename) + + self.verticalLayout.addWidget(self.groupBox_output) + + # 进度条 + self.progressBar = QProgressBar(encrypt_pdf_view) + self.progressBar.setObjectName(u"progressBar") + self.progressBar.setValue(0) + self.verticalLayout.addWidget(self.progressBar) + + # 加密按钮 + self.horizontalLayout_encrypt = QHBoxLayout() + self.horizontalLayout_encrypt.setObjectName(u"horizontalLayout_encrypt") + self.horizontalLayout_encrypt.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + + self.btn_encrypt = QPushButton(encrypt_pdf_view) + self.btn_encrypt.setObjectName(u"btn_encrypt") + self.btn_encrypt.setMinimumSize(QSize(120, 40)) + self.horizontalLayout_encrypt.addWidget(self.btn_encrypt) + + self.horizontalLayout_encrypt.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout.addLayout(self.horizontalLayout_encrypt) + + self.retranslateUi(encrypt_pdf_view) + + QMetaObject.connectSlotsByName(encrypt_pdf_view) + + def retranslateUi(self, encrypt_pdf_view): + encrypt_pdf_view.setWindowTitle(QCoreApplication.translate("encrypt_pdf_view", u"PDF加密", None)) + self.label.setText(QCoreApplication.translate("encrypt_pdf_view", u"选择PDF文件:", None)) + self.btn_choose_file.setText(QCoreApplication.translate("encrypt_pdf_view", u"选择文件", None)) + + self.groupBox_encrypt_options.setTitle(QCoreApplication.translate("encrypt_pdf_view", u"加密设置", None)) + self.label_pwd_info.setText(QCoreApplication.translate("encrypt_pdf_view", u"说明:\n1. 用户密码:打开文档时需要输入的密码\n2. 所有者密码:修改文档权限或解密时需要的密码\n\n建议:可以只设置一种密码,也可以两种同时设置。如只需限制查看,仅设置用户密码即可;如只需限制编辑,仅设置所有者密码即可。", None)) + self.label_user_pwd.setText(QCoreApplication.translate("encrypt_pdf_view", u"用户密码:", None)) + self.label_confirm_user_pwd.setText(QCoreApplication.translate("encrypt_pdf_view", u"确认用户密码:", None)) + self.label_owner_pwd.setText(QCoreApplication.translate("encrypt_pdf_view", u"所有者密码:", None)) + self.label_confirm_owner_pwd.setText(QCoreApplication.translate("encrypt_pdf_view", u"确认所有者密码:", None)) + + self.label_enc_level.setText(QCoreApplication.translate("encrypt_pdf_view", u"加密级别:", None)) + self.label_permissions.setText(QCoreApplication.translate("encrypt_pdf_view", u"权限设置:", None)) + self.checkBox_print.setText(QCoreApplication.translate("encrypt_pdf_view", u"允许打印", None)) + self.checkBox_copy.setText(QCoreApplication.translate("encrypt_pdf_view", u"允许复制内容", None)) + self.checkBox_modify.setText(QCoreApplication.translate("encrypt_pdf_view", u"允许修改文档", None)) + self.checkBox_annotate.setText(QCoreApplication.translate("encrypt_pdf_view", u"允许添加注释", None)) + + self.groupBox_output.setTitle(QCoreApplication.translate("encrypt_pdf_view", u"输出选项", None)) + self.label_output.setText(QCoreApplication.translate("encrypt_pdf_view", u"输出目录:", None)) + self.label_output_dir.setText(QCoreApplication.translate("encrypt_pdf_view", u"", None)) + self.btn_choose_output_dir.setText(QCoreApplication.translate("encrypt_pdf_view", u"选择目录", None)) + + self.label_filename.setText(QCoreApplication.translate("encrypt_pdf_view", u"文件名:", None)) + self.lineEdit_filename.setText(QCoreApplication.translate("encrypt_pdf_view", u"加密文件", None)) + + self.btn_encrypt.setText(QCoreApplication.translate("encrypt_pdf_view", u"加密PDF", None)) \ No newline at end of file diff --git a/app/ui/pdf_tools/split/__init__.py b/app/ui/pdf_tools/split/__init__.py new file mode 100644 index 0000000..5fb79b9 --- /dev/null +++ b/app/ui/pdf_tools/split/__init__.py @@ -0,0 +1,4 @@ +# PDF拆分功能模块 +""" +PDF拆分相关功能 +""" \ No newline at end of file diff --git a/app/ui/pdf_tools/split/__pycache__/__init__.cpython-313.pyc b/app/ui/pdf_tools/split/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..db27cd7 Binary files /dev/null and b/app/ui/pdf_tools/split/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/split/__pycache__/split.cpython-313.pyc b/app/ui/pdf_tools/split/__pycache__/split.cpython-313.pyc new file mode 100644 index 0000000..3f9d430 Binary files /dev/null and b/app/ui/pdf_tools/split/__pycache__/split.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/split/__pycache__/split_ui.cpython-313.pyc b/app/ui/pdf_tools/split/__pycache__/split_ui.cpython-313.pyc new file mode 100644 index 0000000..ef78cdc Binary files /dev/null and b/app/ui/pdf_tools/split/__pycache__/split_ui.cpython-313.pyc differ diff --git a/app/ui/pdf_tools/split/split.py b/app/ui/pdf_tools/split/split.py new file mode 100644 index 0000000..381825c --- /dev/null +++ b/app/ui/pdf_tools/split/split.py @@ -0,0 +1,358 @@ +import os.path +import re +from typing import List, Tuple + +import fitz +from PySide6.QtCore import Signal, QThread, QUrl, Qt, QFile, QIODevice, QTextStream +from PySide6.QtGui import QDesktopServices, QPixmap, QIcon, QFont, QFontMetrics +from PySide6.QtWidgets import QWidget, QMessageBox, QFileDialog, QButtonGroup, QDialog + +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.split.split_ui import Ui_split_pdf_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 SplitControl(QWidget, Ui_split_pdf_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_prefix = "拆分文件" + self.total_pages = 0 + self.router = router + self.router_path = (self.parent().router_path if self.parent() else '') + '/拆分PDF' + 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_split.clicked.connect(self.split_pdf) + + # 选项按钮组 + self.option_group = QButtonGroup(self) + self.option_group.addButton(self.radioButton_by_pages) + self.option_group.addButton(self.radioButton_by_ranges) + self.option_group.addButton(self.radioButton_single_page) + self.option_group.addButton(self.radioButton_all_pages) + self.radioButton_by_pages.toggled.connect(self.update_option_ui) + self.radioButton_by_ranges.toggled.connect(self.update_option_ui) + self.radioButton_single_page.toggled.connect(self.update_option_ui) + self.radioButton_all_pages.toggled.connect(self.update_option_ui) + + # 输出选项连接 + self.lineEdit_prefix.textChanged.connect(self.change_output_prefix) + 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_split.setEnabled(False) + + def init_ui(self): + self.btn_split.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_prefix.setText(self.output_prefix) + + def update_option_ui(self): + # 更新选项界面状态 + self.spinBox_pages.setEnabled(self.radioButton_by_pages.isChecked()) + self.lineEdit_ranges.setEnabled(self.radioButton_by_ranges.isChecked()) + self.spinBox_single.setEnabled(self.radioButton_single_page.isChecked()) + + # 更新单页提取的最大值 + if self.total_pages > 0 and self.radioButton_single_page.isChecked(): + self.spinBox_single.setMaximum(self.total_pages) + + def change_output_prefix(self): + self.output_prefix = common.correct_filename(self.lineEdit_prefix.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.btn_split.setEnabled(True) + + # 获取PDF页数 + try: + with fitz.open(file_path) as pdf: + self.total_pages = len(pdf) + # 更新单页提取的最大值 + self.spinBox_single.setMaximum(self.total_pages) + except Exception as e: + logger.error(f"读取PDF文件错误: {str(e)}") + QMessageBox.critical(self, "错误", f"无法读取PDF文件: {str(e)}") + self.btn_split.setEnabled(False) + return + + # 如果未设置输出目录,默认使用与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) + + # 输出文件前缀格式:原文件名+拆分+文件 + filename = os.path.basename(file_path) + filename_without_ext = os.path.splitext(filename)[0] + new_prefix = f"{filename_without_ext}_拆分文件" + self.output_prefix = new_prefix + self.lineEdit_prefix.setText(new_prefix) + + def split_pdf(self): + if not os.path.exists(self.input_file_path): + QMessageBox.critical(self, "错误", "请先选择PDF文件") + return + + # 获取输出目录 + if self.comboBox_output_dir.currentText() == '自定义目录' and self.output_dir: + output_directory = self.output_dir + else: + output_directory = os.path.dirname(self.input_file_path) + + # 如果输出目录不存在,创建它 + if not os.path.exists(output_directory): + try: + os.makedirs(output_directory) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建输出目录: {str(e)}") + return + + # 根据选项确定拆分方式 + split_ranges = [] + + # 按页数拆分 + if self.radioButton_by_pages.isChecked(): + pages_per_file = self.spinBox_pages.value() + if pages_per_file <= 0: + QMessageBox.warning(self, "警告", "每个文件的页数必须大于0") + return + + # 计算拆分范围 + for i in range(0, self.total_pages, pages_per_file): + end = min(i + pages_per_file - 1, self.total_pages - 1) + split_ranges.append((i, end)) + + # 按页码范围拆分 + elif self.radioButton_by_ranges.isChecked(): + ranges_text = self.lineEdit_ranges.text().strip() + if not ranges_text: + QMessageBox.warning(self, "警告", "请输入页码范围") + return + + try: + split_ranges = self.parse_page_ranges(ranges_text) + if not split_ranges: + QMessageBox.warning(self, "警告", "无效的页码范围") + return + + # 验证页码是否在范围内 + for start, end in split_ranges: + if start < 0 or end >= self.total_pages or start > end: + QMessageBox.warning(self, "警告", f"页码范围 {start+1}-{end+1} 超出文件范围(1-{self.total_pages})") + return + except ValueError as e: + QMessageBox.warning(self, "警告", str(e)) + return + + # 提取单页 + elif self.radioButton_single_page.isChecked(): + page_num = self.spinBox_single.value() - 1 # 转为0基索引 + if page_num < 0 or page_num >= self.total_pages: + QMessageBox.warning(self, "警告", f"页码必须在1到{self.total_pages}之间") + return + + split_ranges = [(page_num, page_num)] + + # 拆分为单页 + elif self.radioButton_all_pages.isChecked(): + split_ranges = [(i, i) for i in range(self.total_pages)] + + # 禁用按钮,开始任务 + self.btn_split.setEnabled(False) + self.startBusy() + + # 创建并启动工作线程 + input_file = PdfFile(self.input_file_path) + self.worker = SplitThread(input_file, self.output_prefix, output_directory, split_ranges) + self.worker.progressSignal.connect(self.update_progress) + self.worker.okSignal.connect(self.split_finish) + self.worker.start() + + def update_progress(self, value): + self.progressBar.setValue(value) + + def split_finish(self, result_data): + self.stopBusy() + success, output_dir = result_data + + if success: + reply = QMessageBox(self) + reply.setIcon(QMessageBox.Information) + reply.setWindowTitle('完成') + reply.setText(f"PDF拆分成功") + btn = reply.addButton('打开文件夹', QMessageBox.ActionRole) + btn.clicked.connect(lambda: open_file_explorer(output_dir)) + reply.addButton("确认", QMessageBox.AcceptRole) + reply.exec_() + else: + QMessageBox.critical(self, "错误", f"PDF拆分失败: {output_dir}") + + # 恢复UI状态 + self.btn_split.setEnabled(True) + self.progressBar.setValue(0) + self.worker = None + + def parse_page_ranges(self, ranges_text: str) -> List[Tuple[int, int]]: + """解析页码范围文本,返回(开始页,结束页)的列表,页码从0开始""" + result = [] + # 验证格式 + if not re.match(r'^(\d+(-\d+)?)(,\d+(-\d+)?)*$', ranges_text): + raise ValueError("页码范围格式不正确。正确格式示例: 1-5,7,10-12") + + ranges = ranges_text.split(',') + for r in ranges: + if '-' in r: + start, end = r.split('-') + try: + start_idx = int(start) - 1 # 转为0基索引 + end_idx = int(end) - 1 + if start_idx > end_idx: + raise ValueError(f"范围 {start}-{end} 中,起始页不能大于结束页") + result.append((start_idx, end_idx)) + except ValueError: + raise ValueError(f"无效的页码范围: {r}") + else: + try: + page_idx = int(r) - 1 # 转为0基索引 + result.append((page_idx, page_idx)) + except ValueError: + raise ValueError(f"无效的页码: {r}") + return result + + def closeEvent(self, event): + super().closeEvent(event) + self.okSignal.emit(True) + + +class SplitThread(QThread): + okSignal = Signal(tuple) # (success, output_dir) + progressSignal = Signal(int) + + def __init__(self, input_file: PdfFile, output_prefix: str, output_dir: str, page_ranges: List[Tuple[int, int]]): + super().__init__() + self.input_file = input_file + self.output_prefix = output_prefix + self.output_dir = output_dir + self.page_ranges = page_ranges + + def run(self): + try: + # 打开输入PDF + pdf_document = fitz.open(self.input_file.file_path) + total_ranges = len(self.page_ranges) + + for i, (start_page, end_page) in enumerate(self.page_ranges): + # 创建新的PDF文档 + output_pdf = fitz.open() + + # 复制页面 + for page_num in range(start_page, end_page + 1): + output_pdf.insert_pdf(pdf_document, from_page=page_num, to_page=page_num) + + # 生成输出文件名 + if len(self.page_ranges) == 1: + output_filename = f"{self.output_prefix}.pdf" + else: + # 根据页码范围生成文件名 + if start_page == end_page: + page_part = f"{start_page + 1}" + else: + page_part = f"{start_page + 1}-{end_page + 1}" + output_filename = f"{self.output_prefix}_{page_part}.pdf" + + output_path = os.path.join(self.output_dir, output_filename) + output_path = common.usable_filepath(output_path) + + # 保存PDF + output_pdf.save(output_path) + output_pdf.close() + + # 更新进度 + progress = int((i + 1) / total_ranges * 100) + self.progressSignal.emit(progress) + + pdf_document.close() + self.okSignal.emit((True, self.output_dir)) + + except Exception as e: + logger.error(f"PDF拆分错误: {str(e)}") + self.okSignal.emit((False, 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 = SplitControl(router) + view.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/app/ui/pdf_tools/split/split_ui.py b/app/ui/pdf_tools/split/split_ui.py new file mode 100644 index 0000000..2b15c03 --- /dev/null +++ b/app/ui/pdf_tools/split/split_ui.py @@ -0,0 +1,215 @@ +from PySide6.QtCore import QCoreApplication, QMetaObject, QSize +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import (QCheckBox, QComboBox, QLabel, QLineEdit, QProgressBar, QPushButton, + QRadioButton, QSizePolicy, QSpacerItem, QVBoxLayout, QHBoxLayout, QWidget, + QGroupBox, QSpinBox) + + +class Ui_split_pdf_view(object): + def setupUi(self, split_pdf_view): + if not split_pdf_view.objectName(): + split_pdf_view.setObjectName(u"split_pdf_view") + split_pdf_view.resize(800, 600) + self.verticalLayout = QVBoxLayout(split_pdf_view) + self.verticalLayout.setObjectName(u"verticalLayout") + + # 文件选择区域 + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.label = QLabel(split_pdf_view) + self.label.setObjectName(u"label") + self.horizontalLayout.addWidget(self.label) + + self.lineEdit_pdf_path = QLineEdit(split_pdf_view) + self.lineEdit_pdf_path.setObjectName(u"lineEdit_pdf_path") + self.lineEdit_pdf_path.setReadOnly(True) + self.horizontalLayout.addWidget(self.lineEdit_pdf_path) + + self.btn_choose_file = QPushButton(split_pdf_view) + self.btn_choose_file.setObjectName(u"btn_choose_file") + self.horizontalLayout.addWidget(self.btn_choose_file) + + self.verticalLayout.addLayout(self.horizontalLayout) + + # 拆分选项区域 + self.groupBox_split_options = QGroupBox(split_pdf_view) + self.groupBox_split_options.setObjectName(u"groupBox_split_options") + self.verticalLayout_options = QVBoxLayout(self.groupBox_split_options) + self.verticalLayout_options.setObjectName(u"verticalLayout_options") + + # 按页数拆分 + self.radioButton_by_pages = QRadioButton(self.groupBox_split_options) + self.radioButton_by_pages.setObjectName(u"radioButton_by_pages") + self.radioButton_by_pages.setChecked(True) + self.verticalLayout_options.addWidget(self.radioButton_by_pages) + + self.horizontalLayout_pages = QHBoxLayout() + self.horizontalLayout_pages.setObjectName(u"horizontalLayout_pages") + self.label_pages = QLabel(self.groupBox_split_options) + self.label_pages.setObjectName(u"label_pages") + self.horizontalLayout_pages.addWidget(self.label_pages) + + self.spinBox_pages = QSpinBox(self.groupBox_split_options) + self.spinBox_pages.setObjectName(u"spinBox_pages") + self.spinBox_pages.setMinimum(1) + self.spinBox_pages.setValue(1) + self.spinBox_pages.setMinimumWidth(80) + self.spinBox_pages.setMaximumWidth(120) + font = self.spinBox_pages.font() + font.setPointSize(10) + self.spinBox_pages.setFont(font) + self.horizontalLayout_pages.addWidget(self.spinBox_pages) + + self.horizontalLayout_pages.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout_options.addLayout(self.horizontalLayout_pages) + + # 按页码拆分 + self.radioButton_by_ranges = QRadioButton(self.groupBox_split_options) + self.radioButton_by_ranges.setObjectName(u"radioButton_by_ranges") + self.verticalLayout_options.addWidget(self.radioButton_by_ranges) + + self.horizontalLayout_ranges = QHBoxLayout() + self.horizontalLayout_ranges.setObjectName(u"horizontalLayout_ranges") + self.label_ranges = QLabel(self.groupBox_split_options) + self.label_ranges.setObjectName(u"label_ranges") + self.horizontalLayout_ranges.addWidget(self.label_ranges) + + self.lineEdit_ranges = QLineEdit(self.groupBox_split_options) + self.lineEdit_ranges.setObjectName(u"lineEdit_ranges") + self.lineEdit_ranges.setEnabled(False) + self.lineEdit_ranges.setMinimumWidth(180) + font = self.lineEdit_ranges.font() + font.setPointSize(10) + self.lineEdit_ranges.setFont(font) + self.horizontalLayout_ranges.addWidget(self.lineEdit_ranges) + + self.horizontalLayout_ranges.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout_options.addLayout(self.horizontalLayout_ranges) + + # 提取单页 + self.radioButton_single_page = QRadioButton(self.groupBox_split_options) + self.radioButton_single_page.setObjectName(u"radioButton_single_page") + self.verticalLayout_options.addWidget(self.radioButton_single_page) + + self.horizontalLayout_single = QHBoxLayout() + self.horizontalLayout_single.setObjectName(u"horizontalLayout_single") + self.label_single = QLabel(self.groupBox_split_options) + self.label_single.setObjectName(u"label_single") + self.horizontalLayout_single.addWidget(self.label_single) + + self.spinBox_single = QSpinBox(self.groupBox_split_options) + self.spinBox_single.setObjectName(u"spinBox_single") + self.spinBox_single.setMinimum(1) + self.spinBox_single.setValue(1) + self.spinBox_single.setEnabled(False) + self.spinBox_single.setMinimumWidth(80) + self.spinBox_single.setMaximumWidth(120) + font = self.spinBox_single.font() + font.setPointSize(10) + self.spinBox_single.setFont(font) + self.horizontalLayout_single.addWidget(self.spinBox_single) + + self.horizontalLayout_single.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout_options.addLayout(self.horizontalLayout_single) + + # 拆分为单页 + self.radioButton_all_pages = QRadioButton(self.groupBox_split_options) + self.radioButton_all_pages.setObjectName(u"radioButton_all_pages") + self.verticalLayout_options.addWidget(self.radioButton_all_pages) + + self.verticalLayout.addWidget(self.groupBox_split_options) + + # 输出选项区域 + self.groupBox_output = QGroupBox(split_pdf_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 = QLabel(self.groupBox_output) + self.label_output.setObjectName(u"label_output") + self.horizontalLayout_output_dir.addWidget(self.label_output) + + self.comboBox_output_dir = QComboBox(self.groupBox_output) + self.comboBox_output_dir.addItem("PDF相同目录") + self.comboBox_output_dir.addItem("自定义目录") + self.comboBox_output_dir.setObjectName(u"comboBox_output_dir") + self.horizontalLayout_output_dir.addWidget(self.comboBox_output_dir) + + 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_prefix = QHBoxLayout() + self.horizontalLayout_prefix.setObjectName(u"horizontalLayout_prefix") + self.label_prefix = QLabel(self.groupBox_output) + self.label_prefix.setObjectName(u"label_prefix") + self.horizontalLayout_prefix.addWidget(self.label_prefix) + + self.lineEdit_prefix = QLineEdit(self.groupBox_output) + self.lineEdit_prefix.setObjectName(u"lineEdit_prefix") + self.horizontalLayout_prefix.addWidget(self.lineEdit_prefix) + + self.verticalLayout_output.addLayout(self.horizontalLayout_prefix) + + self.verticalLayout.addWidget(self.groupBox_output) + + # 进度条 + self.progressBar = QProgressBar(split_pdf_view) + self.progressBar.setObjectName(u"progressBar") + self.progressBar.setValue(0) + self.verticalLayout.addWidget(self.progressBar) + + # 拆分按钮 + self.horizontalLayout_split = QHBoxLayout() + self.horizontalLayout_split.setObjectName(u"horizontalLayout_split") + self.horizontalLayout_split.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + + self.btn_split = QPushButton(split_pdf_view) + self.btn_split.setObjectName(u"btn_split") + self.btn_split.setMinimumSize(QSize(120, 40)) + self.horizontalLayout_split.addWidget(self.btn_split) + + self.horizontalLayout_split.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.verticalLayout.addLayout(self.horizontalLayout_split) + + self.retranslateUi(split_pdf_view) + + QMetaObject.connectSlotsByName(split_pdf_view) + + def retranslateUi(self, split_pdf_view): + split_pdf_view.setWindowTitle(QCoreApplication.translate("split_pdf_view", u"PDF拆分", None)) + self.label.setText(QCoreApplication.translate("split_pdf_view", u"选择PDF文件:", None)) + self.btn_choose_file.setText(QCoreApplication.translate("split_pdf_view", u"选择文件", None)) + + self.groupBox_split_options.setTitle(QCoreApplication.translate("split_pdf_view", u"拆分选项", None)) + self.radioButton_by_pages.setText(QCoreApplication.translate("split_pdf_view", u"按页数拆分", None)) + self.label_pages.setText(QCoreApplication.translate("split_pdf_view", u"每个文件页数:", None)) + + self.radioButton_by_ranges.setText(QCoreApplication.translate("split_pdf_view", u"按页码范围拆分", None)) + self.label_ranges.setText(QCoreApplication.translate("split_pdf_view", u"页码范围:", None)) + self.lineEdit_ranges.setPlaceholderText(QCoreApplication.translate("split_pdf_view", u"例如: 1-5,6-10,11-15", None)) + + self.radioButton_single_page.setText(QCoreApplication.translate("split_pdf_view", u"提取单页", None)) + self.label_single.setText(QCoreApplication.translate("split_pdf_view", u"页码:", None)) + + self.radioButton_all_pages.setText(QCoreApplication.translate("split_pdf_view", u"拆分为单页文件", None)) + + self.groupBox_output.setTitle(QCoreApplication.translate("split_pdf_view", u"输出选项", None)) + self.label_output.setText(QCoreApplication.translate("split_pdf_view", u"输出目录:", None)) + self.label_output_dir.setText(QCoreApplication.translate("split_pdf_view", u"", None)) + self.btn_choose_output_dir.setText(QCoreApplication.translate("split_pdf_view", u"选择目录", None)) + + self.label_prefix.setText(QCoreApplication.translate("split_pdf_view", u"文件名:", None)) + self.lineEdit_prefix.setText(QCoreApplication.translate("split_pdf_view", u"拆分文件", None)) + + self.btn_split.setText(QCoreApplication.translate("split_pdf_view", u"拆分PDF", None)) \ No newline at end of file diff --git a/app/ui/setting/__pycache__/about_dialog.cpython-313.pyc b/app/ui/setting/__pycache__/about_dialog.cpython-313.pyc new file mode 100644 index 0000000..15cb72e Binary files /dev/null and b/app/ui/setting/__pycache__/about_dialog.cpython-313.pyc differ diff --git a/app/ui/setting/__pycache__/seetingUi.cpython-313.pyc b/app/ui/setting/__pycache__/seetingUi.cpython-313.pyc new file mode 100644 index 0000000..047889e Binary files /dev/null and b/app/ui/setting/__pycache__/seetingUi.cpython-313.pyc differ diff --git a/app/ui/setting/__pycache__/setting.cpython-313.pyc b/app/ui/setting/__pycache__/setting.cpython-313.pyc new file mode 100644 index 0000000..9aea1ef Binary files /dev/null and b/app/ui/setting/__pycache__/setting.cpython-313.pyc differ diff --git a/app/ui/video_tools/__pycache__/video_tool.cpython-313.pyc b/app/ui/video_tools/__pycache__/video_tool.cpython-313.pyc new file mode 100644 index 0000000..7aac7a3 Binary files /dev/null and b/app/ui/video_tools/__pycache__/video_tool.cpython-313.pyc differ diff --git a/app/ui/video_tools/__pycache__/video_tool_ui.cpython-313.pyc b/app/ui/video_tools/__pycache__/video_tool_ui.cpython-313.pyc new file mode 100644 index 0000000..7eb441d Binary files /dev/null and b/app/ui/video_tools/__pycache__/video_tool_ui.cpython-313.pyc differ diff --git a/app/util/__pycache__/__init__.cpython-313.pyc b/app/util/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4c7248a Binary files /dev/null and b/app/util/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/util/__pycache__/common.cpython-313.pyc b/app/util/__pycache__/common.cpython-313.pyc new file mode 100644 index 0000000..35d59e1 Binary files /dev/null and b/app/util/__pycache__/common.cpython-313.pyc differ diff --git a/pdf2docx/__pycache__/__init__.cpython-313.pyc b/pdf2docx/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b87202c Binary files /dev/null and b/pdf2docx/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/__pycache__/converter.cpython-313.pyc b/pdf2docx/__pycache__/converter.cpython-313.pyc new file mode 100644 index 0000000..6d330b9 Binary files /dev/null and b/pdf2docx/__pycache__/converter.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/Block.cpython-313.pyc b/pdf2docx/common/__pycache__/Block.cpython-313.pyc new file mode 100644 index 0000000..ba02e1b Binary files /dev/null and b/pdf2docx/common/__pycache__/Block.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/Collection.cpython-313.pyc b/pdf2docx/common/__pycache__/Collection.cpython-313.pyc new file mode 100644 index 0000000..de4f6a3 Binary files /dev/null and b/pdf2docx/common/__pycache__/Collection.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/Element.cpython-313.pyc b/pdf2docx/common/__pycache__/Element.cpython-313.pyc new file mode 100644 index 0000000..6b235fb Binary files /dev/null and b/pdf2docx/common/__pycache__/Element.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/__init__.cpython-313.pyc b/pdf2docx/common/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..be17455 Binary files /dev/null and b/pdf2docx/common/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/algorithm.cpython-313.pyc b/pdf2docx/common/__pycache__/algorithm.cpython-313.pyc new file mode 100644 index 0000000..18abfd7 Binary files /dev/null and b/pdf2docx/common/__pycache__/algorithm.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/constants.cpython-313.pyc b/pdf2docx/common/__pycache__/constants.cpython-313.pyc new file mode 100644 index 0000000..28b6f82 Binary files /dev/null and b/pdf2docx/common/__pycache__/constants.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/docx.cpython-313.pyc b/pdf2docx/common/__pycache__/docx.cpython-313.pyc new file mode 100644 index 0000000..d5712a2 Binary files /dev/null and b/pdf2docx/common/__pycache__/docx.cpython-313.pyc differ diff --git a/pdf2docx/common/__pycache__/share.cpython-313.pyc b/pdf2docx/common/__pycache__/share.cpython-313.pyc new file mode 100644 index 0000000..5404d62 Binary files /dev/null and b/pdf2docx/common/__pycache__/share.cpython-313.pyc differ diff --git a/pdf2docx/font/__pycache__/Fonts.cpython-313.pyc b/pdf2docx/font/__pycache__/Fonts.cpython-313.pyc new file mode 100644 index 0000000..6d00301 Binary files /dev/null and b/pdf2docx/font/__pycache__/Fonts.cpython-313.pyc differ diff --git a/pdf2docx/font/__pycache__/__init__.cpython-313.pyc b/pdf2docx/font/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..db1f7db Binary files /dev/null and b/pdf2docx/font/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/image/__pycache__/Image.cpython-313.pyc b/pdf2docx/image/__pycache__/Image.cpython-313.pyc new file mode 100644 index 0000000..8f57079 Binary files /dev/null and b/pdf2docx/image/__pycache__/Image.cpython-313.pyc differ diff --git a/pdf2docx/image/__pycache__/ImageBlock.cpython-313.pyc b/pdf2docx/image/__pycache__/ImageBlock.cpython-313.pyc new file mode 100644 index 0000000..dbcb57d Binary files /dev/null and b/pdf2docx/image/__pycache__/ImageBlock.cpython-313.pyc differ diff --git a/pdf2docx/image/__pycache__/ImageSpan.cpython-313.pyc b/pdf2docx/image/__pycache__/ImageSpan.cpython-313.pyc new file mode 100644 index 0000000..ee1bb20 Binary files /dev/null and b/pdf2docx/image/__pycache__/ImageSpan.cpython-313.pyc differ diff --git a/pdf2docx/image/__pycache__/ImagesExtractor.cpython-313.pyc b/pdf2docx/image/__pycache__/ImagesExtractor.cpython-313.pyc new file mode 100644 index 0000000..da88325 Binary files /dev/null and b/pdf2docx/image/__pycache__/ImagesExtractor.cpython-313.pyc differ diff --git a/pdf2docx/image/__pycache__/__init__.cpython-313.pyc b/pdf2docx/image/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d1df5b4 Binary files /dev/null and b/pdf2docx/image/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/Blocks.cpython-313.pyc b/pdf2docx/layout/__pycache__/Blocks.cpython-313.pyc new file mode 100644 index 0000000..30c01c3 Binary files /dev/null and b/pdf2docx/layout/__pycache__/Blocks.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/Column.cpython-313.pyc b/pdf2docx/layout/__pycache__/Column.cpython-313.pyc new file mode 100644 index 0000000..d246806 Binary files /dev/null and b/pdf2docx/layout/__pycache__/Column.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/Layout.cpython-313.pyc b/pdf2docx/layout/__pycache__/Layout.cpython-313.pyc new file mode 100644 index 0000000..724b26f Binary files /dev/null and b/pdf2docx/layout/__pycache__/Layout.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/Section.cpython-313.pyc b/pdf2docx/layout/__pycache__/Section.cpython-313.pyc new file mode 100644 index 0000000..ce008de Binary files /dev/null and b/pdf2docx/layout/__pycache__/Section.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/Sections.cpython-313.pyc b/pdf2docx/layout/__pycache__/Sections.cpython-313.pyc new file mode 100644 index 0000000..b994865 Binary files /dev/null and b/pdf2docx/layout/__pycache__/Sections.cpython-313.pyc differ diff --git a/pdf2docx/layout/__pycache__/__init__.cpython-313.pyc b/pdf2docx/layout/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..46b8bd2 Binary files /dev/null and b/pdf2docx/layout/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/BasePage.cpython-313.pyc b/pdf2docx/page/__pycache__/BasePage.cpython-313.pyc new file mode 100644 index 0000000..8c647c6 Binary files /dev/null and b/pdf2docx/page/__pycache__/BasePage.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/Page.cpython-313.pyc b/pdf2docx/page/__pycache__/Page.cpython-313.pyc new file mode 100644 index 0000000..4f72a0f Binary files /dev/null and b/pdf2docx/page/__pycache__/Page.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/Pages.cpython-313.pyc b/pdf2docx/page/__pycache__/Pages.cpython-313.pyc new file mode 100644 index 0000000..76278ac Binary files /dev/null and b/pdf2docx/page/__pycache__/Pages.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/RawPage.cpython-313.pyc b/pdf2docx/page/__pycache__/RawPage.cpython-313.pyc new file mode 100644 index 0000000..59c2d3d Binary files /dev/null and b/pdf2docx/page/__pycache__/RawPage.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/RawPageFactory.cpython-313.pyc b/pdf2docx/page/__pycache__/RawPageFactory.cpython-313.pyc new file mode 100644 index 0000000..bcfab32 Binary files /dev/null and b/pdf2docx/page/__pycache__/RawPageFactory.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/RawPageFitz.cpython-313.pyc b/pdf2docx/page/__pycache__/RawPageFitz.cpython-313.pyc new file mode 100644 index 0000000..b1a9d98 Binary files /dev/null and b/pdf2docx/page/__pycache__/RawPageFitz.cpython-313.pyc differ diff --git a/pdf2docx/page/__pycache__/__init__.cpython-313.pyc b/pdf2docx/page/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b9b9fe8 Binary files /dev/null and b/pdf2docx/page/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/shape/__pycache__/Path.cpython-313.pyc b/pdf2docx/shape/__pycache__/Path.cpython-313.pyc new file mode 100644 index 0000000..d294caa Binary files /dev/null and b/pdf2docx/shape/__pycache__/Path.cpython-313.pyc differ diff --git a/pdf2docx/shape/__pycache__/Paths.cpython-313.pyc b/pdf2docx/shape/__pycache__/Paths.cpython-313.pyc new file mode 100644 index 0000000..14a92c3 Binary files /dev/null and b/pdf2docx/shape/__pycache__/Paths.cpython-313.pyc differ diff --git a/pdf2docx/shape/__pycache__/Shape.cpython-313.pyc b/pdf2docx/shape/__pycache__/Shape.cpython-313.pyc new file mode 100644 index 0000000..91139a2 Binary files /dev/null and b/pdf2docx/shape/__pycache__/Shape.cpython-313.pyc differ diff --git a/pdf2docx/shape/__pycache__/Shapes.cpython-313.pyc b/pdf2docx/shape/__pycache__/Shapes.cpython-313.pyc new file mode 100644 index 0000000..b0c9544 Binary files /dev/null and b/pdf2docx/shape/__pycache__/Shapes.cpython-313.pyc differ diff --git a/pdf2docx/shape/__pycache__/__init__.cpython-313.pyc b/pdf2docx/shape/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..673046b Binary files /dev/null and b/pdf2docx/shape/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/Cell.cpython-313.pyc b/pdf2docx/table/__pycache__/Cell.cpython-313.pyc new file mode 100644 index 0000000..374e79f Binary files /dev/null and b/pdf2docx/table/__pycache__/Cell.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/Cells.cpython-313.pyc b/pdf2docx/table/__pycache__/Cells.cpython-313.pyc new file mode 100644 index 0000000..7ce3e1f Binary files /dev/null and b/pdf2docx/table/__pycache__/Cells.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/Row.cpython-313.pyc b/pdf2docx/table/__pycache__/Row.cpython-313.pyc new file mode 100644 index 0000000..7e0baf7 Binary files /dev/null and b/pdf2docx/table/__pycache__/Row.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/Rows.cpython-313.pyc b/pdf2docx/table/__pycache__/Rows.cpython-313.pyc new file mode 100644 index 0000000..bfaaec1 Binary files /dev/null and b/pdf2docx/table/__pycache__/Rows.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/TableBlock.cpython-313.pyc b/pdf2docx/table/__pycache__/TableBlock.cpython-313.pyc new file mode 100644 index 0000000..338ef05 Binary files /dev/null and b/pdf2docx/table/__pycache__/TableBlock.cpython-313.pyc differ diff --git a/pdf2docx/table/__pycache__/__init__.cpython-313.pyc b/pdf2docx/table/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..25020ea Binary files /dev/null and b/pdf2docx/table/__pycache__/__init__.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/Char.cpython-313.pyc b/pdf2docx/text/__pycache__/Char.cpython-313.pyc new file mode 100644 index 0000000..2bbd2a9 Binary files /dev/null and b/pdf2docx/text/__pycache__/Char.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/Line.cpython-313.pyc b/pdf2docx/text/__pycache__/Line.cpython-313.pyc new file mode 100644 index 0000000..8ce6449 Binary files /dev/null and b/pdf2docx/text/__pycache__/Line.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/Lines.cpython-313.pyc b/pdf2docx/text/__pycache__/Lines.cpython-313.pyc new file mode 100644 index 0000000..5df26b3 Binary files /dev/null and b/pdf2docx/text/__pycache__/Lines.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/Spans.cpython-313.pyc b/pdf2docx/text/__pycache__/Spans.cpython-313.pyc new file mode 100644 index 0000000..228f94c Binary files /dev/null and b/pdf2docx/text/__pycache__/Spans.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/TextBlock.cpython-313.pyc b/pdf2docx/text/__pycache__/TextBlock.cpython-313.pyc new file mode 100644 index 0000000..46fbb1c Binary files /dev/null and b/pdf2docx/text/__pycache__/TextBlock.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/TextSpan.cpython-313.pyc b/pdf2docx/text/__pycache__/TextSpan.cpython-313.pyc new file mode 100644 index 0000000..3492b8e Binary files /dev/null and b/pdf2docx/text/__pycache__/TextSpan.cpython-313.pyc differ diff --git a/pdf2docx/text/__pycache__/__init__.cpython-313.pyc b/pdf2docx/text/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6dcf32d Binary files /dev/null and b/pdf2docx/text/__pycache__/__init__.cpython-313.pyc differ diff --git a/readme.md b/readme.md index 6e95f21..214f247 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,9 @@ * PDF工具箱 * 合并PDF✅ + * 拆分PDF✅ + * PDF加密✅ + * PDF解密✅ * 文档转换 * PDF转Word * 网页转PDF @@ -24,11 +27,25 @@ ![img_1.png](doc/images/img_1.png) ![img_3.png](doc/images/img_3.png) +## PDF工具箱功能说明 + +### PDF合并 +将多个PDF文件合并为一个文件,支持设置文件顺序和页面范围,可以保留原PDF的书签。 + +### PDF拆分 +支持多种拆分方式:按页数拆分、按页码范围拆分、提取单页、拆分为单页文件。输出文件格式为"原文件名_拆分文件_页码.pdf"。 + +### PDF加密 +为PDF文件添加密码保护,支持设置用户密码(打开文档)和所有者密码(编辑文档),可自定义权限控制(打印、复制、修改等),支持多种加密方式(AES-128, RC4-128, RC4-40)。 + +### PDF解密 +提供多种解密方式: +- 常规解密:使用用户提供的密码解密 +- 密码破解:使用John the Ripper自动破解PDF密码(需额外安装),支持字典攻击和暴力破解 + ## 计划中功能 * PDF工具箱 - * 拆分PDF - * PDF加密 * 添加水印 * 去除水印 * 文档转换 diff --git a/requirements.txt b/requirements.txt index f4bf6b0..18aa050 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,6 @@ numpy~=2.1.3 opencv-python~=4.10.0.84 fonttools~=4.54.1 setuptools~=75.3.0 -Cython~=3.0.11 \ No newline at end of file +Cython~=3.0.11 +PyPDF2~=3.0.1 +pdf2john~=0.2.0