24 分钟阅读

基于python,PDF工具箱不同开发阶段的代码实现

需求导向型,结果导向型。工具,是服务于最终用户的需求的。

python安装:

去python官网,下载并安装python可执行程序。

依赖安装:

pip install PyQt5 PyMuPDF PyPDF2 pikepdf Pillow

各阶段实现代码:

1.PDF解密工具开发代码

import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QPushButton, QLineEdit, QFileDialog, QMessageBox, QListWidget, 
                             QListWidgetItem, QAbstractItemView, QComboBox)
from PyQt5.QtCore import Qt, QMimeData
from PyPDF2 import PdfReader, PdfWriter, PdfMerger
from pikepdf import Pdf, Encryption, Permissions


class DragDropLineEdit(QLineEdit):
    """支持PDF拖放的自定义输入框(基于网页1/2实现)"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
                    event.acceptProposedAction()
                    return
        event.ignore()

    def dropEvent(self, event):
        for url in event.mimeData().urls():
            file_path = url.toLocalFile()
            if file_path.lower().endswith('.pdf'):
                self.setText(file_path)
                break

class PDFToolsUI(QMainWindow):
    """PDF全能处理工具主界面"""
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PDF解密小工具 v1.0")
        self.setGeometry(300, 300, 800, 500)
        self.init_ui()

    def init_ui(self):
        """界面初始化"""
        tabs = QTabWidget()
        self.setCentralWidget(tabs)

        # 解密模块
        decrypt_tab = QWidget()
        self.decrypt_ui(decrypt_tab)
        tabs.addTab(decrypt_tab, "密码解除")

        # 加密模块
        encrypt_tab = QWidget()
        self.encrypt_ui(encrypt_tab)
        tabs.addTab(encrypt_tab, "文件加密")

        # 合并模块
        merge_tab = QWidget()
        self.merge_ui(merge_tab)
        tabs.addTab(merge_tab, "文件合并")

    def decrypt_ui(self, tab):
        """密码解除界面"""
        layout = QVBoxLayout()

        # 文件选择(支持拖放)
        file_layout = QHBoxLayout()
        self.decrypt_input = DragDropLineEdit()
        btn_choose = QPushButton("选择文件")
        btn_choose.clicked.connect(lambda: self.choose_file(self.decrypt_input))

        file_layout.addWidget(QLabel("输入文件:"))
        file_layout.addWidget(self.decrypt_input)
        file_layout.addWidget(btn_choose)

        # 密码输入
        self.decrypt_pass = QLineEdit()
        self.decrypt_pass.setEchoMode(QLineEdit.Password)

        # 操作按钮
        btn_decrypt = QPushButton("开始解密")
        btn_decrypt.clicked.connect(self.handle_decrypt)

        layout.addLayout(file_layout)
        layout.addWidget(QLabel("输入密码:"))
        layout.addWidget(self.decrypt_pass)
        layout.addWidget(btn_decrypt)
        tab.setLayout(layout)

    def encrypt_ui(self, tab):
        """文件加密界面"""
        layout = QVBoxLayout()

        # 输入文件选择(支持拖放)
        input_layout = QHBoxLayout()
        self.encrypt_input = DragDropLineEdit()
        btn_input = QPushButton("选择文件")
        btn_input.clicked.connect(lambda: self.choose_file(self.encrypt_input))
        input_layout.addWidget(QLabel("输入文件:"))
        input_layout.addWidget(self.encrypt_input)
        input_layout.addWidget(btn_input)

        # 输出路径选择
        output_layout = QHBoxLayout()
        self.encrypt_output = QLineEdit()
        btn_output = QPushButton("另存为")
        btn_output.clicked.connect(self.choose_output)
        output_layout.addWidget(QLabel("输出路径:"))
        output_layout.addWidget(self.encrypt_output)
        output_layout.addWidget(btn_output)

        # 加密参数
        param_layout = QHBoxLayout()
        self.encrypt_pass = QLineEdit()
        self.encrypt_pass.setEchoMode(QLineEdit.Password)
        self.encrypt_algo = QComboBox()
        self.encrypt_algo.addItems(["AES-128", "AES-256"])

        param_layout.addWidget(QLabel("加密密码:"))
        param_layout.addWidget(self.encrypt_pass)
        param_layout.addWidget(QLabel("算法:"))
        param_layout.addWidget(self.encrypt_algo)

        # 操作按钮
        btn_encrypt = QPushButton("开始加密")
        btn_encrypt.clicked.connect(self.handle_encrypt)

        layout.addLayout(input_layout)
        layout.addLayout(output_layout)
        layout.addLayout(param_layout)
        layout.addWidget(btn_encrypt)
        tab.setLayout(layout)

    def merge_ui(self, tab):
        """文件合并界面(支持拖放添加文件)"""
        layout = QVBoxLayout()

        # 文件列表(基于网页3实现拖放)
        self.merge_list = QListWidget()
        self.merge_list.setDragDropMode(QAbstractItemView.InternalMove)
        self.merge_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.merge_list.setAcceptDrops(True)
        self.merge_list.dragEnterEvent = self.merge_drag_enter
        self.merge_list.dropEvent = self.merge_drop

        # 操作按钮
        btn_layout = QHBoxLayout()
        btn_add = QPushButton("添加文件")
        btn_add.clicked.connect(self.add_merge_files)
        btn_remove = QPushButton("移除选中")
        btn_remove.clicked.connect(lambda: self.remove_items(self.merge_list))
        btn_up = QPushButton("上移")
        btn_up.clicked.connect(lambda: self.move_item(self.merge_list, -1))
        btn_down = QPushButton("下移")
        btn_down.clicked.connect(lambda: self.move_item(self.merge_list, 1))

        btn_layout.addWidget(btn_add)
        btn_layout.addWidget(btn_remove)
        btn_layout.addWidget(btn_up)
        btn_layout.addWidget(btn_down)

        # 输出路径
        output_layout = QHBoxLayout()
        self.merge_output = QLineEdit()
        btn_output = QPushButton("选择路径")
        btn_output.clicked.connect(self.choose_merge_output)
        output_layout.addWidget(QLabel("输出文件:"))
        output_layout.addWidget(self.merge_output)
        output_layout.addWidget(btn_output)

        # 合并按钮
        btn_merge = QPushButton("开始合并")
        btn_merge.clicked.connect(self.handle_merge)

        layout.addWidget(self.merge_list)
        layout.addLayout(btn_layout)
        layout.addLayout(output_layout)
        layout.addWidget(btn_merge)
        tab.setLayout(layout)

    # 核心功能方法(确保所有槽函数存在)
    def handle_decrypt(self):
        """解密处理(网页2验证)"""
        input_path = self.decrypt_input.text()
        password = self.decrypt_pass.text()

        if not os.path.exists(input_path):
            QMessageBox.critical(self, "错误", "文件不存在!")
            return

        output_path = f"{os.path.splitext(input_path)[0]}_unlocked.pdf"

        try:
            with Pdf.open(input_path, password=password) as pdf:
                pdf.save(output_path)
            QMessageBox.information(self, "成功", f"文件已保存至:\n{output_path}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"解密失败:\n{str(e)}")

    def handle_encrypt(self):
        """加密处理"""
        input_path = self.encrypt_input.text()
        output_path = self.encrypt_output.text()
        password = self.encrypt_pass.text()

        # 检查输入文件是否存在
        if not os.path.exists(input_path):
            QMessageBox.critical(self, "错误", f"输入文件不存在: {input_path}")
            return

        if not all([input_path, output_path, password]):
            QMessageBox.critical(self, "错误", "请填写所有字段!")
            return

        try:
            # 使用PyPDF2进行加密
            reader = PdfReader(input_path)
            writer = PdfWriter()

            # 复制所有页面
            for page in reader.pages:
                writer.add_page(page)

            # 设置加密
            writer.encrypt(password)

            # 确保输出目录存在
            output_dir = os.path.dirname(output_path)
            if output_dir and not os.path.exists(output_dir):
                os.makedirs(output_dir)

            # 保存加密后的文件
            with open(output_path, "wb") as f:
                writer.write(f)

            QMessageBox.information(self, "成功", "文件加密完成!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加密失败:\n{str(e)}")





    def handle_merge(self):
        """合并处理"""
        output_path = self.merge_output.text()
        if not output_path:
            QMessageBox.critical(self, "错误", "请选择输出路径!")
            return

        merger = PdfMerger()
        try:
            for i in range(self.merge_list.count()):
                file_path = self.merge_list.item(i).text()
                merger.append(file_path)

            merger.write(output_path)
            QMessageBox.information(self, "成功", "文件合并完成!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"合并失败:\n{str(e)}")
        finally:
            merger.close()

    # 通用工具方法
    def choose_file(self, target_field):
        """文件选择对话框"""
        path, _ = QFileDialog.getOpenFileName(
            self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        if path:
            target_field.setText(path)

    def choose_output(self):
        """加密输出路径选择"""
        path, _ = QFileDialog.getSaveFileName(
            self, "保存文件", "", "PDF文件 (*.pdf)")
        if path:
            self.encrypt_output.setText(path)

    def choose_merge_output(self):
        """合并输出路径选择"""
        path, _ = QFileDialog.getSaveFileName(
            self, "保存合并文件", "", "PDF文件 (*.pdf)")
        if path:
            self.merge_output.setText(path)

    def add_merge_files(self):
        """添加合并文件"""
        files, _ = QFileDialog.getOpenFileNames(
            self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        if files:
            for f in files:
                item = QListWidgetItem(f)
                self.merge_list.addItem(item)

    def remove_items(self, list_widget):
        """移除选中项"""
        for item in list_widget.selectedItems():
            list_widget.takeItem(list_widget.row(item))

    def move_item(self, list_widget, direction):
        """调整顺序"""
        current_row = list_widget.currentRow()
        if current_row >= 0:
            new_row = current_row + direction
            if 0 <= new_row < list_widget.count():
                item = list_widget.takeItem(current_row)
                list_widget.insertItem(new_row, item)
                list_widget.setCurrentRow(new_row)

    # 合并列表拖放事件(基于网页3实现)
    def merge_drag_enter(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
                    event.acceptProposedAction()
                    return
        event.ignore()

    def merge_drop(self, event):
        for url in event.mimeData().urls():
            file_path = url.toLocalFile()
            if file_path.lower().endswith('.pdf'):
                self.merge_list.addItem(QListWidgetItem(file_path))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = PDFToolsUI()
    window.show()
    sys.exit(app.exec_())

2.PDF提取及导出为图片开发代码

import sys
import os
import fitz
from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox,
                             QLabel, QLineEdit, QComboBox, QRadioButton, QPushButton,
                             QHBoxLayout, QVBoxLayout, QGroupBox, QWidget)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtGui import QDragEnterEvent, QDropEvent

class WorkerThread(QThread):
    finished = pyqtSignal(bool, str)
    error = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.params = {
            'dpi': 300,
            'alpha': False,
            'colorspace': fitz.csRGB,
            'output_dir': ''  # 添加默认的输出目录
        }

    def set_params(self, file_path, output_dir, pages, export_pdf, img_format, dpi):
        self.params.update({
            'file_path': file_path,
            'output_dir': output_dir,
            'pages': self._parse_pages(pages),
            'export_pdf': export_pdf,
            'img_format': img_format.lower(),
            'dpi': dpi,
            'alpha': (img_format.lower() == 'png')
        })

    def _parse_pages(self, pages_str):
        pages = []
        for part in pages_str.replace(',', ',').split(','):
            part = part.strip()
            if not part:
                continue
            if '-' in part:
                start_end = part.split('-')
                if len(start_end) != 2:
                    raise ValueError("无效的页码范围格式")
                start = int(start_end[0])
                end = int(start_end[1])
                pages.extend(range(start, end+1))
            else:
                pages.append(int(part))
        return sorted(set(pages))

    def run(self):
        try:
            doc = fitz.open(self.params['file_path'])
            total_pages = doc.page_count

            valid_pages = [p-1 for p in self.params['pages'] if 0 < p <= total_pages]
            if not valid_pages:
                raise ValueError("所有页码均超出文档范围")

            if self.params['export_pdf']:
                self._export_pdf(doc, valid_pages)
            else:
                self._export_images(doc, valid_pages)

            doc.close()
            self.finished.emit(True, self.params['output_dir'])

        except Exception as e:
            self.error.emit(str(e))

    def _export_pdf(self, doc, pages):
        new_doc = fitz.open()
        for p in pages:
            new_doc.insert_pdf(doc, from_page=p, to_page=p)
        output_path = os.path.join(self.params['output_dir'], 'extracted.pdf')
        new_doc.save(output_path, garbage=3, deflate=True)
        new_doc.close()

    def _export_images(self, doc, pages):
        for idx, p in enumerate(pages):
            page = doc[p]
            # 修正抗锯齿参数名称
            pix = page.get_pixmap(
                dpi=self.params['dpi'],
                alpha=self.params['alpha'],
                colorspace=self.params['colorspace'],
                annots=True,
                # anti_aliasing=True  # 参数名改为下划线形式
            )
            output_path = os.path.join(
                self.params['output_dir'],
                f'page_{p+1}_dpi{self.params["dpi"]}.{self.params["img_format"]}'
            )
            if self.params['img_format'] == 'jpg':
                pix.save(output_path, jpg_quality=95)
            else:
                pix.save(output_path)

class PdfToolApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.worker = WorkerThread()
        self._connect_signals()
        self.setAcceptDrops(True)

    def init_ui(self):
        self.setWindowTitle('PDF文档提取及导出图片小工具 v1.0')
        self.setGeometry(300, 300, 600, 320)

        # 文件选择组
        file_group = QGroupBox("文件操作")
        self.txt_file = QLineEdit()
        self.txt_file.setPlaceholderText("拖拽PDF文件到此或点击浏览...")
        btn_browse = QPushButton("浏览")
        btn_browse.clicked.connect(self._browse_file)

        # 输出设置组
        output_group = QGroupBox("输出设置")
        self.txt_output = QLineEdit()
        btn_output = QPushButton("选择目录")
        btn_output.clicked.connect(self._browse_dir)
        self.rad_pdf = QRadioButton("导出为PDF")
        self.rad_img = QRadioButton("导出为图片")
        self.rad_pdf.setChecked(True)
        self.cmb_format = QComboBox()
        self.cmb_format.addItems(['PNG', 'JPG'])
        self.cmb_dpi = QComboBox()
        self.cmb_dpi.addItems(['150 (网页)', '300 (印刷)', '600 (高清)'])
        self.txt_pages = QLineEdit()
        self.txt_pages.setPlaceholderText("输入页码范围(如:1-3 或 1,3,5)")

        # 布局
        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("PDF文件:"))
        file_layout.addWidget(self.txt_file)
        file_layout.addWidget(btn_browse)
        file_group.setLayout(file_layout)

        output_layout = QVBoxLayout()
        output_layout.addWidget(QLabel("输出目录:"))
        output_layout.addWidget(self.txt_output)
        output_layout.addWidget(btn_output)

        pages_layout = QHBoxLayout()
        pages_layout.addWidget(QLabel("页码范围:"))
        pages_layout.addWidget(self.txt_pages)

        format_layout = QHBoxLayout()
        format_layout.addWidget(self.rad_pdf)
        format_layout.addWidget(self.rad_img)
        format_layout.addWidget(self.cmb_format)
        format_layout.addWidget(QLabel("DPI:"))
        format_layout.addWidget(self.cmb_dpi)

        output_group.setLayout(output_layout)
        output_layout.addLayout(pages_layout)
        output_layout.addLayout(format_layout)

        # 主布局
        main_layout = QVBoxLayout()
        main_layout.addWidget(file_group)
        main_layout.addWidget(output_group)

        central_widget = QWidget()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

    def _connect_signals(self):
        # 确保初始状态下,如果选择了PDF导出,则禁用格式选择
        self.cmb_format.setEnabled(self.rad_img.isChecked())

        self.rad_pdf.toggled.connect(lambda: self.cmb_format.setEnabled(False))
        self.rad_img.toggled.connect(lambda: self.cmb_format.setEnabled(True))

        btn_execute = QPushButton("开始转换", self)
        btn_execute.clicked.connect(self._start_conversion)
        self.centralWidget().layout().addWidget(btn_execute)

        self.worker.finished.connect(self._on_finished)
        self.worker.error.connect(self._show_error)

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event: QDropEvent):
        for url in event.mimeData().urls():
            if url.isLocalFile() and url.fileName().lower().endswith('.pdf'):
                # 使用 os.path.normpath 统一路径分隔符
                self.txt_file.setText(os.path.normpath(url.toLocalFile()))
                break

    def _browse_file(self):
        path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF Files (*.pdf)")
        if path:
            self.txt_file.setText(path)

    def _browse_dir(self):
        path = QFileDialog.getExistingDirectory(self, "选择输出目录")
        if path:
            self.txt_output.setText(path)

    def _start_conversion(self):
        params = {
            'file_path': self.txt_file.text(),
            'output_dir': self.txt_output.text(),
            'pages': self.txt_pages.text() or "1-1",
            'export_pdf': self.rad_pdf.isChecked(),
            'img_format': self.cmb_format.currentText(),
            'dpi': int(self.cmb_dpi.currentText().split()[0])
        }

        if not os.path.exists(params['file_path']):
            QMessageBox.warning(self, "错误", "PDF文件路径无效")
            return

        if not os.path.exists(params['output_dir']):
            os.makedirs(params['output_dir'], exist_ok=True)

        try:
            _ = self.worker._parse_pages(params['pages'])
        except ValueError as e:
            QMessageBox.critical(self, "输入错误", f"页码格式错误:{str(e)}")
            return

        self.worker.set_params(**params)
        self.worker.start()

    def _on_finished(self, success, output_dir):
        QMessageBox.information(self, "完成", f"文件已保存至:\n{output_dir}")

    def _show_error(self, msg):
        QMessageBox.critical(self, "错误", f"处理失败:{msg}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = PdfToolApp()
    window.show()
    sys.exit(app.exec_())

3.图片合成PDF开发代码

import sys
import os
import re
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageOps
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                            QHBoxLayout, QPushButton, QFileDialog, QListWidget,
                            QLabel, QProgressDialog, QGroupBox, QCheckBox,
                            QSlider, QComboBox, QMessageBox, QSplitter,
                            QListWidgetItem)
from PyQt5.QtCore import Qt, QDir, QSize
from PyQt5.QtGui import QPixmap, QImage

class ImageToPDFConverter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.preview_size = QSize(300, 400)  # 保持QSize定义
        self.image_files = []
        self.current_dir = ""
        self.settings = {
            'resolution': 300,
            'compression': 75,
            'page_size': '原始尺寸',
            'auto_rotate': True
        }
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle('图片转PDF工具')
        self.setGeometry(300, 300, 1000, 600)

        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QHBoxLayout(main_widget)

        splitter = QSplitter(Qt.Horizontal)

        # 左侧面板
        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)

        # 文件管理区域
        dir_group = QGroupBox("文件管理")
        dir_layout = QVBoxLayout(dir_group)
        self.btn_choose = QPushButton("选择目录")
        self.btn_choose.clicked.connect(self.choose_directory)
        self.lbl_dir = QLabel("未选择目录")

        btn_group = QHBoxLayout()
        self.btn_add_files = QPushButton("添加文件")
        self.btn_remove_selected = QPushButton("移除选中")
        self.btn_clear_list = QPushButton("清空列表")
        btn_group.addWidget(self.btn_add_files)
        btn_group.addWidget(self.btn_remove_selected)
        btn_group.addWidget(self.btn_clear_list)

        dir_layout.addWidget(self.btn_choose)
        dir_layout.addWidget(self.lbl_dir)
        dir_layout.addLayout(btn_group)

        # 文件列表
        self.list_widget = QListWidget()
        self.list_widget.setDragDropMode(QListWidget.InternalMove)
        self.list_widget.itemSelectionChanged.connect(self.show_preview)

        # 设置区域
        settings_group = QGroupBox("转换设置")
        settings_layout = QVBoxLayout(settings_group)
        self.page_size_combo = QComboBox()
        self.page_size_combo.addItems(["原始尺寸", "A4 (210x297mm)", "Letter (216x279mm)"])
        self.compression_check = QCheckBox("启用压缩")
        self.compression_slider = QSlider(Qt.Horizontal)
        self.compression_slider.setRange(1, 100)
        self.compression_slider.setValue(75)
        self.rotate_check = QCheckBox("自动旋转")
        self.rotate_check.setChecked(True)

        settings_layout.addWidget(QLabel("页面尺寸:"))
        settings_layout.addWidget(self.page_size_combo)
        settings_layout.addWidget(self.compression_check)
        settings_layout.addWidget(self.compression_slider)
        settings_layout.addWidget(self.rotate_check)

        # 组装左侧布局
        left_layout.addWidget(dir_group)
        left_layout.addWidget(QLabel("文件列表:"))
        left_layout.addWidget(self.list_widget)
        left_layout.addWidget(settings_group)

        # 右侧预览区域
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        self.preview_label = QLabel("预览区域")
        self.preview_label.setAlignment(Qt.AlignCenter)
        self.preview_label.setFixedSize(self.preview_size)  # 正确使用QSize
        right_layout.addWidget(self.preview_label)

        # 转换按钮
        self.btn_convert = QPushButton("开始转换")
        self.btn_convert.clicked.connect(self.convert_to_pdf)
        left_layout.addWidget(self.btn_convert)

        # 连接按钮事件
        self.btn_add_files.clicked.connect(self.add_single_file)
        self.btn_remove_selected.clicked.connect(self.remove_selected_files)
        self.btn_clear_list.clicked.connect(self.clear_file_list)

        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        main_layout.addWidget(splitter)

    def show_preview(self):
        """修复尺寸转换错误的预览方法"""
        if not self.list_widget.currentItem():
            self.preview_label.clear()
            return

        try:
            # 获取完整路径
            filename = self.list_widget.currentItem().text()
            full_path = os.path.join(self.current_dir, filename) if self.current_dir else filename

            with Image.open(full_path) as img:
                # 自动旋转
                if self.settings['auto_rotate']:
                    img = ImageOps.exif_transpose(img)

                # 修复:将QSize转换为元组
                thumbnail_size = (
                    self.preview_size.width(), 
                    self.preview_size.height()
                )
                img.thumbnail(thumbnail_size)  # 使用元组参数

                # 处理透明通道
                if img.mode == 'RGBA':
                    background = Image.new('RGB', img.size, (255, 255, 255))
                    background.paste(img, mask=img.split()[3])
                    img = background
                elif img.mode not in ['RGB', 'L']:
                    img = img.convert('RGB')

                # 转换为QImage
                if img.mode == 'RGB':
                    format = QImage.Format_RGB888
                    bytes_per_line = img.width * 3
                elif img.mode == 'L':
                    format = QImage.Format_Grayscale8
                    bytes_per_line = img.width
                else:
                    format = QImage.Format_RGBA8888
                    bytes_per_line = img.width * 4

                qimg = QImage(img.tobytes(), img.width, img.height, 
                            bytes_per_line, format)

                # 保持宽高比缩放
                pixmap = QPixmap.fromImage(qimg).scaled(
                    self.preview_size.width(), 
                    self.preview_size.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
                self.preview_label.setPixmap(pixmap)

        except Exception as e:
            QMessageBox.warning(self, "预览错误", f"{str(e)}")
            self.preview_label.setText("预览不可用")

    # 其他保持不变的方法...
    def choose_directory(self):
        directory = QFileDialog.getExistingDirectory(self, "选择目录", QDir.homePath())
        if directory:
            self.current_dir = directory
            self.lbl_dir.setText(directory)
            self.scan_image_files(directory)

    def scan_image_files(self, directory):
        valid_ext = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp']

        def natural_sort(s):
            return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', s)]

        files = sorted(
            [f for f in os.listdir(directory) if os.path.splitext(f)[1].lower() in valid_ext],
            key=natural_sort
        )
        self.image_files = files
        self.update_file_list()

    def update_file_list(self):
        self.list_widget.clear()
        for f in self.image_files:
            item = QListWidgetItem(f)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)
            self.list_widget.addItem(item)

    def add_single_file(self):
        files, _ = QFileDialog.getOpenFileNames(self, "选择文件", 
            self.current_dir or QDir.homePath(),
            "图片文件 (*.jpg *.jpeg *.png *.bmp *.tiff *.webp)")
        if files:
            if self.current_dir:
                new_files = [os.path.relpath(f, self.current_dir) for f in files]
            else:
                new_files = files
                self.current_dir = os.path.dirname(files[0])
                self.lbl_dir.setText(self.current_dir)

            self.image_files.extend(new_files)
            self.update_file_list()

    def remove_selected_files(self):
        selected = sorted([self.list_widget.row(item) for item in self.list_widget.selectedItems()], reverse=True)
        for row in selected:
            del self.image_files[row]
        self.update_file_list()

    def clear_file_list(self):
        self.image_files.clear()
        self.list_widget.clear()

    def convert_to_pdf(self):
        if not self.image_files:
            QMessageBox.warning(self, "错误", "请先添加图片文件")
            return

        save_path, _ = QFileDialog.getSaveFileName(self, "保存PDF", 
                                                 QDir.homePath(), 
                                                 "PDF文件 (*.pdf)")
        if not save_path:
            return

        files = [os.path.join(self.current_dir, f) for f in self.get_checked_files()]

        progress = QProgressDialog("转换中...", "取消", 0, len(files), self)
        progress.setWindowModality(Qt.WindowModal)

        try:
            with ThreadPoolExecutor() as executor:
                futures = []
                images = []

                for file_path in files:
                    futures.append(executor.submit(self.process_image, file_path))

                for i, future in enumerate(futures):
                    if progress.wasCanceled():
                        break
                    progress.setValue(i)
                    img = future.result()
                    if img:
                        images.append(img)

                if images:
                    images[0].save(save_path, "PDF", 
                                 resolution=self.settings['resolution'],
                                 save_all=True, 
                                 append_images=images[1:],
                                 quality=self.settings['compression'])
                    QMessageBox.information(self, "完成", "转换成功!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"转换失败: {str(e)}")
        finally:
            progress.close()

    def process_image(self, file_path):
        try:
            img = Image.open(file_path)
            if self.settings['auto_rotate']:
                img = ImageOps.exif_transpose(img)

            img = self.resize_image(img)
            if img.mode != 'RGB':
                img = img.convert('RGB')
            return img
        except Exception as e:
            QMessageBox.warning(self, "错误", f"处理失败: {os.path.basename(file_path)}\n{str(e)}")
            return None

    def resize_image(self, img):
        size_map = {
            'A4 (210x297mm)': (2480, 3508),
            'Letter (216x279mm)': (2550, 3300)
        }
        target = self.settings['page_size']

        if target == '原始尺寸':
            return img

        if target in size_map:
            img.thumbnail(size_map[target])
        return img

    def get_checked_files(self):
        return [self.list_widget.item(i).text() 
               for i in range(self.list_widget.count())
               if self.list_widget.item(i).checkState() == Qt.Checked]

if __name__ == '__main__':
    # 高DPI设置
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

    app = QApplication(sys.argv)
    window = ImageToPDFConverter()
    window.show()
    sys.exit(app.exec_())

4.功能多合一及添加标签处理功能开发代码

import sys
import os
import re
import fitz
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageOps
from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox,
                             QLabel, QLineEdit, QComboBox, QRadioButton, QPushButton,
                             QHBoxLayout, QVBoxLayout, QGroupBox, QWidget, QTabWidget,
                             QListWidget, QListWidgetItem, QAbstractItemView, QCheckBox,
                             QSlider, QProgressDialog, QSplitter, QTreeWidget, QTreeWidgetItem,
                             QSpinBox)

from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDir, QSize, QMimeData
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QPixmap, QImage
from PyPDF2 import PdfReader, PdfWriter, PdfMerger
from pikepdf import Pdf, Encryption, Permissions

# PDF转图片功能的工作线程
class WorkerThread(QThread):
    finished = pyqtSignal(bool, str)
    error = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.params = {
            'dpi': 300,
            'alpha': False,
            'colorspace': fitz.csRGB,
            'output_dir': ''
        }

    def set_params(self, file_path, output_dir, pages, export_pdf, img_format, dpi):
        self.params.update({
            'file_path': file_path,
            'output_dir': output_dir,
            'pages': self._parse_pages(pages),
            'export_pdf': export_pdf,
            'img_format': img_format.lower(),
            'dpi': dpi,
            'alpha': (img_format.lower() == 'png')
        })

    def _parse_pages(self, pages_str):
        pages = []
        for part in pages_str.replace(',', ',').split(','):
            part = part.strip()
            if not part:
                continue
            if '-' in part:
                start_end = part.split('-')
                if len(start_end) != 2:
                    raise ValueError("无效的页码范围格式")
                start = int(start_end[0])
                end = int(start_end[1])
                pages.extend(range(start, end+1))
            else:
                pages.append(int(part))
        return sorted(set(pages))

    def run(self):
        try:
            doc = fitz.open(self.params['file_path'])
            total_pages = doc.page_count
            
            valid_pages = [p-1 for p in self.params['pages'] if 0 < p <= total_pages]
            if not valid_pages:
                raise ValueError("所有页码均超出文档范围")

            if self.params['export_pdf']:
                self._export_pdf(doc, valid_pages)
            else:
                self._export_images(doc, valid_pages)
            
            doc.close()
            self.finished.emit(True, self.params['output_dir'])
        
        except Exception as e:
            self.error.emit(str(e))

    def _export_pdf(self, doc, pages):
        new_doc = fitz.open()
        for p in pages:
            new_doc.insert_pdf(doc, from_page=p, to_page=p)
        output_path = os.path.join(self.params['output_dir'], 'extracted.pdf')
        new_doc.save(output_path, garbage=3, deflate=True)
        new_doc.close()

    def _export_images(self, doc, pages):
        for idx, p in enumerate(pages):
            page = doc[p]
            pix = page.get_pixmap(
                dpi=self.params['dpi'],
                alpha=self.params['alpha'],
                colorspace=self.params['colorspace'],
                annots=True,
            )
            output_path = os.path.join(
                self.params['output_dir'],
                f'page_{p+1}_dpi{self.params["dpi"]}.{self.params["img_format"]}'
            )
            if self.params['img_format'] == 'jpg':
                pix.save(output_path, jpg_quality=95)
            else:
                pix.save(output_path)

# 支持拖放的自定义输入框
class DragDropLineEdit(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
                    event.acceptProposedAction()
                    return
        event.ignore()

    def dropEvent(self, event):
        for url in event.mimeData().urls():
            file_path = url.toLocalFile()
            if file_path.lower().endswith('.pdf'):
                self.setText(file_path)
                break

# PDF转图片功能模块
class PdfToolApp(QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.worker = WorkerThread()
        self._connect_signals()
        self.setAcceptDrops(True)

    def init_ui(self):
        # 文件选择组
        file_group = QGroupBox("文件操作")
        self.txt_file = QLineEdit()
        self.txt_file.setPlaceholderText("拖拽PDF文件到此或点击浏览...")
        btn_browse = QPushButton("浏览")
        btn_browse.clicked.connect(self._browse_file)
        
        # 输出设置组
        output_group = QGroupBox("输出设置")
        self.txt_output = QLineEdit()
        btn_output = QPushButton("选择目录")
        btn_output.clicked.connect(self._browse_dir)
        self.rad_pdf = QRadioButton("导出为PDF")
        self.rad_img = QRadioButton("导出为图片")
        self.rad_pdf.setChecked(True)
        self.cmb_format = QComboBox()
        self.cmb_format.addItems(['PNG', 'JPG'])
        self.cmb_dpi = QComboBox()
        self.cmb_dpi.addItems(['150 (网页)', '300 (印刷)', '600 (高清)'])
        self.txt_pages = QLineEdit()
        self.txt_pages.setPlaceholderText("输入页码范围(如:1-3 或 1,3,5)")

        # 布局
        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("PDF文件:"))
        file_layout.addWidget(self.txt_file)
        file_layout.addWidget(btn_browse)
        file_group.setLayout(file_layout)

        output_layout = QVBoxLayout()
        output_layout.addWidget(QLabel("输出目录:"))
        output_layout.addWidget(self.txt_output)
        output_layout.addWidget(btn_output)
        
        pages_layout = QHBoxLayout()
        pages_layout.addWidget(QLabel("页码范围:"))
        pages_layout.addWidget(self.txt_pages)
        
        format_layout = QHBoxLayout()
        format_layout.addWidget(self.rad_pdf)
        format_layout.addWidget(self.rad_img)
        format_layout.addWidget(self.cmb_format)
        format_layout.addWidget(QLabel("DPI:"))
        format_layout.addWidget(self.cmb_dpi)
        
        output_group.setLayout(output_layout)
        output_layout.addLayout(pages_layout)
        output_layout.addLayout(format_layout)

        # 主布局
        main_layout = QVBoxLayout()
        main_layout.addWidget(file_group)
        main_layout.addWidget(output_group)
        
        self.setLayout(main_layout)

    def _connect_signals(self):
        # 确保初始状态下,如果选择了PDF导出,则禁用格式选择
        self.cmb_format.setEnabled(self.rad_img.isChecked())
        
        self.rad_pdf.toggled.connect(lambda: self.cmb_format.setEnabled(False))
        self.rad_img.toggled.connect(lambda: self.cmb_format.setEnabled(True))
        
        btn_execute = QPushButton("开始转换", self)
        btn_execute.clicked.connect(self._start_conversion)
        self.layout().addWidget(btn_execute)
        
        self.worker.finished.connect(self._on_finished)
        self.worker.error.connect(self._show_error)

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event: QDropEvent):
        for url in event.mimeData().urls():
            if url.isLocalFile() and url.fileName().lower().endswith('.pdf'):
                self.txt_file.setText(os.path.normpath(url.toLocalFile()))
                break

    def _browse_file(self):
        path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF Files (*.pdf)")
        if path:
            self.txt_file.setText(path)

    def _browse_dir(self):
        path = QFileDialog.getExistingDirectory(self, "选择输出目录")
        if path:
            self.txt_output.setText(path)

    def _start_conversion(self):
        params = {
            'file_path': self.txt_file.text(),
            'output_dir': self.txt_output.text(),
            'pages': self.txt_pages.text() or "1-1",
            'export_pdf': self.rad_pdf.isChecked(),
            'img_format': self.cmb_format.currentText(),
            'dpi': int(self.cmb_dpi.currentText().split()[0])
        }
        
        if not os.path.exists(params['file_path']):
            QMessageBox.warning(self, "错误", "PDF文件路径无效")
            return
            
        if not os.path.exists(params['output_dir']):
            os.makedirs(params['output_dir'], exist_ok=True)

        try:
            _ = self.worker._parse_pages(params['pages'])
        except ValueError as e:
            QMessageBox.critical(self, "输入错误", f"页码格式错误:{str(e)}")
            return

        self.worker.set_params(**params)
        self.worker.start()

    def _on_finished(self, success, output_dir):
        QMessageBox.information(self, "完成", f"文件已保存至:\n{output_dir}")

    def _show_error(self, msg):
        QMessageBox.critical(self, "错误", f"处理失败:{msg}")

# PDF密码解除与加密功能模块
class PDFToolsUI(QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        """界面初始化"""
        tabs = QTabWidget()
        layout = QVBoxLayout(self)
        layout.addWidget(tabs)

        # 解密模块
        decrypt_tab = QWidget()
        self.decrypt_ui(decrypt_tab)
        tabs.addTab(decrypt_tab, "密码解除")

        # 加密模块
        encrypt_tab = QWidget()
        self.encrypt_ui(encrypt_tab)
        tabs.addTab(encrypt_tab, "文件加密")

        # 合并模块
        merge_tab = QWidget()
        self.merge_ui(merge_tab)
        tabs.addTab(merge_tab, "文件合并")

    def decrypt_ui(self, tab):
        """密码解除界面"""
        layout = QVBoxLayout()

        # 文件选择(支持拖放)
        file_layout = QHBoxLayout()
        self.decrypt_input = DragDropLineEdit()
        btn_choose = QPushButton("选择文件")
        btn_choose.clicked.connect(lambda: self.choose_file(self.decrypt_input))

        file_layout.addWidget(QLabel("输入文件:"))
        file_layout.addWidget(self.decrypt_input)
        file_layout.addWidget(btn_choose)

        # 密码输入
        self.decrypt_pass = QLineEdit()
        self.decrypt_pass.setEchoMode(QLineEdit.Password)

        # 操作按钮
        btn_decrypt = QPushButton("开始解密")
        btn_decrypt.clicked.connect(self.handle_decrypt)

        layout.addLayout(file_layout)
        layout.addWidget(QLabel("输入密码:"))
        layout.addWidget(self.decrypt_pass)
        layout.addWidget(btn_decrypt)
        tab.setLayout(layout)

    def encrypt_ui(self, tab):
        """文件加密界面"""
        layout = QVBoxLayout()

        # 输入文件选择(支持拖放)
        input_layout = QHBoxLayout()
        self.encrypt_input = DragDropLineEdit()
        btn_input = QPushButton("选择文件")
        btn_input.clicked.connect(lambda: self.choose_file(self.encrypt_input))
        input_layout.addWidget(QLabel("输入文件:"))
        input_layout.addWidget(self.encrypt_input)
        input_layout.addWidget(btn_input)

        # 输出路径选择
        output_layout = QHBoxLayout()
        self.encrypt_output = QLineEdit()
        btn_output = QPushButton("另存为")
        btn_output.clicked.connect(self.choose_output)
        output_layout.addWidget(QLabel("输出路径:"))
        output_layout.addWidget(self.encrypt_output)
        output_layout.addWidget(btn_output)

        # 加密参数
        param_layout = QHBoxLayout()
        self.encrypt_pass = QLineEdit()
        self.encrypt_pass.setEchoMode(QLineEdit.Password)
        self.encrypt_algo = QComboBox()
        self.encrypt_algo.addItems(["AES-128", "AES-256"])

        param_layout.addWidget(QLabel("加密密码:"))
        param_layout.addWidget(self.encrypt_pass)
        param_layout.addWidget(QLabel("算法:"))
        param_layout.addWidget(self.encrypt_algo)

        # 操作按钮
        btn_encrypt = QPushButton("开始加密")
        btn_encrypt.clicked.connect(self.handle_encrypt)

        layout.addLayout(input_layout)
        layout.addLayout(output_layout)
        layout.addLayout(param_layout)
        layout.addWidget(btn_encrypt)
        tab.setLayout(layout)
    
    def merge_ui(self, tab):
        """文件合并界面"""
        layout = QVBoxLayout()

        # 文件列表(支持拖放)
        self.merge_list = QListWidget()
        self.merge_list.setDragDropMode(QAbstractItemView.InternalMove)
        self.merge_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.merge_list.setAcceptDrops(True)
        self.merge_list.dragEnterEvent = self.merge_drag_enter
        self.merge_list.dropEvent = self.merge_drop

        # 操作按钮
        btn_layout = QHBoxLayout()
        btn_add = QPushButton("添加文件")
        btn_add.clicked.connect(self.add_merge_files)
        btn_remove = QPushButton("移除选中")
        btn_remove.clicked.connect(lambda: self.remove_items(self.merge_list))
        btn_up = QPushButton("上移")
        btn_up.clicked.connect(lambda: self.move_item(self.merge_list, -1))
        btn_down = QPushButton("下移")
        btn_down.clicked.connect(lambda: self.move_item(self.merge_list, 1))

        btn_layout.addWidget(btn_add)
        btn_layout.addWidget(btn_remove)
        btn_layout.addWidget(btn_up)
        btn_layout.addWidget(btn_down)

        # 输出路径
        output_layout = QHBoxLayout()
        self.merge_output = QLineEdit()
        btn_output = QPushButton("选择路径")
        btn_output.clicked.connect(self.choose_merge_output)
        output_layout.addWidget(QLabel("输出文件:"))
        output_layout.addWidget(self.merge_output)
        output_layout.addWidget(btn_output)

        # 合并按钮
        btn_merge = QPushButton("开始合并")
        btn_merge.clicked.connect(self.handle_merge)

        layout.addWidget(self.merge_list)
        layout.addLayout(btn_layout)
        layout.addLayout(output_layout)
        layout.addWidget(btn_merge)
        tab.setLayout(layout)
    
    def handle_decrypt(self):
        """解密处理"""
        input_path = self.decrypt_input.text()
        password = self.decrypt_pass.text()

        if not os.path.exists(input_path):
            QMessageBox.critical(self, "错误", "文件不存在!")
            return

        output_path = f"{os.path.splitext(input_path)[0]}_unlocked.pdf"

        try:
            with Pdf.open(input_path, password=password) as pdf:
                pdf.save(output_path)
            QMessageBox.information(self, "成功", f"文件已保存至:\n{output_path}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"解密失败:\n{str(e)}")

    def handle_encrypt(self):
        """加密处理"""
        input_path = self.encrypt_input.text()
        output_path = self.encrypt_output.text()
        password = self.encrypt_pass.text()

        # 检查输入文件是否存在
        if not os.path.exists(input_path):
            QMessageBox.critical(self, "错误", f"输入文件不存在: {input_path}")
            return

        if not all([input_path, output_path, password]):
            QMessageBox.critical(self, "错误", "请填写所有字段!")
            return

        try:
            # 使用PyPDF2进行加密
            reader = PdfReader(input_path)
            writer = PdfWriter()
            
            # 复制所有页面
            for page in reader.pages:
                writer.add_page(page)
            
            # 设置加密
            writer.encrypt(password)
            
            # 确保输出目录存在
            output_dir = os.path.dirname(output_path)
            if output_dir and not os.path.exists(output_dir):
                os.makedirs(output_dir)
            
            # 保存加密后的文件
            with open(output_path, "wb") as f:
                writer.write(f)
                
            QMessageBox.information(self, "成功", "文件加密完成!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加密失败:\n{str(e)}")

    def handle_merge(self):
        """合并处理"""
        output_path = self.merge_output.text()
        if not output_path:
            QMessageBox.critical(self, "错误", "请选择输出路径!")
            return

        if self.merge_list.count() == 0:
            QMessageBox.critical(self, "错误", "请添加至少一个PDF文件!")
            return

        merger = PdfMerger()
        try:
            for i in range(self.merge_list.count()):
                file_path = self.merge_list.item(i).text()
                merger.append(file_path)

            merger.write(output_path)
            QMessageBox.information(self, "成功", "文件合并完成!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"合并失败:\n{str(e)}")
        finally:
            merger.close()

    
    def choose_file(self, target_field):
        """文件选择对话框"""
        path, _ = QFileDialog.getOpenFileName(
            self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        if path:
            target_field.setText(path)

    def choose_output(self):
        """加密输出路径选择"""
        path, _ = QFileDialog.getSaveFileName(
            self, "保存文件", "", "PDF文件 (*.pdf)")
        if path:
            self.encrypt_output.setText(path)

    def choose_merge_output(self):
        """合并输出路径选择"""
        path, _ = QFileDialog.getSaveFileName(
            self, "保存合并文件", "", "PDF文件 (*.pdf)")
        if path:
            self.merge_output.setText(path)

    def add_merge_files(self):
        """添加合并文件"""
        files, _ = QFileDialog.getOpenFileNames(
            self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        if files:
            for f in files:
                item = QListWidgetItem(f)
                self.merge_list.addItem(item)

    def remove_items(self, list_widget):
        """移除选中项"""
        for item in list_widget.selectedItems():
            list_widget.takeItem(list_widget.row(item))

    def move_item(self, list_widget, direction):
        """调整顺序"""
        current_row = list_widget.currentRow()
        if current_row >= 0:
            new_row = current_row + direction
            if 0 <= new_row < list_widget.count():
                item = list_widget.takeItem(current_row)
                list_widget.insertItem(new_row, item)
                list_widget.setCurrentRow(new_row)

    # 合并列表拖放事件
    def merge_drag_enter(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
                    event.acceptProposedAction()
                    return
        event.ignore()

    def merge_drop(self, event):
        for url in event.mimeData().urls():
            file_path = url.toLocalFile()
            if file_path.lower().endswith('.pdf'):
                self.merge_list.addItem(QListWidgetItem(file_path))




        


# 图片转PDF功能模块
class ImageToPDFConverter(QWidget):
    def __init__(self):
        super().__init__()
        self.preview_size = QSize(300, 400)
        self.image_files = []
        self.current_dir = ""
        self.settings = {
            'resolution': 300,
            'compression': 75,
            'page_size': '原始尺寸',
            'auto_rotate': True
        }
        self.init_ui()

    def init_ui(self):
        main_layout = QVBoxLayout(self)
        
        splitter = QSplitter(Qt.Horizontal)
        
        # 左侧面板
        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)
        
        # 文件管理区域
        dir_group = QGroupBox("文件管理")
        dir_layout = QVBoxLayout(dir_group)
        self.btn_choose = QPushButton("选择目录")
        self.btn_choose.clicked.connect(self.choose_directory)
        self.lbl_dir = QLabel("未选择目录")
        
        btn_group = QHBoxLayout()
        self.btn_add_files = QPushButton("添加文件")
        self.btn_remove_selected = QPushButton("移除选中")
        self.btn_clear_list = QPushButton("清空列表")
        btn_group.addWidget(self.btn_add_files)
        btn_group.addWidget(self.btn_remove_selected)
        btn_group.addWidget(self.btn_clear_list)
        
        dir_layout.addWidget(self.btn_choose)
        dir_layout.addWidget(self.lbl_dir)
        dir_layout.addLayout(btn_group)
        
        # 文件列表
        self.list_widget = QListWidget()
        self.list_widget.setDragDropMode(QListWidget.InternalMove)
        self.list_widget.itemSelectionChanged.connect(self.show_preview)
        
        # 设置区域
        settings_group = QGroupBox("转换设置")
        settings_layout = QVBoxLayout(settings_group)
        self.page_size_combo = QComboBox()
        self.page_size_combo.addItems(["原始尺寸", "A4 (210x297mm)", "Letter (216x279mm)"])

        # 添加压缩相关设置
        compression_group = QGroupBox("压缩设置")
        compression_layout = QVBoxLayout(compression_group)
        self.compression_check = QCheckBox("启用压缩")
        self.compression_slider = QSlider(Qt.Horizontal)
        self.compression_slider.setRange(1, 100)
        self.compression_slider.setValue(75)
        self.compression_slider.setEnabled(False)
        self.compression_check.toggled.connect(self.compression_slider.setEnabled)

        # 添加图像预处理选项
        self.grayscale_check = QCheckBox("转换为灰度")
        self.downsample_check = QCheckBox("降低分辨率")
        self.downsample_combo = QComboBox()
        self.downsample_combo.addItems(["150 DPI", "200 DPI", "300 DPI"])
        self.downsample_combo.setEnabled(False)
        self.downsample_check.toggled.connect(self.downsample_combo.setEnabled)

        compression_layout.addWidget(self.compression_check)
        compression_layout.addWidget(self.compression_slider)
        compression_layout.addWidget(self.grayscale_check)
        compression_layout.addWidget(self.downsample_check)
        compression_layout.addWidget(self.downsample_combo)
        compression_group.setLayout(compression_layout)

        # 添加高级压缩选项
        advanced_group = QGroupBox("高级设置")
        advanced_layout = QVBoxLayout(advanced_group)
        self.optimize_check = QCheckBox("优化图像")
        self.jpeg_quality_label = QLabel("JPEG质量: 75")
        self.jpeg_quality_slider = QSlider(Qt.Horizontal)
        self.jpeg_quality_slider.setRange(10, 100)
        self.jpeg_quality_slider.setValue(75)
        self.jpeg_quality_slider.valueChanged.connect(
            lambda v: self.jpeg_quality_label.setText(f"JPEG质量: {v}")
        )

        advanced_layout.addWidget(self.optimize_check)
        advanced_layout.addWidget(self.jpeg_quality_label)
        advanced_layout.addWidget(self.jpeg_quality_slider)
        advanced_group.setLayout(advanced_layout)

        # 将高级设置添加到主设置布局
        settings_layout.addWidget(compression_group)
        settings_layout.addWidget(advanced_group)


        # 添加图像格式转换选项
        self.convert_format_check = QCheckBox("转换图像格式")
        self.convert_format_combo = QComboBox()
        self.convert_format_combo.addItems(["JPEG", "PNG", "WEBP"])
        self.convert_format_combo.setEnabled(False)
        self.convert_format_check.toggled.connect(self.convert_format_combo.setEnabled)

        advanced_layout.addWidget(self.convert_format_check)
        advanced_layout.addWidget(self.convert_format_combo)


        # 添加自动旋转选项
        self.rotate_check = QCheckBox("自动旋转")
        self.rotate_check.setChecked(True)
        
        settings_layout.addWidget(QLabel("页面尺寸:"))
        settings_layout.addWidget(self.page_size_combo)
        settings_layout.addWidget(self.compression_check)
        settings_layout.addWidget(self.compression_slider)
        settings_layout.addWidget(self.rotate_check)
        
        # 组装左侧布局
        left_layout.addWidget(dir_group)
        left_layout.addWidget(QLabel("文件列表:"))
        left_layout.addWidget(self.list_widget)
        left_layout.addWidget(settings_group)
        
        # 右侧预览区域
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        self.preview_label = QLabel("预览区域")
        self.preview_label.setAlignment(Qt.AlignCenter)
        self.preview_label.setFixedSize(self.preview_size)
        right_layout.addWidget(self.preview_label)
        
        # 转换按钮
        self.btn_convert = QPushButton("开始转换")
        self.btn_convert.clicked.connect(self.convert_to_pdf)
        left_layout.addWidget(self.btn_convert)
        
        # 连接按钮事件
        self.btn_add_files.clicked.connect(self.add_single_file)
        self.btn_remove_selected.clicked.connect(self.remove_selected_files)
        self.btn_clear_list.clicked.connect(self.clear_file_list)
        
        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        main_layout.addWidget(splitter)

    def show_preview(self):
        """显示预览图像"""
        if not self.list_widget.currentItem():
            self.preview_label.clear()
            return

        try:
            # 获取完整路径
            filename = self.list_widget.currentItem().text()
            full_path = os.path.join(self.current_dir, filename) if self.current_dir else filename
            
            with Image.open(full_path) as img:
                # 自动旋转
                if self.settings['auto_rotate']:
                    img = ImageOps.exif_transpose(img)
                
                # 转换QSize为元组
                thumbnail_size = (
                    self.preview_size.width(), 
                    self.preview_size.height()
                )
                img.thumbnail(thumbnail_size)
                
                # 处理透明通道
                if img.mode == 'RGBA':
                    background = Image.new('RGB', img.size, (255, 255, 255))
                    background.paste(img, mask=img.split()[3])
                    img = background
                elif img.mode not in ['RGB', 'L']:
                    img = img.convert('RGB')

                # 转换为QImage
                if img.mode == 'RGB':
                    format = QImage.Format_RGB888
                    bytes_per_line = img.width * 3
                elif img.mode == 'L':
                    format = QImage.Format_Grayscale8
                    bytes_per_line = img.width
                else:
                    format = QImage.Format_RGBA8888
                    bytes_per_line = img.width * 4

                qimg = QImage(img.tobytes(), img.width, img.height, 
                            bytes_per_line, format)

                # 保持宽高比缩放
                pixmap = QPixmap.fromImage(qimg).scaled(
                    self.preview_size.width(), 
                    self.preview_size.height(),
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation
                )
                self.preview_label.setPixmap(pixmap)

        except Exception as e:
            QMessageBox.warning(self, "预览错误", f"{str(e)}")
            self.preview_label.setText("预览不可用")

    def choose_directory(self):
        directory = QFileDialog.getExistingDirectory(self, "选择目录", QDir.homePath())
        if directory:
            self.current_dir = directory
            self.lbl_dir.setText(directory)
            self.scan_image_files(directory)

    def scan_image_files(self, directory):
        valid_ext = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp']
        
        def natural_sort(s):
            return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', s)]
        
        files = sorted(
            [f for f in os.listdir(directory) if os.path.splitext(f)[1].lower() in valid_ext],
            key=natural_sort
        )
        self.image_files = files
        self.update_file_list()

    def update_file_list(self):
        self.list_widget.clear()
        for f in self.image_files:
            item = QListWidgetItem(f)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)
            self.list_widget.addItem(item)


    def add_single_file(self):
        files, _ = QFileDialog.getOpenFileNames(self, "选择文件", 
            self.current_dir or QDir.homePath(),
            "图片文件 (*.jpg *.jpeg *.png *.bmp *.tiff *.webp)")
        if files:
            if self.current_dir:
                new_files = [os.path.relpath(f, self.current_dir) for f in files]
            else:
                new_files = files
                self.current_dir = os.path.dirname(files[0])
                self.lbl_dir.setText(self.current_dir)
            
            self.image_files.extend(new_files)
            self.update_file_list()

    def remove_selected_files(self):
        selected = sorted([self.list_widget.row(item) for item in self.list_widget.selectedItems()], reverse=True)
        for row in selected:
            del self.image_files[row]
        self.update_file_list()

    def clear_file_list(self):
        self.image_files.clear()
        self.list_widget.clear()

    def _optimize_pdf(self, pdf_path):
        """对生成的PDF进行后处理优化"""
        try:
            # 创建临时文件路径
            temp_path = pdf_path + ".temp"
            
            # 使用PyPDF2进行优化
            reader = PdfReader(pdf_path)
            writer = PdfWriter()
            
            # 复制所有页面并应用压缩
            for page in reader.pages:
                writer.add_page(page)
            
            # 设置压缩选项
            writer.add_metadata(reader.metadata)
            
            # 保存优化后的文件
            with open(temp_path, "wb") as f:
                writer.write(f)
            
            # 替换原文件
            os.replace(temp_path, pdf_path)
        except Exception as e:
            print(f"PDF优化失败: {str(e)}")


    def convert_to_pdf(self):
        if not self.image_files:
            QMessageBox.warning(self, "错误", "请先添加图片文件")
            return
        
        save_path, _ = QFileDialog.getSaveFileName(self, "保存PDF", 
                                                QDir.homePath(), 
                                                "PDF文件 (*.pdf)")
        if not save_path:
            return
        
        files = [os.path.join(self.current_dir, f) for f in self.get_checked_files()]
        
        # 获取压缩设置
        compression_quality = self.compression_slider.value() if self.compression_check.isChecked() else 95
        self.settings['compression'] = compression_quality
        
        progress = QProgressDialog("转换中...", "取消", 0, len(files), self)
        progress.setWindowModality(Qt.WindowModal)
        
        try:
            with ThreadPoolExecutor() as executor:
                futures = []
                images = []
                
                for file_path in files:
                    futures.append(executor.submit(self.process_image, file_path))
                
                for i, future in enumerate(futures):
                    if progress.wasCanceled():
                        break
                    progress.setValue(i)
                    img = future.result()
                    if img:
                        images.append(img)
                
                if images:
                    # 获取压缩设置
                    compression_quality = self.compression_slider.value() if self.compression_check.isChecked() else 95
                    jpeg_quality = self.jpeg_quality_slider.value()
                    
                    # 创建保存选项
                    save_options = {
                        "resolution": self.settings['resolution'],
                        "save_all": True,
                        "append_images": images[1:],
                        "quality": compression_quality,
                    }
                    
                    # 如果启用了优化选项
                    if self.optimize_check.isChecked():
                        save_options["optimize"] = True
                    
                    # 保存PDF
                    images[0].save(save_path, "PDF", **save_options)
                    
                    # 如果需要进一步压缩,可以使用PyPDF2进行后处理
                    if self.optimize_check.isChecked():
                        self._optimize_pdf(save_path)
                        
                    QMessageBox.information(self, "完成", "转换成功!")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"转换失败: {str(e)}")
        finally:
            progress.close()


    def process_image(self, file_path):
        try:
            img = Image.open(file_path)
            
            # 自动旋转
            if self.settings['auto_rotate']:
                img = ImageOps.exif_transpose(img)
            
            # 调整大小
            img = self.resize_image(img)
            
            # 转换图像格式(如果选中)
            if self.convert_format_check.isChecked():
                format_name = self.convert_format_combo.currentText().lower()
                if format_name != img.format.lower():
                    # 创建新图像
                    new_img = Image.new('RGB', img.size, (255, 255, 255))
                    if img.mode == 'RGBA':
                        new_img.paste(img, mask=img.split()[3])
                    else:
                        new_img.paste(img)
                    
                    # 使用内存中转换而不是保存到磁盘
                    img = new_img


            # 转换为灰度(如果选中)
            if self.grayscale_check.isChecked():
                img = img.convert('L')
                # 转回RGB模式以兼容PDF保存
                img = img.convert('RGB')
            elif img.mode != 'RGB':
                img = img.convert('RGB')
            
            # 降低分辨率(如果选中)
            if self.downsample_check.isChecked():
                dpi_text = self.downsample_combo.currentText()
                target_dpi = int(dpi_text.split()[0])
                # 计算新尺寸
                if hasattr(img, 'info') and 'dpi' in img.info:
                    original_dpi = img.info['dpi'][0]
                    if original_dpi > target_dpi:
                        scale_factor = target_dpi / original_dpi
                        new_size = (int(img.width * scale_factor), int(img.height * scale_factor))
                        img = img.resize(new_size, Image.LANCZOS)
            
            return img
        except Exception as e:
            QMessageBox.warning(self, "错误", f"处理失败: {os.path.basename(file_path)}\n{str(e)}")
            return None


    def resize_image(self, img):
        size_map = {
            'A4 (210x297mm)': (2480, 3508),
            'Letter (216x279mm)': (2550, 3300)
        }
        target = self.settings['page_size']
        
        if target == '原始尺寸':
            return img
        
        if target in size_map:
            img.thumbnail(size_map[target])
        return img

    def get_checked_files(self):
        return [self.list_widget.item(i).text() 
               for i in range(self.list_widget.count())
               if self.list_widget.item(i).checkState() == Qt.Checked]


# PDF书签和目录管理功能模块
class PDFBookmarkManager(QWidget):
    def __init__(self):
        super().__init__()
        self.pdf_document = None
        self.bookmarks = []
        self.init_ui()
        
    def init_ui(self):
        main_layout = QVBoxLayout(self)
        
        # 文件选择区域
        file_group = QGroupBox("PDF文件")
        file_layout = QHBoxLayout()
        self.pdf_path_edit = DragDropLineEdit()
        self.pdf_path_edit.setPlaceholderText("拖拽PDF文件到此或点击浏览...")
        browse_btn = QPushButton("浏览")
        browse_btn.clicked.connect(self.browse_pdf)
        load_btn = QPushButton("加载")
        load_btn.clicked.connect(self.load_pdf)
        
        file_layout.addWidget(QLabel("PDF文件:"))
        file_layout.addWidget(self.pdf_path_edit)
        file_layout.addWidget(browse_btn)
        file_layout.addWidget(load_btn)
        file_group.setLayout(file_layout)
        
        # 创建分割器,左侧是书签树,右侧是PDF预览和书签编辑
        splitter = QSplitter(Qt.Horizontal)
        
        # 左侧 - 书签树
        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)
        
        self.bookmark_tree = QTreeWidget()
        self.bookmark_tree.setHeaderLabels(["书签标题", "页码"])
        self.bookmark_tree.itemSelectionChanged.connect(self.on_bookmark_selected)
        
        bookmark_controls = QHBoxLayout()
        add_btn = QPushButton("添加书签")
        add_btn.clicked.connect(self.add_bookmark)
        edit_btn = QPushButton("编辑书签")
        edit_btn.clicked.connect(self.edit_bookmark)
        remove_btn = QPushButton("删除书签")
        remove_btn.clicked.connect(self.remove_bookmark)
        
        bookmark_controls.addWidget(add_btn)
        bookmark_controls.addWidget(edit_btn)
        bookmark_controls.addWidget(remove_btn)
        
        left_layout.addWidget(QLabel("书签列表:"))
        left_layout.addWidget(self.bookmark_tree)
        left_layout.addLayout(bookmark_controls)
        
        # 右侧 - 书签编辑和PDF预览
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        
        # 书签编辑区域
        edit_group = QGroupBox("书签编辑")
        edit_layout = QVBoxLayout()
        
        title_layout = QHBoxLayout()
        title_layout.addWidget(QLabel("标题:"))
        self.title_edit = QLineEdit()
        title_layout.addWidget(self.title_edit)
        
        page_layout = QHBoxLayout()
        page_layout.addWidget(QLabel("页码:"))
        self.page_spin = QComboBox()
        page_layout.addWidget(self.page_spin)
        
        level_layout = QHBoxLayout()
        level_layout.addWidget(QLabel("层级:"))
        self.level_spin = QComboBox()
        self.level_spin.addItems(["1", "2", "3", "4", "5"])
        level_layout.addWidget(self.level_spin)
        
        save_btn = QPushButton("保存书签")
        save_btn.clicked.connect(self.save_bookmark)
        
        edit_layout.addLayout(title_layout)
        edit_layout.addLayout(page_layout)
        edit_layout.addLayout(level_layout)
        edit_layout.addWidget(save_btn)
        edit_group.setLayout(edit_layout)
        
        # 自动生成目录区域
        toc_group = QGroupBox("自动生成目录")
        toc_layout = QVBoxLayout()
        
        self.detect_headings_btn = QPushButton("检测标题文本")
        self.detect_headings_btn.clicked.connect(self.detect_headings)
        
        self.auto_toc_btn = QPushButton("生成目录")
        self.auto_toc_btn.clicked.connect(self.generate_toc)
        
        toc_layout.addWidget(self.detect_headings_btn)
        toc_layout.addWidget(self.auto_toc_btn)
        toc_group.setLayout(toc_layout)
        
        # 保存PDF区域
        save_group = QGroupBox("保存PDF")
        save_layout = QVBoxLayout()

        self.save_pdf_btn = QPushButton("保存带书签的PDF")
        self.save_pdf_btn.clicked.connect(self.save_pdf_with_bookmarks)

        self.merge_toc_btn = QPushButton("合并目录到PDF")
        self.merge_toc_btn.clicked.connect(self.merge_toc_with_pdf)

        save_layout.addWidget(self.save_pdf_btn)
        save_layout.addWidget(self.merge_toc_btn)
        save_group.setLayout(save_layout)

        
        # 组装右侧面板
        right_layout.addWidget(edit_group)
        right_layout.addWidget(toc_group)
        right_layout.addWidget(save_group)
        right_layout.addStretch()
        
        # 添加到分割器
        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        
        # 设置分割器比例
        splitter.setSizes([300, 300])
        
        # 添加到主布局
        main_layout.addWidget(file_group)
        main_layout.addWidget(splitter)





    def browse_pdf(self):
        """浏览并选择PDF文件"""
        path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        if path:
            self.pdf_path_edit.setText(path)
            
    def load_pdf(self):
        """加载PDF文件并提取书签"""
        pdf_path = self.pdf_path_edit.text()
        if not pdf_path or not os.path.exists(pdf_path):
            QMessageBox.warning(self, "错误", "请选择有效的PDF文件")
            return
            
        try:
            # 使用PyMuPDF加载PDF
            self.pdf_document = fitz.open(pdf_path)
            
            # 更新页码下拉框
            self.page_spin.clear()
            for i in range(1, self.pdf_document.page_count + 1):
                self.page_spin.addItem(str(i))
                
            # 提取现有书签
            self.bookmarks = self._extract_bookmarks()
            self._update_bookmark_tree()
            
            QMessageBox.information(self, "成功", f"PDF文件已加载,共{self.pdf_document.page_count}页")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"加载PDF失败: {str(e)}")
            
    def _extract_bookmarks(self):
        """从PDF中提取书签"""
        bookmarks = []
        if self.pdf_document:
            try:
                toc = self.pdf_document.get_toc()
                for item in toc:
                    level, title, page = item
                    # 确保标题是Unicode字符串
                    if isinstance(title, bytes):
                        title = title.decode('utf-8', errors='replace')
                    bookmarks.append({
                        'level': level,
                        'title': title,
                        'page': page
                    })
            except Exception as e:
                QMessageBox.warning(self, "警告", f"提取书签时出错: {str(e)}")
        return bookmarks

        
    def _update_bookmark_tree(self):
        """更新书签树显示"""
        self.bookmark_tree.clear()
        
        # 创建根节点字典,用于构建树结构
        root_items = {}
        
        for bookmark in self.bookmarks:
            level = bookmark['level']
            title = bookmark['title']
            page = bookmark['page']
            
            item = QTreeWidgetItem([title, str(page)])
            
            # 根据层级确定父节点
            if level == 1:
                self.bookmark_tree.addTopLevelItem(item)
                root_items[title] = item
            else:
                # 查找父节点
                parent_level = level - 1
                parent_found = False
                
                # 从当前书签向前查找最近的上一级书签
                for i in range(self.bookmarks.index(bookmark) - 1, -1, -1):
                    if self.bookmarks[i]['level'] == parent_level:
                        parent_title = self.bookmarks[i]['title']
                        if parent_title in root_items:
                            root_items[parent_title].addChild(item)
                            parent_found = True
                            break
                
                # 如果没有找到父节点,添加到根节点
                if not parent_found:
                    self.bookmark_tree.addTopLevelItem(item)
                
                # 将当前节点添加到根节点字典
                root_items[title] = item
        
        # 展开所有节点
        self.bookmark_tree.expandAll()


    
    def on_bookmark_selected(self):
        """当书签被选中时更新编辑区域"""
        selected_items = self.bookmark_tree.selectedItems()
        if selected_items:
            item = selected_items[0]
            title = item.text(0)
            page = item.text(1)
            
            # 查找对应的书签
            for bookmark in self.bookmarks:
                if bookmark['title'] == title and str(bookmark['page']) == page:
                    self.title_edit.setText(title)
                    self.page_spin.setCurrentText(str(page))
                    self.level_spin.setCurrentText(str(bookmark['level']))
                    break
                    
    def add_bookmark(self):
        """添加新书签"""
        if not self.pdf_document:
            QMessageBox.warning(self, "错误", "请先加载PDF文件")
            return
            
        # 创建新书签
        new_bookmark = {
            'level': 1,
            'title': "新书签",
            'page': 1
        }
        
        self.bookmarks.append(new_bookmark)
        self._update_bookmark_tree()
        
        # 选中新添加的书签
        last_item = self.bookmark_tree.topLevelItem(self.bookmark_tree.topLevelItemCount() - 1)
        self.bookmark_tree.setCurrentItem(last_item)
        
    def edit_bookmark(self):
        """编辑选中的书签"""
        selected_items = self.bookmark_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "错误", "请先选择一个书签")
            return
            
        item = selected_items[0]
        title = item.text(0)
        page = item.text(1)
        
        # 查找对应的书签
        for bookmark in self.bookmarks:
            if bookmark['title'] == title and str(bookmark['page']) == page:
                # 更新编辑区域
                self.title_edit.setText(title)
                self.page_spin.setCurrentText(str(page))
                self.level_spin.setCurrentText(str(bookmark['level']))
                break
                
    def remove_bookmark(self):
        """删除选中的书签"""
        selected_items = self.bookmark_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "错误", "请先选择一个书签")
            return
            
        item = selected_items[0]
        title = item.text(0)
        page = item.text(1)
        
        # 查找并删除对应的书签
        for i, bookmark in enumerate(self.bookmarks):
            if bookmark['title'] == title and str(bookmark['page']) == page:
                del self.bookmarks[i]
                break
                
        self._update_bookmark_tree()
        
    def save_bookmark(self):
        """保存编辑后的书签"""
        selected_items = self.bookmark_tree.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "错误", "请先选择一个书签")
            return
            
        item = selected_items[0]
        old_title = item.text(0)
        old_page = item.text(1)
        
        # 获取新的书签信息
        new_title = self.title_edit.text()
        
        # 确保标题不为空
        if not new_title.strip():
            QMessageBox.warning(self, "错误", "书签标题不能为空")
            return
            
        new_page = int(self.page_spin.currentText())
        new_level = int(self.level_spin.currentText())
        
        # 更新书签
        for bookmark in self.bookmarks:
            if bookmark['title'] == old_title and str(bookmark['page']) == old_page:
                bookmark['title'] = new_title
                bookmark['page'] = new_page
                bookmark['level'] = new_level
                break
                
        self._update_bookmark_tree()
        QMessageBox.information(self, "成功", "书签已更新")



    def detect_headings(self):
        """检测PDF中的标题文本"""
        if not self.pdf_document:
            QMessageBox.warning(self, "错误", "请先加载PDF文件")
            return
            
        try:
            # 创建进度对话框
            progress = QProgressDialog("正在检测标题...", "取消", 0, self.pdf_document.page_count, self)
            progress.setWindowModality(Qt.WindowModal)
            
            detected_headings = []
            
            # 遍历每一页
            for page_num in range(self.pdf_document.page_count):
                if progress.wasCanceled():
                    break
                    
                progress.setValue(page_num)
                
                # 获取页面
                page = self.pdf_document[page_num]
                
                # 提取文本块
                blocks = page.get_text("dict")["blocks"]
                
                for block in blocks:
                    if "lines" in block:
                        for line in block["lines"]:
                            if "spans" in line:
                                for span in line["spans"]:
                                    # 检查是否可能是标题(字体大小较大或者是粗体)
                                    font_size = span["size"]
                                    font_flags = span["flags"]
                                    text = span["text"].strip()
                                    
                                    # 如果字体大于12或者是粗体,且文本不为空且长度合适
                                    if (font_size > 12 or (font_flags & 2)) and text and 1 < len(text) < 100:
                                        # 确定标题级别
                                        level = 1
                                        if font_size < 14:
                                            level = 3
                                        elif font_size < 16:
                                            level = 2
                                            
                                        # 添加到检测到的标题列表
                                        detected_headings.append({
                                            'level': level,
                                            'title': text,
                                            'page': page_num + 1  # 页码从1开始
                                        })
            
            progress.setValue(self.pdf_document.page_count)
            
            # 更新书签列表
            if detected_headings:
                # 询问用户是否替换现有书签
                if self.bookmarks:
                    reply = QMessageBox.question(self, "确认", 
                                            "是否用检测到的标题替换现有书签?",
                                            QMessageBox.Yes | QMessageBox.No)
                    if reply == QMessageBox.Yes:
                        self.bookmarks = detected_headings
                    else:
                        # 合并书签
                        self.bookmarks.extend(detected_headings)
                else:
                    self.bookmarks = detected_headings
                    
                self._update_bookmark_tree()
                QMessageBox.information(self, "成功", f"检测到{len(detected_headings)}个标题")
            else:
                QMessageBox.information(self, "结果", "未检测到标题")
                
        except Exception as e:
            QMessageBox.critical(self, "错误", f"检测标题失败: {str(e)}")
            

    def generate_toc(self):
        """生成目录页"""
        if not self.pdf_document or not self.bookmarks:
            QMessageBox.warning(self, "错误", "请先加载PDF文件并添加书签")
            return
            
        try:
            # 创建新的PDF文档作为目录页
            toc_doc = fitz.open()
            toc_page = toc_doc.new_page()
            
            # 设置支持中文的字体
            # 使用系统内置的中文字体,如果没有则使用通用字体
            font = "china-s" # 中文简体字体
            fallback_font = "fireflysung" # 备用中文字体
            
            # 尝试加载中文字体,如果失败则使用内置字体
            try:
                toc_page.insert_font(fontname=font)
            except:
                try:
                    toc_page.insert_font(fontname=fallback_font)
                    font = fallback_font
                except:
                    font = "helv" # 回退到默认字体
                    QMessageBox.warning(self, "警告", "未找到中文字体,可能导致中文显示不正确")
            
            font_size = 12
            title_font_size = 18
            
            # 添加目录标题
            toc_page.insert_text((50, 50), "目录", fontname=font, fontsize=title_font_size, color=(0, 0, 0))
            
            # 添加书签条目
            y = 80
            for bookmark in self.bookmarks:
                level = bookmark['level']
                title = bookmark['title']
                page = bookmark['page']
                
                # 根据层级缩进
                x = 50 + (level - 1) * 20
                
                # 插入文本,确保使用支持中文的字体
                toc_page.insert_text((x, y), f"{title} ................... {page}", 
                                fontname=font, fontsize=font_size, color=(0, 0, 0))
                
                y += 20
                
                # 如果超出页面,创建新页
                if y > 800:
                    toc_page = toc_doc.new_page()
                    y = 50
            
            # 保存目录页
            toc_path = os.path.splitext(self.pdf_path_edit.text())[0] + "_toc.pdf"
            toc_doc.save(toc_path)
            toc_doc.close()
            
            QMessageBox.information(self, "成功", f"目录已生成并保存至:\n{toc_path}")
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"生成目录失败: {str(e)}")


    def save_pdf_with_bookmarks(self):
        """保存带书签的PDF"""
        if not self.pdf_document or not self.bookmarks:
            QMessageBox.warning(self, "错误", "请先加载PDF文件并添加书签")
            return
            
        # 选择保存路径
        save_path, _ = QFileDialog.getSaveFileName(
            self, "保存带书签的PDF", 
            os.path.splitext(self.pdf_path_edit.text())[0] + "_bookmarked.pdf",
            "PDF文件 (*.pdf)")
            
        if not save_path:
            return
            
        try:
            # 准备目录结构
            toc = []
            for bookmark in self.bookmarks:
                toc.append([bookmark['level'], bookmark['title'], bookmark['page']])
            
            # 设置新的目录
            self.pdf_document.set_toc(toc)
            
            # 保存PDF
            self.pdf_document.save(save_path)
            
            QMessageBox.information(self, "成功", f"带书签的PDF已保存至:\n{save_path}")
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存PDF失败: {str(e)}")


    def merge_toc_with_pdf(self):
        """将生成的目录页合并到原PDF中"""
        if not self.pdf_document:
            QMessageBox.warning(self, "错误", "请先加载PDF文件")
            return
            
        # 检查目录文件是否存在
        toc_path = os.path.splitext(self.pdf_path_edit.text())[0] + "_toc.pdf"
        if not os.path.exists(toc_path):
            QMessageBox.warning(self, "错误", "请先生成目录")
            return
            
        # 选择保存路径
        save_path, _ = QFileDialog.getSaveFileName(
            self, "保存带目录的PDF", 
            os.path.splitext(self.pdf_path_edit.text())[0] + "_with_toc.pdf",
            "PDF文件 (*.pdf)")
            
        if not save_path:
            return
            
        try:
            # 打开目录文件
            toc_doc = fitz.open(toc_path)
            
            # 创建新文档
            new_doc = fitz.open()
            
            # 首先添加目录页
            for page_num in range(toc_doc.page_count):
                new_doc.insert_pdf(toc_doc, from_page=page_num, to_page=page_num)
                
            # 然后添加原PDF的所有页面
            for page_num in range(self.pdf_document.page_count):
                new_doc.insert_pdf(self.pdf_document, from_page=page_num, to_page=page_num)
                
            # 更新书签页码(加上目录页数)
            toc = []
            for bookmark in self.bookmarks:
                level = bookmark['level']
                title = bookmark['title']
                # 确保标题是Unicode字符串
                if isinstance(title, bytes):
                    title = title.decode('utf-8', errors='replace')
                page = bookmark['page'] + toc_doc.page_count
                toc.append([level, title, page])
                
            # 设置新的目录
            new_doc.set_toc(toc)
            
            # 保存PDF
            new_doc.save(save_path)
            
            # 关闭文档
            toc_doc.close()
            new_doc.close()
            
            QMessageBox.information(self, "成功", f"带目录的PDF已保存至:\n{save_path}")
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"合并目录失败: {str(e)}")





# 主窗口类,整合所有功能模块
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PDF工具箱 version-20250515")
        self.setGeometry(100, 100, 900, 600)
        
        # 创建标签页控件
        self.tabs = QTabWidget()
        self.setCentralWidget(self.tabs)
        
        # 添加PDF密码解除与加密功能
        pdf_tools_tab = PDFToolsUI()
        self.tabs.addTab(pdf_tools_tab, "PDF密码工具")

        # 添加PDF转图片功能
        pdf_to_image_tab = PdfToolApp()
        self.tabs.addTab(pdf_to_image_tab, "PDF转图片")
        
        # 添加图片转PDF功能
        image_to_pdf_tab = ImageToPDFConverter()
        self.tabs.addTab(image_to_pdf_tab, "图片转PDF")

        # 添加PDF书签和目录功能
        pdf_bookmark_tab = PDFBookmarkManager()
        self.tabs.addTab(pdf_bookmark_tab, "PDF书签管理")
        
        



# 主函数
if __name__ == '__main__':
    # 高DPI设置
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
        
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())