Mode pellicule : bande style film 35mm activable dans l'admin
- Nouveau toggle "Mode pellicule" dans l'onglet Fonctions de l'admin - _draw_pellicule() : bande noire cinéma avec perforations haut/bas, cadres numérotés (01A, 02A...) ; le live view est rendu dans le cadre courant, les photos prises occupent leur case, les futures sont en gris - draw_preview() / draw_compte() : utilisent _draw_pellicule() quand activé - _make_composite() : génère une bande simple (non doublée) en mode pellicule - Thumbnails chargés à _PELL_FH (748px) pour la qualité native en pellicule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,13 @@ PHOTOS_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
W, H = 1920, 1280
|
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 ───────────────────────────────────────────────────────────────────
|
# ── Thèmes ───────────────────────────────────────────────────────────────────
|
||||||
THEMES = {
|
THEMES = {
|
||||||
"base": {
|
"base": {
|
||||||
@@ -783,7 +790,9 @@ class PB:
|
|||||||
nb = 4
|
nb = 4
|
||||||
delai = self.cfg.get("camera",{}).get("compte_a_rebours",3)
|
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
|
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
|
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):
|
for i in range(nb):
|
||||||
self.num_ph=i; self.etat=S_PREV; time.sleep(1.5)
|
self.num_ph=i; self.etat=S_PREV; time.sleep(1.5)
|
||||||
@@ -793,7 +802,7 @@ class PB:
|
|||||||
if ch:
|
if ch:
|
||||||
self.photos.append(ch); self.compteur+=1; self._sav_cpt()
|
self.photos.append(ch); self.compteur+=1; self._sav_cpt()
|
||||||
try:
|
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"))
|
self._prev_thumbs.append(pygame.image.fromstring(img.tobytes(), img.size, "RGB"))
|
||||||
except:
|
except:
|
||||||
self._prev_thumbs.append(None)
|
self._prev_thumbs.append(None)
|
||||||
@@ -831,19 +840,34 @@ class PB:
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
def _make_composite(self, photos):
|
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))
|
canvas = Image.new("RGB", (W, H), (0, 0, 0))
|
||||||
|
|
||||||
if self.format in ("bande3x2", "bande4x2"):
|
if self.format in ("bande3x2", "bande4x2"):
|
||||||
nb = 3 if self.format == "bande3x2" else 4
|
nb = 3 if self.format == "bande3x2" else 4
|
||||||
pw = W // nb
|
pw = W // nb
|
||||||
sh = (H - 150) // 2
|
if is_pell:
|
||||||
for i, ch in enumerate(photos[:nb]):
|
# Bande unique centrée verticalement
|
||||||
try:
|
sh = H - 160
|
||||||
img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw, sh), Image.LANCZOS)
|
top = 80
|
||||||
canvas.paste(img, (i * pw, 5))
|
for i, ch in enumerate(photos[:nb]):
|
||||||
canvas.paste(img, (i * pw, sh + 10))
|
try:
|
||||||
except Exception as e:
|
img = ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw, sh), Image.LANCZOS)
|
||||||
print(f"composite bande: {e}")
|
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
|
else: # grille_4
|
||||||
gw = W // 2 - 15
|
gw = W // 2 - 15
|
||||||
gh = H // 2 - 75
|
gh = H // 2 - 75
|
||||||
@@ -854,6 +878,7 @@ class PB:
|
|||||||
canvas.paste(img, pos[i])
|
canvas.paste(img, pos[i])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"composite grille: {e}")
|
print(f"composite grille: {e}")
|
||||||
|
|
||||||
dest = "/tmp/photobooth_composite.jpg"
|
dest = "/tmp/photobooth_composite.jpg"
|
||||||
canvas.save(dest, "JPEG", quality=90)
|
canvas.save(dest, "JPEG", quality=90)
|
||||||
return dest
|
return dest
|
||||||
@@ -1065,6 +1090,103 @@ class PB:
|
|||||||
return btns
|
return btns
|
||||||
|
|
||||||
# ── Preview / Compte / Capture ────────────────────────────────────────────
|
# ── 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):
|
def _draw_multi_strip(self):
|
||||||
"""Overlay bas : cases multipose (done/courant/futur)."""
|
"""Overlay bas : cases multipose (done/courant/futur)."""
|
||||||
T = self.T
|
T = self.T
|
||||||
@@ -1103,19 +1225,30 @@ class PB:
|
|||||||
lbl = self.F["lg"].render(str(i + 1), True, T["texte2"])
|
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))
|
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):
|
def draw_preview(self):
|
||||||
self._lv()
|
if self._is_pell():
|
||||||
if self.mode == "multi":
|
self._draw_pellicule(show_live=True)
|
||||||
|
elif self.mode == "multi":
|
||||||
|
self._lv()
|
||||||
self._draw_multi_strip()
|
self._draw_multi_strip()
|
||||||
else:
|
else:
|
||||||
|
self._lv()
|
||||||
bande=pygame.Surface((W,80),pygame.SRCALPHA); bande.fill((0,0,0,170))
|
bande=pygame.Surface((W,80),pygame.SRCALPHA); bande.fill((0,0,0,170))
|
||||||
self.scr.blit(bande,(0,H-80))
|
self.scr.blit(bande,(0,H-80))
|
||||||
s=self.F["sm"].render("Souriez !",True,(255,255,255))
|
s=self.F["sm"].render("Souriez !",True,(255,255,255))
|
||||||
self.scr.blit(s,(W//2-s.get_width()//2,H-62))
|
self.scr.blit(s,(W//2-s.get_width()//2,H-62))
|
||||||
|
|
||||||
def draw_compte(self):
|
def draw_compte(self):
|
||||||
self._lv(); self._overlay(110)
|
T = self.T
|
||||||
ch=str(self.chiffre); 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))
|
s=self.F["xxl"].render(ch,True,(255,255,255))
|
||||||
hr=s.get_width()//2+90
|
hr=s.get_width()//2+90
|
||||||
hs=pygame.Surface((hr*2,hr*2),pygame.SRCALPHA)
|
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)
|
pygame.draw.circle(hs,(*T["prim"],a),(hr,hr),hr-i*16)
|
||||||
self.scr.blit(hs,(W//2-hr,H//2-hr))
|
self.scr.blit(hs,(W//2-hr,H//2-hr))
|
||||||
self._c(s,H//2-s.get_height()//2)
|
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()
|
self._draw_multi_strip()
|
||||||
else:
|
elif self.mode == "simple":
|
||||||
total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4)
|
total=1
|
||||||
info=self.F["sm"].render(f"Photo {self.num_ph+1} / {total}",True,T["texte2"])
|
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))
|
self.scr.blit(info,(W//2-info.get_width()//2,H-90))
|
||||||
|
|
||||||
@@ -1540,16 +1673,19 @@ class PB:
|
|||||||
def _adm_fonctions(self,btns,y):
|
def _adm_fonctions(self,btns,y):
|
||||||
feat=self.cfg.get("fonctionnalites",{})
|
feat=self.cfg.get("fonctionnalites",{})
|
||||||
items=[("photo_simple","Photo simple"),("multi_shot","Pellicule"),
|
items=[("photo_simple","Photo simple"),("multi_shot","Pellicule"),
|
||||||
("impression","Impression"), ("email","Email"),
|
("pellicule_mode","Mode pellicule"),("impression","Impression"),
|
||||||
("qr_code","QR Code"), ("filtres","Filtres"),
|
("email","Email"), ("qr_code","QR Code"),
|
||||||
("overlays","Overlays"), ("chroma_key","Chroma key"),
|
("filtres","Filtres"), ("overlays","Overlays"),
|
||||||
("galerie","Galerie web"), ("compteur_feat","Compteur")]
|
("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
|
cw=860; gap=28; rh=64
|
||||||
lx=W//2-cw-gap//2; rx=W//2+gap//2
|
lx=W//2-cw-gap//2; rx=W//2+gap//2
|
||||||
for i,(key,label) in enumerate(items):
|
for i,(key,label) in enumerate(items):
|
||||||
col=i%2; row=i//2
|
col=i%2; row=i//2
|
||||||
x=lx if col==0 else rx
|
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):
|
def _adm_materiel(self,btns,y):
|
||||||
T=self.T; cc=self.cfg.get("camera",{}); ci=self.cfg.get("impression",{})
|
T=self.T; cc=self.cfg.get("camera",{}); ci=self.cfg.get("impression",{})
|
||||||
|
|||||||
Reference in New Issue
Block a user