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>
179 lines
5.9 KiB
Python
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
|