import io import logging import subprocess import time from datetime import datetime from pathlib import Path from backend.config import DOSSIER_PHOTOS, charger_config log = logging.getLogger("photobooth.camera") # Essayer d'importer gphoto2, sinon mode simulation try: import gphoto2 as gp GPHOTO2_DISPONIBLE = True except ImportError: GPHOTO2_DISPONIBLE = False log.warning("python-gphoto2 non installe, mode simulation active") class Camera: """Controle de l'appareil DSLR via gphoto2.""" def __init__(self): self.camera = None self.contexte = None self.connectee = False def connecter(self) -> bool: """Connecte l'appareil photo.""" if not GPHOTO2_DISPONIBLE: log.info("Mode simulation : camera virtuelle connectee") self.connectee = True return True try: self.contexte = gp.Context() self.camera = gp.Camera() self.camera.init(self.contexte) self.connectee = True log.info("Camera DSLR connectee") return True except gp.GPhoto2Error as e: log.error(f"Erreur connexion camera : {e}") self.connectee = False return False def deconnecter(self): """Deconnecte l'appareil photo.""" if self.camera and GPHOTO2_DISPONIBLE: try: self.camera.exit(self.contexte) except gp.GPhoto2Error: pass self.camera = None self.contexte = None self.connectee = False log.info("Camera deconnectee") def capturer(self) -> Path | None: """Capture une photo et retourne le chemin du fichier.""" if not self.connectee: log.error("Camera non connectee") return None horodatage = datetime.now().strftime("%Y%m%d_%H%M%S_%f") nom_fichier = f"photo_{horodatage}.jpg" chemin_dest = DOSSIER_PHOTOS / nom_fichier if not GPHOTO2_DISPONIBLE: return self._capture_simulation(chemin_dest) try: chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE, self.contexte) fichier_camera = self.camera.file_get( chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, self.contexte ) fichier_camera.save(str(chemin_dest)) log.info(f"Photo capturee : {chemin_dest}") return chemin_dest except gp.GPhoto2Error as e: log.error(f"Erreur capture : {e}") return None def preview(self) -> bytes | None: """Capture un apercu live (preview) en JPEG.""" if not self.connectee: return None if not GPHOTO2_DISPONIBLE: return self._preview_simulation() try: fichier = self.camera.capture_preview(self.contexte) donnees = fichier.get_data_and_size() return bytes(donnees) except gp.GPhoto2Error as e: log.error(f"Erreur preview : {e}") return None def lister_appareils(self) -> list[str]: """Liste les appareils photo detectes.""" if not GPHOTO2_DISPONIBLE: return ["[Simulation] Camera virtuelle"] try: appareils = gp.Camera.autodetect(self.contexte or gp.Context()) return [f"{nom} ({port})" for nom, port in appareils] except gp.GPhoto2Error: return [] def _capture_simulation(self, chemin: Path) -> Path: """Genere une image de test en mode simulation.""" from PIL import Image, ImageDraw, ImageFont img = Image.new("RGB", (1920, 1280), color=(40, 40, 40)) draw = ImageDraw.Draw(img) texte = f"SIMULATION\n{datetime.now().strftime('%H:%M:%S')}" draw.text((960, 640), texte, fill=(255, 255, 255), anchor="mm") # Dessiner un cadre draw.rectangle([50, 50, 1870, 1230], outline=(233, 30, 99), width=4) img.save(chemin, "JPEG", quality=95) log.info(f"Photo simulation : {chemin}") return chemin def _preview_simulation(self) -> bytes: """Genere un apercu de test en mode simulation.""" from PIL import Image, ImageDraw img = Image.new("RGB", (640, 480), color=(30, 30, 30)) draw = ImageDraw.Draw(img) draw.text((320, 240), "PREVIEW", fill=(200, 200, 200), anchor="mm") draw.ellipse([300, 220, 340, 260], outline=(233, 30, 99), width=2) buf = io.BytesIO() img.save(buf, "JPEG", quality=70) return buf.getvalue() # Instance globale camera = Camera()