Files
copydev-imposing-tool/app/main_window.py
Jules 9cc256ff51 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>
2026-03-21 07:49:44 +01:00

601 lines
22 KiB
Python

"""Fenetre principale de Copydev Imposing Tool — V2."""
import os
import sys
import tempfile
import shutil
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QFileDialog, QMessageBox, QStatusBar,
QSplitter, QProgressBar, QApplication, QToolBar)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QAction, QIcon, QDragEnterEvent, QDropEvent
from app.pdf_viewer import PdfViewer
from app.page_panel import PagePanel
from app.dialogs.booklet_dialog import BookletDialog
from app.dialogs.nup_dialog import NupDialog
from app.dialogs.step_repeat_dialog import StepRepeatDialog
from app.dialogs.manual_impose_dialog import ManualImposeDialog
from app.dialogs.join_pages_dialog import JoinPagesDialog
from app.dialogs.insert_blank_dialog import InsertBlankDialog
from app.dialogs.page_numbers_dialog import PageNumbersDialog
from app.dialogs.masking_dialog import MaskingDialog
from app.dialogs.bleeds_dialog import BleedsDialog
from app.engine.booklet import create_booklet
from app.engine.nup import nup_pages
from app.engine.step_repeat import step_and_repeat
from app.engine.manual_impose import manual_impose
from app.engine.join_pages import join_pages
from app.engine.insert_blank import insert_blank_pages
from app.engine.page_numbers import add_page_numbers
from app.engine.masking import apply_masking
from app.engine.bleeds import define_bleeds
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
from app.updater import VERSION
self.setWindowTitle(f"CopyDev Imposing Tool v{VERSION} — www.copydev.fr")
self.setMinimumSize(1100, 750)
# Icone
_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")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
self._current_file = None
self._result_file = None
# Drag & drop
self.setAcceptDrops(True)
self._setup_menu()
self._setup_toolbar()
self._setup_ui()
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):
menubar = self.menuBar()
# Fichier
file_menu = menubar.addMenu("Fichier")
self._add_action(file_menu, "Ouvrir PDF...", "Ctrl+O", self._open_file)
self._add_action(file_menu, "Enregistrer le resultat...", "Ctrl+S", self._save_result)
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)
# Imposition
impose_menu = menubar.addMenu("Imposition")
self._add_action(impose_menu, "Creer un livret", None, self._do_booklet)
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)
# 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")
self._add_action(help_menu, "A propos de CopyDev Imposing Tool", None, self._show_about)
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):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Splitter : vignettes | viewer
splitter = QSplitter(Qt.Orientation.Horizontal)
# Panneau vignettes
self._page_panel = PagePanel()
self._page_panel.page_selected.connect(self._on_thumbnail_click)
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)
# Viewer PDF
self._viewer = PdfViewer()
self._viewer.page_changed.connect(self._on_page_changed)
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):
self._statusbar = QStatusBar()
self.setStatusBar(self._statusbar)
self._status_label = QLabel("Pret — Glissez un PDF ou utilisez Fichier > Ouvrir")
self._statusbar.addWidget(self._status_label, 1)
self._file_label = QLabel("")
self._statusbar.addPermanentWidget(self._file_label)
brand = QLabel(" CopyDev — contact@copydev.fr ")
brand.setStyleSheet("color: #555; font-size: 10px;")
self._statusbar.addPermanentWidget(brand)
# ── Callbacks ──
def _on_page_changed(self, page, total):
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):
path, _ = QFileDialog.getOpenFileName(self, "Ouvrir un fichier PDF", "", "PDF (*.pdf)")
if path:
self._open_pdf(path)
def _open_pdf(self, path):
self._current_file = path
self._result_file = None
self._viewer.load_pdf(path)
self._page_panel.load_thumbnails(self._viewer)
self._file_label.setText(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):
source = self._result_file or self._current_file
if not source:
QMessageBox.warning(self, "Erreur", "Aucun fichier a enregistrer.")
return
path, _ = QFileDialog.getSaveFileName(self, "Enregistrer le resultat", "", "PDF (*.pdf)")
if path:
if not path.lower().endswith(".pdf"):
path += ".pdf"
shutil.copy2(source, path)
self._status_label.setText(f"Enregistre : {os.path.basename(path)}")
def _require_file(self):
if not self._current_file:
QMessageBox.warning(self, "Erreur", "Veuillez d'abord ouvrir un fichier PDF.")
return False
return True
def _make_temp(self):
fd, path = tempfile.mkstemp(suffix=".pdf")
os.close(fd)
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):
self._result_file = output_path
self._current_file = output_path
self._viewer.load_pdf(output_path)
self._page_panel.load_thumbnails(self._viewer)
self._hide_progress()
self._status_label.setText("Traitement termine")
QTimer.singleShot(100, self._viewer.zoom_fit)
# ── Actions imposition ──
def _do_booklet(self):
if not self._require_file():
return
dlg = BookletDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
create_booklet(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_nup(self):
if not self._require_file():
return
dlg = NupDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
nup_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_step_repeat(self):
if not self._require_file():
return
dlg = StepRepeatDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
step_and_repeat(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_manual_impose(self):
if not self._require_file():
return
from pypdf import PdfReader
reader = PdfReader(self._current_file)
dlg = ManualImposeDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
manual_impose(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_join_pages(self):
if not self._require_file():
return
dlg = JoinPagesDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
join_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_insert_blank(self):
if not self._require_file():
return
from pypdf import PdfReader
reader = PdfReader(self._current_file)
dlg = InsertBlankDialog(total_pages=len(reader.pages), parent=self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
insert_blank_pages(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_page_numbers(self):
if not self._require_file():
return
dlg = PageNumbersDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
add_page_numbers(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_masking(self):
if not self._require_file():
return
dlg = MaskingDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
apply_masking(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e))
def _do_bleeds(self):
if not self._require_file():
return
dlg = BleedsDialog(self)
if dlg.exec():
self._show_progress()
settings = dlg.get_settings()
output = self._make_temp()
try:
define_bleeds(self._current_file, output, **settings)
self._show_result(output)
except Exception as e:
self._hide_progress()
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):
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QDialogButtonBox
from PyQt6.QtGui import QPixmap, QFont
dlg = QDialog(self)
dlg.setWindowTitle("A propos — CopyDev Imposing Tool")
dlg.setFixedSize(520, 480)
dlg.setStyleSheet("QDialog { background-color: #0f172a; }")
layout = QVBoxLayout(dlg)
layout.setContentsMargins(40, 30, 40, 25)
# Logo
logo_label = QLabel()
_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")
if os.path.exists(logo_path):
pixmap = QPixmap(logo_path).scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
logo_label.setPixmap(pixmap)
logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(logo_label)
layout.addSpacing(10)
title = QLabel("COPYDEV IMPOSING TOOL")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
title.setStyleSheet("color: white;")
layout.addWidget(title)
from app.updater import VERSION
version = QLabel(f"Version {VERSION}")
version.setAlignment(Qt.AlignmentFlag.AlignCenter)
version.setFont(QFont("Segoe UI", 10))
version.setStyleSheet("color: #64748b;")
layout.addWidget(version)
layout.addSpacing(15)
desc = QLabel(
"Outil professionnel d'imposition PDF\n"
"Livrets, n-up, repetition, imposition manuelle,\n"
"numerotation, masquage et fonds perdus."
)
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc.setFont(QFont("Segoe UI", 10))
desc.setStyleSheet("color: #94a3b8;")
desc.setWordWrap(True)
layout.addWidget(desc)
layout.addSpacing(20)
sep = QLabel()
sep.setFixedHeight(1)
sep.setStyleSheet("background-color: #1e293b;")
layout.addWidget(sep)
layout.addSpacing(15)
contact = QLabel(
'<p style="text-align:center; line-height: 1.6;">'
'<span style="font-size:14px; font-weight:bold; color:white;">CopyDev</span><br>'
'<span style="color:#94a3b8;">Jules Dubois — Developpeur logiciel</span><br><br>'
'<span style="color:#94a3b8;">Email : </span>'
'<a href="mailto:contact@copydev.fr" style="color:#38bdf8;">contact@copydev.fr</a><br>'
'<span style="color:#94a3b8;">Web : </span>'
'<a href="https://copydev.fr" style="color:#38bdf8;">www.copydev.fr</a>'
'</p>'
)
contact.setAlignment(Qt.AlignmentFlag.AlignCenter)
contact.setOpenExternalLinks(True)
layout.addWidget(contact)
layout.addStretch()
copy_label = QLabel("2026 CopyDev — Tous droits reserves\nLogiciel sous licence — Reproduction interdite")
copy_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
copy_label.setFont(QFont("Segoe UI", 8))
copy_label.setStyleSheet("color: #334155;")
layout.addWidget(copy_label)
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.rejected.connect(dlg.close)
layout.addWidget(buttons)
dlg.exec()