- Nouveau layout : menus en haut, vignettes a gauche, viewer au centre - Panneau vignettes redimensionnable (splitter) - Miniatures de chaque page - Drag & drop pour reordonner - Clic droit : supprimer page, inserer page blanche - Clic = naviguer vers la page - Viewer PDF ameliore : - Ajustement auto a l'ouverture - Roulette = defiler les pages - Ctrl+roulette = zoom - Drag & drop de fichier PDF sur la fenetre - Barre de progression pendant les traitements - Fusion de PDF (Fichier > Fusionner) - Enlever les fonds perdus (Annotations > Enlever) - Menus par categorie : Imposition, Pages, Annotations - Toolbar : Ouvrir, Enregistrer, Zoom, info page - Style sombre coherent CopyDev - Suppression du numero de telephone dans A propos Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
"""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
|