Formats impression configurables + recadrage au ratio
Config admin : - Chaque format (10x15, 13x18, 15x20) activable avec checkbox - Imprimante CUPS assignable par format Écran impression : - Boutons de format (seuls les activés apparaissent) - Cadre de recadrage au ratio du format (3:2, 18:13, 4:3) - Auto-détection portrait/paysage - Cadre déplaçable avec poignées tactiles - Galerie → Valider → écran impression avec print_list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -297,7 +297,7 @@ class ImportScreen(QWidget):
|
|||||||
self.summary_label.setStyleSheet("color: #999;")
|
self.summary_label.setStyleSheet("color: #999;")
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Envoie les photos sélectionnées à l'impression."""
|
"""Envoie les photos sélectionnées à l'écran d'impression."""
|
||||||
to_print = []
|
to_print = []
|
||||||
for card in self.cards:
|
for card in self.cards:
|
||||||
if card.quantity > 0:
|
if card.quantity > 0:
|
||||||
@@ -306,9 +306,8 @@ class ImportScreen(QWidget):
|
|||||||
if not to_print:
|
if not to_print:
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: envoyer à l'écran d'impression avec les quantités
|
self.main_window.print_screen.set_print_list(to_print)
|
||||||
# Pour l'instant, on envoie la première photo
|
self.main_window.show_print()
|
||||||
self.main_window.show_print(to_print[0][0])
|
|
||||||
|
|
||||||
def get_print_list(self):
|
def get_print_list(self):
|
||||||
"""Retourne la liste [(path, quantity), ...] pour l'impression."""
|
"""Retourne la liste [(path, quantity), ...] pour l'impression."""
|
||||||
|
|||||||
@@ -1,90 +1,259 @@
|
|||||||
"""Écran d'impression — prévisualisation et lancement impression."""
|
"""Écran d'impression — choix du format, recadrage au ratio, impression."""
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
QLabel, QComboBox
|
QLabel, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt, QRectF
|
||||||
from PyQt6.QtGui import QFont, QPixmap
|
from PyQt6.QtGui import QFont, QPixmap, QImage, QPainter, QColor
|
||||||
|
|
||||||
|
from ui.widgets.config_dialog import load_config
|
||||||
|
from ui.widgets.crop_overlay import CropOverlay
|
||||||
|
from ui.style import scale, scaled_font, Sizes, back_btn_style, green_btn, blue_btn
|
||||||
|
|
||||||
|
# Ratios par format (largeur / hauteur, paysage)
|
||||||
|
FORMAT_RATIOS = {
|
||||||
|
"10x15": 1.5, # 15/10
|
||||||
|
"13x18": 1.385, # 18/13
|
||||||
|
"15x20": 1.333, # 20/15
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrintScreen(QWidget):
|
class PrintScreen(QWidget):
|
||||||
FORMATS = {
|
|
||||||
"10x15 cm": (10, 15),
|
|
||||||
"13x18 cm": (13, 18),
|
|
||||||
"15x20 cm": (15, 20),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, main_window):
|
def __init__(self, main_window):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.image_path = None
|
self.image_path = None
|
||||||
|
self.pixmap_item = None
|
||||||
|
self.crop_overlay = None
|
||||||
|
self.selected_format = None
|
||||||
|
self.print_list = [] # [(path, quantity), ...]
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
layout.setSpacing(30)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
header = QHBoxLayout()
|
header = QHBoxLayout()
|
||||||
back_btn = QPushButton("Retour éditeur")
|
self.back_btn = QPushButton("< Retour")
|
||||||
back_btn.setFont(QFont("", 14))
|
self.back_btn.clicked.connect(self._go_back)
|
||||||
back_btn.clicked.connect(lambda: self.main_window.show_editor())
|
header.addWidget(self.back_btn)
|
||||||
header.addWidget(back_btn)
|
|
||||||
header.addStretch()
|
self.title = QLabel("Choisissez le format et recadrez")
|
||||||
|
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
header.addWidget(self.title, 1)
|
||||||
|
|
||||||
|
header.addSpacing(100)
|
||||||
layout.addLayout(header)
|
layout.addLayout(header)
|
||||||
|
|
||||||
# Prévisualisation
|
# Boutons de format
|
||||||
self.preview = QLabel()
|
self.format_bar = QHBoxLayout()
|
||||||
self.preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.format_bar.setSpacing(10)
|
||||||
self.preview.setMinimumSize(400, 300)
|
self.format_buttons = {}
|
||||||
self.preview.setStyleSheet("background-color: #333; border-radius: 10px;")
|
layout.addLayout(self.format_bar)
|
||||||
layout.addWidget(self.preview)
|
|
||||||
|
|
||||||
# Sélection format
|
# Zone de recadrage (QGraphicsScene)
|
||||||
format_layout = QHBoxLayout()
|
self.scene = QGraphicsScene()
|
||||||
format_label = QLabel("Format :")
|
self.view = QGraphicsView(self.scene)
|
||||||
format_label.setFont(QFont("", 16))
|
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
format_layout.addWidget(format_label)
|
self.view.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||||
|
self.view.setStyleSheet("background-color: #333; border: none;")
|
||||||
|
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
layout.addWidget(self.view, 1)
|
||||||
|
|
||||||
self.format_combo = QComboBox()
|
# Info recadrage
|
||||||
self.format_combo.setFont(QFont("", 16))
|
self.info_label = QLabel("Déplacez le cadre pour recadrer votre photo")
|
||||||
self.format_combo.addItems(self.FORMATS.keys())
|
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.format_combo.setMinimumHeight(50)
|
self.info_label.setStyleSheet("color: #999;")
|
||||||
format_layout.addWidget(self.format_combo)
|
layout.addWidget(self.info_label)
|
||||||
layout.addLayout(format_layout)
|
|
||||||
|
|
||||||
# Bouton imprimer
|
# Bouton imprimer
|
||||||
print_btn = QPushButton("IMPRIMER")
|
self.print_btn = QPushButton("IMPRIMER")
|
||||||
print_btn.setFont(QFont("", 24, QFont.Weight.Bold))
|
self.print_btn.clicked.connect(self._print)
|
||||||
print_btn.setMinimumSize(300, 80)
|
layout.addWidget(self.print_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
print_btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
def showEvent(self, event):
|
||||||
background-color: #4CAF50;
|
super().showEvent(event)
|
||||||
color: white;
|
from PyQt6.QtCore import QTimer
|
||||||
border-radius: 15px;
|
QTimer.singleShot(0, self._apply_responsive)
|
||||||
}
|
|
||||||
QPushButton:pressed { background-color: #388E3C; }
|
def resizeEvent(self, event):
|
||||||
""")
|
super().resizeEvent(event)
|
||||||
print_btn.clicked.connect(self._print)
|
self._apply_responsive()
|
||||||
layout.addWidget(print_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
|
||||||
|
def _apply_responsive(self):
|
||||||
|
self.back_btn.setFont(scaled_font(self, Sizes.BUTTON_TEXT, bold=True))
|
||||||
|
self.back_btn.setMinimumHeight(scale(self, Sizes.BTN_HEIGHT))
|
||||||
|
self.back_btn.setStyleSheet(back_btn_style(self))
|
||||||
|
|
||||||
|
self.title.setFont(scaled_font(self, Sizes.HEADING, bold=True))
|
||||||
|
self.info_label.setFont(scaled_font(self, Sizes.HINT))
|
||||||
|
|
||||||
|
self.print_btn.setFont(scaled_font(self, 4.0, bold=True))
|
||||||
|
self.print_btn.setMinimumSize(scale(self, 35), scale(self, Sizes.BTN_HEIGHT * 1.5))
|
||||||
|
self.print_btn.setStyleSheet(green_btn(self))
|
||||||
|
|
||||||
|
# Style boutons format
|
||||||
|
for fmt, btn in self.format_buttons.items():
|
||||||
|
btn.setFont(scaled_font(self, Sizes.BUTTON_TEXT))
|
||||||
|
btn.setMinimumHeight(scale(self, Sizes.BTN_HEIGHT))
|
||||||
|
if fmt == self.selected_format:
|
||||||
|
btn.setStyleSheet(green_btn(self))
|
||||||
|
else:
|
||||||
|
btn.setStyleSheet(blue_btn(self))
|
||||||
|
|
||||||
def set_image(self, path):
|
def set_image(self, path):
|
||||||
|
"""Charge une image et affiche les formats disponibles."""
|
||||||
self.image_path = path
|
self.image_path = path
|
||||||
pixmap = QPixmap(path)
|
self.print_list = [(path, 1)]
|
||||||
if not pixmap.isNull():
|
self._load_formats()
|
||||||
scaled = pixmap.scaled(
|
self._load_image()
|
||||||
400, 300,
|
|
||||||
Qt.AspectRatioMode.KeepAspectRatio,
|
|
||||||
Qt.TransformationMode.SmoothTransformation
|
|
||||||
)
|
|
||||||
self.preview.setPixmap(scaled)
|
|
||||||
|
|
||||||
def _print(self):
|
def set_print_list(self, print_list):
|
||||||
|
"""Reçoit la liste [(path, qty), ...] depuis la galerie."""
|
||||||
|
self.print_list = print_list
|
||||||
|
if print_list:
|
||||||
|
self.image_path = print_list[0][0]
|
||||||
|
self._load_formats()
|
||||||
|
self._load_image()
|
||||||
|
|
||||||
|
def _load_formats(self):
|
||||||
|
"""Charge les formats activés depuis la config."""
|
||||||
|
# Clear existing buttons
|
||||||
|
while self.format_bar.count():
|
||||||
|
item = self.format_bar.takeAt(0)
|
||||||
|
if item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
self.format_buttons = {}
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
formats = config.get("formats", {})
|
||||||
|
self.selected_format = None
|
||||||
|
|
||||||
|
for fmt_name, fmt_conf in formats.items():
|
||||||
|
if not fmt_conf.get("enabled", False):
|
||||||
|
continue
|
||||||
|
btn = QPushButton(f"{fmt_name} cm")
|
||||||
|
btn.clicked.connect(lambda checked, f=fmt_name: self._select_format(f))
|
||||||
|
self.format_bar.addWidget(btn)
|
||||||
|
self.format_buttons[fmt_name] = btn
|
||||||
|
|
||||||
|
# Sélectionner le premier par défaut
|
||||||
|
if self.selected_format is None:
|
||||||
|
self.selected_format = fmt_name
|
||||||
|
|
||||||
|
# Si aucun format activé, activer 10x15 par défaut
|
||||||
|
if not self.format_buttons:
|
||||||
|
btn = QPushButton("10x15 cm")
|
||||||
|
btn.clicked.connect(lambda: self._select_format("10x15"))
|
||||||
|
self.format_bar.addWidget(btn)
|
||||||
|
self.format_buttons["10x15"] = btn
|
||||||
|
self.selected_format = "10x15"
|
||||||
|
|
||||||
|
self._apply_responsive()
|
||||||
|
|
||||||
|
def _load_image(self):
|
||||||
|
"""Charge l'image dans la scène et place le cadre de recadrage."""
|
||||||
if not self.image_path:
|
if not self.image_path:
|
||||||
return
|
return
|
||||||
fmt = self.format_combo.currentText()
|
self.scene.clear()
|
||||||
size = self.FORMATS[fmt]
|
self.crop_overlay = None
|
||||||
# TODO: redimensionner selon le format et envoyer à CUPS
|
pixmap = QPixmap(self.image_path)
|
||||||
print(f"Impression {self.image_path} en {fmt} ({size[0]}x{size[1]} cm)")
|
if pixmap.isNull():
|
||||||
|
return
|
||||||
|
self.pixmap_item = self.scene.addPixmap(pixmap)
|
||||||
|
self.scene.setSceneRect(QRectF(pixmap.rect()))
|
||||||
|
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
|
||||||
|
self._place_crop_overlay()
|
||||||
|
|
||||||
|
def _select_format(self, fmt_name):
|
||||||
|
"""Change le format sélectionné et met à jour le cadre."""
|
||||||
|
self.selected_format = fmt_name
|
||||||
|
self._apply_responsive()
|
||||||
|
self._place_crop_overlay()
|
||||||
|
|
||||||
|
def _place_crop_overlay(self):
|
||||||
|
"""Place le cadre de recadrage au ratio du format sélectionné."""
|
||||||
|
if not self.pixmap_item:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Supprimer l'ancien overlay
|
||||||
|
if self.crop_overlay:
|
||||||
|
self.scene.removeItem(self.crop_overlay)
|
||||||
|
self.crop_overlay = None
|
||||||
|
|
||||||
|
ratio = FORMAT_RATIOS.get(self.selected_format, 1.5)
|
||||||
|
img_rect = self.pixmap_item.boundingRect()
|
||||||
|
img_w = img_rect.width()
|
||||||
|
img_h = img_rect.height()
|
||||||
|
|
||||||
|
# Déterminer orientation : paysage ou portrait selon l'image
|
||||||
|
img_ratio = img_w / img_h
|
||||||
|
if img_ratio < 1:
|
||||||
|
# Image portrait → cadre portrait (inverser le ratio)
|
||||||
|
ratio = 1.0 / ratio
|
||||||
|
|
||||||
|
# Calculer la plus grande zone de crop possible dans l'image
|
||||||
|
if img_w / img_h > ratio:
|
||||||
|
# Image plus large que le ratio → hauteur max, largeur ajustée
|
||||||
|
crop_h = img_h * 0.9
|
||||||
|
crop_w = crop_h * ratio
|
||||||
|
else:
|
||||||
|
# Image plus haute que le ratio → largeur max, hauteur ajustée
|
||||||
|
crop_w = img_w * 0.9
|
||||||
|
crop_h = crop_w / ratio
|
||||||
|
|
||||||
|
# Centrer le cadre
|
||||||
|
x = (img_w - crop_w) / 2
|
||||||
|
y = (img_h - crop_h) / 2
|
||||||
|
|
||||||
|
crop_rect = QRectF(x, y, crop_w, crop_h)
|
||||||
|
self.crop_overlay = CropOverlay(crop_rect)
|
||||||
|
self.crop_overlay.set_bounds(img_rect)
|
||||||
|
self.scene.addItem(self.crop_overlay)
|
||||||
|
|
||||||
|
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
|
||||||
|
|
||||||
|
def _go_back(self):
|
||||||
|
self.main_window.show_import()
|
||||||
|
|
||||||
|
def _print(self):
|
||||||
|
if not self.image_path or not self.selected_format:
|
||||||
|
return
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
formats = config.get("formats", {})
|
||||||
|
fmt_conf = formats.get(self.selected_format, {})
|
||||||
|
printer = fmt_conf.get("printer", "")
|
||||||
|
ratio = FORMAT_RATIOS.get(self.selected_format, 1.5)
|
||||||
|
|
||||||
|
# Récupérer le rectangle de crop
|
||||||
|
if self.crop_overlay:
|
||||||
|
crop_rect = self.crop_overlay.get_crop_rect().toRect()
|
||||||
|
else:
|
||||||
|
crop_rect = None
|
||||||
|
|
||||||
|
# Pour chaque photo de la liste
|
||||||
|
for path, qty in self.print_list:
|
||||||
|
pixmap = QPixmap(path)
|
||||||
|
if pixmap.isNull():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Appliquer le crop si défini (utiliser le même crop relatif)
|
||||||
|
if crop_rect:
|
||||||
|
cropped = pixmap.copy(crop_rect)
|
||||||
|
else:
|
||||||
|
cropped = pixmap
|
||||||
|
|
||||||
|
# TODO: envoyer à CUPS
|
||||||
|
print(f"[Impression] {path} x{qty} → {self.selected_format} cm"
|
||||||
|
f" (printer: {printer or 'défaut'},"
|
||||||
|
f" crop: {crop_rect.width()}x{crop_rect.height() if crop_rect else 'none'})")
|
||||||
|
|
||||||
|
# Feedback
|
||||||
|
total = sum(q for _, q in self.print_list)
|
||||||
|
self.info_label.setText(f"Impression lancée — {total} photo{'s' if total > 1 else ''}")
|
||||||
|
self.info_label.setStyleSheet("color: #4CAF50; font-weight: bold;")
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ DEFAULT_CONFIG = {
|
|||||||
"imap_password": "",
|
"imap_password": "",
|
||||||
"imap_folder": "INBOX",
|
"imap_folder": "INBOX",
|
||||||
"imap_interval": 30,
|
"imap_interval": 30,
|
||||||
# Impression
|
# Formats d'impression (activable + imprimante par format)
|
||||||
"printer_name": "",
|
"formats": {
|
||||||
"print_format": "10x15 cm",
|
"10x15": {"enabled": True, "printer": "", "ratio": 1.5},
|
||||||
|
"13x18": {"enabled": False, "printer": "", "ratio": 1.385},
|
||||||
|
"15x20": {"enabled": False, "printer": "", "ratio": 1.333},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -211,22 +214,25 @@ class ConfigDialog(QDialog):
|
|||||||
grid.addWidget(self.imap_interval_input, row, 1)
|
grid.addWidget(self.imap_interval_input, row, 1)
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
# ── Impression ──
|
# ── Formats d'impression ──
|
||||||
grid.addWidget(self._section_title("Impression"), row, 0, 1, 2)
|
grid.addWidget(self._section_title("Formats d'impression"), row, 0, 1, 2)
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
grid.addWidget(self._label("Imprimante :"), row, 0)
|
formats = self.config.get("formats", DEFAULT_CONFIG["formats"])
|
||||||
self.printer_input = self._input(self.config["printer_name"], "Nom CUPS (vide = par défaut)")
|
self.format_widgets = {}
|
||||||
grid.addWidget(self.printer_input, row, 1)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
grid.addWidget(self._label("Format par défaut :"), row, 0)
|
for fmt_name, fmt_conf in formats.items():
|
||||||
self.format_combo = QComboBox()
|
# Checkbox activé
|
||||||
self.format_combo.setFont(QFont("", 12))
|
cb = QCheckBox(f"{fmt_name} cm")
|
||||||
self.format_combo.setMinimumHeight(35)
|
cb.setFont(QFont("", 12))
|
||||||
self.format_combo.addItems(["10x15 cm", "13x18 cm", "15x20 cm"])
|
cb.setChecked(fmt_conf.get("enabled", False))
|
||||||
self.format_combo.setCurrentText(self.config["print_format"])
|
grid.addWidget(cb, row, 0)
|
||||||
grid.addWidget(self.format_combo, row, 1)
|
|
||||||
|
# Imprimante associée
|
||||||
|
printer = self._input(fmt_conf.get("printer", ""), "Imprimante CUPS (vide = défaut)")
|
||||||
|
grid.addWidget(printer, row, 1)
|
||||||
|
|
||||||
|
self.format_widgets[fmt_name] = {"checkbox": cb, "printer": printer}
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
scroll.setWidget(content)
|
scroll.setWidget(content)
|
||||||
@@ -272,8 +278,14 @@ class ConfigDialog(QDialog):
|
|||||||
self.config["imap_password"] = self.imap_pass_input.text()
|
self.config["imap_password"] = self.imap_pass_input.text()
|
||||||
self.config["imap_folder"] = self.imap_folder_input.text()
|
self.config["imap_folder"] = self.imap_folder_input.text()
|
||||||
self.config["imap_interval"] = self.imap_interval_input.value()
|
self.config["imap_interval"] = self.imap_interval_input.value()
|
||||||
self.config["printer_name"] = self.printer_input.text()
|
# Formats d'impression
|
||||||
self.config["print_format"] = self.format_combo.currentText()
|
formats = self.config.get("formats", dict(DEFAULT_CONFIG["formats"]))
|
||||||
|
for fmt_name, widgets in self.format_widgets.items():
|
||||||
|
if fmt_name not in formats:
|
||||||
|
formats[fmt_name] = {}
|
||||||
|
formats[fmt_name]["enabled"] = widgets["checkbox"].isChecked()
|
||||||
|
formats[fmt_name]["printer"] = widgets["printer"].text()
|
||||||
|
self.config["formats"] = formats
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user