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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"fonctionnalites": {
|
||||
"photo_simple": true,
|
||||
"multi_shot": true,
|
||||
"gif": true,
|
||||
"filtres": true,
|
||||
"overlays": true,
|
||||
"chroma_key": false,
|
||||
@@ -46,11 +45,6 @@
|
||||
"mode": "strip",
|
||||
"delai_entre_photos": 2
|
||||
},
|
||||
"gif": {
|
||||
"nombre_frames": 4,
|
||||
"delai_entre_frames": 0.8,
|
||||
"duree_frame_ms": 300
|
||||
},
|
||||
"chroma_key": {
|
||||
"couleur": "#00ff00",
|
||||
"tolerance": 40,
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
<div class="mode-icone">🎴</div>
|
||||
<span>Multi-shot</span>
|
||||
</button>
|
||||
<button class="btn-mode" data-mode="gif" id="btn-gif">
|
||||
<div class="mode-icone">🎦</div>
|
||||
<span>GIF</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn-retour" onclick="allerA('accueil')">Retour</button>
|
||||
</section>
|
||||
@@ -143,7 +139,6 @@
|
||||
<div class="toggle-groupe">
|
||||
<label class="toggle"><input type="checkbox" id="tog-photo-simple" checked><span class="toggle-slider"></span> Photo simple</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-multi-shot"><span class="toggle-slider"></span> Multi-shot</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-gif"><span class="toggle-slider"></span> GIF anime</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-filtres"><span class="toggle-slider"></span> Filtres</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-overlays"><span class="toggle-slider"></span> Overlays / Cadres</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-chroma-key"><span class="toggle-slider"></span> Chroma key (fond vert)</label>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
const TOGGLES_MAP = {
|
||||
'tog-photo-simple': 'photo_simple',
|
||||
'tog-multi-shot': 'multi_shot',
|
||||
'tog-gif': 'gif',
|
||||
'tog-filtres': 'filtres',
|
||||
'tog-overlays': 'overlays',
|
||||
'tog-chroma-key': 'chroma_key',
|
||||
|
||||
@@ -30,7 +30,6 @@ function appliquerConfig() {
|
||||
|
||||
// Visibilite des modes
|
||||
toggleVisible('btn-multi', fonc.multi_shot);
|
||||
toggleVisible('btn-gif', fonc.gif);
|
||||
toggleVisible('btn-imprimer', fonc.impression);
|
||||
toggleVisible('btn-email', fonc.email);
|
||||
toggleVisible('btn-qr', fonc.qr_code);
|
||||
|
||||
@@ -71,7 +71,6 @@ async function lancerCapture() {
|
||||
function getNombrePhotos() {
|
||||
if (modeActuel === 'simple') return 1;
|
||||
if (modeActuel === 'multi') return config.multi_shot?.nombre_photos || 3;
|
||||
if (modeActuel === 'gif') return config.gif?.nombre_frames || 4;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -109,14 +108,7 @@ async function traiterCapture() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeActuel === 'gif') {
|
||||
// Creer le GIF
|
||||
const resultat = await apiPost('/api/gif', { photos: photosSession });
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
afficherPreviewPhoto('/data/exports/' + resultat.nom);
|
||||
}
|
||||
} else if (modeActuel === 'multi') {
|
||||
if (modeActuel === 'multi') {
|
||||
// Creer le strip/collage
|
||||
const mode = config.multi_shot?.mode || 'strip';
|
||||
let resultat;
|
||||
|
||||
Reference in New Issue
Block a user