Files
copydev-imposing-tool/app/pdf_viewer.py
Jules 9cc256ff51 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>
2026-03-21 07:49:44 +01:00

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