- Les sous-menus sont maintenant des entrees individuelles dans le menu contextuel (pas de SubCommands qui ne fonctionne pas) - Chaque action a sa propre entree : CopyDev: Livret A4, Livret A3, etc. - Le unregister nettoie toutes les entrees - Suppression de l'ancienne entree CopyDevImpose vide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1462 lines
59 KiB
Python
1462 lines
59 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, QRect
|
|
from PyQt6.QtGui import QAction, QIcon, QDragEnterEvent, QDropEvent, QPageLayout
|
|
|
|
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
|
|
from app.engine.unbooklet import unbooklet
|
|
from app.engine.trim_shift import trim_and_shift
|
|
from app.engine.extract_content import extract_text_from_page, extract_images_from_page
|
|
from app.dialogs.trim_shift_dialog import TrimShiftDialog
|
|
from app.dialogs.edit_content_dialog import EditContentDialog
|
|
|
|
|
|
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
|
|
self._original_file = None
|
|
self._crop_settings = {}
|
|
self._undo_stack = [] # pile de fichiers pour annuler
|
|
self._redo_stack = []
|
|
|
|
# Drag & drop
|
|
self.setAcceptDrops(True)
|
|
|
|
self._setup_menu()
|
|
self._setup_toolbar()
|
|
self._setup_ui()
|
|
self._setup_statusbar()
|
|
|
|
# Style gris moderne avec accents cyan
|
|
self.setStyleSheet("""
|
|
* { font-family: 'Segoe UI', sans-serif; }
|
|
|
|
QMainWindow { background: #e8e8e8; }
|
|
|
|
QMenuBar {
|
|
background: #f2f2f2; color: #2d2d2d; font-size: 13px;
|
|
padding: 2px 0; border-bottom: 1px solid #d0d0d0;
|
|
}
|
|
QMenuBar::item { padding: 5px 12px; border-radius: 4px; margin: 1px 2px; }
|
|
QMenuBar::item:selected { background: #d8d8d8; }
|
|
|
|
QMenu {
|
|
background: #ffffff; color: #2d2d2d; border: 1px solid #d0d0d0;
|
|
border-radius: 8px; padding: 4px;
|
|
}
|
|
QMenu::item { padding: 6px 24px; border-radius: 4px; }
|
|
QMenu::item:selected { background: #e0f4ff; color: #006fa6; }
|
|
QMenu::separator { background: #e0e0e0; height: 1px; margin: 4px 12px; }
|
|
|
|
QToolBar {
|
|
background: #f2f2f2; border: none; spacing: 2px; padding: 4px;
|
|
border-bottom: 1px solid #d0d0d0;
|
|
}
|
|
QToolBar QToolButton {
|
|
color: #444; padding: 5px 10px; border-radius: 6px;
|
|
font-size: 12px; border: 1px solid transparent;
|
|
}
|
|
QToolBar QToolButton:hover {
|
|
background: #d8f0f8; border: 1px solid #00b4d8; color: #006fa6;
|
|
}
|
|
QToolBar QToolButton:pressed { background: #b8e6f0; }
|
|
|
|
QStatusBar {
|
|
background: #f2f2f2; color: #888; font-size: 11px;
|
|
border-top: 1px solid #d0d0d0;
|
|
}
|
|
|
|
QSplitter::handle { background: #d0d0d0; width: 2px; }
|
|
QSplitter::handle:hover { background: #00b4d8; }
|
|
|
|
QDialog { background: #f5f5f5; color: #2d2d2d; }
|
|
|
|
QGroupBox {
|
|
color: #2d2d2d; border: 1px solid #d0d0d0; border-radius: 8px;
|
|
margin-top: 12px; padding-top: 16px; font-weight: bold;
|
|
background: #ffffff;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin; left: 12px; padding: 0 6px;
|
|
color: #00b4d8;
|
|
}
|
|
|
|
QLabel { color: #2d2d2d; }
|
|
|
|
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QTextEdit {
|
|
background: #ffffff; color: #2d2d2d; border: 1px solid #ccc;
|
|
border-radius: 6px; padding: 5px 8px;
|
|
}
|
|
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus, QTextEdit:focus {
|
|
border: 1px solid #00b4d8;
|
|
}
|
|
QComboBox::drop-down { border: none; padding-right: 8px; }
|
|
QComboBox QAbstractItemView {
|
|
background: #ffffff; color: #2d2d2d; border: 1px solid #d0d0d0;
|
|
selection-background-color: #e0f4ff;
|
|
selection-color: #006fa6;
|
|
}
|
|
|
|
QPushButton {
|
|
background: #f0f0f0; color: #2d2d2d; border: 1px solid #ccc;
|
|
border-radius: 6px; padding: 6px 14px; font-size: 12px;
|
|
}
|
|
QPushButton:hover { background: #d8f0f8; border-color: #00b4d8; color: #006fa6; }
|
|
QPushButton:pressed { background: #b8e6f0; }
|
|
|
|
QRadioButton, QCheckBox { color: #2d2d2d; spacing: 6px; }
|
|
|
|
QDialogButtonBox QPushButton { min-width: 80px; }
|
|
|
|
QProgressBar { background: #e0e0e0; border: none; height: 3px; }
|
|
QProgressBar::chunk { background: #00b4d8; border-radius: 1px; }
|
|
|
|
QScrollBar:vertical {
|
|
background: #f0f0f0; width: 10px; border: none;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background: #c0c0c0; border-radius: 5px; min-height: 30px;
|
|
}
|
|
QScrollBar::handle:vertical:hover { background: #a0a0a0; }
|
|
QScrollBar::add-line, QScrollBar::sub-line { height: 0; }
|
|
|
|
QScrollBar:horizontal {
|
|
background: #f0f0f0; height: 10px; border: none;
|
|
}
|
|
QScrollBar::handle:horizontal {
|
|
background: #c0c0c0; border-radius: 5px; min-width: 30px;
|
|
}
|
|
QScrollBar::handle:horizontal:hover { background: #a0a0a0; }
|
|
""")
|
|
|
|
# ── 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)
|
|
|
|
# Edition
|
|
edit_menu = menubar.addMenu("Edition")
|
|
self._add_action(edit_menu, "Annuler", "Ctrl+Z", self._do_undo)
|
|
self._add_action(edit_menu, "Refaire", "Ctrl+Y", self._do_redo)
|
|
|
|
# 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)
|
|
impose_menu.addSeparator()
|
|
self._add_action(impose_menu, "Delivretiser", None, self._do_unbooklet)
|
|
|
|
# Pages
|
|
pages_menu = menubar.addMenu("Pages")
|
|
self._add_action(pages_menu, "Recadrer...", None, self._do_crop)
|
|
self._add_action(pages_menu, "Trim && Shift...", None, self._do_trim_shift)
|
|
pages_menu.addSeparator()
|
|
self._add_action(pages_menu, "Pivoter +90", None, lambda: self._rotate_current(90))
|
|
self._add_action(pages_menu, "Pivoter -90", None, lambda: self._rotate_current(-90))
|
|
self._add_action(pages_menu, "Pivoter 180", None, lambda: self._rotate_current(180))
|
|
self._add_action(pages_menu, "Inverser l'ordre des pages", None, self._do_reverse_pages)
|
|
self._add_action(pages_menu, "Separer paires/impaires", None, self._do_shuffle_even_odd)
|
|
pages_menu.addSeparator()
|
|
self._add_action(pages_menu, "Inserer des pages blanches", None, self._do_insert_blank)
|
|
self._add_action(pages_menu, "Creer un document test...", None, self._do_create_sample)
|
|
|
|
# Annotations
|
|
annot_menu = menubar.addMenu("Annotations")
|
|
self._add_action(annot_menu, "Editer le contenu...", None, self._do_edit_content)
|
|
annot_menu.addSeparator()
|
|
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)
|
|
|
|
# Macros
|
|
self._macros_menu = menubar.addMenu("Macros")
|
|
self._add_action(self._macros_menu, "Enregistrer la derniere action...", None, self._do_save_macro)
|
|
self._add_action(self._macros_menu, "Gerer les macros...", None, self._do_manage_macros)
|
|
self._macros_menu.addSeparator()
|
|
self._refresh_macros_menu()
|
|
|
|
# Aide
|
|
help_menu = menubar.addMenu("Aide")
|
|
self._add_action(help_menu, "Rechercher une mise a jour...", None, self._do_check_update)
|
|
help_menu.addSeparator()
|
|
self._add_action(help_menu, "Integrer a Windows (menu contextuel + association PDF)", None, self._do_register_windows)
|
|
self._add_action(help_menu, "Retirer l'integration Windows", None, self._do_unregister_windows)
|
|
help_menu.addSeparator()
|
|
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: #f2f2f2; border-left: 1px solid #d0d0d0;
|
|
padding: 6px 4px; spacing: 1px;
|
|
}
|
|
QToolBar QToolButton {
|
|
color: #555; padding: 7px 4px; border-radius: 6px;
|
|
font-size: 11px; min-width: 64px;
|
|
border: 1px solid transparent;
|
|
}
|
|
QToolBar QToolButton:hover {
|
|
background: #d8f0f8; color: #006fa6;
|
|
border: 1px solid #00b4d8;
|
|
}
|
|
QToolBar QToolButton:pressed { background: #b8e6f0; }
|
|
""")
|
|
self.addToolBar(Qt.ToolBarArea.RightToolBarArea, rtb)
|
|
|
|
# Section Imposition
|
|
rtb.addWidget(self._make_section_label("IMPOSITION"))
|
|
rtb.addAction("Livret", self._do_booklet)
|
|
rtb.addAction("Delivretiser", self._do_unbooklet)
|
|
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("Trim/Shift", self._do_trim_shift)
|
|
rtb.addAction("Pg blanche", self._do_insert_blank)
|
|
rtb.addAction("Inverser", self._do_reverse_pages)
|
|
rtb.addAction("Fusionner", self._do_merge)
|
|
rtb.addSeparator()
|
|
|
|
# Section Annotations
|
|
rtb.addWidget(self._make_section_label("ANNOTATIONS"))
|
|
rtb.addAction("Editer", self._do_edit_content)
|
|
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: #00b4d8; font-size: 9px; font-weight: bold;
|
|
letter-spacing: 2px; padding: 8px 0 3px 0;
|
|
border-bottom: 1px solid #d0d0d0; margin: 0 4px;
|
|
""")
|
|
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)
|
|
self._page_panel.page_rotate_requested.connect(self._on_page_rotate)
|
|
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._original_file = path # reset l'original
|
|
self._crop_settings = {} # reset les crops
|
|
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)}")
|
|
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
|
|
|
|
_FUN_MESSAGES = [
|
|
"Pliage des pages en origami...",
|
|
"Consultation des esprits de Gutenberg...",
|
|
"Negociation avec les pixels...",
|
|
"Alignement des atomes de papier...",
|
|
"Invocation du dieu de l'imposition...",
|
|
"Repassage des pages froissees...",
|
|
"Calibrage du massicot virtuel...",
|
|
"Appel a un ami imprimeur...",
|
|
"Les pages font la queue...",
|
|
"Rechauffement de l'encre numerique...",
|
|
"Comptage des points par pouce...",
|
|
"Meditation sur les marges...",
|
|
"Rangement des pages par ordre alphabetique... non, numerique !",
|
|
"Verification que le papier est bien a plat...",
|
|
]
|
|
|
|
def _show_progress(self):
|
|
import random
|
|
self._progress.setRange(0, 0)
|
|
self._progress.show()
|
|
msg = random.choice(self._FUN_MESSAGES)
|
|
self._status_label.setText(msg)
|
|
QApplication.processEvents()
|
|
|
|
def _hide_progress(self):
|
|
self._progress.hide()
|
|
self._progress.setRange(0, 1)
|
|
|
|
def _show_result(self, output_path):
|
|
# Sauvegarder l'etat actuel pour annulation
|
|
if self._current_file:
|
|
self._undo_stack.append(self._current_file)
|
|
self._redo_stack.clear()
|
|
if len(self._undo_stack) > 20:
|
|
self._undo_stack.pop(0)
|
|
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)
|
|
|
|
def _do_undo(self):
|
|
if not self._undo_stack:
|
|
return
|
|
self._redo_stack.append(self._current_file)
|
|
prev = self._undo_stack.pop()
|
|
self._current_file = prev
|
|
self._result_file = prev
|
|
self._viewer.load_pdf(prev)
|
|
self._page_panel.load_thumbnails(self._viewer)
|
|
self._status_label.setText("Annule")
|
|
QTimer.singleShot(100, self._viewer.zoom_fit)
|
|
|
|
def _do_redo(self):
|
|
if not self._redo_stack:
|
|
return
|
|
self._undo_stack.append(self._current_file)
|
|
nxt = self._redo_stack.pop()
|
|
self._current_file = nxt
|
|
self._result_file = nxt
|
|
self._viewer.load_pdf(nxt)
|
|
self._page_panel.load_thumbnails(self._viewer)
|
|
self._status_label.setText("Refait")
|
|
QTimer.singleShot(100, self._viewer.zoom_fit)
|
|
|
|
# ── Rotation ──
|
|
|
|
def _on_page_rotate(self, page_index, degrees):
|
|
if not self._current_file:
|
|
return
|
|
from pypdf import PdfReader, PdfWriter
|
|
reader = PdfReader(self._current_file)
|
|
writer = PdfWriter()
|
|
for i, page in enumerate(reader.pages):
|
|
if i == page_index:
|
|
page = page.rotate(degrees)
|
|
writer.add_page(page)
|
|
output = self._make_temp()
|
|
with open(output, "wb") as f:
|
|
writer.write(f)
|
|
self._show_result(output)
|
|
|
|
def _rotate_current(self, degrees):
|
|
if not self._require_file():
|
|
return
|
|
self._on_page_rotate(self._viewer.current_page, degrees)
|
|
|
|
def _do_reverse_pages(self):
|
|
if not self._require_file():
|
|
return
|
|
from pypdf import PdfReader, PdfWriter
|
|
self._show_progress()
|
|
reader = PdfReader(self._current_file)
|
|
writer = PdfWriter()
|
|
for page in reversed(reader.pages):
|
|
writer.add_page(page)
|
|
output = self._make_temp()
|
|
with open(output, "wb") as f:
|
|
writer.write(f)
|
|
self._show_result(output)
|
|
self._status_label.setText("Ordre des pages inverse")
|
|
|
|
def _do_unbooklet(self):
|
|
if not self._require_file():
|
|
return
|
|
self._show_progress()
|
|
output = self._make_temp()
|
|
try:
|
|
unbooklet(self._current_file, output)
|
|
self._show_result(output)
|
|
self._status_label.setText("Delivretisation terminee")
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
def _do_trim_shift(self):
|
|
if not self._require_file():
|
|
return
|
|
from pypdf import PdfReader
|
|
reader = PdfReader(self._current_file)
|
|
dlg = TrimShiftDialog(total_pages=len(reader.pages), parent=self)
|
|
if dlg.exec():
|
|
self._show_progress()
|
|
settings = dlg.get_settings()
|
|
output = self._make_temp()
|
|
try:
|
|
trim_and_shift(self._current_file, output, **settings)
|
|
self._last_action = {"action": "trim_shift", "settings": settings}
|
|
self._show_result(output)
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
# ── 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)
|
|
self._last_action = {"action": "booklet", "settings": settings}
|
|
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._last_action = {"action": "nup", "settings": 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._last_action = {"action": "step_repeat", "settings": 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_edit_content(self):
|
|
if not self._require_file():
|
|
return
|
|
cur = self._viewer.current_page
|
|
texts = extract_text_from_page(self._current_file, cur)
|
|
images = extract_images_from_page(self._current_file, cur)
|
|
dlg = EditContentDialog(text_items=texts, image_items=images, parent=self)
|
|
if dlg.exec():
|
|
actions = dlg.get_actions()
|
|
if not actions:
|
|
return
|
|
self._show_progress()
|
|
from pypdf import PdfReader, PdfWriter
|
|
from app.engine.pdf_utils import get_page_size
|
|
from reportlab.pdfgen import canvas as rl_canvas
|
|
import io
|
|
reader = PdfReader(self._current_file)
|
|
writer = PdfWriter()
|
|
page = reader.pages[cur]
|
|
w, h = get_page_size(page)
|
|
|
|
# Appliquer les masquages de texte
|
|
mask_lines = [a["line_index"] for a in actions if a["type"] == "mask_text"]
|
|
if mask_lines:
|
|
packet = io.BytesIO()
|
|
c = rl_canvas.Canvas(packet, pagesize=(w, h))
|
|
c.setFillColorRGB(1, 1, 1)
|
|
line_h = h / max(len(texts), 1)
|
|
for li in mask_lines:
|
|
y = h - (li + 1) * line_h
|
|
c.rect(0, y, w, line_h, fill=1, stroke=0)
|
|
c.showPage()
|
|
c.save()
|
|
packet.seek(0)
|
|
overlay = PdfReader(packet)
|
|
page.merge_page(overlay.pages[0])
|
|
|
|
# Remplacer les images
|
|
for a in actions:
|
|
if a["type"] == "replace_image" and a.get("new_path"):
|
|
from app.engine.extract_content import replace_image_in_page
|
|
# On fait un write intermediaire
|
|
tmp = self._make_temp()
|
|
for ii, pp in enumerate(reader.pages):
|
|
writer.add_page(pp)
|
|
with open(tmp, "wb") as f:
|
|
writer.write(f)
|
|
replace_image_in_page(tmp, tmp, cur, a["name"], a["new_path"])
|
|
reader = PdfReader(tmp)
|
|
writer = PdfWriter()
|
|
for pp in reader.pages:
|
|
writer.add_page(pp)
|
|
output = self._make_temp()
|
|
with open(output, "wb") as f:
|
|
writer.write(f)
|
|
self._show_result(output)
|
|
return
|
|
|
|
for i, p in enumerate(reader.pages):
|
|
writer.add_page(p)
|
|
output = self._make_temp()
|
|
with open(output, "wb") as f:
|
|
writer.write(f)
|
|
self._show_result(output)
|
|
self._status_label.setText("Contenu edite")
|
|
|
|
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, pt_to_mm
|
|
|
|
# Toujours travailler depuis l'original pour permettre la re-edition
|
|
source = self._original_file or self._current_file
|
|
reader = PdfReader(source)
|
|
cur_page = self._viewer.current_page
|
|
page = reader.pages[cur_page]
|
|
w, h = get_page_size(page)
|
|
|
|
# Recharger l'original dans le viewer pour voir la page entiere
|
|
self._viewer.load_pdf(source)
|
|
self._viewer.go_to_page(cur_page)
|
|
self._page_panel.load_thumbnails(self._viewer)
|
|
QApplication.processEvents()
|
|
|
|
# Si on a des settings de crop existants pour cette page, pre-remplir
|
|
existing = self._crop_settings.get(cur_page, None)
|
|
|
|
self._crop_dlg = CropDialog(
|
|
page_width_pt=w, page_height_pt=h,
|
|
total_pages=len(reader.pages),
|
|
current_page=cur_page,
|
|
viewer=self._viewer,
|
|
existing_crop=existing,
|
|
parent=self
|
|
)
|
|
|
|
def on_crop_accepted():
|
|
if not hasattr(self, '_crop_dlg') or not self._crop_dlg:
|
|
return
|
|
dlg_ref = self._crop_dlg
|
|
self._crop_dlg = None
|
|
try:
|
|
settings = dlg_ref.get_settings()
|
|
except Exception:
|
|
return
|
|
if not settings.get("pages"):
|
|
return
|
|
|
|
# Sauvegarder les settings de crop par page
|
|
for pg in settings["pages"]:
|
|
self._crop_settings[pg] = {
|
|
"crop_top": settings["crop_top"],
|
|
"crop_bottom": settings["crop_bottom"],
|
|
"crop_left": settings["crop_left"],
|
|
"crop_right": settings["crop_right"],
|
|
}
|
|
|
|
# Appliquer le crop depuis l'original
|
|
self._show_progress()
|
|
source_file = self._original_file or self._current_file
|
|
output = self._make_temp()
|
|
try:
|
|
crop_pages(source_file, output, **settings)
|
|
self._result_file = output
|
|
self._current_file = output
|
|
self._viewer.load_pdf(output)
|
|
self._viewer.go_to_page(cur_page)
|
|
self._page_panel.load_thumbnails(self._viewer)
|
|
self._hide_progress()
|
|
self._status_label.setText(
|
|
f"Recadrage applique — {len(settings['pages'])} page(s)"
|
|
)
|
|
QTimer.singleShot(100, self._viewer.zoom_fit)
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
def on_crop_rejected():
|
|
# Revenir au fichier actuel (croppe ou non)
|
|
current = self._current_file
|
|
self._viewer.load_pdf(current)
|
|
self._viewer.go_to_page(cur_page)
|
|
self._page_panel.load_thumbnails(self._viewer)
|
|
self._crop_dlg = None
|
|
|
|
self._crop_dlg.accepted.connect(on_crop_accepted)
|
|
self._crop_dlg.rejected.connect(on_crop_rejected)
|
|
self._crop_dlg.show()
|
|
|
|
def _do_print(self):
|
|
if not self._require_file():
|
|
return
|
|
try:
|
|
from PyQt6.QtPrintSupport import QPrinter, QPrintDialog
|
|
from PyQt6.QtGui import QPainter, QImage
|
|
import pypdfium2 as pdfium
|
|
|
|
source = self._result_file or self._current_file
|
|
pdf = pdfium.PdfDocument(source)
|
|
|
|
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
|
|
printer.setPageOrientation(
|
|
QPageLayout.Orientation.Landscape
|
|
if pdf[0].get_size()[0] > pdf[0].get_size()[1]
|
|
else QPageLayout.Orientation.Portrait
|
|
)
|
|
|
|
dialog = QPrintDialog(printer, self)
|
|
dialog.setWindowTitle("Imprimer — CopyDev Imposing Tool")
|
|
if dialog.exec() != QPrintDialog.DialogCode.Accepted:
|
|
return
|
|
|
|
self._show_progress()
|
|
self._status_label.setText("Impression en cours...")
|
|
QApplication.processEvents()
|
|
|
|
painter = QPainter()
|
|
painter.begin(printer)
|
|
|
|
for i in range(len(pdf)):
|
|
if i > 0:
|
|
printer.newPage()
|
|
|
|
page = pdf[i]
|
|
# Rendu a haute resolution
|
|
dpi = printer.resolution()
|
|
scale = dpi / 72
|
|
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)
|
|
|
|
# Calculer le placement sur la page imprimee
|
|
page_rect = printer.pageRect(QPrinter.Unit.Point)
|
|
pw, ph = page.get_size()
|
|
scale_x = page_rect.width() / pil_image.width
|
|
scale_y = page_rect.height() / pil_image.height
|
|
s = min(scale_x, scale_y)
|
|
|
|
target_w = int(pil_image.width * s)
|
|
target_h = int(pil_image.height * s)
|
|
x = int((page_rect.width() - target_w) / 2)
|
|
y = int((page_rect.height() - target_h) / 2)
|
|
|
|
from PyQt6.QtCore import QRect
|
|
painter.drawImage(QRect(x, y, target_w, target_h), qimage)
|
|
|
|
painter.end()
|
|
self._hide_progress()
|
|
self._status_label.setText(f"Impression terminee — {len(pdf)} page(s)")
|
|
|
|
except ImportError:
|
|
QMessageBox.critical(self, "Erreur",
|
|
"Le module d'impression n'est pas disponible.\n"
|
|
"Installez PyQt6 avec le support impression.")
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", f"Erreur d'impression :\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):
|
|
from app.dialogs.merge_dialog import MergeDialog
|
|
# Pre-remplir avec le fichier courant si ouvert
|
|
initial = [self._current_file] if self._current_file else []
|
|
dlg = MergeDialog(initial_files=initial, parent=self)
|
|
if dlg.exec():
|
|
files = dlg.get_files()
|
|
if len(files) < 2:
|
|
QMessageBox.information(self, "Fusion", "Selectionnez au moins 2 fichiers PDF.")
|
|
return
|
|
from pypdf import PdfWriter
|
|
self._show_progress()
|
|
try:
|
|
writer = PdfWriter()
|
|
for path in files:
|
|
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(files)} fichiers terminee")
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
# ── A propos ──
|
|
|
|
# ── Macros ──
|
|
|
|
def _last_action_info(self):
|
|
return getattr(self, '_last_action', None)
|
|
|
|
def _do_save_macro(self):
|
|
from PyQt6.QtWidgets import QInputDialog
|
|
name, ok = QInputDialog.getText(self, "Enregistrer la macro", "Nom de la macro :")
|
|
if ok and name.strip():
|
|
info = self._last_action_info()
|
|
if info:
|
|
from app.presets.preset_manager import save_preset
|
|
save_preset(name.strip(), info["action"], info["settings"])
|
|
self._refresh_macros_menu()
|
|
self._status_label.setText(f"Macro '{name.strip()}' enregistree")
|
|
else:
|
|
QMessageBox.information(self, "Macro", "Effectuez d'abord une action a enregistrer.")
|
|
|
|
def _do_manage_macros(self):
|
|
from app.presets.preset_manager import list_presets, delete_preset
|
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QHBoxLayout
|
|
presets = list_presets()
|
|
if not presets:
|
|
QMessageBox.information(self, "Macros", "Aucune macro enregistree.")
|
|
return
|
|
dlg = QDialog(self)
|
|
dlg.setWindowTitle("Gerer les macros")
|
|
dlg.setMinimumWidth(350)
|
|
layout = QVBoxLayout(dlg)
|
|
lst = QListWidget()
|
|
lst.addItems(presets.keys())
|
|
layout.addWidget(lst)
|
|
bl = QHBoxLayout()
|
|
btn_del = QPushButton("Supprimer")
|
|
def do_del():
|
|
item = lst.currentItem()
|
|
if item:
|
|
delete_preset(item.text())
|
|
lst.takeItem(lst.row(item))
|
|
self._refresh_macros_menu()
|
|
btn_del.clicked.connect(do_del)
|
|
bl.addWidget(btn_del)
|
|
btn_close = QPushButton("Fermer")
|
|
btn_close.clicked.connect(dlg.close)
|
|
bl.addWidget(btn_close)
|
|
layout.addLayout(bl)
|
|
dlg.exec()
|
|
|
|
def _refresh_macros_menu(self):
|
|
from app.presets.preset_manager import list_presets
|
|
# Supprimer les actions apres le separateur
|
|
actions = self._macros_menu.actions()
|
|
found_sep = False
|
|
for act in actions:
|
|
if act.isSeparator():
|
|
found_sep = True
|
|
continue
|
|
if found_sep:
|
|
self._macros_menu.removeAction(act)
|
|
# Ajouter les presets
|
|
presets = list_presets()
|
|
for name in presets:
|
|
self._macros_menu.addAction(name, lambda n=name: self._run_macro(n))
|
|
|
|
def _run_macro(self, name):
|
|
from app.presets.preset_manager import list_presets
|
|
presets = list_presets()
|
|
preset = presets.get(name)
|
|
if not preset or not self._require_file():
|
|
return
|
|
action = preset["action"]
|
|
settings = preset["settings"]
|
|
self._show_progress()
|
|
output = self._make_temp()
|
|
try:
|
|
if action == "booklet":
|
|
create_booklet(self._current_file, output, **settings)
|
|
elif action == "nup":
|
|
nup_pages(self._current_file, output, **settings)
|
|
self._last_action = {"action": "nup", "settings": settings}
|
|
elif action == "step_repeat":
|
|
step_and_repeat(self._current_file, output, **settings)
|
|
self._last_action = {"action": "step_repeat", "settings": settings}
|
|
elif action == "crop":
|
|
crop_pages(self._current_file, output, **settings)
|
|
elif action == "trim_shift":
|
|
trim_and_shift(self._current_file, output, **settings)
|
|
self._last_action = {"action": "trim_shift", "settings": settings}
|
|
else:
|
|
self._hide_progress()
|
|
QMessageBox.warning(self, "Macro", f"Action inconnue : {action}")
|
|
return
|
|
self._show_result(output)
|
|
self._status_label.setText(f"Macro '{name}' executee")
|
|
except Exception as e:
|
|
self._hide_progress()
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
# ── Pages supplementaires ──
|
|
|
|
def _do_shuffle_even_odd(self):
|
|
"""Separer pages paires et impaires en deux documents."""
|
|
if not self._require_file():
|
|
return
|
|
from pypdf import PdfReader, PdfWriter
|
|
reader = PdfReader(self._current_file)
|
|
writer_odd = PdfWriter()
|
|
writer_even = PdfWriter()
|
|
for i, page in enumerate(reader.pages):
|
|
if (i + 1) % 2 == 0:
|
|
writer_even.add_page(page)
|
|
else:
|
|
writer_odd.add_page(page)
|
|
# Sauvegarder les deux
|
|
path_odd, _ = QFileDialog.getSaveFileName(self, "Enregistrer pages impaires", "", "PDF (*.pdf)")
|
|
if not path_odd:
|
|
return
|
|
with open(path_odd, "wb") as f:
|
|
writer_odd.write(f)
|
|
path_even, _ = QFileDialog.getSaveFileName(self, "Enregistrer pages paires", "", "PDF (*.pdf)")
|
|
if path_even:
|
|
with open(path_even, "wb") as f:
|
|
writer_even.write(f)
|
|
self._status_label.setText("Pages paires/impaires separees")
|
|
|
|
def _do_create_sample(self):
|
|
"""Creer un document test numerote."""
|
|
from PyQt6.QtWidgets import QInputDialog
|
|
num, ok = QInputDialog.getInt(self, "Document test", "Nombre de pages :", 8, 1, 200)
|
|
if not ok:
|
|
return
|
|
from reportlab.pdfgen import canvas as rl_canvas
|
|
from reportlab.lib.pagesizes import A4
|
|
output = self._make_temp()
|
|
c = rl_canvas.Canvas(output, pagesize=A4)
|
|
for i in range(1, num + 1):
|
|
c.setFont("Helvetica-Bold", 72)
|
|
c.drawCentredString(A4[0] / 2, A4[1] / 2, str(i))
|
|
c.setFont("Helvetica", 14)
|
|
c.drawCentredString(A4[0] / 2, A4[1] / 2 - 40, f"Page {i} sur {num}")
|
|
c.setFont("Helvetica", 10)
|
|
c.drawCentredString(A4[0] / 2, 30, "CopyDev Imposing Tool — Document test")
|
|
c.showPage()
|
|
c.save()
|
|
self._open_pdf(output)
|
|
self._status_label.setText(f"Document test de {num} pages cree")
|
|
|
|
def _do_register_windows(self):
|
|
if sys.platform != "win32":
|
|
QMessageBox.information(self, "Windows uniquement", "Cette fonction est reservee a Windows.")
|
|
return
|
|
import subprocess
|
|
exe = sys.executable if getattr(sys, 'frozen', False) else os.path.abspath("main.py")
|
|
try:
|
|
base = "HKCU\\Software\\Classes\\SystemFileAssociations\\.pdf\\shell"
|
|
cmds = [
|
|
# Association PDF (Ouvrir avec)
|
|
f'reg add "HKCU\\Software\\Classes\\.pdf\\OpenWithProgids" /v "CopyDevImpose" /t REG_SZ /d "" /f',
|
|
f'reg add "HKCU\\Software\\Classes\\CopyDevImpose" /ve /d "CopyDev Imposing Tool" /f',
|
|
f'reg add "HKCU\\Software\\Classes\\CopyDevImpose\\shell\\open\\command" /ve /d "\\"{exe}\\" \\"%1\\"" /f',
|
|
f'reg add "HKCU\\Software\\Classes\\CopyDevImpose\\DefaultIcon" /ve /d "{exe},0" /f',
|
|
# Entrees individuelles dans le menu contextuel
|
|
# Ouvrir avec CopyDev
|
|
f'reg add "{base}\\CopyDev_Ouvrir" /ve /d "Ouvrir avec CopyDev Impose" /f',
|
|
f'reg add "{base}\\CopyDev_Ouvrir" /v "Icon" /t REG_SZ /d "{exe},0" /f',
|
|
f'reg add "{base}\\CopyDev_Ouvrir\\command" /ve /d "\\"{exe}\\" \\"%1\\"" /f',
|
|
# Livret A4
|
|
f'reg add "{base}\\CopyDev_LivretA4" /ve /d "CopyDev: Livret A4" /f',
|
|
f'reg add "{base}\\CopyDev_LivretA4\\command" /ve /d "\\"{exe}\\" --action booklet_a4 \\"%1\\"" /f',
|
|
# Livret A3
|
|
f'reg add "{base}\\CopyDev_LivretA3" /ve /d "CopyDev: Livret A3" /f',
|
|
f'reg add "{base}\\CopyDev_LivretA3\\command" /ve /d "\\"{exe}\\" --action booklet_a3 \\"%1\\"" /f',
|
|
# 2x2
|
|
f'reg add "{base}\\CopyDev_2x2" /ve /d "CopyDev: 2x2 par feuille" /f',
|
|
f'reg add "{base}\\CopyDev_2x2\\command" /ve /d "\\"{exe}\\" --action nup_2x2 \\"%1\\"" /f',
|
|
# Delivretiser
|
|
f'reg add "{base}\\CopyDev_Delivretiser" /ve /d "CopyDev: Delivretiser" /f',
|
|
f'reg add "{base}\\CopyDev_Delivretiser\\command" /ve /d "\\"{exe}\\" --action unbooklet \\"%1\\"" /f',
|
|
]
|
|
for cmd in cmds:
|
|
subprocess.run(cmd, shell=True, capture_output=True)
|
|
QMessageBox.information(self, "Integration Windows",
|
|
"CopyDev Imposing Tool est maintenant integre a Windows !\n\n"
|
|
"- Clic droit sur un PDF > CopyDev Impose\n"
|
|
"- Ouvrir avec > CopyDev Imposing Tool")
|
|
self._status_label.setText("Integration Windows effectuee")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", f"Echec de l'integration :\n{e}")
|
|
|
|
def _do_unregister_windows(self):
|
|
if sys.platform != "win32":
|
|
return
|
|
import subprocess
|
|
try:
|
|
base = "HKCU\\Software\\Classes\\SystemFileAssociations\\.pdf\\shell"
|
|
cmds = [
|
|
'reg delete "HKCU\\Software\\Classes\\CopyDevImpose" /f',
|
|
'reg delete "HKCU\\Software\\Classes\\.pdf\\OpenWithProgids" /v "CopyDevImpose" /f',
|
|
f'reg delete "{base}\\CopyDevImpose" /f',
|
|
f'reg delete "{base}\\CopyDev_Ouvrir" /f',
|
|
f'reg delete "{base}\\CopyDev_LivretA4" /f',
|
|
f'reg delete "{base}\\CopyDev_LivretA3" /f',
|
|
f'reg delete "{base}\\CopyDev_2x2" /f',
|
|
f'reg delete "{base}\\CopyDev_Delivretiser" /f',
|
|
]
|
|
for cmd in cmds:
|
|
subprocess.run(cmd, shell=True, capture_output=True)
|
|
QMessageBox.information(self, "Integration Windows", "Integration Windows retiree.")
|
|
self._status_label.setText("Integration Windows retiree")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", str(e))
|
|
|
|
def _do_check_update(self):
|
|
from app.updater import check_for_update, do_update, VERSION
|
|
self._status_label.setText("Recherche de mises a jour...")
|
|
QApplication.processEvents()
|
|
update_available, remote_version = check_for_update()
|
|
if update_available:
|
|
reply = QMessageBox.question(
|
|
self, "Mise a jour disponible",
|
|
f"Nouvelle version : {remote_version}\n"
|
|
f"Version actuelle : {VERSION}\n\n"
|
|
"Mettre a jour maintenant ?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
success, msg = do_update()
|
|
if success:
|
|
QMessageBox.information(self, "Mise a jour", f"{msg}\n\nRedemarrez l'application.")
|
|
else:
|
|
QMessageBox.warning(self, "Mise a jour", f"Echec :\n{msg}")
|
|
else:
|
|
QMessageBox.information(self, "Mise a jour", "Vous etes a jour !")
|
|
self._status_label.setText("Pret")
|
|
|
|
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()
|