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>
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
"""Fenetre principale de Copydev Imposing Tool."""
|
"""Fenetre principale de Copydev Imposing Tool — V2."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import shutil
|
||||||
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QPushButton, QLabel, QFileDialog, QMessageBox,
|
QLabel, QFileDialog, QMessageBox, QStatusBar,
|
||||||
QGroupBox, QStatusBar, QMenuBar, QSplitter,
|
QSplitter, QProgressBar, QApplication, QToolBar)
|
||||||
QProgressBar)
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtGui import QAction, QIcon, QDragEnterEvent, QDropEvent
|
||||||
from PyQt6.QtGui import QAction
|
|
||||||
|
|
||||||
from app.pdf_viewer import PdfViewer
|
from app.pdf_viewer import PdfViewer
|
||||||
|
from app.page_panel import PagePanel
|
||||||
|
|
||||||
from app.dialogs.booklet_dialog import BookletDialog
|
from app.dialogs.booklet_dialog import BookletDialog
|
||||||
from app.dialogs.nup_dialog import NupDialog
|
from app.dialogs.nup_dialog import NupDialog
|
||||||
from app.dialogs.step_repeat_dialog import StepRepeatDialog
|
from app.dialogs.step_repeat_dialog import StepRepeatDialog
|
||||||
@@ -36,172 +39,259 @@ class MainWindow(QMainWindow):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
from app.updater import VERSION
|
from app.updater import VERSION
|
||||||
self.setWindowTitle(f"CopyDev Imposing Tool v{VERSION} — www.copydev.fr")
|
self.setWindowTitle(f"CopyDev Imposing Tool v{VERSION} — www.copydev.fr")
|
||||||
self.setMinimumSize(1000, 700)
|
self.setMinimumSize(1100, 750)
|
||||||
|
|
||||||
# Icone de la fenetre
|
# Icone
|
||||||
import sys as _sys
|
_base = sys._MEIPASS if getattr(sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
from PyQt6.QtGui import QIcon
|
|
||||||
_base = _sys._MEIPASS if getattr(_sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
icon_path = os.path.join(_base, "resources", "logo.png")
|
icon_path = os.path.join(_base, "resources", "logo.png")
|
||||||
if os.path.exists(icon_path):
|
if os.path.exists(icon_path):
|
||||||
self.setWindowIcon(QIcon(icon_path))
|
self.setWindowIcon(QIcon(icon_path))
|
||||||
|
|
||||||
self._current_file = None
|
self._current_file = None
|
||||||
self._result_file = None
|
self._result_file = None
|
||||||
|
|
||||||
|
# Drag & drop
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
self._setup_menu()
|
self._setup_menu()
|
||||||
|
self._setup_toolbar()
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
self._setup_statusbar()
|
self._setup_statusbar()
|
||||||
|
|
||||||
|
# Style global
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QMainWindow { background: #1a1a2e; }
|
||||||
|
QMenuBar { background: #16213e; color: #e0e0e0; font-size: 13px; padding: 2px; }
|
||||||
|
QMenuBar::item:selected { background: #0f3460; }
|
||||||
|
QMenu { background: #16213e; color: #e0e0e0; border: 1px solid #333; }
|
||||||
|
QMenu::item:selected { background: #0f3460; }
|
||||||
|
QMenu::separator { background: #333; height: 1px; margin: 4px 10px; }
|
||||||
|
QToolBar { background: #16213e; border: none; spacing: 4px; padding: 2px; }
|
||||||
|
QToolBar QToolButton { color: #e0e0e0; padding: 4px 8px; border-radius: 3px; }
|
||||||
|
QToolBar QToolButton:hover { background: #0f3460; }
|
||||||
|
QStatusBar { background: #0f0f23; color: #888; font-size: 11px; }
|
||||||
|
QSplitter::handle { background: #333; width: 3px; }
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── Drag & Drop ──
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||||
|
if event.mimeData().hasUrls():
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
if url.toLocalFile().lower().endswith('.pdf'):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
return
|
||||||
|
|
||||||
|
def dropEvent(self, event: QDropEvent):
|
||||||
|
for url in event.mimeData().urls():
|
||||||
|
path = url.toLocalFile()
|
||||||
|
if path.lower().endswith('.pdf'):
|
||||||
|
self._open_pdf(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Menu ──
|
||||||
|
|
||||||
def _setup_menu(self):
|
def _setup_menu(self):
|
||||||
menubar = self.menuBar()
|
menubar = self.menuBar()
|
||||||
|
|
||||||
# Menu Fichier
|
# Fichier
|
||||||
file_menu = menubar.addMenu("Fichier")
|
file_menu = menubar.addMenu("Fichier")
|
||||||
|
self._add_action(file_menu, "Ouvrir PDF...", "Ctrl+O", self._open_file)
|
||||||
open_act = QAction("Ouvrir PDF...", self)
|
self._add_action(file_menu, "Enregistrer le resultat...", "Ctrl+S", self._save_result)
|
||||||
open_act.setShortcut("Ctrl+O")
|
|
||||||
open_act.triggered.connect(self._open_file)
|
|
||||||
file_menu.addAction(open_act)
|
|
||||||
|
|
||||||
save_act = QAction("Enregistrer le resultat...", self)
|
|
||||||
save_act.setShortcut("Ctrl+S")
|
|
||||||
save_act.triggered.connect(self._save_result)
|
|
||||||
file_menu.addAction(save_act)
|
|
||||||
|
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
|
self._add_action(file_menu, "Fusionner des PDF...", "Ctrl+M", self._do_merge)
|
||||||
|
file_menu.addSeparator()
|
||||||
|
self._add_action(file_menu, "Quitter", "Ctrl+Q", self.close)
|
||||||
|
|
||||||
quit_act = QAction("Quitter", self)
|
# Imposition
|
||||||
quit_act.setShortcut("Ctrl+Q")
|
impose_menu = menubar.addMenu("Imposition")
|
||||||
quit_act.triggered.connect(self.close)
|
self._add_action(impose_menu, "Creer un livret", None, self._do_booklet)
|
||||||
file_menu.addAction(quit_act)
|
self._add_action(impose_menu, "N pages par feuille", None, self._do_nup)
|
||||||
|
self._add_action(impose_menu, "Repetition (Step && Repeat)", None, self._do_step_repeat)
|
||||||
|
self._add_action(impose_menu, "Imposition manuelle", None, self._do_manual_impose)
|
||||||
|
self._add_action(impose_menu, "Joindre deux pages", None, self._do_join_pages)
|
||||||
|
|
||||||
# Menu Aide
|
# Pages
|
||||||
|
pages_menu = menubar.addMenu("Pages")
|
||||||
|
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
|
||||||
|
|
||||||
|
# Annotations
|
||||||
|
annot_menu = menubar.addMenu("Annotations")
|
||||||
|
self._add_action(annot_menu, "Numerotation de pages", None, self._do_page_numbers)
|
||||||
|
self._add_action(annot_menu, "Masquage (ruban blanc)", None, self._do_masking)
|
||||||
|
annot_menu.addSeparator()
|
||||||
|
self._add_action(annot_menu, "Definir les fonds perdus", None, self._do_bleeds)
|
||||||
|
self._add_action(annot_menu, "Enlever les fonds perdus", None, self._do_remove_bleeds)
|
||||||
|
|
||||||
|
# Aide
|
||||||
help_menu = menubar.addMenu("Aide")
|
help_menu = menubar.addMenu("Aide")
|
||||||
about_act = QAction("A propos...", self)
|
self._add_action(help_menu, "A propos de CopyDev Imposing Tool", None, self._show_about)
|
||||||
about_act.triggered.connect(self._show_about)
|
|
||||||
help_menu.addAction(about_act)
|
def _add_action(self, menu, text, shortcut, callback):
|
||||||
|
act = QAction(text, self)
|
||||||
|
if shortcut:
|
||||||
|
act.setShortcut(shortcut)
|
||||||
|
act.triggered.connect(callback)
|
||||||
|
menu.addAction(act)
|
||||||
|
|
||||||
|
# ── Toolbar ──
|
||||||
|
|
||||||
|
def _setup_toolbar(self):
|
||||||
|
tb = QToolBar()
|
||||||
|
tb.setMovable(False)
|
||||||
|
self.addToolBar(tb)
|
||||||
|
|
||||||
|
tb.addAction("Ouvrir", self._open_file)
|
||||||
|
tb.addAction("Enregistrer", self._save_result)
|
||||||
|
tb.addSeparator()
|
||||||
|
tb.addAction("Zoom +", lambda: self._viewer.zoom_in())
|
||||||
|
tb.addAction("Zoom -", lambda: self._viewer.zoom_out())
|
||||||
|
tb.addAction("Ajuster", lambda: self._viewer.zoom_fit())
|
||||||
|
tb.addSeparator()
|
||||||
|
self._page_label = QLabel(" Aucun fichier ")
|
||||||
|
self._page_label.setStyleSheet("color: #aaa; padding: 0 10px;")
|
||||||
|
tb.addWidget(self._page_label)
|
||||||
|
|
||||||
|
# ── UI principale ──
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
central = QWidget()
|
central = QWidget()
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
main_layout = QHBoxLayout(central)
|
layout = QVBoxLayout(central)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
# === Panneau gauche : boutons ===
|
# Splitter : vignettes | viewer
|
||||||
left_panel = QWidget()
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
left_panel.setFixedWidth(200)
|
|
||||||
left_layout = QVBoxLayout(left_panel)
|
|
||||||
left_layout.setContentsMargins(5, 5, 5, 5)
|
|
||||||
|
|
||||||
# Ouvrir
|
# Panneau vignettes
|
||||||
btn_open = QPushButton("Ouvrir PDF...")
|
self._page_panel = PagePanel()
|
||||||
btn_open.clicked.connect(self._open_file)
|
self._page_panel.page_selected.connect(self._on_thumbnail_click)
|
||||||
left_layout.addWidget(btn_open)
|
self._page_panel.page_delete_requested.connect(self._on_page_delete)
|
||||||
|
self._page_panel.page_insert_requested.connect(self._on_page_insert)
|
||||||
|
self._page_panel.pages_reordered.connect(self._on_pages_reordered)
|
||||||
|
splitter.addWidget(self._page_panel)
|
||||||
|
|
||||||
btn_save = QPushButton("Enregistrer resultat...")
|
# Viewer PDF
|
||||||
btn_save.clicked.connect(self._save_result)
|
|
||||||
left_layout.addWidget(btn_save)
|
|
||||||
|
|
||||||
left_layout.addSpacing(10)
|
|
||||||
|
|
||||||
# Imposition
|
|
||||||
grp_impose = QGroupBox("Imposition")
|
|
||||||
il = QVBoxLayout(grp_impose)
|
|
||||||
|
|
||||||
btn_booklet = QPushButton("Creer un livret")
|
|
||||||
btn_booklet.clicked.connect(self._do_booklet)
|
|
||||||
il.addWidget(btn_booklet)
|
|
||||||
|
|
||||||
btn_nup = QPushButton("N pages par feuille")
|
|
||||||
btn_nup.clicked.connect(self._do_nup)
|
|
||||||
il.addWidget(btn_nup)
|
|
||||||
|
|
||||||
btn_repeat = QPushButton("Repetition")
|
|
||||||
btn_repeat.clicked.connect(self._do_step_repeat)
|
|
||||||
il.addWidget(btn_repeat)
|
|
||||||
|
|
||||||
btn_manual = QPushButton("Imposition manuelle")
|
|
||||||
btn_manual.clicked.connect(self._do_manual_impose)
|
|
||||||
il.addWidget(btn_manual)
|
|
||||||
|
|
||||||
btn_join = QPushButton("Joindre deux pages")
|
|
||||||
btn_join.clicked.connect(self._do_join_pages)
|
|
||||||
il.addWidget(btn_join)
|
|
||||||
|
|
||||||
left_layout.addWidget(grp_impose)
|
|
||||||
|
|
||||||
# Pages
|
|
||||||
grp_pages = QGroupBox("Pages")
|
|
||||||
pl = QVBoxLayout(grp_pages)
|
|
||||||
|
|
||||||
btn_blank = QPushButton("Pages blanches")
|
|
||||||
btn_blank.clicked.connect(self._do_insert_blank)
|
|
||||||
pl.addWidget(btn_blank)
|
|
||||||
|
|
||||||
left_layout.addWidget(grp_pages)
|
|
||||||
|
|
||||||
# Annotations
|
|
||||||
grp_annot = QGroupBox("Annotations")
|
|
||||||
al = QVBoxLayout(grp_annot)
|
|
||||||
|
|
||||||
btn_numbers = QPushButton("Numerotation")
|
|
||||||
btn_numbers.clicked.connect(self._do_page_numbers)
|
|
||||||
al.addWidget(btn_numbers)
|
|
||||||
|
|
||||||
btn_mask = QPushButton("Masquage")
|
|
||||||
btn_mask.clicked.connect(self._do_masking)
|
|
||||||
al.addWidget(btn_mask)
|
|
||||||
|
|
||||||
btn_bleeds = QPushButton("Fonds perdus")
|
|
||||||
btn_bleeds.clicked.connect(self._do_bleeds)
|
|
||||||
al.addWidget(btn_bleeds)
|
|
||||||
|
|
||||||
left_layout.addWidget(grp_annot)
|
|
||||||
left_layout.addStretch()
|
|
||||||
|
|
||||||
main_layout.addWidget(left_panel)
|
|
||||||
|
|
||||||
# === Zone centrale : viewer PDF ===
|
|
||||||
self._viewer = PdfViewer()
|
self._viewer = PdfViewer()
|
||||||
self._viewer.page_changed.connect(self._on_page_changed)
|
self._viewer.page_changed.connect(self._on_page_changed)
|
||||||
main_layout.addWidget(self._viewer, 1)
|
splitter.addWidget(self._viewer)
|
||||||
|
|
||||||
|
splitter.setSizes([160, 900])
|
||||||
|
splitter.setStretchFactor(0, 0)
|
||||||
|
splitter.setStretchFactor(1, 1)
|
||||||
|
|
||||||
|
layout.addWidget(splitter)
|
||||||
|
|
||||||
|
# Barre de progression
|
||||||
|
self._progress = QProgressBar()
|
||||||
|
self._progress.setFixedHeight(3)
|
||||||
|
self._progress.setTextVisible(False)
|
||||||
|
self._progress.setStyleSheet("""
|
||||||
|
QProgressBar { background: #0f0f23; border: none; }
|
||||||
|
QProgressBar::chunk { background: #0066cc; }
|
||||||
|
""")
|
||||||
|
self._progress.hide()
|
||||||
|
layout.addWidget(self._progress)
|
||||||
|
|
||||||
|
# ── Statusbar ──
|
||||||
|
|
||||||
def _setup_statusbar(self):
|
def _setup_statusbar(self):
|
||||||
self._statusbar = QStatusBar()
|
self._statusbar = QStatusBar()
|
||||||
self.setStatusBar(self._statusbar)
|
self.setStatusBar(self._statusbar)
|
||||||
self._status_label = QLabel("Pret")
|
self._status_label = QLabel("Pret — Glissez un PDF ou utilisez Fichier > Ouvrir")
|
||||||
self._statusbar.addWidget(self._status_label, 1)
|
self._statusbar.addWidget(self._status_label, 1)
|
||||||
self._file_label = QLabel("")
|
self._file_label = QLabel("")
|
||||||
self._statusbar.addPermanentWidget(self._file_label)
|
self._statusbar.addPermanentWidget(self._file_label)
|
||||||
brand = QLabel(" CopyDev — contact@copydev.fr ")
|
brand = QLabel(" CopyDev — contact@copydev.fr ")
|
||||||
brand.setStyleSheet("color: #64748b; font-size: 10px;")
|
brand.setStyleSheet("color: #555; font-size: 10px;")
|
||||||
self._statusbar.addPermanentWidget(brand)
|
self._statusbar.addPermanentWidget(brand)
|
||||||
|
|
||||||
|
# ── Callbacks ──
|
||||||
|
|
||||||
def _on_page_changed(self, page, total):
|
def _on_page_changed(self, page, total):
|
||||||
pass
|
self._page_label.setText(f" Page {page + 1} / {total} | Zoom {int(self._viewer.zoom_level * 100)}% ")
|
||||||
|
self._page_panel.select_page(page)
|
||||||
|
|
||||||
|
def _on_thumbnail_click(self, index):
|
||||||
|
self._viewer.go_to_page(index)
|
||||||
|
|
||||||
|
def _on_page_delete(self, index):
|
||||||
|
if not self._current_file:
|
||||||
|
return
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
reader = PdfReader(self._current_file)
|
||||||
|
if len(reader.pages) <= 1:
|
||||||
|
QMessageBox.warning(self, "Erreur", "Impossible de supprimer la derniere page.")
|
||||||
|
return
|
||||||
|
writer = PdfWriter()
|
||||||
|
for i, page in enumerate(reader.pages):
|
||||||
|
if i != index:
|
||||||
|
writer.add_page(page)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
|
||||||
|
def _on_page_insert(self, after_index):
|
||||||
|
if not self._current_file:
|
||||||
|
return
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
from app.engine.pdf_utils import get_page_size
|
||||||
|
reader = PdfReader(self._current_file)
|
||||||
|
writer = PdfWriter()
|
||||||
|
ref_page = reader.pages[min(after_index, len(reader.pages) - 1)]
|
||||||
|
w, h = get_page_size(ref_page)
|
||||||
|
for i, page in enumerate(reader.pages):
|
||||||
|
writer.add_page(page)
|
||||||
|
if i == after_index:
|
||||||
|
writer.add_blank_page(w, h)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
|
||||||
|
def _on_pages_reordered(self, indices):
|
||||||
|
if not self._current_file:
|
||||||
|
return
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
reader = PdfReader(self._current_file)
|
||||||
|
writer = PdfWriter()
|
||||||
|
for idx in indices:
|
||||||
|
if 0 <= idx < len(reader.pages):
|
||||||
|
writer.add_page(reader.pages[idx])
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._current_file = output
|
||||||
|
self._result_file = output
|
||||||
|
|
||||||
|
# ── Fichiers ──
|
||||||
|
|
||||||
def _open_file(self):
|
def _open_file(self):
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)")
|
||||||
self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)"
|
|
||||||
)
|
|
||||||
if path:
|
if path:
|
||||||
|
self._open_pdf(path)
|
||||||
|
|
||||||
|
def _open_pdf(self, path):
|
||||||
self._current_file = path
|
self._current_file = path
|
||||||
self._result_file = None
|
self._result_file = None
|
||||||
self._viewer.load_pdf(path)
|
self._viewer.load_pdf(path)
|
||||||
|
self._page_panel.load_thumbnails(self._viewer)
|
||||||
self._file_label.setText(os.path.basename(path))
|
self._file_label.setText(os.path.basename(path))
|
||||||
self._status_label.setText(f"Fichier charge : {os.path.basename(path)}")
|
self._status_label.setText(f"Fichier charge : {os.path.basename(path)}")
|
||||||
|
# Ajuster apres affichage
|
||||||
|
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||||
|
|
||||||
def _save_result(self):
|
def _save_result(self):
|
||||||
source = self._result_file or self._current_file
|
source = self._result_file or self._current_file
|
||||||
if not source:
|
if not source:
|
||||||
QMessageBox.warning(self, "Erreur", "Aucun fichier a enregistrer.")
|
QMessageBox.warning(self, "Erreur", "Aucun fichier a enregistrer.")
|
||||||
return
|
return
|
||||||
|
path, _ = QFileDialog.getSaveFileName(self, "Enregistrer le resultat", "", "PDF (*.pdf)")
|
||||||
path, _ = QFileDialog.getSaveFileName(
|
|
||||||
self, "Enregistrer le resultat", "", "PDF (*.pdf)"
|
|
||||||
)
|
|
||||||
if path:
|
if path:
|
||||||
if not path.lower().endswith(".pdf"):
|
if not path.lower().endswith(".pdf"):
|
||||||
path += ".pdf"
|
path += ".pdf"
|
||||||
import shutil
|
|
||||||
shutil.copy2(source, path)
|
shutil.copy2(source, path)
|
||||||
self._status_label.setText(f"Enregistre : {os.path.basename(path)}")
|
self._status_label.setText(f"Enregistre : {os.path.basename(path)}")
|
||||||
|
|
||||||
@@ -216,22 +306,40 @@ class MainWindow(QMainWindow):
|
|||||||
os.close(fd)
|
os.close(fd)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def _show_progress(self):
|
||||||
|
self._progress.setRange(0, 0)
|
||||||
|
self._progress.show()
|
||||||
|
self._status_label.setText("Traitement en cours...")
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
def _hide_progress(self):
|
||||||
|
self._progress.hide()
|
||||||
|
self._progress.setRange(0, 1)
|
||||||
|
|
||||||
def _show_result(self, output_path):
|
def _show_result(self, output_path):
|
||||||
self._result_file = output_path
|
self._result_file = output_path
|
||||||
|
self._current_file = output_path
|
||||||
self._viewer.load_pdf(output_path)
|
self._viewer.load_pdf(output_path)
|
||||||
|
self._page_panel.load_thumbnails(self._viewer)
|
||||||
|
self._hide_progress()
|
||||||
self._status_label.setText("Traitement termine")
|
self._status_label.setText("Traitement termine")
|
||||||
|
QTimer.singleShot(100, self._viewer.zoom_fit)
|
||||||
|
|
||||||
|
# ── Actions imposition ──
|
||||||
|
|
||||||
def _do_booklet(self):
|
def _do_booklet(self):
|
||||||
if not self._require_file():
|
if not self._require_file():
|
||||||
return
|
return
|
||||||
dlg = BookletDialog(self)
|
dlg = BookletDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
create_booklet(self._current_file, output, **settings)
|
create_booklet(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_nup(self):
|
def _do_nup(self):
|
||||||
@@ -239,12 +347,14 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = NupDialog(self)
|
dlg = NupDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
nup_pages(self._current_file, output, **settings)
|
nup_pages(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_step_repeat(self):
|
def _do_step_repeat(self):
|
||||||
@@ -252,12 +362,14 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = StepRepeatDialog(self)
|
dlg = StepRepeatDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
step_and_repeat(self._current_file, output, **settings)
|
step_and_repeat(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_manual_impose(self):
|
def _do_manual_impose(self):
|
||||||
@@ -267,12 +379,14 @@ class MainWindow(QMainWindow):
|
|||||||
reader = PdfReader(self._current_file)
|
reader = PdfReader(self._current_file)
|
||||||
dlg = ManualImposeDialog(total_pages=len(reader.pages), parent=self)
|
dlg = ManualImposeDialog(total_pages=len(reader.pages), parent=self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
manual_impose(self._current_file, output, **settings)
|
manual_impose(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_join_pages(self):
|
def _do_join_pages(self):
|
||||||
@@ -280,12 +394,14 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = JoinPagesDialog(self)
|
dlg = JoinPagesDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
join_pages(self._current_file, output, **settings)
|
join_pages(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_insert_blank(self):
|
def _do_insert_blank(self):
|
||||||
@@ -295,12 +411,14 @@ class MainWindow(QMainWindow):
|
|||||||
reader = PdfReader(self._current_file)
|
reader = PdfReader(self._current_file)
|
||||||
dlg = InsertBlankDialog(total_pages=len(reader.pages), parent=self)
|
dlg = InsertBlankDialog(total_pages=len(reader.pages), parent=self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
insert_blank_pages(self._current_file, output, **settings)
|
insert_blank_pages(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_page_numbers(self):
|
def _do_page_numbers(self):
|
||||||
@@ -308,12 +426,14 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = PageNumbersDialog(self)
|
dlg = PageNumbersDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
add_page_numbers(self._current_file, output, **settings)
|
add_page_numbers(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_masking(self):
|
def _do_masking(self):
|
||||||
@@ -321,12 +441,14 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = MaskingDialog(self)
|
dlg = MaskingDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
apply_masking(self._current_file, output, **settings)
|
apply_masking(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
def _do_bleeds(self):
|
def _do_bleeds(self):
|
||||||
@@ -334,30 +456,75 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
dlg = BleedsDialog(self)
|
dlg = BleedsDialog(self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
|
self._show_progress()
|
||||||
settings = dlg.get_settings()
|
settings = dlg.get_settings()
|
||||||
output = self._make_temp()
|
output = self._make_temp()
|
||||||
try:
|
try:
|
||||||
define_bleeds(self._current_file, output, **settings)
|
define_bleeds(self._current_file, output, **settings)
|
||||||
self._show_result(output)
|
self._show_result(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
QMessageBox.critical(self, "Erreur", str(e))
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
|
def _do_remove_bleeds(self):
|
||||||
|
if not self._require_file():
|
||||||
|
return
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
self._show_progress()
|
||||||
|
try:
|
||||||
|
reader = PdfReader(self._current_file)
|
||||||
|
writer = PdfWriter()
|
||||||
|
for page in reader.pages:
|
||||||
|
if hasattr(page, 'trimbox'):
|
||||||
|
page.trimbox = page.mediabox
|
||||||
|
if hasattr(page, 'bleedbox'):
|
||||||
|
page.bleedbox = page.mediabox
|
||||||
|
writer.add_page(page)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
|
def _do_merge(self):
|
||||||
|
paths, _ = QFileDialog.getOpenFileNames(self, "Selectionner les PDF a fusionner", "", "PDF (*.pdf)")
|
||||||
|
if not paths or len(paths) < 2:
|
||||||
|
if paths and len(paths) == 1:
|
||||||
|
QMessageBox.information(self, "Fusion", "Selectionnez au moins 2 fichiers PDF.")
|
||||||
|
return
|
||||||
|
from pypdf import PdfWriter
|
||||||
|
self._show_progress()
|
||||||
|
try:
|
||||||
|
writer = PdfWriter()
|
||||||
|
for path in paths:
|
||||||
|
writer.append(path)
|
||||||
|
output = self._make_temp()
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
self._show_result(output)
|
||||||
|
self._status_label.setText(f"Fusion de {len(paths)} fichiers terminee")
|
||||||
|
except Exception as e:
|
||||||
|
self._hide_progress()
|
||||||
|
QMessageBox.critical(self, "Erreur", str(e))
|
||||||
|
|
||||||
|
# ── A propos ──
|
||||||
|
|
||||||
def _show_about(self):
|
def _show_about(self):
|
||||||
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox
|
||||||
from PyQt6.QtGui import QPixmap, QFont
|
from PyQt6.QtGui import QPixmap, QFont
|
||||||
from PyQt6.QtCore import Qt
|
|
||||||
|
|
||||||
dlg = QDialog(self)
|
dlg = QDialog(self)
|
||||||
dlg.setWindowTitle("A propos — CopyDev Imposing Tool")
|
dlg.setWindowTitle("A propos — CopyDev Imposing Tool")
|
||||||
dlg.setFixedSize(520, 520)
|
dlg.setFixedSize(520, 480)
|
||||||
dlg.setStyleSheet("QDialog { background-color: #0f172a; }")
|
dlg.setStyleSheet("QDialog { background-color: #0f172a; }")
|
||||||
layout = QVBoxLayout(dlg)
|
layout = QVBoxLayout(dlg)
|
||||||
layout.setContentsMargins(40, 30, 40, 25)
|
layout.setContentsMargins(40, 30, 40, 25)
|
||||||
|
|
||||||
# Logo
|
# Logo
|
||||||
logo_label = QLabel()
|
logo_label = QLabel()
|
||||||
import sys as _sys
|
_base = sys._MEIPASS if getattr(sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
_base = _sys._MEIPASS if getattr(_sys, 'frozen', False) else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
logo_path = os.path.join(_base, "resources", "logo.png")
|
logo_path = os.path.join(_base, "resources", "logo.png")
|
||||||
if os.path.exists(logo_path):
|
if os.path.exists(logo_path):
|
||||||
pixmap = QPixmap(logo_path).scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio,
|
pixmap = QPixmap(logo_path).scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
@@ -368,14 +535,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addSpacing(10)
|
layout.addSpacing(10)
|
||||||
|
|
||||||
# Titre
|
|
||||||
title = QLabel("COPYDEV IMPOSING TOOL")
|
title = QLabel("COPYDEV IMPOSING TOOL")
|
||||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
|
title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
|
||||||
title.setStyleSheet("color: white;")
|
title.setStyleSheet("color: white;")
|
||||||
layout.addWidget(title)
|
layout.addWidget(title)
|
||||||
|
|
||||||
# Version
|
|
||||||
from app.updater import VERSION
|
from app.updater import VERSION
|
||||||
version = QLabel(f"Version {VERSION}")
|
version = QLabel(f"Version {VERSION}")
|
||||||
version.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
version.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
@@ -385,7 +550,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addSpacing(15)
|
layout.addSpacing(15)
|
||||||
|
|
||||||
# Description
|
|
||||||
desc = QLabel(
|
desc = QLabel(
|
||||||
"Outil professionnel d'imposition PDF\n"
|
"Outil professionnel d'imposition PDF\n"
|
||||||
"Livrets, n-up, repetition, imposition manuelle,\n"
|
"Livrets, n-up, repetition, imposition manuelle,\n"
|
||||||
@@ -399,7 +563,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addSpacing(20)
|
layout.addSpacing(20)
|
||||||
|
|
||||||
# Separateur
|
|
||||||
sep = QLabel()
|
sep = QLabel()
|
||||||
sep.setFixedHeight(1)
|
sep.setFixedHeight(1)
|
||||||
sep.setStyleSheet("background-color: #1e293b;")
|
sep.setStyleSheet("background-color: #1e293b;")
|
||||||
@@ -407,7 +570,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addSpacing(15)
|
layout.addSpacing(15)
|
||||||
|
|
||||||
# Coordonnees
|
|
||||||
contact = QLabel(
|
contact = QLabel(
|
||||||
'<p style="text-align:center; line-height: 1.6;">'
|
'<p style="text-align:center; line-height: 1.6;">'
|
||||||
'<span style="font-size:14px; font-weight:bold; color:white;">CopyDev</span><br>'
|
'<span style="font-size:14px; font-weight:bold; color:white;">CopyDev</span><br>'
|
||||||
@@ -415,9 +577,7 @@ class MainWindow(QMainWindow):
|
|||||||
'<span style="color:#94a3b8;">Email : </span>'
|
'<span style="color:#94a3b8;">Email : </span>'
|
||||||
'<a href="mailto:contact@copydev.fr" style="color:#38bdf8;">contact@copydev.fr</a><br>'
|
'<a href="mailto:contact@copydev.fr" style="color:#38bdf8;">contact@copydev.fr</a><br>'
|
||||||
'<span style="color:#94a3b8;">Web : </span>'
|
'<span style="color:#94a3b8;">Web : </span>'
|
||||||
'<a href="https://copydev.fr" style="color:#38bdf8;">www.copydev.fr</a><br>'
|
'<a href="https://copydev.fr" style="color:#38bdf8;">www.copydev.fr</a>'
|
||||||
'<span style="color:#94a3b8;">Tel : </span>'
|
|
||||||
'<span style="color:#e2e8f0;">06 52 07 92 35</span>'
|
|
||||||
'</p>'
|
'</p>'
|
||||||
)
|
)
|
||||||
contact.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
contact.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
@@ -426,14 +586,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
# Copyright
|
|
||||||
copy_label = QLabel("2026 CopyDev — Tous droits reserves\nLogiciel sous licence — Reproduction interdite")
|
copy_label = QLabel("2026 CopyDev — Tous droits reserves\nLogiciel sous licence — Reproduction interdite")
|
||||||
copy_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
copy_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
copy_label.setFont(QFont("Segoe UI", 8))
|
copy_label.setFont(QFont("Segoe UI", 8))
|
||||||
copy_label.setStyleSheet("color: #334155;")
|
copy_label.setStyleSheet("color: #334155;")
|
||||||
layout.addWidget(copy_label)
|
layout.addWidget(copy_label)
|
||||||
|
|
||||||
# Bouton Fermer
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||||
buttons.setStyleSheet("QPushButton { background: #1e293b; color: white; border: 1px solid #334155; padding: 6px 20px; border-radius: 4px; } QPushButton:hover { background: #334155; }")
|
buttons.setStyleSheet("QPushButton { background: #1e293b; color: white; border: 1px solid #334155; padding: 6px 20px; border-radius: 4px; } QPushButton:hover { background: #334155; }")
|
||||||
buttons.rejected.connect(dlg.close)
|
buttons.rejected.connect(dlg.close)
|
||||||
|
|||||||
109
app/page_panel.py
Normal file
109
app/page_panel.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""Panneau lateral de vignettes des pages — V2."""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
||||||
|
QAbstractItemView, QMenu, QSizePolicy)
|
||||||
|
from PyQt6.QtGui import QIcon, QPixmap
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||||||
|
|
||||||
|
|
||||||
|
class PagePanel(QWidget):
|
||||||
|
"""Panneau de vignettes avec drag & drop, suppression, insertion."""
|
||||||
|
|
||||||
|
page_selected = pyqtSignal(int) # page index cliquee
|
||||||
|
pages_reordered = pyqtSignal(list) # nouvelle liste d'indices
|
||||||
|
page_delete_requested = pyqtSignal(int) # demande suppression
|
||||||
|
page_insert_requested = pyqtSignal(int) # demande insertion page blanche apres
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumWidth(100)
|
||||||
|
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self._list = QListWidget()
|
||||||
|
self._list.setIconSize(QSize(120, 170))
|
||||||
|
self._list.setSpacing(4)
|
||||||
|
self._list.setStyleSheet("""
|
||||||
|
QListWidget {
|
||||||
|
background: #252525;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QListWidget::item:selected {
|
||||||
|
background: #0066cc;
|
||||||
|
border: 1px solid #0088ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self._list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
|
self._list.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||||||
|
self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
|
self._list.customContextMenuRequested.connect(self._context_menu)
|
||||||
|
self._list.currentRowChanged.connect(self._on_row_changed)
|
||||||
|
self._list.model().rowsMoved.connect(self._on_rows_moved)
|
||||||
|
|
||||||
|
layout.addWidget(self._list)
|
||||||
|
|
||||||
|
def load_thumbnails(self, viewer):
|
||||||
|
"""Charger les miniatures depuis le viewer."""
|
||||||
|
self._list.clear()
|
||||||
|
for i in range(viewer.total_pages):
|
||||||
|
thumb = viewer.get_page_thumbnail(i, 120)
|
||||||
|
item = QListWidgetItem()
|
||||||
|
item.setText(f" Page {i + 1}")
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, i)
|
||||||
|
if thumb:
|
||||||
|
item.setIcon(QIcon(thumb))
|
||||||
|
self._list.addItem(item)
|
||||||
|
|
||||||
|
def select_page(self, index):
|
||||||
|
"""Selectionner une page sans emettre le signal."""
|
||||||
|
self._list.blockSignals(True)
|
||||||
|
self._list.setCurrentRow(index)
|
||||||
|
self._list.blockSignals(False)
|
||||||
|
|
||||||
|
def _on_row_changed(self, row):
|
||||||
|
if row >= 0:
|
||||||
|
self.page_selected.emit(row)
|
||||||
|
|
||||||
|
def _on_rows_moved(self):
|
||||||
|
indices = []
|
||||||
|
for i in range(self._list.count()):
|
||||||
|
item = self._list.item(i)
|
||||||
|
original_index = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
indices.append(original_index)
|
||||||
|
self.pages_reordered.emit(indices)
|
||||||
|
|
||||||
|
def _context_menu(self, pos):
|
||||||
|
item = self._list.itemAt(pos)
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
row = self._list.row(item)
|
||||||
|
|
||||||
|
menu = QMenu(self)
|
||||||
|
menu.setStyleSheet("""
|
||||||
|
QMenu { background: #333; color: #eee; border: 1px solid #555; }
|
||||||
|
QMenu::item:selected { background: #0066cc; }
|
||||||
|
""")
|
||||||
|
|
||||||
|
act_insert = menu.addAction("Inserer une page blanche apres")
|
||||||
|
act_delete = menu.addAction("Supprimer cette page")
|
||||||
|
|
||||||
|
action = menu.exec(self._list.mapToGlobal(pos))
|
||||||
|
if action == act_delete:
|
||||||
|
self.page_delete_requested.emit(row)
|
||||||
|
elif action == act_insert:
|
||||||
|
self.page_insert_requested.emit(row)
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
"""Widget de visualisation PDF avec pypdfium2."""
|
"""Widget de visualisation PDF avec pypdfium2 — V2."""
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QPushButton, QSpinBox, QScrollArea, QSizePolicy)
|
QScrollArea, QSizePolicy, QApplication)
|
||||||
from PyQt6.QtGui import QImage, QPixmap
|
from PyQt6.QtGui import QImage, QPixmap, QWheelEvent
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
||||||
import pypdfium2 as pdfium
|
import pypdfium2 as pdfium
|
||||||
|
|
||||||
|
|
||||||
class PdfViewer(QWidget):
|
class PdfViewer(QWidget):
|
||||||
"""Widget d'apercu PDF avec navigation et zoom."""
|
"""Widget d'apercu PDF avec zoom molette et navigation fluide."""
|
||||||
|
|
||||||
page_changed = pyqtSignal(int, int) # page_num, total_pages
|
page_changed = pyqtSignal(int, int) # page_num (0-based), total_pages
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._pdf = None
|
self._pdf = None
|
||||||
|
self._pdf_path = None
|
||||||
self._current_page = 0
|
self._current_page = 0
|
||||||
self._total_pages = 0
|
self._total_pages = 0
|
||||||
self._zoom = 1.0
|
self._zoom = 1.0
|
||||||
@@ -23,73 +24,48 @@ class PdfViewer(QWidget):
|
|||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
# Zone d'affichage avec scroll
|
|
||||||
self._scroll = QScrollArea()
|
self._scroll = QScrollArea()
|
||||||
self._scroll.setWidgetResizable(True)
|
self._scroll.setWidgetResizable(True)
|
||||||
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._scroll.setStyleSheet("QScrollArea { background: #1e1e1e; border: none; }")
|
||||||
|
|
||||||
self._image_label = QLabel()
|
self._image_label = QLabel()
|
||||||
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
self._image_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
||||||
|
self._image_label.setStyleSheet("background: #1e1e1e;")
|
||||||
self._scroll.setWidget(self._image_label)
|
self._scroll.setWidget(self._image_label)
|
||||||
|
|
||||||
|
# Intercepter la molette sur le scroll area
|
||||||
|
self._scroll.wheelEvent = self._on_wheel
|
||||||
|
|
||||||
layout.addWidget(self._scroll, 1)
|
layout.addWidget(self._scroll, 1)
|
||||||
|
|
||||||
# Barre de navigation
|
def _on_wheel(self, event: QWheelEvent):
|
||||||
nav = QHBoxLayout()
|
"""Molette = pages, Ctrl+Molette = zoom."""
|
||||||
|
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
||||||
self._btn_prev = QPushButton("<")
|
delta = event.angleDelta().y()
|
||||||
self._btn_prev.setFixedWidth(40)
|
if delta > 0:
|
||||||
self._btn_prev.clicked.connect(self._prev_page)
|
self._zoom = min(self._zoom * 1.15, 5.0)
|
||||||
nav.addWidget(self._btn_prev)
|
else:
|
||||||
|
self._zoom = max(self._zoom / 1.15, 0.2)
|
||||||
self._page_spin = QSpinBox()
|
self._render_page()
|
||||||
self._page_spin.setMinimum(1)
|
else:
|
||||||
self._page_spin.setMaximum(1)
|
delta = event.angleDelta().y()
|
||||||
self._page_spin.valueChanged.connect(self._go_to_page)
|
if delta < 0:
|
||||||
nav.addWidget(self._page_spin)
|
self.next_page()
|
||||||
|
elif delta > 0:
|
||||||
self._label_total = QLabel("/ 0")
|
self.prev_page()
|
||||||
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):
|
def load_pdf(self, path):
|
||||||
"""Charger un fichier PDF."""
|
"""Charger un fichier PDF."""
|
||||||
try:
|
try:
|
||||||
|
self._pdf_path = path
|
||||||
self._pdf = pdfium.PdfDocument(path)
|
self._pdf = pdfium.PdfDocument(path)
|
||||||
self._total_pages = len(self._pdf)
|
self._total_pages = len(self._pdf)
|
||||||
self._current_page = 0
|
self._current_page = 0
|
||||||
self._page_spin.setMaximum(self._total_pages)
|
self._zoom_fit()
|
||||||
self._page_spin.setValue(1)
|
|
||||||
self._label_total.setText(f"/ {self._total_pages}")
|
|
||||||
self._render_page()
|
self._render_page()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._image_label.setText(f"Erreur: {e}")
|
self._image_label.setText(f"Erreur: {e}")
|
||||||
@@ -100,9 +76,7 @@ class PdfViewer(QWidget):
|
|||||||
self._pdf = pdfium.PdfDocument(data)
|
self._pdf = pdfium.PdfDocument(data)
|
||||||
self._total_pages = len(self._pdf)
|
self._total_pages = len(self._pdf)
|
||||||
self._current_page = 0
|
self._current_page = 0
|
||||||
self._page_spin.setMaximum(self._total_pages)
|
self._zoom_fit()
|
||||||
self._page_spin.setValue(1)
|
|
||||||
self._label_total.setText(f"/ {self._total_pages}")
|
|
||||||
self._render_page()
|
self._render_page()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._image_label.setText(f"Erreur: {e}")
|
self._image_label.setText(f"Erreur: {e}")
|
||||||
@@ -113,44 +87,17 @@ class PdfViewer(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
page = self._pdf[self._current_page]
|
page = self._pdf[self._current_page]
|
||||||
# Rendu a 150 DPI * zoom
|
|
||||||
scale = 150 / 72 * self._zoom
|
scale = 150 / 72 * self._zoom
|
||||||
bitmap = page.render(scale=scale)
|
bitmap = page.render(scale=scale)
|
||||||
pil_image = bitmap.to_pil()
|
pil_image = bitmap.to_pil()
|
||||||
|
|
||||||
# Convertir PIL -> QImage -> QPixmap
|
|
||||||
data = pil_image.tobytes("raw", "RGBA")
|
data = pil_image.tobytes("raw", "RGBA")
|
||||||
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
|
qimage = QImage(data, pil_image.width, pil_image.height, QImage.Format.Format_RGBA8888)
|
||||||
pixmap = QPixmap.fromImage(qimage)
|
pixmap = QPixmap.fromImage(qimage)
|
||||||
|
|
||||||
self._image_label.setPixmap(pixmap)
|
self._image_label.setPixmap(pixmap)
|
||||||
self._image_label.adjustSize()
|
self._image_label.adjustSize()
|
||||||
self._label_zoom.setText(f"{int(self._zoom * 100)}%")
|
self.page_changed.emit(self._current_page, self._total_pages)
|
||||||
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):
|
def _zoom_fit(self):
|
||||||
"""Ajuster le zoom pour que la page tienne dans la zone visible."""
|
"""Ajuster le zoom pour que la page tienne dans la zone visible."""
|
||||||
@@ -160,15 +107,56 @@ class PdfViewer(QWidget):
|
|||||||
pw, ph = page.get_size()
|
pw, ph = page.get_size()
|
||||||
vw = self._scroll.viewport().width() - 20
|
vw = self._scroll.viewport().width() - 20
|
||||||
vh = self._scroll.viewport().height() - 20
|
vh = self._scroll.viewport().height() - 20
|
||||||
|
if vw <= 0 or vh <= 0:
|
||||||
|
self._zoom = 1.0
|
||||||
|
return
|
||||||
scale_x = vw / pw
|
scale_x = vw / pw
|
||||||
scale_y = vh / ph
|
scale_y = vh / ph
|
||||||
self._zoom = min(scale_x, scale_y) * (72 / 150)
|
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()
|
self._render_page()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Rafraichir l'affichage."""
|
|
||||||
self._render_page()
|
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
|
@property
|
||||||
def current_page(self):
|
def current_page(self):
|
||||||
return self._current_page
|
return self._current_page
|
||||||
@@ -176,3 +164,11 @@ class PdfViewer(QWidget):
|
|||||||
@property
|
@property
|
||||||
def total_pages(self):
|
def total_pages(self):
|
||||||
return self._total_pages
|
return self._total_pages
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zoom_level(self):
|
||||||
|
return self._zoom
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pdf_path(self):
|
||||||
|
return self._pdf_path
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
|
|||||||
BRANCH = "master"
|
BRANCH = "master"
|
||||||
|
|
||||||
# Version actuelle (incrementee a chaque release)
|
# Version actuelle (incrementee a chaque release)
|
||||||
VERSION = "1.0.0"
|
VERSION = "2.0.0"
|
||||||
|
|
||||||
|
|
||||||
def get_remote_version():
|
def get_remote_version():
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.0
|
2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user