一个PDF合并器

效果

直接上代码


from PySide6.QtWidgets import (
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QVBoxLayout,
    QApplication,
    QWidget,
    QLabel,
    QScrollArea,
)
from PySide6.QtCore import Signal
from PySide6.QtCore import Qt, QSize, Signal,  QMimeData, QPoint, QRect, QUrl
from PySide6.QtGui import QPixmap, QPainter, QImage, QDrag

import subprocess
import pathlib
import fitz #pip install PyMuPdf
import weakref
import qdarktheme


class PDFInfo:
    def __init__(self, path: pathlib.Path):
        self.path: pathlib.Path = path
        self.pixmap: QPixmap = None  # type: ignore
        self.page_count: int = 0

    def load_pixmap(self):
        if not self.pixmap:
            doc = fitz.open(self.path)
            self.page_count = len(doc)
            page = doc.load_page(0)
            pm = page.get_pixmap()  # type: ignore
            imgfmt = (
                QImage.Format.Format_RGBA8888
                if pm.alpha
                else QImage.Format.Format_RGB888
            )
            self.pixmap = QPixmap.fromImage(
                QImage(pm.samples, pm.width, pm.height, pm.stride, imgfmt)
            )
        return self.pixmap

    def word(self, ind: int):
        return f"{ind}: 共{self.page_count}页"



class card(QGroupBox):
    doubleClicked = Signal(int)
    cardDropped = Signal(int, int)  # 参数1:源card索引, 参数2:目标card索引

    def __init__(self, parent):
        super().__init__(parent)
        self.pp = weakref.ref(parent)
        self.pdf_info: PDFInfo = None  # type: ignore
        self.setAcceptDrops(True)

    def supportedDropActions(self):
        return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction

    def setui(self):
        self.lb = QLabel()
        self.lb.setScaledContents(True)
        hbox = QHBoxLayout()
        self.setLayout(hbox)
        hbox.addWidget(self.lb)
        self.setFixedSize(QSize(200, 160))

        return self

    def setdata(self, pdf_info: PDFInfo):
        self.pdf_info = pdf_info
        if self.pdf_info.page_count > 1:
            self.setProperty("muti", True)
        else:
            self.setProperty("muti", False)
        # self.setTitle(f"{pdf_info.path.stem}: 共{pdf_info.page_count}页")

    def mouseDoubleClickEvent(self, event):
        if self.pdf_info:
            self.doubleClicked.emit(self.pdf_info.path)
        super().mouseDoubleClickEvent(event)

    def mousePressEvent(self, event):
        self.dragStartPosition = event.position().toPoint()
        super().mousePressEvent(event)

    def mime_encode(self):

        mime = QMimeData()
        # 设置自定义格式的拖动数据(传入当前card在box中的索引)
        main_window = self.pp()

        if isinstance(main_window, box):
            card_index = main_window.cards.index(self)
            mime.setData("application/x-item", str(card_index).encode("utf-8"))

        return mime

    def mime_decode(self, mime: QMimeData):
        if mime.hasFormat("application/x-item"):
            return int(mime.data("application/x-item").data())
        else:
            return None

    def mouseMoveEvent(self, event):
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return
        offset = (event.position().toPoint() - self.dragStartPosition).manhattanLength()
        if offset < QApplication.startDragDistance():
            # 记录当前card的索引
            mime = self.mime_encode()

            drag = QDrag(self)
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)
            # 设置拖动预览图像
            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)
            drag.setHotSpot(event.position().toPoint())
            drag.exec()

    def dragEnterEvent(self, event):
        mime = event.mimeData()
        if mime.hasFormat("application/x-item"):
            event.acceptProposedAction()

    def dropEvent(self, event):
        s = self.mime_decode(event.mimeData())
        if s is not None:
            parent = self.pp()
            if parent and isinstance(parent, box):
                target_index = parent.cards.index(self)
                if s != target_index and s != target_index - 1:
                    self.cardDropped.emit(s, target_index)
        event.accept()

    def paintEvent(self, event):
        super().paintEvent(event)
        if not self.pdf_info:
            return

        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 设置字体
        font = painter.font()
        font.setPointSize(8)
        painter.setFont(font)

        # 设置文本颜色
        cl = painter.background().color()
        painter.setPen(cl)
        painter.setBrush(cl)

        # 获取文本内容
        text = self.pdf_info.path.name

        # 计算文本绘制位置
        text_rect = painter.fontMetrics().boundingRect(text)
        if text_rect.width() > self.width() - 20:
            text = text[:18] + "..."
            text_rect = painter.fontMetrics().boundingRect(text)
        x = (self.width() - text_rect.width()) // 2
        y = self.height() - 5
        d = 3
        r2 = QRect(
            x - d, y - 10 - d, text_rect.width() + 2 * d, text_rect.height() + 2 * d
        )
        painter.drawRect(r2)
        painter.setPen(Qt.GlobalColor.black)
        painter.setBrush(Qt.BrushStyle.NoBrush)
        # 绘制文本
        painter.drawText(x, y, text)


class EndLabel(QLabel):
    cardDropped = Signal(int)

    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("background-color: lightgray; border: 1px solid gray;")
        self.setFixedSize(200, 150)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        mime = event.mimeData()
        if mime.hasFormat("application/x-item"):
            event.acceptProposedAction()

    def dropEvent(self, event):
        mime = event.mimeData()
        if mime.hasFormat("application/x-item"):
            source_index = int(mime.data("application/x-item").data())
            self.cardDropped.emit(source_index)
        event.accept()


class c1(QLabel):
    sg_dropped = Signal(list)

    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setFixedSize(200, 100)
        self.setProperty("level", "funcCard1")
        self.setAcceptDrops(True)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

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

    def dropEvent(self, event):
        urls = event.mimeData().urls()
        if urls:
            ret = []
            for file in urls:
                file_path = file.toLocalFile()
                if file_path.lower().endswith(".pdf"):
                    ret.append(pathlib.Path(file_path).absolute())
            if ret:
                self.sg_dropped.emit(ret)

        event.accept()


class c2(QLabel):
    sg_dropped = Signal(int)

    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setFixedSize(200, 100)
        self.setProperty("level", "funcCard2")
        self.setAcceptDrops(True)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

    def dragEnterEvent(self, event):
        mime = event.mimeData()
        if mime.hasFormat("application/x-item"):
            event.acceptProposedAction()

    def dropEvent(self, event):
        mime = event.mimeData()
        if mime.hasFormat("application/x-item"):
            source_index = int(mime.data("application/x-item").data())
            self.sg_dropped.emit(source_index)
        event.accept()


class c3(QLabel):
    sg_dropped = Signal(int)

    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setFixedSize(200, 100)
        self.setProperty("level", "funcCard3")
        self.setAcceptDrops(True)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.temp_pdf = None
        self.dragStartPosition = QPoint()

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.dragStartPosition = event.position().toPoint()
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return

        # 检查拖动距离是否足够
        if (
            event.position().toPoint() - self.dragStartPosition
        ).manhattanLength() < QApplication.startDragDistance():
            return

        try:
            # 合并所有PDF文件
            output_path = pathlib.Path("temp.pdf").absolute()
            doc = fitz.open()

            parent = self.parent()
            if isinstance(parent, mainw) and parent.w1.cards:
                for card in parent.w1.cards:
                    if hasattr(card, "pdf_info"):
                        try:
                            src_doc = fitz.open(card.pdf_info.path)
                            doc.insert_pdf(src_doc)
                            src_doc.close()
                        except Exception as e:
                            print(f"Error processing {card.pdf_info.path}: {e}")

                if len(doc) > 0:
                    doc.save(output_path)
                    print(f"PDF合并完成,保存到: {output_path}")
                    doc.close()
                    self.temp_pdf = output_path

                    # 创建拖拽操作
                    drag = QDrag(self)
                    mime = QMimeData()
                    urls = [QUrl.fromLocalFile(str(output_path))]
                    print(urls)
                    mime.setUrls(urls)
                    drag.setMimeData(mime)

                    # 设置拖拽预览图像
                    pixmap = QPixmap(self.size())
                    self.render(pixmap)
                    drag.setPixmap(pixmap)
                    drag.setHotSpot(event.position().toPoint())

                    # 执行拖拽操作
                    result = drag.exec(
                        Qt.DropAction.CopyAction | Qt.DropAction.MoveAction
                    )
                    print(f"拖拽操作结果: {result}")
                else:
                    print("没有可合并的PDF文件")
                    doc.close()
        except Exception as e:
            print(f"拖拽操作出错: {e}")


class box(QScrollArea):
    def __init__(self, parent=None):
        super().__init__(parent)
        # self.setAcceptDrops(True)
        self.cards: list[card] = []


        # 创建容器widget和布局
        self.container = QWidget()
        self.grid = QGridLayout(self.container)
        self.container.setLayout(self.grid)

        # 设置滚动区域属性
        self.setWidgetResizable(True)
        self.setWidget(self.container)
        self.setMinimumSize(450, 350)  # 设置最小窗口尺寸

        # 添加始终显示在末尾的特殊EndLabel
        self.end_label = EndLabel("拖到此处移动到最后", self.container)
        self.end_label.cardDropped.connect(self.move_card_to_end)

    def setui(self):
        # self.load_pdfs(r"C:\Users\Administrator\Desktop\ZJ\5.6")
        return self

    def load_pdfs(self, folder_path):
        pp = pathlib.Path(folder_path).absolute()
        for i, pdf_file in enumerate(pp.glob("*.pdf")):
            self.add_pdf_item(pdf_file, i)

    def add_pdf_item(self, pdf_path, pos):
        pdf_info = PDFInfo(pdf_path)
        pdf_info.load_pixmap()
        cd = card(self).setui()
        cd.setdata(pdf_info)
        cd.lb.setPixmap(pdf_info.load_pixmap())
        cd.doubleClicked.connect(self.on_item_double_clicked)
        cd.cardDropped.connect(self.move_card)

        row = pos // (self.width() // 200)
        col = pos % (self.width() // 200)
        self.grid.addWidget(cd, row, col)
        self.cards.append(cd)

    def resizeEvent(self, event):
        self._resize()
        super().resizeEvent(event)

    def _resize(self):
        self.rearrange_items()
        # 计算并设置高度: card高度*(行数+2)
        cols = max(1, self.width() // 200)
        rows = (len(self.cards) + cols - 1) // cols + 2  # 行数+2
        self.container.setFixedHeight(150 * rows)

    def rearrange_items(self):
        cols = max(1, self.width() // 200)
        # 清除布局
        while self.grid.count():
            item = self.grid.takeAt(0)
            if item.widget():
                item.widget().setParent(None)

        # 添加所有普通card
        cd: card
        for i, cd in enumerate(self.cards):
            row = i // cols
            col = i % cols
            self.grid.addWidget(cd, row, col)
            cd.setTitle(cd.pdf_info.word(i + 1))
        # 添加end_label到最后一行第一列
        total_items = len(self.cards)
        rows = (total_items + cols - 1) // cols
        self.grid.addWidget(self.end_label, rows, 0)

    def makepixmap(self, p: pathlib.Path):
        pone = fitz.open(p).load_page(0)
        pm = pone.get_pixmap()  # type: ignore
        imgfmt = (
            QImage.Format.Format_RGBA8888 if pm.alpha else QImage.Format.Format_RGB888
        )
        pimg = QImage(pm.samples, pm.width, pm.height, pm.stride, imgfmt)
        return QPixmap.fromImage(pimg)

    def move_card_to_end(self, source_index):

        if source_index != -1:

            source_card = self.cards.pop(source_index)
            self.cards.append(source_card)
            self.rearrange_items()

    def move_card(self, source_index, target_index):
        if source_index == target_index:
            return

        # 从原位置移除源card
        source_card = self.cards.pop(source_index)

        # 在目标位置插入源card
        if source_index < target_index:
            target_index -= 1
        self.cards.insert(target_index, source_card)

        # 重新排列布局
        self.rearrange_items()

    def on_item_double_clicked(self, index):
        print(f"Item {index} double clicked")
        pinfo:PDFInfo=self.cards[index].pdf_info
        cmd=['explorer',str(pinfo.path.absolute())]
        subprocess.Popen(cmd)


class mainw(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("ZLPDF-PDF合并器")
        ly = QVBoxLayout()
        self.w1 = box().setui()
        ly.addWidget(self.w1)
        hb = QHBoxLayout()
        self.c1 = c1("把要合并的PDF拖到我这里", self)
        self.c2 = c2("把要去掉的PDF拖动到我这里", self)
        self.c3 = c3("把合并后的文件从我这里拖走", self)
        for i in [self.c1, self.c2, self.c3]:
            hb.addWidget(i)
        ly.addLayout(hb)
        self.setLayout(ly)
        self.c1.sg_dropped.connect(self.addpdfs)
        self.c2.sg_dropped.connect(self.remove_pdf)

    def addpdfs(self, ll: list[pathlib.Path]):
        for pdf in ll:
            self.w1.add_pdf_item(pdf, len(self.w1.cards))
        self.w1._resize()

    def remove_pdf(self, index: int):
        """移除指定索引的PDF卡片"""
        if 0 <= index < len(self.w1.cards):
            # 从布局中移除widget
            item = self.w1.grid.takeAt(index)
            if item.widget():
                item.widget().deleteLater()
            # 从cards列表中移除
            self.w1.cards.pop(index)
            # 重新排列剩余卡片
            self.w1.rearrange_items()


sty = """
QLabel[level="funcCard1"]{border:3px solid orange;border-radius:10px;}
QLabel[level="funcCard2"]{border:3px solid red;border-radius:10px;}
QLabel[level="funcCard3"]{border:3px solid green;border-radius:10px;}
card{border:3px solid green;border-radius:6px;margin-bottom:5px;margin-top:8px;padding-top:-3px;}

card[muti='true']{border:3px solid orange;}
card::title{subcontrol-origin:margin;padding:0 3px;}
"""
app = QApplication()

qdarktheme.setup_theme("light")
mw = mainw()
mw.setStyleSheet(sty)
mw.show()
app.exec()

posted @ 2025-05-10 17:52  方头狮  阅读(12)  评论(0)    收藏  举报