diff --git a/assets/frames/gold_frame.png b/assets/frames/gold_frame.png new file mode 100644 index 0000000..ba41de8 Binary files /dev/null and b/assets/frames/gold_frame.png differ diff --git a/assets/frames/white_frame.png b/assets/frames/white_frame.png new file mode 100644 index 0000000..7087ec6 Binary files /dev/null and b/assets/frames/white_frame.png differ diff --git a/assets/goodies/camera.png b/assets/goodies/camera.png new file mode 100644 index 0000000..c30dc50 Binary files /dev/null and b/assets/goodies/camera.png differ diff --git a/assets/goodies/heart.png b/assets/goodies/heart.png new file mode 100644 index 0000000..14e0bcb Binary files /dev/null and b/assets/goodies/heart.png differ diff --git a/assets/goodies/smile.png b/assets/goodies/smile.png new file mode 100644 index 0000000..6aa64dc Binary files /dev/null and b/assets/goodies/smile.png differ diff --git a/assets/goodies/star.png b/assets/goodies/star.png new file mode 100644 index 0000000..6aa64dc Binary files /dev/null and b/assets/goodies/star.png differ diff --git a/assets/goodies/thumbsup.png b/assets/goodies/thumbsup.png new file mode 100644 index 0000000..3dac677 Binary files /dev/null and b/assets/goodies/thumbsup.png differ diff --git a/src/services/printer.py b/src/services/printer.py new file mode 100644 index 0000000..c8d5af6 --- /dev/null +++ b/src/services/printer.py @@ -0,0 +1,162 @@ +"""Service d'impression CUPS — prépare les images et envoie les jobs.""" + +import os +import subprocess +import tempfile +from PIL import Image + +# DPI pour l'impression sublimation +PRINT_DPI = 300 + +# Dimensions en pixels à 300 DPI +FORMAT_PIXELS = { + "10x15": (1181, 1772), # 10cm x 15cm + "13x18": (1535, 2126), # 13cm x 18cm + "15x20": (1772, 2362), # 15cm x 20cm +} + +FORMAT_RATIOS = { + "10x15": 1.5, + "13x18": 1.385, + "15x20": 1.333, +} + + +def list_printers(): + """Liste les imprimantes CUPS disponibles.""" + try: + result = subprocess.run( + ["lpstat", "-p", "-d"], + capture_output=True, text=True, timeout=5 + ) + printers = [] + default = "" + for line in result.stdout.splitlines(): + if line.startswith("printer "): + name = line.split()[1] + printers.append(name) + if "system default destination:" in line: + default = line.split(":")[-1].strip() + return printers, default + except (FileNotFoundError, subprocess.TimeoutExpired): + return [], "" + + +def crop_to_ratio(image, ratio): + """Crop centré au ratio donné (largeur/hauteur en paysage).""" + w, h = image.size + img_ratio = w / h + + # Adapter le ratio au sens de l'image + if w < h: + target_ratio = 1.0 / ratio + else: + target_ratio = ratio + + if img_ratio > target_ratio: + # Image trop large → crop horizontal + new_w = int(h * target_ratio) + offset = (w - new_w) // 2 + return image.crop((offset, 0, offset + new_w, h)) + else: + # Image trop haute → crop vertical + new_h = int(w / target_ratio) + offset = (h - new_h) // 2 + return image.crop((0, offset, w, offset + new_h)) + + +def prepare_for_print(image_path, format_name, crop_rect=None): + """Prépare une image pour l'impression : crop, redimensionnement, export. + + Args: + image_path: chemin de l'image source + format_name: "10x15", "13x18", "15x20" + crop_rect: (x, y, w, h) en pixels de l'image source, ou None pour auto + + Returns: + chemin du fichier temporaire prêt à imprimer + """ + img = Image.open(image_path) + + # Appliquer le crop manuel si fourni + if crop_rect: + x, y, w, h = crop_rect + img = img.crop((x, y, x + w, y + h)) + else: + # Auto-crop au ratio + ratio = FORMAT_RATIOS.get(format_name, 1.5) + img = crop_to_ratio(img, ratio) + + # Redimensionner aux dimensions exactes du format + target = FORMAT_PIXELS.get(format_name, (1181, 1772)) + + # Orienter selon l'image + if img.width < img.height: + # Portrait + target = (min(target), max(target)) + else: + # Paysage + target = (max(target), min(target)) + + img = img.resize(target, Image.Resampling.LANCZOS) + + # Sauvegarder en JPEG haute qualité + output = os.path.join(tempfile.gettempdir(), f"photostation_print_{os.getpid()}.jpg") + img.save(output, "JPEG", quality=95, dpi=(PRINT_DPI, PRINT_DPI)) + return output + + +def print_image(image_path, format_name, printer="", copies=1, crop_rect=None): + """Prépare et imprime une image. + + Args: + image_path: chemin de l'image source + format_name: "10x15", "13x18", "15x20" + printer: nom CUPS (vide = défaut) + copies: nombre de copies + crop_rect: (x, y, w, h) ou None + + Returns: + (success, message) + """ + try: + # Préparer l'image + print_file = prepare_for_print(image_path, format_name, crop_rect) + + # Construire la commande lp + cmd = ["lp"] + if printer: + cmd.extend(["-d", printer]) + cmd.extend(["-n", str(copies)]) + # Options d'impression + cmd.extend(["-o", "fit-to-page"]) + cmd.extend(["-o", f"resolution={PRINT_DPI}dpi"]) + cmd.append(print_file) + + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=30 + ) + + if result.returncode == 0: + # Extraire l'ID du job + job_info = result.stdout.strip() + print(f"[Impression] OK: {job_info}") + return True, job_info + else: + error = result.stderr.strip() or result.stdout.strip() + print(f"[Impression] Erreur: {error}") + return False, error + + except FileNotFoundError: + return False, "Commande lp non trouvée — CUPS installé ?" + except subprocess.TimeoutExpired: + return False, "Timeout impression" + except Exception as e: + return False, str(e) + finally: + # Nettoyage + try: + if 'print_file' in locals(): + os.unlink(print_file) + except OSError: + pass diff --git a/src/ui/screens/print_screen.py b/src/ui/screens/print_screen.py index 76c0b3b..43da397 100644 --- a/src/ui/screens/print_screen.py +++ b/src/ui/screens/print_screen.py @@ -321,15 +321,42 @@ class PrintScreen(QWidget): self.price_label.setStyleSheet("color: #F44336; font-weight: bold;") def _do_print(self, printer=""): - """Lance l'impression réelle.""" + """Lance l'impression réelle via CUPS.""" + from services.printer import print_image + import threading + + self.print_btn.setEnabled(False) + self.print_btn.setText("Impression en cours...") + self.price_label.setText("Envoi à l'imprimante...") + self.price_label.setStyleSheet("color: #FF9800; font-weight: bold;") + + def run_print(): + results = [] + for path, qty in self.print_list: + success, msg = print_image( + path, self.selected_format, + printer=printer, copies=qty + ) + results.append((path, qty, success, msg)) + + # Retour au thread principal + from PyQt6.QtCore import QTimer + from functools import partial + QTimer.singleShot(0, partial(self._print_done, results)) + + threading.Thread(target=run_print, daemon=True).start() + + def _print_done(self, results): + """Callback après impression.""" self.print_btn.setEnabled(True) self.print_btn.setText("IMPRIMER") - for path, qty in self.print_list: - print(f"[Impression] {path} x{qty} → {self.selected_format} cm" - f" (printer: {printer or 'défaut'})") - # TODO: appel CUPS réel + total = sum(q for _, q, _, _ in results) + errors = [msg for _, _, ok, msg in results if not ok] - total = sum(q for _, q in self.print_list) - self.price_label.setText(f"Impression lancée — {total} photo{'s' if total > 1 else ''}") - self.price_label.setStyleSheet("color: #4CAF50; font-weight: bold;") + if errors: + self.price_label.setText(f"Erreur : {errors[0]}") + self.price_label.setStyleSheet("color: #F44336; font-weight: bold;") + else: + self.price_label.setText(f"Impression terminée — {total} photo{'s' if total > 1 else ''}") + self.price_label.setStyleSheet("color: #4CAF50; font-weight: bold;")