V3.6.0 — Demonter pages, masquage visuel, regles, bleeds, corrections

- Demonter les pages : couper en 2 sans reordonner (sous Delivretiser)
- Masquage visuel : selection au curseur sur l'apercu + plage de pages
- Regles horizontale/verticale en mm (menu Affichage)
- Fonds perdus : filigrane rouge pour visualiser les bleeds
- Reperes de coupe : marques a l'exterieur (page agrandie), 2 traits/coin
- Numerotation : couleur configurable via selecteur
- Decalage : Trim & Shift renomme en francais, creep -> chasse
- Recadrages multiples cumulatifs (plages differentes)
- Fix spinbox format personnalise (fleches aleatoires)
- Fix combo livret : option Automatique preservee

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-29 22:12:54 +02:00
parent f7ad806b26
commit 202834a794
13 changed files with 675 additions and 113 deletions

View File

@@ -64,12 +64,18 @@ class CustomSizeDialog(QDialog):
self.spin_w.setRange(10, 5000)
self.spin_w.setValue(210)
self.spin_w.setSuffix(" mm")
self.spin_w.setDecimals(1)
self.spin_w.setSingleStep(1.0)
self.spin_w.setMinimumWidth(100)
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")
self.spin_h.setDecimals(1)
self.spin_h.setSingleStep(1.0)
self.spin_h.setMinimumWidth(100)
gl.addWidget(self.spin_h)
layout.addWidget(grp)

View File

@@ -12,10 +12,27 @@ ADD_LABEL = "Ajouter un format..."
def populate_format_combo(combo, default="A4 Paysage"):
"""Remplir un combo avec les formats standard + custom + options."""
"""Remplir un combo avec les formats standard + custom + options.
Si default n'est pas un format standard, il est conserve en tete du combo.
"""
combo.blockSignals(True)
# Sauvegarder l'item par defaut s'il n'est pas un format standard
keep_first = None
if default and default not in PAGE_SIZES:
# Verifier s'il est deja dans le combo avec des data
idx = combo.findText(default)
if idx >= 0:
keep_first = (default, combo.itemData(idx))
combo.clear()
# Re-ajouter l'item special en tete si necessaire
if keep_first:
combo.addItem(keep_first[0], keep_first[1])
combo.insertSeparator(combo.count())
# Formats standard
for name in PAGE_SIZES:
combo.addItem(name, PAGE_SIZES[name])

View File

@@ -1,61 +1,109 @@
"""Dialogue Masquage (rectangles blancs)."""
"""Dialogue Masquage (rectangles blancs) — avec selection visuelle."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QDoubleSpinBox,
QDialogButtonBox, QPushButton, QListWidget,
QListWidgetItem)
from app.engine.pdf_utils import mm_to_pt
QLineEdit, QRadioButton)
from PyQt6.QtCore import Qt
from app.engine.pdf_utils import mm_to_pt, pt_to_mm
class MaskingDialog(QDialog):
def __init__(self, parent=None):
def __init__(self, page_width_pt=595, page_height_pt=842, total_pages=1,
current_page=0, viewer=None, parent=None):
super().__init__(parent)
self.setWindowTitle("Masquage (ruban blanc)")
self.setMinimumWidth(450)
self.setFixedWidth(380)
self.setModal(False)
self.setWindowFlags(self.windowFlags() | Qt.WindowType.Tool)
self._page_w = page_width_pt
self._page_h = page_height_pt
self._total_pages = total_pages
self._current_page = current_page
self._viewer = viewer
self._rectangles = []
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Definition rectangle
grp_rect = QGroupBox("Definir un rectangle de masquage (mm)")
rl = QHBoxLayout(grp_rect)
# Info
pw_mm = pt_to_mm(self._page_w)
ph_mm = pt_to_mm(self._page_h)
info = QLabel(f"Page : {pw_mm:.1f} x {ph_mm:.1f} mm")
info.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(info)
rl.addWidget(QLabel("X:"))
# Bouton selection visuelle
if self._viewer:
sel_layout = QHBoxLayout()
self.btn_select = QPushButton("Selectionner la zone sur l'apercu")
self.btn_select.setCheckable(True)
self.btn_select.setStyleSheet("""
QPushButton { background: #00b4d8; color: white; padding: 6px 14px;
border-radius: 6px; font-weight: bold; font-size: 11px; }
QPushButton:checked { background: #e74c3c; }
QPushButton:hover { background: #0096b7; }
QPushButton:checked:hover { background: #c0392b; }
""")
self.btn_select.toggled.connect(self._toggle_select_mode)
sel_layout.addWidget(self.btn_select)
sel_layout.addStretch()
layout.addLayout(sel_layout)
self._viewer.selection_changed.connect(self._on_selection_changed)
# Coordonnees manuelles
grp_rect = QGroupBox("Zone de masquage (mm)")
rl = QVBoxLayout(grp_rect)
row1 = QHBoxLayout()
row1.addWidget(QLabel("X :"))
self.spin_x = QDoubleSpinBox()
self.spin_x.setRange(0, 1000)
self.spin_x.setRange(0, 2000)
self.spin_x.setSuffix(" mm")
rl.addWidget(self.spin_x)
rl.addWidget(QLabel("Y:"))
row1.addWidget(self.spin_x)
row1.addWidget(QLabel("Y :"))
self.spin_y = QDoubleSpinBox()
self.spin_y.setRange(0, 1000)
self.spin_y.setRange(0, 2000)
self.spin_y.setSuffix(" mm")
rl.addWidget(self.spin_y)
row1.addWidget(self.spin_y)
rl.addLayout(row1)
rl.addWidget(QLabel("L:"))
row2 = QHBoxLayout()
row2.addWidget(QLabel("Largeur :"))
self.spin_w = QDoubleSpinBox()
self.spin_w.setRange(1, 1000)
self.spin_w.setRange(1, 2000)
self.spin_w.setValue(50)
self.spin_w.setSuffix(" mm")
rl.addWidget(self.spin_w)
rl.addWidget(QLabel("H:"))
row2.addWidget(self.spin_w)
row2.addWidget(QLabel("Hauteur :"))
self.spin_h = QDoubleSpinBox()
self.spin_h.setRange(1, 1000)
self.spin_h.setRange(1, 2000)
self.spin_h.setValue(10)
self.spin_h.setSuffix(" mm")
rl.addWidget(self.spin_h)
row2.addWidget(self.spin_h)
rl.addLayout(row2)
layout.addWidget(grp_rect)
# Boutons ajouter/supprimer
# Bouton ajouter
btn_layout = QHBoxLayout()
btn_add = QPushButton("Ajouter")
btn_add.clicked.connect(self._add_rect)
btn_layout.addWidget(btn_add)
self.btn_add = QPushButton("Ajouter ce masque")
self.btn_add.setStyleSheet("""
QPushButton { background: #2ecc71; color: white; padding: 6px 14px;
border-radius: 6px; font-weight: bold; }
QPushButton:hover { background: #27ae60; }
""")
self.btn_add.clicked.connect(self._add_rect)
btn_layout.addWidget(self.btn_add)
btn_del = QPushButton("Supprimer")
btn_del.setStyleSheet("""
QPushButton { background: #e8e8e8; color: #555; padding: 6px 14px;
border-radius: 6px; border: 1px solid #ccc; }
QPushButton:hover { background: #ddd; }
""")
btn_del.clicked.connect(self._del_rect)
btn_layout.addWidget(btn_del)
btn_layout.addStretch()
@@ -63,15 +111,31 @@ class MaskingDialog(QDialog):
# Liste des rectangles
self.list_rects = QListWidget()
self.list_rects.setMaximumHeight(100)
layout.addWidget(self.list_rects)
# Appliquer a
grp_apply = QGroupBox("Appliquer a")
al = QHBoxLayout(grp_apply)
self.combo_apply = QComboBox()
self.combo_apply.addItems(["Toutes les pages", "Pages paires", "Pages impaires"])
al.addWidget(self.combo_apply)
al = QVBoxLayout(grp_apply)
self.rb_current = QRadioButton(f"Page courante ({self._current_page + 1})")
self.rb_current.setChecked(True)
self.rb_all = QRadioButton("Toutes les pages")
self.rb_even = QRadioButton("Pages paires")
self.rb_odd = QRadioButton("Pages impaires")
self.rb_range = QRadioButton("Plage de pages :")
al.addWidget(self.rb_current)
al.addWidget(self.rb_all)
al.addWidget(self.rb_even)
al.addWidget(self.rb_odd)
range_layout = QHBoxLayout()
range_layout.addWidget(self.rb_range)
self.edit_range = QLineEdit()
self.edit_range.setPlaceholderText("ex: 1-3, 5, 8-10")
self.edit_range.setEnabled(False)
range_layout.addWidget(self.edit_range)
al.addLayout(range_layout)
layout.addWidget(grp_apply)
self.rb_range.toggled.connect(self.edit_range.setEnabled)
# Boutons
buttons = QDialogButtonBox(
@@ -81,32 +145,128 @@ class MaskingDialog(QDialog):
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _toggle_select_mode(self, checked):
if self._viewer:
self._viewer.set_select_mode(checked)
if checked:
self.btn_select.setText("Mode selection actif — dessiner la zone")
else:
self.btn_select.setText("Selectionner la zone sur l'apercu")
def _on_selection_changed(self, left, top, right, bottom):
"""Recoit les ratios de selection du viewer."""
pw_mm = pt_to_mm(self._page_w)
ph_mm = pt_to_mm(self._page_h)
x_mm = left * pw_mm
y_mm = top * ph_mm
w_mm = (right - left) * pw_mm
h_mm = (bottom - top) * ph_mm
if w_mm > 0 and h_mm > 0:
self.spin_x.blockSignals(True)
self.spin_y.blockSignals(True)
self.spin_w.blockSignals(True)
self.spin_h.blockSignals(True)
self.spin_x.setValue(x_mm)
self.spin_y.setValue(y_mm)
self.spin_w.setValue(w_mm)
self.spin_h.setValue(h_mm)
self.spin_x.blockSignals(False)
self.spin_y.blockSignals(False)
self.spin_w.blockSignals(False)
self.spin_h.blockSignals(False)
def _add_rect(self):
x_mm = self.spin_x.value()
y_mm = self.spin_y.value()
w_mm = self.spin_w.value()
h_mm = self.spin_h.value()
# Convertir Y : dans le viewer le Y part du haut, dans le PDF du bas
page_h_mm = pt_to_mm(self._page_h)
pdf_y_mm = page_h_mm - y_mm - h_mm
rect = {
"x": mm_to_pt(self.spin_x.value()),
"y": mm_to_pt(self.spin_y.value()),
"width": mm_to_pt(self.spin_w.value()),
"height": mm_to_pt(self.spin_h.value()),
"x": mm_to_pt(x_mm),
"y": mm_to_pt(pdf_y_mm),
"width": mm_to_pt(w_mm),
"height": mm_to_pt(h_mm),
}
self._rectangles.append(rect)
self.list_rects.addItem(
f"X={self.spin_x.value():.1f} Y={self.spin_y.value():.1f} "
f"L={self.spin_w.value():.1f} H={self.spin_h.value():.1f} mm"
f"X={x_mm:.1f} Y={y_mm:.1f} L={w_mm:.1f} H={h_mm:.1f} mm"
)
# Nettoyer la selection pour en dessiner une autre
if self._viewer:
self._viewer.clear_selection()
def _del_rect(self):
row = self.list_rects.currentRow()
if row >= 0:
self.list_rects.takeItem(row)
self._rectangles.pop(row)
def _parse_range(self):
text = self.edit_range.text().strip()
if not text:
return []
indices = []
for part in text.split(","):
part = part.strip()
if "-" in part:
try:
start, end = part.split("-", 1)
for p in range(int(start), int(end) + 1):
if 1 <= p <= self._total_pages:
indices.append(p - 1)
except ValueError:
pass
else:
try:
p = int(part)
if 1 <= p <= self._total_pages:
indices.append(p - 1)
except ValueError:
pass
return sorted(set(indices))
def closeEvent(self, event):
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
super().closeEvent(event)
def reject(self):
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
super().reject()
def get_settings(self):
apply_map = {
"Toutes les pages": "all",
"Pages paires": "even",
"Pages impaires": "odd"
}
if self._viewer:
self._viewer.set_select_mode(False)
self._viewer.clear_selection()
# Determiner apply_to
if self.rb_current.isChecked():
apply_to = self._current_page + 1 # 1-based pour le moteur
elif self.rb_all.isChecked():
apply_to = "all"
elif self.rb_even.isChecked():
apply_to = "even"
elif self.rb_odd.isChecked():
apply_to = "odd"
else:
# Plage personnalisee
pages = self._parse_range()
if pages:
apply_to = pages # liste d'indices 0-based
else:
apply_to = "all"
return {
"rectangles": self._rectangles,
"apply_to": apply_map[self.combo_apply.currentText()],
"apply_to": apply_to,
}

View File

@@ -2,7 +2,9 @@
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QSpinBox, QLineEdit,
QDialogButtonBox, QDoubleSpinBox)
QDialogButtonBox, QDoubleSpinBox, QPushButton,
QColorDialog)
from PyQt6.QtGui import QColor
from reportlab.lib.units import mm
@@ -11,6 +13,7 @@ class PageNumbersDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("Numerotation de pages")
self.setMinimumWidth(400)
self._color = QColor(0, 0, 0)
self._setup_ui()
def _setup_ui(self):
@@ -45,6 +48,17 @@ class PageNumbersDialog(QDialog):
fl.addWidget(self.spin_size)
layout.addWidget(grp_font)
# Couleur
color_layout = QHBoxLayout()
color_layout.addWidget(QLabel("Couleur :"))
self.btn_color = QPushButton()
self.btn_color.setFixedSize(60, 28)
self._update_color_button()
self.btn_color.clicked.connect(self._pick_color)
color_layout.addWidget(self.btn_color)
color_layout.addStretch()
layout.addLayout(color_layout)
# Texte
grp_text = QGroupBox("Texte")
tl = QHBoxLayout(grp_text)
@@ -100,6 +114,17 @@ class PageNumbersDialog(QDialog):
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _pick_color(self):
color = QColorDialog.getColor(self._color, self, "Couleur de la numerotation")
if color.isValid():
self._color = color
self._update_color_button()
def _update_color_button(self):
self.btn_color.setStyleSheet(
f"background-color: {self._color.name()}; border: 1px solid #999; border-radius: 4px;"
)
def get_settings(self):
apply_map = {
"Toutes les pages": "all",
@@ -110,7 +135,7 @@ class PageNumbersDialog(QDialog):
"position": self.combo_pos.currentText(),
"font_name": self.combo_font.currentText(),
"font_size": self.spin_size.value(),
"color": (0, 0, 0),
"color": (self._color.redF(), self._color.greenF(), self._color.blueF()),
"prefix": self.edit_prefix.text(),
"suffix": self.edit_suffix.text(),
"start_number": self.spin_start.value(),

View File

@@ -1,4 +1,4 @@
"""Dialogue Trim & Shift."""
"""Dialogue Decalage du contenu."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QDoubleSpinBox, QCheckBox,
@@ -9,7 +9,7 @@ from app.engine.pdf_utils import mm_to_pt
class TrimShiftDialog(QDialog):
def __init__(self, total_pages=1, parent=None):
super().__init__(parent)
self.setWindowTitle("Trim && Shift — Decaler le contenu")
self.setWindowTitle("Decalage du contenu")
self.setMinimumWidth(420)
self._total_pages = total_pages
self._setup_ui()
@@ -55,10 +55,10 @@ class TrimShiftDialog(QDialog):
self.chk_different.toggled.connect(self.spin_x_even.setEnabled)
self.chk_different.toggled.connect(self.spin_y_even.setEnabled)
# Creep
grp_creep = QGroupBox("Compensation de creep (livrets)")
# Chasse (creep)
grp_creep = QGroupBox("Compensation de chasse (livrets)")
cl = QHBoxLayout(grp_creep)
cl.addWidget(QLabel("Creep max :"))
cl.addWidget(QLabel("Chasse max :"))
self.spin_creep = QDoubleSpinBox()
self.spin_creep.setRange(0, 20)
self.spin_creep.setSuffix(" mm")