"""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.dialogs.crop_marks_dialog import CropMarksDialog from app.dialogs.crop_dialog import CropDialog from app.dialogs.add_text_dialog import AddTextDialog from app.dialogs.add_image_dialog import AddImageDialog 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 from app.engine.crop_marks import add_crop_marks, remove_crop_marks from app.engine.crop import crop_pages from app.engine.add_content import add_text_to_pages, add_image_to_pages 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) self._add_action(file_menu, "Imprimer...", "Ctrl+P", self._do_print) 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, "Recadrer...", None, self._do_crop) pages_menu.addSeparator() 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, "Ajouter du texte...", None, self._do_add_text) self._add_action(annot_menu, "Ajouter une image...", None, self._do_add_image) annot_menu.addSeparator() 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() annot_menu.addSeparator() self._add_action(annot_menu, "Reperes de coupe...", None, self._do_crop_marks) self._add_action(annot_menu, "Enlever les reperes de coupe", None, self._do_remove_crop_marks) 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) # ── Toolbars ── def _setup_toolbar(self): # Toolbar horizontale en haut (fichier + zoom) tb = QToolBar("Fichier") tb.setMovable(False) self.addToolBar(tb) tb.addAction("Ouvrir", self._open_file) tb.addAction("Enregistrer", self._save_result) tb.addAction("Imprimer", self._do_print) 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) # Toolbar verticale a droite (outils) rtb = QToolBar("Outils") rtb.setMovable(False) rtb.setOrientation(Qt.Orientation.Vertical) rtb.setStyleSheet(""" QToolBar { background: #12122a; border-left: 1px solid #333; padding: 4px 2px; spacing: 2px; } QToolBar QToolButton { color: #ccc; padding: 6px; border-radius: 4px; font-size: 10px; min-width: 60px; } QToolBar QToolButton:hover { background: #0f3460; color: white; } """) self.addToolBar(Qt.ToolBarArea.RightToolBarArea, rtb) # Section Imposition rtb.addWidget(self._make_section_label("IMPOSITION")) rtb.addAction("Livret", self._do_booklet) rtb.addAction("N-Up", self._do_nup) rtb.addAction("Repetition", self._do_step_repeat) rtb.addAction("Manuel", self._do_manual_impose) rtb.addAction("Joindre", self._do_join_pages) rtb.addSeparator() # Section Pages rtb.addWidget(self._make_section_label("PAGES")) rtb.addAction("Recadrer", self._do_crop) rtb.addAction("Pg blanche", self._do_insert_blank) rtb.addAction("Fusionner", self._do_merge) rtb.addSeparator() # Section Annotations rtb.addWidget(self._make_section_label("ANNOTATIONS")) rtb.addAction("Texte", self._do_add_text) rtb.addAction("Image", self._do_add_image) rtb.addAction("Numeros", self._do_page_numbers) rtb.addAction("Masquer", self._do_masking) rtb.addSeparator() # Section Reperes rtb.addWidget(self._make_section_label("REPERES")) rtb.addAction("Coupe +", self._do_crop_marks) rtb.addAction("Coupe -", self._do_remove_crop_marks) rtb.addAction("Bleed +", self._do_bleeds) rtb.addAction("Bleed -", self._do_remove_bleeds) def _make_section_label(self, text): lbl = QLabel(text) lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lbl.setStyleSheet("color: #555; font-size: 9px; font-weight: bold; letter-spacing: 1px; padding: 4px 0 2px 0;") return lbl # ── 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_add_text(self): if not self._require_file(): return from pypdf import PdfReader reader = PdfReader(self._current_file) dlg = AddTextDialog(total_pages=len(reader.pages), current_page=self._viewer.current_page, parent=self) if dlg.exec(): settings = dlg.get_settings() if not settings["text"].strip(): return self._show_progress() output = self._make_temp() try: add_text_to_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_add_image(self): if not self._require_file(): return from pypdf import PdfReader reader = PdfReader(self._current_file) dlg = AddImageDialog(total_pages=len(reader.pages), current_page=self._viewer.current_page, parent=self) if dlg.exec(): settings = dlg.get_settings() if not settings.get("image_path"): QMessageBox.warning(self, "Erreur", "Aucune image selectionnee.") return self._show_progress() output = self._make_temp() try: add_image_to_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_crop(self): if not self._require_file(): return from pypdf import PdfReader from app.engine.pdf_utils import get_page_size reader = PdfReader(self._current_file) page = reader.pages[self._viewer.current_page] w, h = get_page_size(page) dlg = CropDialog( page_width_pt=w, page_height_pt=h, total_pages=len(reader.pages), current_page=self._viewer.current_page, viewer=self._viewer, parent=self ) if dlg.exec(): self._show_progress() settings = dlg.get_settings() output = self._make_temp() try: crop_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_print(self): if not self._require_file(): return import subprocess try: source = self._result_file or self._current_file subprocess.Popen(["cmd", "/c", "start", "", source], shell=True) self._status_label.setText("Ouverture pour impression...") except Exception as e: QMessageBox.critical(self, "Erreur", f"Impossible d'imprimer :\n{e}") def _do_crop_marks(self): if not self._require_file(): return dlg = CropMarksDialog(self) if dlg.exec(): self._show_progress() settings = dlg.get_settings() settings["current_page"] = self._viewer.current_page output = self._make_temp() try: add_crop_marks(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_crop_marks(self): if not self._require_file(): return self._show_progress() try: output = self._make_temp() remove_crop_marks(self._current_file, output) 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( '

' 'CopyDev
' 'Jules Dubois — Developpeur logiciel

' 'Email : ' 'contact@copydev.fr
' 'Web : ' 'www.copydev.fr' '

' ) 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()