Files
copydev-imposing-tool/app/pdf_viewer.py
Jules e3dfd90933 Initial commit — Copydev Imposing Tool
Outil d'imposition PDF desktop (PyQt6) remplacant Quite Imposing Plus.
9 fonctions : livret, n-up, step & repeat, imposition manuelle,
jointure pages, pages blanches, numerotation, masquage, fonds perdus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:54:02 +01:00

179 lines
5.9 KiB
Python

"""Widget de visualisation PDF avec pypdfium2."""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSpinBox, QScrollArea, QSizePolicy)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtCore import Qt, pyqtSignal
import pypdfium2 as pdfium
class PdfViewer(QWidget):
"""Widget d'apercu PDF avec navigation et zoom."""
page_changed = pyqtSignal(int, int) # page_num, total_pages
def __init__(self, parent=None):
super().__init__(parent)
self._pdf = 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)
# Zone d'affichage avec scroll
self._scroll = QScrollArea()
self._scroll.setWidgetResizable(True)
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._image_label = QLabel()
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
self._scroll.setWidget(self._image_label)
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 load_pdf(self, path):
"""Charger un fichier PDF."""
try:
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._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._page_spin.setMaximum(self._total_pages)
self._page_spin.setValue(1)
self._label_total.setText(f"/ {self._total_pages}")
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]
# 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()
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
scale_x = vw / pw
scale_y = vh / ph
self._zoom = min(scale_x, scale_y) * (72 / 150)
self._render_page()
def refresh(self):
"""Rafraichir l'affichage."""
self._render_page()
@property
def current_page(self):
return self._current_page
@property
def total_pages(self):
return self._total_pages