V2.2.0 — Recadrage + Impression

- Recadrage de pages (Pages > Recadrer) :
  - Mode marges : retirer X mm de chaque cote
  - Mode taille finale : definir la taille souhaitee (centrage auto)
  - Apercu en temps reel de la dimension finale
  - Appliquer a : page courante, toutes, paires, impaires, plage
  - Plage de pages personnalisee (ex: 1-3, 5, 8-10)
- Impression (Fichier > Imprimer / Ctrl+P) : ouvre le PDF dans le viewer par defaut

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules
2026-03-21 13:34:22 +01:00
parent c9212a30ea
commit 500309ed53
5 changed files with 316 additions and 2 deletions

230
app/dialogs/crop_dialog.py Normal file
View File

@@ -0,0 +1,230 @@
"""Dialogue Recadrer les pages."""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QDoubleSpinBox, QComboBox, QRadioButton,
QDialogButtonBox, QLineEdit)
from app.engine.pdf_utils import mm_to_pt, pt_to_mm
class CropDialog(QDialog):
def __init__(self, page_width_pt=595, page_height_pt=842, total_pages=1,
current_page=0, parent=None):
super().__init__(parent)
self.setWindowTitle("Recadrer")
self.setMinimumWidth(450)
self._page_w = page_width_pt
self._page_h = page_height_pt
self._total_pages = total_pages
self._current_page = current_page
self._setup_ui()
self._update_preview()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Info page actuelle
pw_mm = pt_to_mm(self._page_w)
ph_mm = pt_to_mm(self._page_h)
info = QLabel(f"Page actuelle : {pw_mm:.1f} x {ph_mm:.1f} mm")
info.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(info)
# Mode
grp_mode = QGroupBox("Mode de recadrage")
ml = QVBoxLayout(grp_mode)
self.rb_margins = QRadioButton("Par marges (retirer de chaque cote)")
self.rb_margins.setChecked(True)
self.rb_size = QRadioButton("Par taille finale")
ml.addWidget(self.rb_margins)
ml.addWidget(self.rb_size)
layout.addWidget(grp_mode)
self.rb_margins.toggled.connect(self._on_mode_changed)
self.rb_size.toggled.connect(self._on_mode_changed)
# Marges de recadrage
self.grp_margins_input = QGroupBox("Marges a retirer (mm)")
mgl = QVBoxLayout(self.grp_margins_input)
self.margin_spins = {}
row1 = QHBoxLayout()
row1.addStretch()
row1.addWidget(QLabel("Haut :"))
self.margin_spins["haut"] = QDoubleSpinBox()
self.margin_spins["haut"].setRange(0, 500)
self.margin_spins["haut"].setSuffix(" mm")
self.margin_spins["haut"].valueChanged.connect(self._update_preview)
row1.addWidget(self.margin_spins["haut"])
row1.addStretch()
mgl.addLayout(row1)
row2 = QHBoxLayout()
row2.addWidget(QLabel("Gauche :"))
self.margin_spins["gauche"] = QDoubleSpinBox()
self.margin_spins["gauche"].setRange(0, 500)
self.margin_spins["gauche"].setSuffix(" mm")
self.margin_spins["gauche"].valueChanged.connect(self._update_preview)
row2.addWidget(self.margin_spins["gauche"])
row2.addStretch()
row2.addWidget(QLabel("Droite :"))
self.margin_spins["droite"] = QDoubleSpinBox()
self.margin_spins["droite"].setRange(0, 500)
self.margin_spins["droite"].setSuffix(" mm")
self.margin_spins["droite"].valueChanged.connect(self._update_preview)
row2.addWidget(self.margin_spins["droite"])
mgl.addLayout(row2)
row3 = QHBoxLayout()
row3.addStretch()
row3.addWidget(QLabel("Bas :"))
self.margin_spins["bas"] = QDoubleSpinBox()
self.margin_spins["bas"].setRange(0, 500)
self.margin_spins["bas"].setSuffix(" mm")
self.margin_spins["bas"].valueChanged.connect(self._update_preview)
row3.addWidget(self.margin_spins["bas"])
row3.addStretch()
mgl.addLayout(row3)
layout.addWidget(self.grp_margins_input)
# Taille finale
self.grp_size_input = QGroupBox("Taille finale (mm)")
sgl = QHBoxLayout(self.grp_size_input)
sgl.addWidget(QLabel("Largeur :"))
self.spin_final_w = QDoubleSpinBox()
self.spin_final_w.setRange(10, 5000)
self.spin_final_w.setValue(pt_to_mm(self._page_w))
self.spin_final_w.setSuffix(" mm")
self.spin_final_w.valueChanged.connect(self._update_preview)
sgl.addWidget(self.spin_final_w)
sgl.addWidget(QLabel("Hauteur :"))
self.spin_final_h = QDoubleSpinBox()
self.spin_final_h.setRange(10, 5000)
self.spin_final_h.setValue(pt_to_mm(self._page_h))
self.spin_final_h.setSuffix(" mm")
self.spin_final_h.valueChanged.connect(self._update_preview)
sgl.addWidget(self.spin_final_h)
layout.addWidget(self.grp_size_input)
self.grp_size_input.setVisible(False)
# Apercu dimension finale
self.lbl_preview = QLabel()
self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;")
layout.addWidget(self.lbl_preview)
# Appliquer a
grp_apply = QGroupBox("Appliquer a")
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(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _on_mode_changed(self):
self.grp_margins_input.setVisible(self.rb_margins.isChecked())
self.grp_size_input.setVisible(self.rb_size.isChecked())
self._update_preview()
def _update_preview(self):
if self.rb_margins.isChecked():
top = self.margin_spins["haut"].value()
bottom = self.margin_spins["bas"].value()
left = self.margin_spins["gauche"].value()
right = self.margin_spins["droite"].value()
w_mm = pt_to_mm(self._page_w) - left - right
h_mm = pt_to_mm(self._page_h) - top - bottom
else:
w_mm = self.spin_final_w.value()
h_mm = self.spin_final_h.value()
if w_mm <= 0 or h_mm <= 0:
self.lbl_preview.setText("Dimensions invalides !")
self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: red; padding: 8px;")
else:
self.lbl_preview.setText(f"Resultat : {w_mm:.1f} x {h_mm:.1f} mm")
self.lbl_preview.setStyleSheet("font-weight: bold; font-size: 13px; color: #0066cc; padding: 8px;")
def _parse_range(self):
"""Parser la plage de pages. Retourne une liste d'indices (0-based)."""
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 get_settings(self):
# Mode
if self.rb_margins.isChecked():
mode = "margins"
crop_top = mm_to_pt(self.margin_spins["haut"].value())
crop_bottom = mm_to_pt(self.margin_spins["bas"].value())
crop_left = mm_to_pt(self.margin_spins["gauche"].value())
crop_right = mm_to_pt(self.margin_spins["droite"].value())
else:
mode = "size"
final_w = mm_to_pt(self.spin_final_w.value())
final_h = mm_to_pt(self.spin_final_h.value())
crop_left = (self._page_w - final_w) / 2
crop_right = crop_left
crop_top = (self._page_h - final_h) / 2
crop_bottom = crop_top
# Pages cibles
if self.rb_current.isChecked():
pages = [self._current_page]
elif self.rb_all.isChecked():
pages = list(range(self._total_pages))
elif self.rb_even.isChecked():
pages = [i for i in range(self._total_pages) if (i + 1) % 2 == 0]
elif self.rb_odd.isChecked():
pages = [i for i in range(self._total_pages) if (i + 1) % 2 != 0]
else:
pages = self._parse_range()
return {
"crop_top": crop_top,
"crop_bottom": crop_bottom,
"crop_left": crop_left,
"crop_right": crop_right,
"pages": pages,
}