V2.1.0 — Formats personnalises + reperes de coupe

- Formats personnalises dans tous les menus deroulants de format :
  - "Personnalise..." : saisie libre largeur x hauteur en mm
  - "Ajouter un format..." : sauvegarder un format custom (persistant)
  - Formats custom affiches dans le dropdown avec separateur
- Reperes de coupe (Annotations > Reperes de coupe) :
  - Longueur, distance du bord, epaisseur configurables
  - Couleur : noir, gris, CMYK
  - Reperes de centrage optionnels
  - Appliquer a : toutes, paires, impaires, page courante
  - Enlever les reperes de coupe (menu)
- Step & Repeat : ajout des marges (haut, bas, gauche, droite)
- Tous les dialogues de format migres vers format_combo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-21 08:00:55 +01:00
parent 9cc256ff51
commit c9212a30ea
11 changed files with 457 additions and 69 deletions

View File

@@ -2,8 +2,8 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QRadioButton, QSpinBox, QComboBox, QGroupBox, QRadioButton, QSpinBox,
QDialogButtonBox, QDoubleSpinBox) QDialogButtonBox)
from app.engine.pdf_utils import PAGE_SIZES from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection
class BookletDialog(QDialog): class BookletDialog(QDialog):
@@ -16,7 +16,6 @@ class BookletDialog(QDialog):
def _setup_ui(self): def _setup_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Type de reliure
grp_binding = QGroupBox("Type de reliure") grp_binding = QGroupBox("Type de reliure")
bl = QVBoxLayout(grp_binding) bl = QVBoxLayout(grp_binding)
self.rb_saddle = QRadioButton("Piqure a cheval (livret agrafe)") self.rb_saddle = QRadioButton("Piqure a cheval (livret agrafe)")
@@ -30,7 +29,6 @@ class BookletDialog(QDialog):
bl.addWidget(self.rb_cut_stacks) bl.addWidget(self.rb_cut_stacks)
layout.addWidget(grp_binding) layout.addWidget(grp_binding)
# Taille signature (perfect bound)
sig_layout = QHBoxLayout() sig_layout = QHBoxLayout()
sig_layout.addWidget(QLabel("Pages par cahier :")) sig_layout.addWidget(QLabel("Pages par cahier :"))
self.spin_signature = QSpinBox() self.spin_signature = QSpinBox()
@@ -41,21 +39,18 @@ class BookletDialog(QDialog):
sig_layout.addWidget(self.spin_signature) sig_layout.addWidget(self.spin_signature)
sig_layout.addStretch() sig_layout.addStretch()
layout.addLayout(sig_layout) layout.addLayout(sig_layout)
self.rb_perfect.toggled.connect(self.spin_signature.setEnabled) self.rb_perfect.toggled.connect(self.spin_signature.setEnabled)
# Taille de feuille
grp_sheet = QGroupBox("Taille de la feuille") grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet) sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :")) sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox() self.combo_sheet = QComboBox()
self.combo_sheet.addItem("Automatique (2x page)") self.combo_sheet.addItem("Automatique (2x page)")
for name in PAGE_SIZES: populate_format_combo(self.combo_sheet, "Automatique (2x page)")
self.combo_sheet.addItem(name) self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self))
sl.addWidget(self.combo_sheet) sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet) layout.addWidget(grp_sheet)
# Alignement
grp_align = QGroupBox("Alignement") grp_align = QGroupBox("Alignement")
al = QVBoxLayout(grp_align) al = QVBoxLayout(grp_align)
self.rb_center = QRadioButton("Centrer") self.rb_center = QRadioButton("Centrer")
@@ -67,7 +62,6 @@ class BookletDialog(QDialog):
al.addWidget(self.rb_bl) al.addWidget(self.rb_bl)
layout.addWidget(grp_align) layout.addWidget(grp_align)
# Boutons OK/Annuler
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
) )
@@ -76,7 +70,6 @@ class BookletDialog(QDialog):
layout.addWidget(buttons) layout.addWidget(buttons)
def get_settings(self): def get_settings(self):
"""Retourner les parametres selectionnes."""
if self.rb_saddle.isChecked(): if self.rb_saddle.isChecked():
binding = "saddle_stitch" binding = "saddle_stitch"
elif self.rb_perfect.isChecked(): elif self.rb_perfect.isChecked():
@@ -86,8 +79,11 @@ class BookletDialog(QDialog):
else: else:
binding = "cut_stacks" binding = "cut_stacks"
sheet_text = self.combo_sheet.currentText() text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES.get(sheet_text, None) if text.startswith("Automatique"):
sheet_size = None
else:
sheet_size = get_selected_size(self.combo_sheet)
if self.rb_center.isChecked(): if self.rb_center.isChecked():
alignment = "center" alignment = "center"

View File

@@ -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()],
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -2,9 +2,9 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox, QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox,
QDialogButtonBox, QPushButton, QListWidget, QDialogButtonBox, QPushButton, QListWidget, QCheckBox)
QCheckBox) from app.engine.pdf_utils import mm_to_pt
from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt from app.dialogs.format_combo import populate_format_combo, get_selected_size, handle_format_selection
class ManualImposeDialog(QDialog): class ManualImposeDialog(QDialog):
@@ -19,53 +19,43 @@ class ManualImposeDialog(QDialog):
def _setup_ui(self): def _setup_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille cible") grp_sheet = QGroupBox("Taille de la feuille cible")
sl = QHBoxLayout(grp_sheet) sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :")) sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox() self.combo_sheet = QComboBox()
for name in PAGE_SIZES: populate_format_combo(self.combo_sheet, "A3 Paysage")
self.combo_sheet.addItem(name) self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self))
self.combo_sheet.setCurrentText("A3 Paysage")
sl.addWidget(self.combo_sheet) sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet) layout.addWidget(grp_sheet)
# Placement
grp_place = QGroupBox("Placer une page") grp_place = QGroupBox("Placer une page")
pl = QHBoxLayout(grp_place) pl = QHBoxLayout(grp_place)
pl.addWidget(QLabel("Page:")) pl.addWidget(QLabel("Page:"))
self.spin_page = QSpinBox() self.spin_page = QSpinBox()
self.spin_page.setRange(1, self._total_pages) self.spin_page.setRange(1, self._total_pages)
pl.addWidget(self.spin_page) pl.addWidget(self.spin_page)
pl.addWidget(QLabel("X:")) pl.addWidget(QLabel("X:"))
self.spin_x = QDoubleSpinBox() self.spin_x = QDoubleSpinBox()
self.spin_x.setRange(0, 2000) self.spin_x.setRange(0, 2000)
self.spin_x.setSuffix(" mm") self.spin_x.setSuffix(" mm")
pl.addWidget(self.spin_x) pl.addWidget(self.spin_x)
pl.addWidget(QLabel("Y:")) pl.addWidget(QLabel("Y:"))
self.spin_y = QDoubleSpinBox() self.spin_y = QDoubleSpinBox()
self.spin_y.setRange(0, 2000) self.spin_y.setRange(0, 2000)
self.spin_y.setSuffix(" mm") self.spin_y.setSuffix(" mm")
pl.addWidget(self.spin_y) pl.addWidget(self.spin_y)
pl.addWidget(QLabel("Rot:")) pl.addWidget(QLabel("Rot:"))
self.combo_rot = QComboBox() self.combo_rot = QComboBox()
self.combo_rot.addItems(["0", "90", "180", "270"]) self.combo_rot.addItems(["0", "90", "180", "270"])
pl.addWidget(self.combo_rot) pl.addWidget(self.combo_rot)
pl.addWidget(QLabel("Ech:")) pl.addWidget(QLabel("Ech:"))
self.spin_scale = QDoubleSpinBox() self.spin_scale = QDoubleSpinBox()
self.spin_scale.setRange(0.1, 10.0) self.spin_scale.setRange(0.1, 10.0)
self.spin_scale.setValue(1.0) self.spin_scale.setValue(1.0)
self.spin_scale.setSingleStep(0.1) self.spin_scale.setSingleStep(0.1)
pl.addWidget(self.spin_scale) pl.addWidget(self.spin_scale)
layout.addWidget(grp_place) layout.addWidget(grp_place)
# Boutons
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
btn_add = QPushButton("Ajouter") btn_add = QPushButton("Ajouter")
btn_add.clicked.connect(self._add_placement) btn_add.clicked.connect(self._add_placement)
@@ -76,15 +66,12 @@ class ManualImposeDialog(QDialog):
btn_layout.addStretch() btn_layout.addStretch()
layout.addLayout(btn_layout) layout.addLayout(btn_layout)
# Liste
self.list_placements = QListWidget() self.list_placements = QListWidget()
layout.addWidget(self.list_placements) layout.addWidget(self.list_placements)
# Crop marks
self.chk_crop = QCheckBox("Reperes de coupe") self.chk_crop = QCheckBox("Reperes de coupe")
layout.addWidget(self.chk_crop) layout.addWidget(self.chk_crop)
# Boutons OK/Annuler
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
) )
@@ -102,7 +89,7 @@ class ManualImposeDialog(QDialog):
} }
self._placements.append(p) self._placements.append(p)
self.list_placements.addItem( 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}" f"Rot={self.combo_rot.currentText()} Ech={self.spin_scale.value():.1f}"
) )
@@ -113,10 +100,8 @@ class ManualImposeDialog(QDialog):
self._placements.pop(row) self._placements.pop(row)
def get_settings(self): def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return { return {
"sheet_size": sheet_size, "sheet_size": get_selected_size(self.combo_sheet),
"placements": self._placements, "placements": self._placements,
"crop_marks": self.chk_crop.isChecked(), "crop_marks": self.chk_crop.isChecked(),
} }

View File

@@ -2,8 +2,10 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QSpinBox, QCheckBox, QComboBox, QGroupBox, QSpinBox, QCheckBox,
QDialogButtonBox, QDoubleSpinBox, QFileDialog) QDialogButtonBox, QDoubleSpinBox, QFileDialog,
from app.engine.pdf_utils import PAGE_SIZES, mm_to_pt 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): class NupDialog(QDialog):
@@ -17,7 +19,6 @@ class NupDialog(QDialog):
def _setup_ui(self): def _setup_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Grille
grp_grid = QGroupBox("Disposition") grp_grid = QGroupBox("Disposition")
gl = QHBoxLayout(grp_grid) gl = QHBoxLayout(grp_grid)
gl.addWidget(QLabel("Colonnes :")) gl.addWidget(QLabel("Colonnes :"))
@@ -32,18 +33,15 @@ class NupDialog(QDialog):
gl.addWidget(self.spin_rows) gl.addWidget(self.spin_rows)
layout.addWidget(grp_grid) layout.addWidget(grp_grid)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille") grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet) sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :")) sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox() self.combo_sheet = QComboBox()
for name in PAGE_SIZES: populate_format_combo(self.combo_sheet, "A4 Paysage")
self.combo_sheet.addItem(name) self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self))
self.combo_sheet.setCurrentText("A4 Paysage")
sl.addWidget(self.combo_sheet) sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet) layout.addWidget(grp_sheet)
# Marges
grp_margins = QGroupBox("Marges (mm)") grp_margins = QGroupBox("Marges (mm)")
ml = QHBoxLayout(grp_margins) ml = QHBoxLayout(grp_margins)
self.margins = {} self.margins = {}
@@ -57,7 +55,6 @@ class NupDialog(QDialog):
self.margins[label.lower()] = spin self.margins[label.lower()] = spin
layout.addWidget(grp_margins) layout.addWidget(grp_margins)
# Gouttiere
grp_gutter = QGroupBox("Gouttiere (mm)") grp_gutter = QGroupBox("Gouttiere (mm)")
gutl = QHBoxLayout(grp_gutter) gutl = QHBoxLayout(grp_gutter)
gutl.addWidget(QLabel("Horizontale :")) gutl.addWidget(QLabel("Horizontale :"))
@@ -74,7 +71,6 @@ class NupDialog(QDialog):
gutl.addWidget(self.spin_gutter_v) gutl.addWidget(self.spin_gutter_v)
layout.addWidget(grp_gutter) layout.addWidget(grp_gutter)
# Options
grp_opts = QGroupBox("Options") grp_opts = QGroupBox("Options")
ol = QVBoxLayout(grp_opts) ol = QVBoxLayout(grp_opts)
self.chk_fit = QCheckBox("Ajuster les pages a la cellule") self.chk_fit = QCheckBox("Ajuster les pages a la cellule")
@@ -88,16 +84,13 @@ class NupDialog(QDialog):
bg_layout.addWidget(self.chk_bg) bg_layout.addWidget(self.chk_bg)
self.lbl_bg = QLabel("Aucun") self.lbl_bg = QLabel("Aucun")
bg_layout.addWidget(self.lbl_bg) bg_layout.addWidget(self.lbl_bg)
from PyQt6.QtWidgets import QPushButton
btn_bg = QPushButton("Choisir...") btn_bg = QPushButton("Choisir...")
btn_bg.clicked.connect(self._choose_bg) btn_bg.clicked.connect(self._choose_bg)
bg_layout.addWidget(btn_bg) bg_layout.addWidget(btn_bg)
bg_layout.addStretch() bg_layout.addStretch()
ol.addLayout(bg_layout) ol.addLayout(bg_layout)
layout.addWidget(grp_opts) layout.addWidget(grp_opts)
# Boutons
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
) )
@@ -113,12 +106,10 @@ class NupDialog(QDialog):
self.chk_bg.setChecked(True) self.chk_bg.setChecked(True)
def get_settings(self): def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return { return {
"cols": self.spin_cols.value(), "cols": self.spin_cols.value(),
"rows": self.spin_rows.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_top": mm_to_pt(self.margins["haut"].value()),
"margin_bottom": mm_to_pt(self.margins["bas"].value()), "margin_bottom": mm_to_pt(self.margins["bas"].value()),
"margin_left": mm_to_pt(self.margins["gauche"].value()), "margin_left": mm_to_pt(self.margins["gauche"].value()),

View File

@@ -3,7 +3,8 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QGroupBox, QSpinBox, QCheckBox, QComboBox, QGroupBox, QSpinBox, QCheckBox,
QDialogButtonBox, QDoubleSpinBox) 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): class StepRepeatDialog(QDialog):
@@ -16,24 +17,20 @@ class StepRepeatDialog(QDialog):
def _setup_ui(self): def _setup_ui(self):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Taille feuille
grp_sheet = QGroupBox("Taille de la feuille") grp_sheet = QGroupBox("Taille de la feuille")
sl = QHBoxLayout(grp_sheet) sl = QHBoxLayout(grp_sheet)
sl.addWidget(QLabel("Format :")) sl.addWidget(QLabel("Format :"))
self.combo_sheet = QComboBox() self.combo_sheet = QComboBox()
for name in PAGE_SIZES: populate_format_combo(self.combo_sheet, "A3 Paysage")
self.combo_sheet.addItem(name) self.combo_sheet.activated.connect(lambda: handle_format_selection(self.combo_sheet, self))
self.combo_sheet.setCurrentText("A3 Paysage")
sl.addWidget(self.combo_sheet) sl.addWidget(self.combo_sheet)
layout.addWidget(grp_sheet) layout.addWidget(grp_sheet)
# Nombre de copies
grp_copies = QGroupBox("Nombre de copies") grp_copies = QGroupBox("Nombre de copies")
cl = QVBoxLayout(grp_copies) cl = QVBoxLayout(grp_copies)
self.chk_auto = QCheckBox("Automatique (remplir la feuille)") self.chk_auto = QCheckBox("Automatique (remplir la feuille)")
self.chk_auto.setChecked(True) self.chk_auto.setChecked(True)
cl.addWidget(self.chk_auto) cl.addWidget(self.chk_auto)
manual = QHBoxLayout() manual = QHBoxLayout()
manual.addWidget(QLabel("Horizontal :")) manual.addWidget(QLabel("Horizontal :"))
self.spin_h = QSpinBox() self.spin_h = QSpinBox()
@@ -49,12 +46,23 @@ class StepRepeatDialog(QDialog):
manual.addWidget(self.spin_v) manual.addWidget(self.spin_v)
cl.addLayout(manual) cl.addLayout(manual)
layout.addWidget(grp_copies) 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)) grp_margins = QGroupBox("Marges (mm)")
self.chk_auto.toggled.connect(lambda checked: self.spin_v.setEnabled(not checked)) 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 entre copies (mm)")
grp_spacing = QGroupBox("Espacement (mm)")
spl = QHBoxLayout(grp_spacing) spl = QHBoxLayout(grp_spacing)
spl.addWidget(QLabel("Horizontal :")) spl.addWidget(QLabel("Horizontal :"))
self.spin_sp_h = QDoubleSpinBox() self.spin_sp_h = QDoubleSpinBox()
@@ -70,11 +78,9 @@ class StepRepeatDialog(QDialog):
spl.addWidget(self.spin_sp_v) spl.addWidget(self.spin_sp_v)
layout.addWidget(grp_spacing) layout.addWidget(grp_spacing)
# Options
self.chk_crop = QCheckBox("Reperes de coupe") self.chk_crop = QCheckBox("Reperes de coupe")
layout.addWidget(self.chk_crop) layout.addWidget(self.chk_crop)
# Boutons
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
) )
@@ -83,10 +89,8 @@ class StepRepeatDialog(QDialog):
layout.addWidget(buttons) layout.addWidget(buttons)
def get_settings(self): def get_settings(self):
sheet_text = self.combo_sheet.currentText()
sheet_size = PAGE_SIZES[sheet_text]
return { 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_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(), "copies_v": None if self.chk_auto.isChecked() else self.spin_v.value(),
"spacing_h": mm_to_pt(self.spin_sp_h.value()), "spacing_h": mm_to_pt(self.spin_sp_h.value()),

85
app/engine/crop_marks.py Normal file
View File

@@ -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

View File

@@ -22,6 +22,7 @@ from app.dialogs.insert_blank_dialog import InsertBlankDialog
from app.dialogs.page_numbers_dialog import PageNumbersDialog from app.dialogs.page_numbers_dialog import PageNumbersDialog
from app.dialogs.masking_dialog import MaskingDialog from app.dialogs.masking_dialog import MaskingDialog
from app.dialogs.bleeds_dialog import BleedsDialog 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.booklet import create_booklet
from app.engine.nup import nup_pages 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.page_numbers import add_page_numbers
from app.engine.masking import apply_masking from app.engine.masking import apply_masking
from app.engine.bleeds import define_bleeds from app.engine.bleeds import define_bleeds
from app.engine.crop_marks import add_crop_marks, remove_crop_marks
class MainWindow(QMainWindow): 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, "Numerotation de pages", None, self._do_page_numbers)
self._add_action(annot_menu, "Masquage (ruban blanc)", None, self._do_masking) self._add_action(annot_menu, "Masquage (ruban blanc)", None, self._do_masking)
annot_menu.addSeparator() 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, "Definir les fonds perdus", None, self._do_bleeds)
self._add_action(annot_menu, "Enlever les fonds perdus", None, self._do_remove_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() self._hide_progress()
QMessageBox.critical(self, "Erreur", str(e)) 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): def _do_merge(self):
paths, _ = QFileDialog.getOpenFileNames(self, "Selectionner les PDF a fusionner", "", "PDF (*.pdf)") paths, _ = QFileDialog.getOpenFileNames(self, "Selectionner les PDF a fusionner", "", "PDF (*.pdf)")
if not paths or len(paths) < 2: if not paths or len(paths) < 2:

View File

@@ -14,7 +14,7 @@ REPO = "jules/copydev-imposing-tool"
BRANCH = "master" BRANCH = "master"
# Version actuelle (incrementee a chaque release) # Version actuelle (incrementee a chaque release)
VERSION = "2.0.0" VERSION = "2.1.0"
def get_remote_version(): def get_remote_version():

View File

@@ -1 +1 @@
2.0.0 2.1.0