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:
@@ -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"
|
||||
|
||||
96
app/dialogs/crop_marks_dialog.py
Normal file
96
app/dialogs/crop_marks_dialog.py
Normal 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()],
|
||||
}
|
||||
104
app/dialogs/custom_size_dialog.py
Normal file
104
app/dialogs/custom_size_dialog.py
Normal 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)
|
||||
93
app/dialogs/format_combo.py
Normal file
93
app/dialogs/format_combo.py
Normal 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))
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
85
app/engine/crop_marks.py
Normal file
85
app/engine/crop_marks.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user