"""Widget de visualisation PDF avec pypdfium2 — V2.""" from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QSizePolicy, QApplication) from PyQt6.QtGui import QImage, QPixmap, QWheelEvent from PyQt6.QtCore import Qt, pyqtSignal, QTimer import pypdfium2 as pdfium class PdfViewer(QWidget): """Widget d'apercu PDF avec zoom molette et navigation fluide.""" page_changed = pyqtSignal(int, int) # page_num (0-based), total_pages def __init__(self, parent=None): super().__init__(parent) self._pdf = None self._pdf_path = None self._current_page = 0 self._total_pages = 0 self._zoom = 1.0 self._setup_ui() def _setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._scroll = QScrollArea() self._scroll.setWidgetResizable(True) self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter) self._scroll.setStyleSheet("QScrollArea { background: #1e1e1e; border: none; }") self._image_label = QLabel() self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored) self._image_label.setStyleSheet("background: #1e1e1e;") self._scroll.setWidget(self._image_label) # Intercepter la molette sur le scroll area self._scroll.wheelEvent = self._on_wheel layout.addWidget(self._scroll, 1) def _on_wheel(self, event: QWheelEvent): """Molette = pages, Ctrl+Molette = zoom.""" if event.modifiers() & Qt.KeyboardModifier.ControlModifier: delta = event.angleDelta().y() if delta > 0: self._zoom = min(self._zoom * 1.15, 5.0) else: self._zoom = max(self._zoom / 1.15, 0.2) self._render_page() else: delta = event.angleDelta().y() if delta < 0: self.next_page() elif delta > 0: self.prev_page() def load_pdf(self, path): """Charger un fichier PDF.""" try: self._pdf_path = path self._pdf = pdfium.PdfDocument(path) self._total_pages = len(self._pdf) self._current_page = 0 self._zoom_fit() self._render_page() except Exception as e: self._image_label.setText(f"Erreur: {e}") def load_pdf_bytes(self, data): """Charger un PDF depuis des bytes.""" try: self._pdf = pdfium.PdfDocument(data) self._total_pages = len(self._pdf) self._current_page = 0 self._zoom_fit() self._render_page() except Exception as e: self._image_label.setText(f"Erreur: {e}") def _render_page(self): """Afficher la page courante.""" if not self._pdf or self._current_page >= self._total_pages: return page = self._pdf[self._current_page] scale = 150 / 72 * self._zoom bitmap = page.render(scale=scale) pil_image = bitmap.to_pil() data = pil_image.tobytes("raw", "RGBA") qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888) pixmap = QPixmap.fromImage(qimage) self._image_label.setPixmap(pixmap) self._image_label.adjustSize() self.page_changed.emit(self._current_page, self._total_pages) def _zoom_fit(self): """Ajuster le zoom pour que la page tienne dans la zone visible.""" if not self._pdf or self._current_page >= self._total_pages: return page = self._pdf[self._current_page] pw, ph = page.get_size() vw = self._scroll.viewport().width() - 20 vh = self._scroll.viewport().height() - 20 if vw <= 0 or vh <= 0: self._zoom = 1.0 return scale_x = vw / pw scale_y = vh / ph self._zoom = min(scale_x, scale_y) * (72 / 150) def prev_page(self): if self._current_page > 0: self._current_page -= 1 self._render_page() def next_page(self): if self._current_page < self._total_pages - 1: self._current_page += 1 self._render_page() def go_to_page(self, page_index): if 0 <= page_index < self._total_pages and page_index != self._current_page: self._current_page = page_index self._render_page() def zoom_in(self): self._zoom = min(self._zoom * 1.25, 5.0) self._render_page() def zoom_out(self): self._zoom = max(self._zoom / 1.25, 0.2) self._render_page() def zoom_fit(self): self._zoom_fit() self._render_page() def refresh(self): self._render_page() def get_page_thumbnail(self, page_index, width=120): """Generer une miniature pour le panneau de vignettes.""" if not self._pdf or page_index >= self._total_pages: return None page = self._pdf[page_index] pw, ph = page.get_size() scale = width / pw bitmap = page.render(scale=scale) pil_image = bitmap.to_pil() data = pil_image.tobytes("raw", "RGBA") qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888) return QPixmap.fromImage(qimage) @property def current_page(self): return self._current_page @property def total_pages(self): return self._total_pages @property def zoom_level(self): return self._zoom @property def pdf_path(self): return self._pdf_path