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:
Jules
2026-03-20 10:54:02 +01:00
commit e3dfd90933
30 changed files with 2364 additions and 0 deletions

178
app/pdf_viewer.py Normal file
View 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