2025-03-27 14:08:10 +01:00

315 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interface de visualisation et de sélection dans les documents PDF
"""
import os
import sys
from PyQt6.QtWidgets import (QMainWindow, QFileDialog, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QComboBox, QWidget, QScrollArea,
QSpinBox, QFrame, QSplitter, QGroupBox, QStatusBar)
from PyQt6.QtCore import Qt, QRectF, QPoint, pyqtSignal
from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor, QImage
from .llm_config_panel import LLMConfigPanel
import fitz # PyMuPDF
class PDFViewer(QMainWindow):
"""Interface principale pour la visualisation et l'annotation de PDFs"""
def __init__(self):
super().__init__()
self.pdf_document = None
self.current_page = 0
self.total_pages = 0
self.zoom_factor = 1.0
self.selection_rect = None
self.selection_start = None
self.selection_active = False
self.selected_regions = [] # Liste des zones sélectionnées
self.init_ui()
def init_ui(self):
"""Initialise l'interface utilisateur"""
self.setWindowTitle("Prétraitement PDF pour Ragflow")
self.setGeometry(100, 100, 1200, 800)
# Barre d'état
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("Prêt")
# Widget principal et layout
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QHBoxLayout(main_widget)
# Splitter pour séparer la visualisation PDF et le panneau de configuration
splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(splitter)
# =================== Partie gauche: Visualisation PDF ====================
pdf_panel = QWidget()
pdf_layout = QVBoxLayout(pdf_panel)
# Barre d'outils pour le PDF
toolbar = QWidget()
toolbar_layout = QHBoxLayout(toolbar)
toolbar_layout.setContentsMargins(0, 0, 0, 0)
# Bouton pour charger un PDF
self.load_btn = QPushButton("Charger PDF")
self.load_btn.clicked.connect(self.load_pdf)
toolbar_layout.addWidget(self.load_btn)
# Navigation
self.prev_btn = QPushButton("Page précédente")
self.prev_btn.clicked.connect(self.prev_page)
self.prev_btn.setEnabled(False)
toolbar_layout.addWidget(self.prev_btn)
self.page_spin = QSpinBox()
self.page_spin.setMinimum(1)
self.page_spin.setMaximum(1)
self.page_spin.valueChanged.connect(self.go_to_page)
toolbar_layout.addWidget(self.page_spin)
self.page_count_label = QLabel(" / 1")
toolbar_layout.addWidget(self.page_count_label)
self.next_btn = QPushButton("Page suivante")
self.next_btn.clicked.connect(self.next_page)
self.next_btn.setEnabled(False)
toolbar_layout.addWidget(self.next_btn)
# Zoom
self.zoom_in_btn = QPushButton("Zoom +")
self.zoom_in_btn.clicked.connect(self.zoom_in)
toolbar_layout.addWidget(self.zoom_in_btn)
self.zoom_out_btn = QPushButton("Zoom -")
self.zoom_out_btn.clicked.connect(self.zoom_out)
toolbar_layout.addWidget(self.zoom_out_btn)
# Bouton pour effacer la sélection
self.clear_sel_btn = QPushButton("Effacer sélection")
self.clear_sel_btn.clicked.connect(self.clear_selection)
toolbar_layout.addWidget(self.clear_sel_btn)
pdf_layout.addWidget(toolbar)
# Zone de visualisation du PDF
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.pdf_label = PDFLabel(self)
self.scroll_area.setWidget(self.pdf_label)
pdf_layout.addWidget(self.scroll_area)
# =================== Partie droite: Panel de configuration ====================
self.config_panel = LLMConfigPanel(self)
# Ajout des deux panneaux au splitter
splitter.addWidget(pdf_panel)
splitter.addWidget(self.config_panel)
# Définir les proportions de départ du splitter
splitter.setSizes([700, 500])
def load_pdf(self):
"""Ouvre un dialogue pour charger un fichier PDF"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Ouvrir un fichier PDF", "", "Fichiers PDF (*.pdf)"
)
if file_path:
try:
self.pdf_document = fitz.open(file_path)
self.total_pages = len(self.pdf_document)
self.current_page = 0
# Mise à jour de l'interface
self.page_spin.setMaximum(self.total_pages)
self.page_count_label.setText(f" / {self.total_pages}")
self.page_spin.setValue(1) # Cette action déclenche go_to_page()
self.prev_btn.setEnabled(True)
self.next_btn.setEnabled(True)
self.render_current_page()
# Mise à jour du titre avec le nom du fichier
file_name = os.path.basename(file_path)
self.setWindowTitle(f"Prétraitement PDF - {file_name}")
self.status_bar.showMessage(f"PDF chargé: {file_name}, {self.total_pages} pages")
except Exception as e:
self.status_bar.showMessage(f"Erreur lors du chargement du PDF: {str(e)}")
def render_current_page(self):
"""Affiche la page courante du PDF"""
if not self.pdf_document:
return
# Récupérer la page actuelle
page = self.pdf_document[self.current_page]
# Facteur de zoom et de qualité pour le rendu
zoom_matrix = fitz.Matrix(2 * self.zoom_factor, 2 * self.zoom_factor)
# Rendu de la page en un pixmap
pixmap = page.get_pixmap(matrix=zoom_matrix, alpha=False)
# Conversion en QImage puis QPixmap
img = QImage(pixmap.samples, pixmap.width, pixmap.height,
pixmap.stride, QImage.Format.Format_RGB888)
pixmap_qt = QPixmap.fromImage(img)
# Mise à jour de l'affichage
self.pdf_label.setPixmap(pixmap_qt)
self.pdf_label.adjustSize()
# Mise à jour de l'état
self.status_bar.showMessage(f"Page {self.current_page + 1}/{self.total_pages}")
def next_page(self):
"""Passe à la page suivante"""
if self.pdf_document and self.current_page < self.total_pages - 1:
self.current_page += 1
self.page_spin.setValue(self.current_page + 1)
def prev_page(self):
"""Passe à la page précédente"""
if self.pdf_document and self.current_page > 0:
self.current_page -= 1
self.page_spin.setValue(self.current_page + 1)
def go_to_page(self, page_num):
"""Va à une page spécifique"""
if self.pdf_document and 1 <= page_num <= self.total_pages:
self.current_page = page_num - 1
self.render_current_page()
def zoom_in(self):
"""Augmente le facteur de zoom"""
self.zoom_factor *= 1.25
self.render_current_page()
def zoom_out(self):
"""Diminue le facteur de zoom"""
self.zoom_factor *= 0.8
self.render_current_page()
def clear_selection(self):
"""Efface la sélection actuelle"""
self.selection_rect = None
self.selection_active = False
self.selection_start = None
self.pdf_label.update()
def add_selection(self, rect, page_num=None):
"""
Ajoute une sélection à la liste des régions sélectionnées
Args:
rect (QRectF): Rectangle de sélection
page_num (int, optional): Numéro de page. Si None, utilise la page courante.
"""
page = self.current_page if page_num is None else page_num
# Ajuster les coordonnées selon le zoom actuel
adjusted_rect = QRectF(
rect.x() / self.zoom_factor,
rect.y() / self.zoom_factor,
rect.width() / self.zoom_factor,
rect.height() / self.zoom_factor
)
selection = {
"page": page,
"rect": adjusted_rect,
"type": "schéma", # Type par défaut
"context": ""
}
self.selected_regions.append(selection)
self.config_panel.update_selection_list()
# Informer l'utilisateur
self.status_bar.showMessage(f"Sélection ajoutée à la page {page + 1}")
class PDFLabel(QLabel):
"""Étiquette personnalisée pour afficher le PDF et gérer les sélections"""
selection_made = pyqtSignal(QRectF)
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Pour permettre de suivre les événements de souris
self.setMouseTracking(True)
def mousePressEvent(self, event):
"""Gère l'événement de clic de souris pour démarrer une sélection"""
if event.button() == Qt.MouseButton.LeftButton:
self.parent.selection_start = event.position()
self.parent.selection_active = True
self.parent.selection_rect = QRectF(event.position(), event.position())
def mouseMoveEvent(self, event):
"""Gère l'événement de déplacement de souris pour mettre à jour la sélection"""
if self.parent.selection_active:
# Mise à jour du rectangle de sélection
self.parent.selection_rect = QRectF(
self.parent.selection_start,
event.position()
).normalized()
# Demande de mise à jour de l'affichage
self.update()
def mouseReleaseEvent(self, event):
"""Gère l'événement de relâchement de souris pour finaliser une sélection"""
if event.button() == Qt.MouseButton.LeftButton and self.parent.selection_active:
# Finalisation de la sélection
self.parent.selection_active = False
# Vérifier que la sélection a une taille minimale
if (self.parent.selection_rect.width() > 10 and
self.parent.selection_rect.height() > 10):
# Ajouter cette sélection à la liste des sélections
self.parent.add_selection(self.parent.selection_rect)
# Émettre le signal de sélection
self.selection_made.emit(self.parent.selection_rect)
# Continuer à afficher le rectangle
self.update()
def paintEvent(self, event):
"""Surcharge pour dessiner la sélection par-dessus le PDF"""
super().paintEvent(event)
# Dessiner le rectangle de sélection si disponible
if self.parent.selection_rect is not None:
painter = QPainter(self)
# Paramètres pour le rectangle de sélection
pen = QPen(QColor(255, 0, 0)) # Rouge
pen.setWidth(2)
pen.setStyle(Qt.PenStyle.DashLine)
painter.setPen(pen)
# Dessiner avec une légère transparence
painter.setOpacity(0.7)
painter.drawRect(self.parent.selection_rect)