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>
This commit is contained in:
178
app/pdf_viewer.py
Normal file
178
app/pdf_viewer.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user