diff --git a/app/dialogs/booklet_dialog.py b/app/dialogs/booklet_dialog.py index e224a26..1f3b67d 100644 --- a/app/dialogs/booklet_dialog.py +++ b/app/dialogs/booklet_dialog.py @@ -2,8 +2,8 @@ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QGroupBox, QRadioButton, QSpinBox, - QDialogButtonBox, QDoubleSpinBox) -from app.engine.pdf_utils import PAGE_SIZES + QDialogButtonBox) +from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection class BookletDialog(QDialog): @@ -16,7 +16,6 @@ class BookletDialog(QDialog): def _setup_ui(self): layout = QVBoxLayout(self) - # Type de reliure grp_binding = QGroupBox("Type de reliure") bl = QVBoxLayout(grp_binding) self.rb_saddle = QRadioButton("Piqure a cheval (livret agrafe)") @@ -30,7 +29,6 @@ class BookletDialog(QDialog): bl.addWidget(self.rb_cut_stacks) layout.addWidget(grp_binding) - # Taille signature (perfect bound) sig_layout = QHBoxLayout() sig_layout.addWidget(QLabel("Pages par cahier :")) self.spin_signature = QSpinBox() @@ -41,21 +39,18 @@ class BookletDialog(QDialog): sig_layout.addWidget(self.spin_signature) sig_layout.addStretch() layout.addLayout(sig_layout) - self.rb_perfect.toggled.connect(self.spin_signature.setEnabled) - # Taille de feuille grp_sheet = QGroupBox("Taille de la feuille") sl = QHBoxLayout(grp_sheet) sl.addWidget(QLabel("Format :")) self.combo_sheet = QComboBox() self.combo_sheet.addItem("Automatique (2x page)") - for name in PAGE_SIZES: - self.combo_sheet.addItem(name) + populate_format_combo(self.combo_sheet, "Automatique (2x page)") + self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self)) sl.addWidget(self.combo_sheet) layout.addWidget(grp_sheet) - # Alignement grp_align = QGroupBox("Alignement") al = QVBoxLayout(grp_align) self.rb_center = QRadioButton("Centrer") @@ -67,7 +62,6 @@ class BookletDialog(QDialog): al.addWidget(self.rb_bl) layout.addWidget(grp_align) - # Boutons OK/Annuler buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -76,7 +70,6 @@ class BookletDialog(QDialog): layout.addWidget(buttons) def get_settings(self): - """Retourner les parametres selectionnes.""" if self.rb_saddle.isChecked(): binding = "saddle_stitch" elif self.rb_perfect.isChecked(): @@ -86,8 +79,11 @@ class BookletDialog(QDialog): else: binding = "cut_stacks" - sheet_text = self.combo_sheet.currentText() - sheet_size = PAGE_SIZES.get(sheet_text, None) + text = self.combo_sheet.currentText() + if text.startswith("Automatique"): + sheet_size = None + else: + sheet_size = get_selected_size(self.combo_sheet) if self.rb_center.isChecked(): alignment = "center" diff --git a/app/dialogs/crop_marks_dialog.py b/app/dialogs/crop_marks_dialog.py new file mode 100644 index 0000000..eff7ff7 --- /dev/null +++ b/app/dialogs/crop_marks_dialog.py @@ -0,0 +1,96 @@ +"""Dialogue Reperes de coupe.""" + +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QGroupBox, QDoubleSpinBox, QCheckBox, QComboBox, + QDialogButtonBox) + + +class CropMarksDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Reperes de coupe") + self.setMinimumWidth(400) + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + grp_size = QGroupBox("Dimensions des reperes") + sl = QVBoxLayout(grp_size) + + h1 = QHBoxLayout() + h1.addWidget(QLabel("Longueur des traits :")) + self.spin_length = QDoubleSpinBox() + self.spin_length.setRange(1, 30) + self.spin_length.setValue(5) + self.spin_length.setSuffix(" mm") + h1.addWidget(self.spin_length) + sl.addLayout(h1) + + h2 = QHBoxLayout() + h2.addWidget(QLabel("Distance du bord :")) + self.spin_offset = QDoubleSpinBox() + self.spin_offset.setRange(0, 20) + self.spin_offset.setValue(2.5) + self.spin_offset.setSuffix(" mm") + h2.addWidget(self.spin_offset) + sl.addLayout(h2) + + h3 = QHBoxLayout() + h3.addWidget(QLabel("Epaisseur :")) + self.spin_weight = QDoubleSpinBox() + self.spin_weight.setRange(0.1, 2.0) + self.spin_weight.setValue(0.25) + self.spin_weight.setSuffix(" pt") + self.spin_weight.setSingleStep(0.05) + h3.addWidget(self.spin_weight) + sl.addLayout(h3) + + layout.addWidget(grp_size) + + grp_opts = QGroupBox("Options") + ol = QVBoxLayout(grp_opts) + + self.chk_all_corners = QCheckBox("4 coins") + self.chk_all_corners.setChecked(True) + ol.addWidget(self.chk_all_corners) + + self.chk_center = QCheckBox("Reperes de centrage") + ol.addWidget(self.chk_center) + + h_color = QHBoxLayout() + h_color.addWidget(QLabel("Couleur :")) + self.combo_color = QComboBox() + self.combo_color.addItems(["Noir", "Gris", "CMYK (toutes les plaques)"]) + h_color.addWidget(self.combo_color) + ol.addLayout(h_color) + + h_apply = QHBoxLayout() + h_apply.addWidget(QLabel("Appliquer a :")) + self.combo_apply = QComboBox() + self.combo_apply.addItems(["Toutes les pages", "Pages paires", "Pages impaires", "Page courante"]) + h_apply.addWidget(self.combo_apply) + ol.addLayout(h_apply) + + layout.addWidget(grp_opts) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def get_settings(self): + from app.engine.pdf_utils import mm_to_pt + color_map = {"Noir": (0, 0, 0), "Gris": (0.5, 0.5, 0.5), "CMYK (toutes les plaques)": (0, 0, 0)} + apply_map = {"Toutes les pages": "all", "Pages paires": "even", "Pages impaires": "odd", "Page courante": "current"} + return { + "mark_length": mm_to_pt(self.spin_length.value()), + "mark_offset": mm_to_pt(self.spin_offset.value()), + "line_width": self.spin_weight.value(), + "all_corners": self.chk_all_corners.isChecked(), + "center_marks": self.chk_center.isChecked(), + "color": color_map[self.combo_color.currentText()], + "apply_to": apply_map[self.combo_apply.currentText()], + } diff --git a/app/dialogs/custom_size_dialog.py b/app/dialogs/custom_size_dialog.py new file mode 100644 index 0000000..47226fa --- /dev/null +++ b/app/dialogs/custom_size_dialog.py @@ -0,0 +1,104 @@ +"""Dialogue pour saisir un format personnalise.""" + +import os +import sys +import json +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QDoubleSpinBox, QLineEdit, QDialogButtonBox, + QGroupBox, QCheckBox) +from app.engine.pdf_utils import mm_to_pt + +CUSTOM_SIZES_FILE = os.path.join( + os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) + else os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + ".custom_sizes.json" +) + + +def load_custom_sizes(): + """Charger les formats personnalises sauvegardes.""" + if not os.path.exists(CUSTOM_SIZES_FILE): + return {} + try: + with open(CUSTOM_SIZES_FILE, "r") as f: + return json.load(f) + except Exception: + return {} + + +def save_custom_sizes(sizes): + """Sauvegarder les formats personnalises.""" + try: + with open(CUSTOM_SIZES_FILE, "w") as f: + json.dump(sizes, f, indent=2) + except Exception: + pass + + +class CustomSizeDialog(QDialog): + """Dialogue pour saisir une taille personnalisee.""" + + def __init__(self, save_mode=False, parent=None): + super().__init__(parent) + self.setWindowTitle("Ajouter un format" if save_mode else "Format personnalise") + self.setMinimumWidth(350) + self._save_mode = save_mode + self._setup_ui() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + if self._save_mode: + name_layout = QHBoxLayout() + name_layout.addWidget(QLabel("Nom du format :")) + self.edit_name = QLineEdit() + self.edit_name.setPlaceholderText("ex: Carte de visite") + name_layout.addWidget(self.edit_name) + layout.addLayout(name_layout) + layout.addSpacing(10) + + grp = QGroupBox("Dimensions (mm)") + gl = QHBoxLayout(grp) + gl.addWidget(QLabel("Largeur :")) + self.spin_w = QDoubleSpinBox() + self.spin_w.setRange(10, 5000) + self.spin_w.setValue(210) + self.spin_w.setSuffix(" mm") + gl.addWidget(self.spin_w) + gl.addWidget(QLabel("Hauteur :")) + self.spin_h = QDoubleSpinBox() + self.spin_h.setRange(10, 5000) + self.spin_h.setValue(297) + self.spin_h.setSuffix(" mm") + gl.addWidget(self.spin_h) + layout.addWidget(grp) + + self.chk_landscape = QCheckBox("Paysage (inverser largeur/hauteur)") + layout.addWidget(self.chk_landscape) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def get_size(self): + """Retourne (largeur, hauteur) en points.""" + w = mm_to_pt(self.spin_w.value()) + h = mm_to_pt(self.spin_h.value()) + if self.chk_landscape.isChecked(): + w, h = h, w + return (w, h) + + def get_name(self): + if self._save_mode: + return self.edit_name.text().strip() + return f"{self.spin_w.value():.0f}x{self.spin_h.value():.0f}mm" + + def get_size_mm(self): + w = self.spin_w.value() + h = self.spin_h.value() + if self.chk_landscape.isChecked(): + w, h = h, w + return (w, h) diff --git a/app/dialogs/format_combo.py b/app/dialogs/format_combo.py new file mode 100644 index 0000000..2499861 --- /dev/null +++ b/app/dialogs/format_combo.py @@ -0,0 +1,93 @@ +"""ComboBox de formats de page avec support personnalise.""" + +from PyQt6.QtWidgets import QComboBox +from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt +from app.dialogs.custom_size_dialog import (CustomSizeDialog, load_custom_sizes, + save_custom_sizes) + + +SEP = "---" +CUSTOM_LABEL = "Personnalise..." +ADD_LABEL = "Ajouter un format..." + + +def populate_format_combo(combo, default="A4 Paysage"): + """Remplir un combo avec les formats standard + custom + options.""" + combo.blockSignals(True) + combo.clear() + + # Formats standard + for name in PAGE_SIZES: + combo.addItem(name, PAGE_SIZES[name]) + + # Formats custom sauvegardes + custom = load_custom_sizes() + if custom: + combo.insertSeparator(combo.count()) + for name, size in custom.items(): + combo.addItem(name, (size[0], size[1])) + + # Options speciales + combo.insertSeparator(combo.count()) + combo.addItem(CUSTOM_LABEL, None) + combo.addItem(ADD_LABEL, None) + + # Selectionner le defaut + idx = combo.findText(default) + if idx >= 0: + combo.setCurrentIndex(idx) + + combo.blockSignals(False) + + +def handle_format_selection(combo, parent=None): + """Gerer la selection dans le combo. Retourne (w, h) en points ou None si annule.""" + text = combo.currentText() + + if text == CUSTOM_LABEL: + dlg = CustomSizeDialog(save_mode=False, parent=parent) + if dlg.exec(): + size = dlg.get_size() + name = dlg.get_name() + # Ajouter temporairement dans le combo + combo.blockSignals(True) + combo.insertItem(combo.count() - 3, name, size) + combo.setCurrentText(name) + combo.blockSignals(False) + return size + # Annule — revenir au precedent + combo.blockSignals(True) + combo.setCurrentIndex(0) + combo.blockSignals(False) + return None + + elif text == ADD_LABEL: + dlg = CustomSizeDialog(save_mode=True, parent=parent) + if dlg.exec(): + name = dlg.get_name() + if not name: + name = dlg.get_name() + size = dlg.get_size() + # Sauvegarder + custom = load_custom_sizes() + w_mm, h_mm = dlg.get_size_mm() + custom[name] = [mm_to_pt(w_mm), mm_to_pt(h_mm)] + save_custom_sizes(custom) + # Recharger le combo + populate_format_combo(combo, name) + return size + combo.blockSignals(True) + combo.setCurrentIndex(0) + combo.blockSignals(False) + return None + + else: + return combo.currentData() + + +def get_selected_size(combo): + """Obtenir la taille selectionnee. Retourne (w, h) en points.""" + data = combo.currentData() + if data: + return data + return PAGE_SIZES.get(combo.currentText(), (595.28, 841.89)) diff --git a/app/dialogs/manual_impose_dialog.py b/app/dialogs/manual_impose_dialog.py index 84e3d12..22c3466 100644 --- a/app/dialogs/manual_impose_dialog.py +++ b/app/dialogs/manual_impose_dialog.py @@ -2,9 +2,9 @@ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox, - QDialogButtonBox, QPushButton, QListWidget, - QCheckBox) -from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt + QDialogButtonBox, QPushButton, QListWidget, QCheckBox) +from app.engine.pdf_utils import mm_to_pt +from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection class ManualImposeDialog(QDialog): @@ -19,53 +19,43 @@ class ManualImposeDialog(QDialog): def _setup_ui(self): layout = QVBoxLayout(self) - # Taille feuille grp_sheet = QGroupBox("Taille de la feuille cible") sl = QHBoxLayout(grp_sheet) sl.addWidget(QLabel("Format :")) self.combo_sheet = QComboBox() - for name in PAGE_SIZES: - self.combo_sheet.addItem(name) - self.combo_sheet.setCurrentText("A3 Paysage") + populate_format_combo(self.combo_sheet, "A3 Paysage") + self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self)) sl.addWidget(self.combo_sheet) layout.addWidget(grp_sheet) - # Placement grp_place = QGroupBox("Placer une page") pl = QHBoxLayout(grp_place) - pl.addWidget(QLabel("Page:")) self.spin_page = QSpinBox() self.spin_page.setRange(1, self._total_pages) pl.addWidget(self.spin_page) - pl.addWidget(QLabel("X:")) self.spin_x = QDoubleSpinBox() self.spin_x.setRange(0, 2000) self.spin_x.setSuffix(" mm") pl.addWidget(self.spin_x) - pl.addWidget(QLabel("Y:")) self.spin_y = QDoubleSpinBox() self.spin_y.setRange(0, 2000) self.spin_y.setSuffix(" mm") pl.addWidget(self.spin_y) - pl.addWidget(QLabel("Rot:")) self.combo_rot = QComboBox() self.combo_rot.addItems(["0", "90", "180", "270"]) pl.addWidget(self.combo_rot) - pl.addWidget(QLabel("Ech:")) self.spin_scale = QDoubleSpinBox() self.spin_scale.setRange(0.1, 10.0) self.spin_scale.setValue(1.0) self.spin_scale.setSingleStep(0.1) pl.addWidget(self.spin_scale) - layout.addWidget(grp_place) - # Boutons btn_layout = QHBoxLayout() btn_add = QPushButton("Ajouter") btn_add.clicked.connect(self._add_placement) @@ -76,15 +66,12 @@ class ManualImposeDialog(QDialog): btn_layout.addStretch() layout.addLayout(btn_layout) - # Liste self.list_placements = QListWidget() layout.addWidget(self.list_placements) - # Crop marks self.chk_crop = QCheckBox("Reperes de coupe") layout.addWidget(self.chk_crop) - # Boutons OK/Annuler buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -102,7 +89,7 @@ class ManualImposeDialog(QDialog): } self._placements.append(p) self.list_placements.addItem( - f"Page {p['page']} - X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} mm " + f"Page {p['page']} — X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} mm " f"Rot={self.combo_rot.currentText()} Ech={self.spin_scale.value():.1f}" ) @@ -113,10 +100,8 @@ class ManualImposeDialog(QDialog): self._placements.pop(row) def get_settings(self): - sheet_text = self.combo_sheet.currentText() - sheet_size = PAGE_SIZES[sheet_text] return { - "sheet_size": sheet_size, + "sheet_size": get_selected_size(self.combo_sheet), "placements": self._placements, "crop_marks": self.chk_crop.isChecked(), } diff --git a/app/dialogs/nup_dialog.py b/app/dialogs/nup_dialog.py index 46ff421..3eddf6e 100644 --- a/app/dialogs/nup_dialog.py +++ b/app/dialogs/nup_dialog.py @@ -2,8 +2,10 @@ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QGroupBox, QSpinBox, QCheckBox, - QDialogButtonBox, QDoubleSpinBox, QFileDialog) -from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt + QDialogButtonBox, QDoubleSpinBox, QFileDialog, + QPushButton) +from app.engine.pdf_utils import mm_to_pt +from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection class NupDialog(QDialog): @@ -17,7 +19,6 @@ class NupDialog(QDialog): def _setup_ui(self): layout = QVBoxLayout(self) - # Grille grp_grid = QGroupBox("Disposition") gl = QHBoxLayout(grp_grid) gl.addWidget(QLabel("Colonnes :")) @@ -32,18 +33,15 @@ class NupDialog(QDialog): gl.addWidget(self.spin_rows) layout.addWidget(grp_grid) - # Taille feuille grp_sheet = QGroupBox("Taille de la feuille") sl = QHBoxLayout(grp_sheet) sl.addWidget(QLabel("Format :")) self.combo_sheet = QComboBox() - for name in PAGE_SIZES: - self.combo_sheet.addItem(name) - self.combo_sheet.setCurrentText("A4 Paysage") + populate_format_combo(self.combo_sheet, "A4 Paysage") + self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self)) sl.addWidget(self.combo_sheet) layout.addWidget(grp_sheet) - # Marges grp_margins = QGroupBox("Marges (mm)") ml = QHBoxLayout(grp_margins) self.margins = {} @@ -57,7 +55,6 @@ class NupDialog(QDialog): self.margins[label.lower()] = spin layout.addWidget(grp_margins) - # Gouttiere grp_gutter = QGroupBox("Gouttiere (mm)") gutl = QHBoxLayout(grp_gutter) gutl.addWidget(QLabel("Horizontale :")) @@ -74,7 +71,6 @@ class NupDialog(QDialog): gutl.addWidget(self.spin_gutter_v) layout.addWidget(grp_gutter) - # Options grp_opts = QGroupBox("Options") ol = QVBoxLayout(grp_opts) self.chk_fit = QCheckBox("Ajuster les pages a la cellule") @@ -88,16 +84,13 @@ class NupDialog(QDialog): bg_layout.addWidget(self.chk_bg) self.lbl_bg = QLabel("Aucun") bg_layout.addWidget(self.lbl_bg) - from PyQt6.QtWidgets import QPushButton btn_bg = QPushButton("Choisir...") btn_bg.clicked.connect(self._choose_bg) bg_layout.addWidget(btn_bg) bg_layout.addStretch() ol.addLayout(bg_layout) - layout.addWidget(grp_opts) - # Boutons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -113,12 +106,10 @@ class NupDialog(QDialog): self.chk_bg.setChecked(True) def get_settings(self): - sheet_text = self.combo_sheet.currentText() - sheet_size = PAGE_SIZES[sheet_text] return { "cols": self.spin_cols.value(), "rows": self.spin_rows.value(), - "sheet_size": sheet_size, + "sheet_size": get_selected_size(self.combo_sheet), "margin_top": mm_to_pt(self.margins["haut"].value()), "margin_bottom": mm_to_pt(self.margins["bas"].value()), "margin_left": mm_to_pt(self.margins["gauche"].value()), diff --git a/app/dialogs/step_repeat_dialog.py b/app/dialogs/step_repeat_dialog.py index 569160b..c1fafcd 100644 --- a/app/dialogs/step_repeat_dialog.py +++ b/app/dialogs/step_repeat_dialog.py @@ -3,7 +3,8 @@ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QGroupBox, QSpinBox, QCheckBox, QDialogButtonBox, QDoubleSpinBox) -from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt +from app.engine.pdf_utils import mm_to_pt +from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection class StepRepeatDialog(QDialog): @@ -16,24 +17,20 @@ class StepRepeatDialog(QDialog): def _setup_ui(self): layout = QVBoxLayout(self) - # Taille feuille grp_sheet = QGroupBox("Taille de la feuille") sl = QHBoxLayout(grp_sheet) sl.addWidget(QLabel("Format :")) self.combo_sheet = QComboBox() - for name in PAGE_SIZES: - self.combo_sheet.addItem(name) - self.combo_sheet.setCurrentText("A3 Paysage") + populate_format_combo(self.combo_sheet, "A3 Paysage") + self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self)) sl.addWidget(self.combo_sheet) layout.addWidget(grp_sheet) - # Nombre de copies grp_copies = QGroupBox("Nombre de copies") cl = QVBoxLayout(grp_copies) self.chk_auto = QCheckBox("Automatique (remplir la feuille)") self.chk_auto.setChecked(True) cl.addWidget(self.chk_auto) - manual = QHBoxLayout() manual.addWidget(QLabel("Horizontal :")) self.spin_h = QSpinBox() @@ -49,12 +46,23 @@ class StepRepeatDialog(QDialog): manual.addWidget(self.spin_v) cl.addLayout(manual) layout.addWidget(grp_copies) + self.chk_auto.toggled.connect(lambda c: self.spin_h.setEnabled(not c)) + self.chk_auto.toggled.connect(lambda c: self.spin_v.setEnabled(not c)) - self.chk_auto.toggled.connect(lambda checked: self.spin_h.setEnabled(not checked)) - self.chk_auto.toggled.connect(lambda checked: self.spin_v.setEnabled(not checked)) + grp_margins = QGroupBox("Marges (mm)") + ml = QHBoxLayout(grp_margins) + self.margins = {} + for label in ["Haut", "Bas", "Gauche", "Droite"]: + ml.addWidget(QLabel(f"{label}:")) + spin = QDoubleSpinBox() + spin.setRange(0, 100) + spin.setValue(0) + spin.setSuffix(" mm") + ml.addWidget(spin) + self.margins[label.lower()] = spin + layout.addWidget(grp_margins) - # Espacement - grp_spacing = QGroupBox("Espacement (mm)") + grp_spacing = QGroupBox("Espacement entre copies (mm)") spl = QHBoxLayout(grp_spacing) spl.addWidget(QLabel("Horizontal :")) self.spin_sp_h = QDoubleSpinBox() @@ -70,11 +78,9 @@ class StepRepeatDialog(QDialog): spl.addWidget(self.spin_sp_v) layout.addWidget(grp_spacing) - # Options self.chk_crop = QCheckBox("Reperes de coupe") layout.addWidget(self.chk_crop) - # Boutons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -83,10 +89,8 @@ class StepRepeatDialog(QDialog): layout.addWidget(buttons) def get_settings(self): - sheet_text = self.combo_sheet.currentText() - sheet_size = PAGE_SIZES[sheet_text] return { - "sheet_size": sheet_size, + "sheet_size": get_selected_size(self.combo_sheet), "copies_h": None if self.chk_auto.isChecked() else self.spin_h.value(), "copies_v": None if self.chk_auto.isChecked() else self.spin_v.value(), "spacing_h": mm_to_pt(self.spin_sp_h.value()), diff --git a/app/engine/crop_marks.py b/app/engine/crop_marks.py new file mode 100644 index 0000000..a3e0799 --- /dev/null +++ b/app/engine/crop_marks.py @@ -0,0 +1,85 @@ +"""Moteur Reperes de coupe autonomes.""" + +from pypdf import PdfReader, PdfWriter +from reportlab.pdfgen import canvas +from .pdf_utils import get_page_size +import io + + +def add_crop_marks(input_path, output_path, mark_length=14.17, mark_offset=7.09, + line_width=0.25, color=(0, 0, 0), center_marks=False, + apply_to="all", current_page=0): + """Ajouter des reperes de coupe sur les pages.""" + reader = PdfReader(input_path) + writer = PdfWriter() + + for i, page in enumerate(reader.pages): + page_num = i + 1 + should_apply = False + if apply_to == "all": + should_apply = True + elif apply_to == "even": + should_apply = page_num % 2 == 0 + elif apply_to == "odd": + should_apply = page_num % 2 != 0 + elif apply_to == "current": + should_apply = i == current_page + + if should_apply: + w, h = get_page_size(page) + packet = io.BytesIO() + c = canvas.Canvas(packet, pagesize=(w, h)) + c.setLineWidth(line_width) + c.setStrokeColorRGB(*color) + + # 4 coins + corners = [(0, 0), (w, 0), (0, h), (w, h)] + for cx, cy in corners: + # Traits horizontaux + if cx == 0: + c.line(cx - mark_offset - mark_length, cy, cx - mark_offset, cy) + else: + c.line(cx + mark_offset, cy, cx + mark_offset + mark_length, cy) + # Traits verticaux + if cy == 0: + c.line(cx, cy - mark_offset - mark_length, cx, cy - mark_offset) + else: + c.line(cx, cy + mark_offset, cx, cy + mark_offset + mark_length) + + # Reperes de centrage + if center_marks: + mid_x = w / 2 + mid_y = h / 2 + ml = mark_length * 0.7 + # Haut + c.line(mid_x, h + mark_offset, mid_x, h + mark_offset + ml) + # Bas + c.line(mid_x, -mark_offset, mid_x, -mark_offset - ml) + # Gauche + c.line(-mark_offset, mid_y, -mark_offset - ml, mid_y) + # Droite + c.line(w + mark_offset, mid_y, w + mark_offset + ml, mid_y) + + c.showPage() + c.save() + packet.seek(0) + overlay = PdfReader(packet) + page.merge_page(overlay.pages[0]) + + writer.add_page(page) + + with open(output_path, "wb") as f: + writer.write(f) + return output_path + + +def remove_crop_marks(input_path, output_path): + """Enlever les reperes de coupe (remet mediabox = cropbox).""" + reader = PdfReader(input_path) + writer = PdfWriter() + for page in reader.pages: + page.cropbox = page.mediabox + writer.add_page(page) + with open(output_path, "wb") as f: + writer.write(f) + return output_path diff --git a/app/main_window.py b/app/main_window.py index 466c226..1ca12cc 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -22,6 +22,7 @@ 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.engine.booklet import create_booklet from app.engine.nup import nup_pages @@ -32,6 +33,7 @@ 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 class MainWindow(QMainWindow): @@ -120,6 +122,10 @@ class MainWindow(QMainWindow): 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) @@ -488,6 +494,34 @@ class MainWindow(QMainWindow): self._hide_progress() QMessageBox.critical(self, "Erreur", str(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: diff --git a/app/updater.py b/app/updater.py index 258bf92..83eec3d 100644 --- a/app/updater.py +++ b/app/updater.py @@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool" BRANCH = "master" # Version actuelle (incrementee a chaque release) -VERSION = "2.0.0" +VERSION = "2.1.0" def get_remote_version(): diff --git a/version.txt b/version.txt index 227cea2..7ec1d6d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.0.0 +2.1.0