PyQt6中PDF文本选择、高亮与拖放功能实现方案求助
问题
使用PyQt6开发桌面应用,需实现PDF渲染、文本选择、高亮及拖放功能。已完成pdfview2.ui转pdfview2.py,并编写pdfviewController.py处理相关逻辑。当前采用QPdfView+MuPDF组合遇到以下问题:
- QPdfView可流畅渲染PDF,但页面以图片形式输出,无法通过鼠标或坐标选中文本
- 若改用MuPDF提取文本后重新渲染,会破坏PDF原有格式
需求:像Microsoft Edge或Adobe Acrobat那样,保留PDF原格式的同时实现精准文本选择、高亮及拖放功能。
用户提供的现有代码片段:
import sys import os import fitz # PyMuPDF from PyQt6.QtWidgets import QMainWindow, QScrollArea, QGraphicsRectItem,QFileDialog, QMessageBox, QToolBar, QPushButton, QApplication, QWidget, QVBoxLayout from PyQt6.QtCore import Qt, QSize, QMimeData, QPoint, QRectF, QSizeF, QPointF from PyQt6.QtGui import QColor, QDrag, QPainter, QPen, QBrush, QImage, QPixmap from PyQt6.QtPdfWidgets import QPdfView from PyQt6.QtPdf import QPdfDocument from Views.pdfview2 import Ui_Pdfviewpane # Assuming you have this setup class PDFViewController(QMainWindow, Ui_Pdfviewpane): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.pdfDocument = QPdfDocument(self) self.pdfView.setDocument(self.pdfDocument) self.pushButton.clicked.connect(self.upload_file) self.add_toolbar() # Initialize highlight color self.highlight_color = QColor("yellow") self.pdfView.setPageMode(QPdfView.PageMode.SinglePage) # Initialize selection variables self.selection_start = None self.selection_end = None self.current_selection_rect = None self.highlighted_rects = [] # Initialize MuPDF document self.mupdf_document = None # Set up the text cursor self.pdfView.setCursor(Qt.CursorShape.IBeamCursor) # Connect mouse events to custom handlers self.pdfView.viewport().installEventFilter(self) self.pdfView.viewport().setMouseTracking(True) def add_toolbar(self): navtb = QToolBar("Navigation") navtb.setIconSize(QSize(16, 16)) upload_file_btn = QPushButton("Upload File") navtb.addWidget(upload_file_btn) upload_file_btn.clicked.connect(self.upload_file) highlight_yellow_btn = QPushButton("Highlight Yellow") navtb.addWidget(highlight_yellow_btn) highlight_yellow_btn.clicked.connect(lambda: self.set_highlight_color('yellow')) highlight_green_btn = QPushButton("Highlight Green") navtb.addWidget(highlight_green_btn) highlight_green_btn.clicked.connect(lambda: self.set_highlight_color('green')) highlight_blue_btn = QPushButton("Highlight Blue") navtb.addWidget(highlight_blue_btn) highlight_blue_btn.clicked.connect(lambda: self.set_highlight_color('blue')) self.addToolBar(navtb) def set_highlight_color(self, color): self.highlight_color = QColor(color) def upload_file(self): options = QFileDialog.Option.ReadOnly file_path, _ = QFileDialog.getOpenFileName(self, 'Open File', os.getenv('HOME'), options=options) if file_path: print(f"Selected file: {file_path}") file_extension = os.path.splitext(file_path)[1].lower() if file_extension == '.pdf': self.render_pdf(file_path) self.load_mupdf_document(file_path) else: QMessageBox.warning(self, "Extension Not Supported", f"The file extension '{file_extension}' is not supported.") else: print("No file selected") def render_pdf(self, file_path): try: self.pdfDocument.load(file_path) print(f"PDF rendered successfully: {file_path}") self.pdfView.setPageMode(QPdfView.PageMode.MultiPage) except Exception as e: print(f"Failed to render PDF: {e}") QMessageBox.critical(self, "Error", f"Failed to render PDF: {e}") def load_mupdf_document(self, file_path): try: self.mupdf_document = fitz.open(file_path) print(f"MuPDF document loaded successfully: {file_path}") except Exception as e: print(f"Failed to load MuPDF document: {e}") QMessageBox.critical(self, "Error", f"Failed to load MuPDF document: {e}") def eventFilter(self, source, event): if source == self.pdfView.viewport(): if event.type() == event.Type.MouseButtonPress: return self.mousePressEvent(event) elif event.type() == event.Type.MouseMove: return self.mouseMoveEvent(event) elif event.type() == event.Type.MouseButtonRelease: return self.mouseReleaseEvent(event) return super().eventFilter(source, event) def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.selection_start = event.pos() self.selection_end = None self.current_selection_rect = QRectF(self.selection_start, QSize()) self.pdfView.viewport().update() return True return False def mouseMoveEvent(self, event): if event.buttons() & Qt.MouseButton.LeftButton: self.selection_end = event.pos() self.current_selection_rect = QRectF(self.selection_start, self.selection_end).normalized() self.pdfView.viewport().update() return True return False def mouseReleaseEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.finalize_selection() return True return False def paintEvent(self, event): painter = QPainter(self.pdfView.viewport()) if self.current_selection_rect: pen = QPen(QColor(0, 0, 255, 100), 2, Qt.PenStyle.DashLine) brush = QBrush(QColor(255, 255, 0, 50)) painter.setPen(pen) painter.setBrush(brush) painter.drawRect(self.current_selection_rect) super().paintEvent(event) def finalize_selection(self): if self.selection_start and self.selection_end: selection_rect = QRectF(self.selection_start, self.selection_end).normalized() highlight_rect = QGraphicsRectItem(selection_rect) highlight_rect.setBrush(QColor(255, 255, 0, 100)) # semi-transparent yellow self.highlighted_rects.append(highlight_rect) self.pdfView.viewport().update() if self.mupdf_document: page = self.mupdf_document.load_page(0) # Load the first page for simplicity text = page.get_text("text", clip=fitz.Rect(selection_rect.left(), selection_rect.top(), selection_rect.right(), selection_rect.bottom())) print("Selected text:", text.strip()) self.drag_text(text.strip()) def drag_text(self, text): drag = QDrag(self) mime_data = QMimeData() mime_data.setText(text) drag.setMimeData(mime_data) drag.exec_(Qt.DropAction.CopyAction)
解决方案
核心思路:保留QPdfView的原格式渲染能力,用MuPDF做文本坐标定位,通过视图坐标与PDF页面坐标的映射实现精准文本选择和高亮。
1. 添加坐标转换工具函数
解决Qt(左上角原点)与MuPDF(左下角原点)的坐标差异,将视图坐标转换为PDF页面的实际坐标:
def view_to_pdf_coords(self, view_pos): # 获取当前激活页面 current_page = self.pdfView.currentPage() if current_page < 0: return -1, QPointF() # 获取页面在视图中的变换矩阵 transform = self.pdfView.viewportTransform() inverted_transform, ok = transform.inverted() if not ok: return -1, QPointF() # 转换为PDF页面坐标(Qt原点在左上角) pdf_pos = inverted_transform.map(view_pos) # 转换为MuPDF坐标(原点在左下角) page_rect = self.pdfDocument.pageRect(current_page) mupdf_y = page_rect.height() - pdf_pos.y() return current_page, QPointF(pdf_pos.x(), mupdf_y)
2. 修正文本选择与高亮逻辑
替换原finalize_selection方法,基于MuPDF提取的文本边界绘制高亮,而非直接使用鼠标选区:
def finalize_selection(self): if not (self.selection_start and self.selection_end): return # 转换选区坐标到PDF页面 start_page, start_pdf_pos = self.view_to_pdf_coords(self.selection_start) end_page, end_pdf_pos = self.view_to_pdf_coords(self.selection_end) # 暂不支持跨页选择(可按需扩展) if start_page != end_page or start_page == -1: QMessageBox.warning(self, "提示", "暂不支持跨页文本选择") self.current_selection_rect = None self.pdfView.viewport().update() return # 构建MuPDF选区矩形 mupdf_rect = fitz.Rect( min(start_pdf_pos.x(), end_pdf_pos.x()), min(start_pdf_pos.y(), end_pdf_pos.y()), max(start_pdf_pos.x(), end_pdf_pos.x()), max(start_pdf_pos.y(), end_pdf_pos.y()) ) # 提取选中文本及每个单词的边界 page = self.mupdf_document.load_page(start_page) text_blocks = page.get_text("words", clip=mupdf_rect) if not text_blocks: self.current_selection_rect = None self.pdfView.viewport().update() return selected_text = " ".join([block[4] for block in text_blocks]) # 将文本边界转换回视图坐标,用于高亮绘制 highlight_regions = [] page_rect_qt = self.pdfDocument.pageRect(start_page) transform = self.pdfView.viewportTransform() for block in text_blocks: # MuPDF边界格式:x0,y0,x1,y1(左下角原点) block_rect_mupdf = fitz.Rect(block[:4]) # 转换为Qt左上角原点坐标 qt_y = page_rect_qt.height() - block_rect_mupdf.y1 block_rect_qt = QRectF( block_rect_mupdf.x0, qt_y, block_rect_mupdf.width(), block_rect_mupdf.height() ) # 转换为视图坐标 view_rect = transform.mapRect(block_rect_qt) highlight_regions.append((view_rect, self.highlight_color)) # 保存高亮区域并刷新视图 self.highlighted_rects.extend(highlight_regions) self.current_selection_rect = None self.pdfView.viewport().update() # 触发文本拖放 self.drag_text(selected_text.strip())
3. 修正高亮绘制逻辑
更新paintEvent方法,绘制所有保存的高亮区域:
def paintEvent(self, event): painter = QPainter(self.pdfView.viewport()) # 绘制临时选区 if self.current_selection_rect: pen = QPen(QColor(0, 0, 255, 100), 2, Qt.PenStyle.DashLine) brush = QBrush(QColor(255, 255, 0, 50)) painter.setPen(pen) painter.setBrush(brush) painter.drawRect(self.current_selection_rect) # 绘制永久高亮区域 for rect, color in self.highlighted_rects: brush = QBrush(QColor(color.red(), color.green(), color.blue(), 100)) painter.setBrush(brush) painter.setPen(Qt.PenStyle.NoPen) painter.drawRect(rect) super().paintEvent(event)
关键说明
- 格式保留:QPdfView负责渲染PDF原格式,MuPDF仅做文本数据提取,两者互不干扰
- 精准选择:通过坐标映射和MuPDF的文本边界提取,实现贴合实际文本的高亮效果,而非粗糙的矩形选区
- 可扩展性:可快速扩展支持跨页选择、高亮保存/加载、文本搜索定位等功能
内容的提问来源于stack exchange,提问作者Suraj Paikekar
相关产品推荐
相关产品推荐

