diff --git a/photobooth_pygame.py b/photobooth_pygame.py index c83d7cd..cc44b8b 100644 --- a/photobooth_pygame.py +++ b/photobooth_pygame.py @@ -36,6 +36,13 @@ PHOTOS_DIR.mkdir(parents=True, exist_ok=True) W, H = 1920, 1280 +# ── Constantes pellicule ────────────────────────────────────────────────────── +_FILM_H = 860 # hauteur totale de la bande +_SPR_H = 50 # hauteur zone perforations (haut et bas) +_SPR_GAP = 6 # espace entre perfos et cadre +_PELL_GAP = 6 # espace entre cadres +_PELL_FH = _FILM_H - 2 * (_SPR_H + _SPR_GAP) # hauteur d'un cadre = 748 + # ── Thèmes ─────────────────────────────────────────────────────────────────── THEMES = { "base": { @@ -783,7 +790,9 @@ class PB: nb = 4 delai = self.cfg.get("camera",{}).get("compte_a_rebours",3) self.photos=[]; self._prev_thumbs=[]; self.gal_surfs=[]; self.gal_ready=False; self.session_qr=None; self.session_url=None + is_pell = self.cfg.get("fonctionnalites",{}).get("pellicule_mode",False) strip_bw = W // nb - 4 + thumb_h = _PELL_FH if is_pell else 200 # haute résolution en mode pellicule for i in range(nb): self.num_ph=i; self.etat=S_PREV; time.sleep(1.5) @@ -793,7 +802,7 @@ class PB: if ch: self.photos.append(ch); self.compteur+=1; self._sav_cpt() try: - img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((strip_bw, 200), Image.LANCZOS) + img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((strip_bw, thumb_h), Image.LANCZOS) self._prev_thumbs.append(pygame.image.fromstring(img.tobytes(), img.size, "RGB")) except: self._prev_thumbs.append(None) @@ -831,19 +840,34 @@ class PB: except: pass def _make_composite(self, photos): - """Crée une image composite (bande ou grille) à partir des photos.""" + """Crée une image composite (bande ou grille) à partir des photos. + Mode pellicule : bande simple. Sinon : bande doublée ou grille.""" + is_pell = self.cfg.get("fonctionnalites",{}).get("pellicule_mode",False) canvas = Image.new("RGB", (W, H), (0, 0, 0)) + if self.format in ("bande3x2", "bande4x2"): nb = 3 if self.format == "bande3x2" else 4 pw = W // nb - sh = (H - 150) // 2 - for i, ch in enumerate(photos[:nb]): - try: - img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw, sh), Image.LANCZOS) - canvas.paste(img, (i * pw, 5)) - canvas.paste(img, (i * pw, sh + 10)) - except Exception as e: - print(f"composite bande: {e}") + if is_pell: + # Bande unique centrée verticalement + sh = H - 160 + top = 80 + for i, ch in enumerate(photos[:nb]): + try: + img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw, sh), Image.LANCZOS) + canvas.paste(img, (i * pw, top)) + except Exception as e: + print(f"composite pellicule: {e}") + else: + # Bande doublée (comportement original) + sh = (H - 150) // 2 + for i, ch in enumerate(photos[:nb]): + try: + img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw, sh), Image.LANCZOS) + canvas.paste(img, (i * pw, 5)) + canvas.paste(img, (i * pw, sh + 10)) + except Exception as e: + print(f"composite bande: {e}") else: # grille_4 gw = W // 2 - 15 gh = H // 2 - 75 @@ -854,6 +878,7 @@ class PB: canvas.paste(img, pos[i]) except Exception as e: print(f"composite grille: {e}") + dest = "/tmp/photobooth_composite.jpg" canvas.save(dest, "JPEG", quality=90) return dest @@ -1065,6 +1090,103 @@ class PB: return btns # ── Preview / Compte / Capture ──────────────────────────────────────────── + def _draw_pellicule(self, show_live=True): + """Affichage style pellicule 35mm : bande unique avec cadres numérotés.""" + T = self.T + nb = 3 if self.format == "bande3x2" else 4 + + # Dimensions + film_y = (H - _FILM_H) // 2 # 210 + frame_y = film_y + _SPR_H + _SPR_GAP # 266 + gap = _PELL_GAP + fw = (W - gap * (nb + 1)) // nb # largeur d'un cadre + fh = _PELL_FH # 748 + + # ── Fond noir cinéma ── + self.scr.fill((8, 8, 8)) + + # ── Corps de la bande (fond sombre + bords) ── + pygame.draw.rect(self.scr, (18, 18, 18), (0, film_y, W, _FILM_H)) + pygame.draw.rect(self.scr, (35, 35, 35), (0, film_y, W, _FILM_H), 1) + + # ── Perforations (sprocket holes) haut et bas ── + hole_w, hole_h = 32, 22 + hole_y_top = film_y + _SPR_H // 2 - hole_h // 2 + hole_y_bot = film_y + _FILM_H - _SPR_H // 2 - hole_h // 2 + x = 18 + while x + hole_w < W - 10: + for hy in (hole_y_top, hole_y_bot): + # Cadre clair + pygame.draw.rect(self.scr, (180, 180, 180), (x, hy, hole_w, hole_h), border_radius=5) + # Trou sombre intérieur + pygame.draw.rect(self.scr, (8, 8, 8), (x + 3, hy + 3, hole_w - 6, hole_h - 6), border_radius=3) + x += 62 + + # ── Séparateurs entre cadres ── + for i in range(1, nb): + sx = gap + i * (fw + gap) - gap // 2 + pygame.draw.rect(self.scr, (8, 8, 8), (sx - 2, frame_y - 2, 4, fh + 4)) + + # ── Cadres ── + for i in range(nb): + fx = gap + i * (fw + gap) + fy = frame_y + + if i < len(self._prev_thumbs): + # Photo prise + th = self._prev_thumbs[i] + if th: + ts = th if th.get_size() == (fw, fh) else pygame.transform.scale(th, (fw, fh)) + self.scr.blit(ts, (fx, fy)) + # Léger grain/vignette pellicule + vig = pygame.Surface((fw, fh), pygame.SRCALPHA) + vig.fill((0, 0, 0, 18)) + self.scr.blit(vig, (fx, fy)) + pygame.draw.rect(self.scr, T["prim"], (fx, fy, fw, fh), 2) + # Coche validée + ck = self.F["xs"].render("✓", True, T["prim"]) + cb = pygame.Surface((ck.get_width() + 10, ck.get_height() + 6), pygame.SRCALPHA) + cb.fill((0, 0, 0, 160)) + self.scr.blit(cb, (fx + fw - ck.get_width() - 14, fy + 4)) + self.scr.blit(ck, (fx + fw - ck.get_width() - 9, fy + 7)) + + elif i == self.num_ph and show_live: + # Live view dans le cadre courant + if self.sim: + t = time.time() + v = int(30 + 15 * math.sin(t * 2)) + sim_s = pygame.Surface((fw, fh)); sim_s.fill((0, v, 0)) + self.scr.blit(sim_s, (fx, fy)) + else: + with self.flock: + if self.frame: + lv = pygame.transform.scale(self.frame, (fw, fh)) + self.scr.blit(lv, (fx, fy)) + else: + pygame.draw.rect(self.scr, (25, 25, 25), (fx, fy, fw, fh)) + # Cadre pulsant + pulse = int(210 + 45 * math.sin(time.time() * 5)) + pygame.draw.rect(self.scr, (pulse, pulse, pulse), (fx, fy, fw, fh), 3) + # Label "EN COURS" + lbl = self.F["xxs"].render(f"▶ {i + 1}", True, (255, 255, 255)) + lb = pygame.Surface((lbl.get_width() + 12, lbl.get_height() + 6), pygame.SRCALPHA) + lb.fill((0, 0, 0, 160)) + self.scr.blit(lb, (fx + 4, fy + 5)) + self.scr.blit(lbl, (fx + 10, fy + 8)) + + else: + # Placeholder futur + pygame.draw.rect(self.scr, (12, 12, 12), (fx, fy, fw, fh)) + pygame.draw.rect(self.scr, (45, 45, 45), (fx, fy, fw, fh), 1) + lbl = self.F["md"].render(str(i + 1), True, (45, 45, 45)) + self.scr.blit(lbl, (fx + fw // 2 - lbl.get_width() // 2, fy + fh // 2 - lbl.get_height() // 2)) + + # Numéro de frame style pellicule (sous le cadre, dans la zone sprocket bas) + num_y = frame_y + fh + _SPR_GAP + 4 + if num_y + 20 < film_y + _FILM_H: + nf = self.F["xxs"].render(f"{i + 1:02d}A", True, (90, 90, 90)) + self.scr.blit(nf, (fx + fw // 2 - nf.get_width() // 2, num_y)) + def _draw_multi_strip(self): """Overlay bas : cases multipose (done/courant/futur).""" T = self.T @@ -1103,19 +1225,30 @@ class PB: lbl = self.F["lg"].render(str(i + 1), True, T["texte2"]) self.scr.blit(lbl, (bx + bw // 2 - lbl.get_width() // 2, by + bh // 2 - lbl.get_height() // 2)) + def _is_pell(self): + return self.mode == "multi" and self.cfg.get("fonctionnalites",{}).get("pellicule_mode",False) + def draw_preview(self): - self._lv() - if self.mode == "multi": + if self._is_pell(): + self._draw_pellicule(show_live=True) + elif self.mode == "multi": + self._lv() self._draw_multi_strip() else: + self._lv() bande=pygame.Surface((W,80),pygame.SRCALPHA); bande.fill((0,0,0,170)) self.scr.blit(bande,(0,H-80)) s=self.F["sm"].render("Souriez !",True,(255,255,255)) self.scr.blit(s,(W//2-s.get_width()//2,H-62)) def draw_compte(self): - self._lv(); self._overlay(110) - ch=str(self.chiffre); T=self.T + T = self.T + if self._is_pell(): + self._draw_pellicule(show_live=True) + self._overlay(90) + else: + self._lv(); self._overlay(110) + ch=str(self.chiffre) s=self.F["xxl"].render(ch,True,(255,255,255)) hr=s.get_width()//2+90 hs=pygame.Surface((hr*2,hr*2),pygame.SRCALPHA) @@ -1124,10 +1257,10 @@ class PB: pygame.draw.circle(hs,(*T["prim"],a),(hr,hr),hr-i*16) self.scr.blit(hs,(W//2-hr,H//2-hr)) self._c(s,H//2-s.get_height()//2) - if self.mode == "multi": + if self.mode == "multi" and not self._is_pell(): self._draw_multi_strip() - else: - total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4) + elif self.mode == "simple": + total=1 info=self.F["sm"].render(f"Photo {self.num_ph+1} / {total}",True,T["texte2"]) self.scr.blit(info,(W//2-info.get_width()//2,H-90)) @@ -1540,16 +1673,19 @@ class PB: def _adm_fonctions(self,btns,y): feat=self.cfg.get("fonctionnalites",{}) items=[("photo_simple","Photo simple"),("multi_shot","Pellicule"), - ("impression","Impression"), ("email","Email"), - ("qr_code","QR Code"), ("filtres","Filtres"), - ("overlays","Overlays"), ("chroma_key","Chroma key"), - ("galerie","Galerie web"), ("compteur_feat","Compteur")] + ("pellicule_mode","Mode pellicule"),("impression","Impression"), + ("email","Email"), ("qr_code","QR Code"), + ("filtres","Filtres"), ("overlays","Overlays"), + ("chroma_key","Chroma key"), ("galerie","Galerie web"), + ("compteur_feat","Compteur"), ] + # pellicule_mode désactivé par défaut (contrairement aux autres) + defaults = {"pellicule_mode": False} cw=860; gap=28; rh=64 lx=W//2-cw-gap//2; rx=W//2+gap//2 for i,(key,label) in enumerate(items): col=i%2; row=i//2 x=lx if col==0 else rx - self._tog_row(btns,key,label,feat.get(key,True),x,y+row*(rh+10),cw) + self._tog_row(btns,key,label,feat.get(key,defaults.get(key,True)),x,y+row*(rh+10),cw) def _adm_materiel(self,btns,y): T=self.T; cc=self.cfg.get("camera",{}); ci=self.cfg.get("impression",{})