Supprimer GIF, multi-shot en format 10x15cm

- Suppression du mode GIF (gif_maker.py, routes, UI)
- Multi-shot (strip/collage) produit maintenant des images 10x15cm
  (1200x1800px @ 300dpi) pret pour impression sublimation
- Recadrage intelligent (crop centre) pour remplir les cellules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 22:55:49 +01:00
parent c8e4ec80c1
commit 3b3271a32f
9 changed files with 56 additions and 106 deletions

View File

@@ -7,55 +7,80 @@ from backend.config import DOSSIER_EXPORTS, charger_config
log = logging.getLogger("photobooth.collage")
# Format 10x15cm a 300dpi
LARGEUR_10x15 = 1200
HAUTEUR_10x15 = 1800
def creer_strip(photos: list[Path], marge: int = 20, couleur_fond: str = "#ffffff") -> Path:
"""Cree une bande verticale (strip) avec les photos."""
def _crop_center(img: Image.Image, ratio_cible: float) -> Image.Image:
"""Recadre une image au centre pour correspondre au ratio cible."""
ratio_img = img.width / img.height
if ratio_img > ratio_cible:
# Trop large, couper les cotes
nouvelle_largeur = int(img.height * ratio_cible)
offset = (img.width - nouvelle_largeur) // 2
return img.crop((offset, 0, offset + nouvelle_largeur, img.height))
else:
# Trop haut, couper haut/bas
nouvelle_hauteur = int(img.width / ratio_cible)
offset = (img.height - nouvelle_hauteur) // 2
return img.crop((0, offset, img.width, offset + nouvelle_hauteur))
def creer_strip(photos: list[Path], marge: int = 30, couleur_fond: str = "#ffffff") -> Path:
"""Cree une bande verticale (strip) au format 10x15cm."""
nb = len(photos)
images = [Image.open(p) for p in photos]
# Uniformiser la largeur
largeur = min(img.width for img in images)
# Calculer la taille de chaque cellule dans le format 10x15
espace_disponible_h = HAUTEUR_10x15 - marge * (nb + 1)
hauteur_cellule = espace_disponible_h // nb
largeur_cellule = LARGEUR_10x15 - marge * 2
ratio_cellule = largeur_cellule / hauteur_cellule
# Recadrer et redimensionner chaque photo
images_redim = []
for img in images:
ratio = largeur / img.width
hauteur = int(img.height * ratio)
images_redim.append(img.resize((largeur, hauteur), Image.LANCZOS))
img_crop = _crop_center(img, ratio_cellule)
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
images_redim.append(img_redim)
hauteur_totale = sum(img.height for img in images_redim) + marge * (len(images_redim) + 1)
largeur_totale = largeur + marge * 2
strip = Image.new("RGB", (largeur_totale, hauteur_totale), couleur_fond)
# Assembler sur le canvas 10x15
strip = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
y = marge
for img in images_redim:
strip.paste(img, (marge, y))
y += img.height + marge
y += hauteur_cellule + marge
nom = f"strip_{photos[0].stem}.jpg"
chemin = DOSSIER_EXPORTS / nom
strip.save(chemin, "JPEG", quality=95)
log.info(f"Strip cree : {chemin}")
strip.save(chemin, "JPEG", quality=95, dpi=(300, 300))
log.info(f"Strip 10x15 cree : {chemin} ({nb} photos)")
return chemin
def creer_collage(photos: list[Path], colonnes: int = 2, marge: int = 15,
def creer_collage(photos: list[Path], colonnes: int = 2, marge: int = 25,
couleur_fond: str = "#ffffff") -> Path:
"""Cree un collage en grille avec les photos."""
"""Cree un collage en grille au format 10x15cm."""
nb = len(photos)
images = [Image.open(p) for p in photos]
# Taille uniforme pour chaque cellule
largeur_cellule = min(img.width for img in images)
hauteur_cellule = min(img.height for img in images)
lignes = (nb + colonnes - 1) // colonnes
# Calculer la taille de chaque cellule
largeur_cellule = (LARGEUR_10x15 - marge * (colonnes + 1)) // colonnes
hauteur_cellule = (HAUTEUR_10x15 - marge * (lignes + 1)) // lignes
ratio_cellule = largeur_cellule / hauteur_cellule
# Recadrer et redimensionner
images_redim = []
for img in images:
img_crop = ImageDraw.Draw(img)
img_redim = img.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
img_crop = _crop_center(img, ratio_cellule)
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
images_redim.append(img_redim)
lignes = (len(images_redim) + colonnes - 1) // colonnes
largeur_totale = colonnes * largeur_cellule + (colonnes + 1) * marge
hauteur_totale = lignes * hauteur_cellule + (lignes + 1) * marge
collage = Image.new("RGB", (largeur_totale, hauteur_totale), couleur_fond)
# Assembler sur le canvas 10x15
collage = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
for i, img in enumerate(images_redim):
col = i % colonnes
@@ -66,6 +91,6 @@ def creer_collage(photos: list[Path], colonnes: int = 2, marge: int = 15,
nom = f"collage_{photos[0].stem}.jpg"
chemin = DOSSIER_EXPORTS / nom
collage.save(chemin, "JPEG", quality=95)
log.info(f"Collage cree : {chemin}")
collage.save(chemin, "JPEG", quality=95, dpi=(300, 300))
log.info(f"Collage 10x15 cree : {chemin} ({nb} photos, {colonnes}x{lignes})")
return chemin

View File

@@ -1,41 +0,0 @@
import logging
from pathlib import Path
from PIL import Image
from backend.config import DOSSIER_EXPORTS, charger_config
log = logging.getLogger("photobooth.gif")
def creer_gif(photos: list[Path], duree_frame_ms: int | None = None) -> Path:
"""Cree un GIF anime a partir d'une liste de photos."""
config = charger_config()
if duree_frame_ms is None:
duree_frame_ms = config.get("gif", {}).get("duree_frame_ms", 300)
images = []
for p in photos:
img = Image.open(p).convert("RGB")
# Redimensionner pour un GIF leger
img.thumbnail((800, 600), Image.LANCZOS)
images.append(img)
if not images:
log.error("Aucune image pour le GIF")
return None
nom = f"gif_{photos[0].stem}.gif"
chemin = DOSSIER_EXPORTS / nom
images[0].save(
chemin,
save_all=True,
append_images=images[1:],
duration=duree_frame_ms,
loop=0,
optimize=True,
)
log.info(f"GIF cree : {chemin} ({len(images)} frames, {duree_frame_ms}ms)")
return chemin

View File

@@ -17,7 +17,7 @@ from backend.camera import camera
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES
from backend.collage import creer_strip, creer_collage
from backend.gif_maker import creer_gif
from backend.printer import lister_imprimantes, imprimer
from backend.mailer import envoyer_photo
from backend.qrcode_gen import generer_qr, qr_galerie
@@ -163,7 +163,7 @@ async def api_fonds():
return lister_fonds()
# --- API Collage / GIF ---
# --- API Collage ---
@app.post("/api/strip")
async def api_strip(donnees: dict):
@@ -188,18 +188,6 @@ async def api_collage(donnees: dict):
return {"nom": chemin.name, "chemin": f"/data/exports/{chemin.name}"}
@app.post("/api/gif")
async def api_gif(donnees: dict):
noms = donnees.get("photos", [])
chemins = [DOSSIER_PHOTOS / n for n in noms]
for c in chemins:
if not c.exists():
return JSONResponse({"erreur": f"Photo introuvable : {c.name}"}, status_code=404)
chemin = creer_gif(chemins)
if chemin is None:
return JSONResponse({"erreur": "Echec creation GIF"}, status_code=500)
return {"nom": chemin.name, "chemin": f"/data/exports/{chemin.name}"}
# --- API Impression ---

View File

@@ -2,7 +2,6 @@ fastapi==0.115.0
uvicorn[standard]==0.30.0
python-gphoto2==2.5.0
Pillow==10.4.0
imageio==2.35.0
opencv-python-headless==4.10.0.84
qrcode[pil]==7.4.2
pycups==2.0.4