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系统:
"
+ ""
+ "- Ubuntu/Debian:
sudo apt-get install john "
+ "- Fedora:
sudo dnf install john "
+ "- Arch Linux:
sudo pacman -S john "
+ "
"
+ "3. macOS系统:
"
+ ""
+ "- 使用Homebrew:
brew install john "
+ "
"
+ "验证安装:
"
+ ""
+ "- 打开命令行或终端
"
+ "- 输入
john命令 "
+ "- 如果显示包含\"John the Ripper\"的信息,则安装成功
"
+ "
"
+ "注意事项:
"
+ ""
+ "- 命令安装:pip install pdf2john"
+ "
"
+ )
+
+ 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 @@


+## 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