Multipose: composite upload + strip preview layout

- _make_composite(): crée une image bande (2 rangées) ou grille 2x2 à partir
  des photos prises, sauvée en /tmp/photobooth_composite.jpg
- _upload_booth(): upload le composite unique en mode multi au lieu des photos
  individuelles
- _draw_multi_strip(): overlay bas avec les cases numérotées — thumbnails pour
  les prises faites, cadre pulsant pour la case courante, placeholder gris pour
  les suivantes
- draw_preview() / draw_compte(): utilisent _draw_multi_strip() en mode multi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 17:08:17 +02:00
parent 8ae42e1bc6
commit 1c1f0323cc

View File

@@ -543,6 +543,7 @@ class PB:
self.cap_result = None
self.cap_done = threading.Event()
self.photos = []
self._prev_thumbs = [] # miniatures pour le strip multipose
self.num_ph = 0
self.chiffre = 0
self.photo_active = None
@@ -781,14 +782,21 @@ class PB:
else: # grille_4 et bande4x2
nb = 4
delai = self.cfg.get("camera",{}).get("compte_a_rebours",3)
self.photos=[]; 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
strip_bw = W // nb - 4
for i in range(nb):
self.num_ph=i; self.etat=S_PREV; time.sleep(1.5)
self.etat=S_COMPTE
for c in range(delai,0,-1): self.chiffre=c; time.sleep(1)
self.etat=S_CAP; ch=self._capturer()
if ch: self.photos.append(ch); self.compteur+=1; self._sav_cpt()
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)
self._prev_thumbs.append(pygame.image.fromstring(img.tobytes(), img.size, "RGB"))
except:
self._prev_thumbs.append(None)
time.sleep(0.4)
if not self.photos: self.etat=S_ACCUEIL; return
self._upload_booth_bg(self.photos) # upload + QR session en arrière-plan
@@ -822,15 +830,53 @@ class PB:
c=cfg_load(); c.setdefault("compteur",{})["photos_prises"]=self.compteur; cfg_save(c)
except: pass
def _make_composite(self, photos):
"""Crée une image composite (bande ou grille) à partir des photos."""
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}")
else: # grille_4
gw = W // 2 - 15
gh = H // 2 - 75
pos = [(5, 5), (W // 2 + 5, 5), (5, H // 2 - 65), (W // 2 + 5, H // 2 - 65)]
for i, ch in enumerate(photos[:4]):
try:
img = Image.open(ch).convert("RGB").resize((gw, gh), Image.LANCZOS)
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
def _upload_booth(self, photos):
"""Upload photos vers photobooth.copydev.fr/api/sessions/{code}/upload."""
"""Upload vers photobooth.copydev.fr/api/sessions/{code}/upload.
En mode multi, upload le composite ; en mode simple, upload la photo."""
b = self.cfg.get("booth", {})
url = b.get("url", "https://photobooth.copydev.fr").rstrip("/")
code = b.get("session_code", "")
if not code: return None
# Mode multi → composite unique
if self.mode == "multi" and len(photos) > 1:
try:
to_upload = [self._make_composite(photos)]
except Exception as e:
print(f"composite échoué, upload individuel: {e}")
to_upload = photos
else:
to_upload = photos
from urllib.request import Request, urlopen
ok = 0
for ch in photos:
for ch in to_upload:
try:
boundary = "----PBUpload"
with open(ch, "rb") as f: data = f.read()
@@ -1019,14 +1065,53 @@ class PB:
return btns
# ── Preview / Compte / Capture ────────────────────────────────────────────
def _draw_multi_strip(self):
"""Overlay bas : cases multipose (done/courant/futur)."""
T = self.T
nb = 3 if self.format == "bande3x2" else 4
STRIP_H = 200
STRIP_Y = H - STRIP_H - 10
BW = W // nb
# Fond semi-transparent
bg = pygame.Surface((W, STRIP_H + 20), pygame.SRCALPHA)
bg.fill((0, 0, 0, 160))
self.scr.blit(bg, (0, STRIP_Y - 10))
for i in range(nb):
bx = i * BW + 2
by = STRIP_Y
bw = BW - 4
bh = STRIP_H
if i < len(self._prev_thumbs):
th = self._prev_thumbs[i]
if th:
ts = th if th.get_size() == (bw, bh) else pygame.transform.scale(th, (bw, bh))
self.scr.blit(ts, (bx, by))
pygame.draw.rect(self.scr, T["prim"], (bx, by, bw, bh), 2)
tick = self.F["xs"].render("✓", True, T["prim"])
self.scr.blit(tick, (bx + bw - tick.get_width() - 6, by + 4))
elif i == self.num_ph:
# Case courante : cadre pulsant blanc
pulse = int(200 + 55 * math.sin(time.time() * 5))
pygame.draw.rect(self.scr, (pulse, pulse, pulse), (bx, by, bw, bh), 3)
lbl = self.F["xs"].render(f"▶ {i + 1}", True, (255, 255, 255))
self.scr.blit(lbl, (bx + 8, by + 6))
else:
ph = pygame.Surface((bw, bh), pygame.SRCALPHA)
ph.fill((30, 30, 30, 140))
self.scr.blit(ph, (bx, by))
pygame.draw.rect(self.scr, T["texte2"], (bx, by, bw, bh), 1)
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 draw_preview(self):
self._lv()
bande=pygame.Surface((W,80),pygame.SRCALPHA); bande.fill((0,0,0,170))
self.scr.blit(bande,(0,H-80))
nb=len(self.photos)+1
total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4)
s=self.F["sm"].render(f"Photo {nb} / {total} — Souriez !",True,(255,255,255))
self.scr.blit(s,(W//2-s.get_width()//2,H-62))
if self.mode == "multi":
self._draw_multi_strip()
else:
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)
@@ -1039,9 +1124,12 @@ 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)
total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4)
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))
if self.mode == "multi":
self._draw_multi_strip()
else:
total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4)
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))
def draw_capture(self):
self._lv(); self._overlay(90)