V2.0.0 — Refonte interface complete

- 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>
This commit is contained in:
Jules
2026-03-21 07:49:44 +01:00
parent de3e83b57e
commit 9cc256ff51
5 changed files with 488 additions and 225 deletions

View File

@@ -1,20 +1,21 @@
"""Widget de visualisation PDF avec pypdfium2."""
"""Widget de visualisation PDF avec pypdfium2 — V2."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSpinBox, QScrollArea, QSizePolicy)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtCore import Qt, pyqtSignal
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 navigation et zoom."""
"""Widget d'apercu PDF avec zoom molette et navigation fluide."""
page_changed = pyqtSignal(int, int) # page_num, total_pages
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
@@ -23,73 +24,48 @@ class PdfViewer(QWidget):
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Zone d'affichage avec scroll
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)
# Barre de navigation
nav = QHBoxLayout()
self._btn_prev = QPushButton("<")
self._btn_prev.setFixedWidth(40)
self._btn_prev.clicked.connect(self._prev_page)
nav.addWidget(self._btn_prev)
self._page_spin = QSpinBox()
self._page_spin.setMinimum(1)
self._page_spin.setMaximum(1)
self._page_spin.valueChanged.connect(self._go_to_page)
nav.addWidget(self._page_spin)
self._label_total = QLabel("/ 0")
nav.addWidget(self._label_total)
self._btn_next = QPushButton(">")
self._btn_next.setFixedWidth(40)
self._btn_next.clicked.connect(self._next_page)
nav.addWidget(self._btn_next)
nav.addSpacing(20)
self._btn_zoom_out = QPushButton("-")
self._btn_zoom_out.setFixedWidth(30)
self._btn_zoom_out.clicked.connect(self._zoom_out)
nav.addWidget(self._btn_zoom_out)
self._label_zoom = QLabel("100%")
self._label_zoom.setFixedWidth(45)
self._label_zoom.setAlignment(Qt.AlignmentFlag.AlignCenter)
nav.addWidget(self._label_zoom)
self._btn_zoom_in = QPushButton("+")
self._btn_zoom_in.setFixedWidth(30)
self._btn_zoom_in.clicked.connect(self._zoom_in)
nav.addWidget(self._btn_zoom_in)
self._btn_fit = QPushButton("Ajuster")
self._btn_fit.clicked.connect(self._zoom_fit)
nav.addWidget(self._btn_fit)
nav.addStretch()
layout.addLayout(nav)
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._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._zoom_fit()
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
@@ -100,9 +76,7 @@ class PdfViewer(QWidget):
self._pdf = pdfium.PdfDocument(data)
self._total_pages = len(self._pdf)
self._current_page = 0
self._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
self._zoom_fit()
self._render_page()
except Exception as e:
self._image_label.setText(f"Erreur: {e}")
@@ -113,44 +87,17 @@ class PdfViewer(QWidget):
return
page = self._pdf[self._current_page]
# Rendu a 150 DPI * zoom
scale = 150 / 72 * self._zoom
bitmap = page.render(scale=scale)
pil_image = bitmap.to_pil()
# Convertir PIL -> QImage -> QPixmap
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._label_zoom.setText(f"{int(self._zoom * 100)}%")
self.page_changed.emit(self._current_page + 1, self._total_pages)
def _prev_page(self):
if self._current_page > 0:
self._current_page -= 1
self._page_spin.setValue(self._current_page + 1)
def _next_page(self):
if self._current_page < self._total_pages - 1:
self._current_page += 1
self._page_spin.setValue(self._current_page + 1)
def _go_to_page(self, value):
page = value - 1
if 0 <= page < self._total_pages and page != self._current_page:
self._current_page = page
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.25)
self._render_page()
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."""
@@ -160,15 +107,56 @@ class PdfViewer(QWidget):
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):
"""Rafraichir l'affichage."""
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
@@ -176,3 +164,11 @@ class PdfViewer(QWidget):
@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