Strip pellicule horizontal 4 photos + impression 2 bandes 10x15 paysage
- Strip: 4 photos cote a cote style pellicule avec perforations - Impression: 2 bandes identiques sur 10x15 paysage - Apercu: montre une seule bande, imprime les 2 - Multi-shot: defaut 4 photos au lieu de 3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,52 +10,118 @@ log = logging.getLogger("photobooth.collage")
|
|||||||
# Format 10x15cm a 300dpi
|
# Format 10x15cm a 300dpi
|
||||||
LARGEUR_10x15 = 1200
|
LARGEUR_10x15 = 1200
|
||||||
HAUTEUR_10x15 = 1800
|
HAUTEUR_10x15 = 1800
|
||||||
|
# Format 10x15cm paysage
|
||||||
|
LARGEUR_10x15_L = 1800
|
||||||
|
HAUTEUR_10x15_L = 1200
|
||||||
|
|
||||||
|
|
||||||
def _crop_center(img: Image.Image, ratio_cible: float) -> Image.Image:
|
def _crop_center(img: Image.Image, ratio_cible: float) -> Image.Image:
|
||||||
"""Recadre une image au centre pour correspondre au ratio cible."""
|
"""Recadre une image au centre pour correspondre au ratio cible."""
|
||||||
ratio_img = img.width / img.height
|
ratio_img = img.width / img.height
|
||||||
if ratio_img > ratio_cible:
|
if ratio_img > ratio_cible:
|
||||||
# Trop large, couper les cotes
|
|
||||||
nouvelle_largeur = int(img.height * ratio_cible)
|
nouvelle_largeur = int(img.height * ratio_cible)
|
||||||
offset = (img.width - nouvelle_largeur) // 2
|
offset = (img.width - nouvelle_largeur) // 2
|
||||||
return img.crop((offset, 0, offset + nouvelle_largeur, img.height))
|
return img.crop((offset, 0, offset + nouvelle_largeur, img.height))
|
||||||
else:
|
else:
|
||||||
# Trop haut, couper haut/bas
|
|
||||||
nouvelle_hauteur = int(img.width / ratio_cible)
|
nouvelle_hauteur = int(img.width / ratio_cible)
|
||||||
offset = (img.height - nouvelle_hauteur) // 2
|
offset = (img.height - nouvelle_hauteur) // 2
|
||||||
return img.crop((0, offset, img.width, offset + nouvelle_hauteur))
|
return img.crop((0, offset, img.width, offset + nouvelle_hauteur))
|
||||||
|
|
||||||
|
|
||||||
def creer_strip(photos: list[Path], marge: int = 30, couleur_fond: str = "#ffffff") -> Path:
|
def creer_strip(photos: list[Path], marge: int = 20, couleur_fond: str = "#ffffff") -> Path:
|
||||||
"""Cree une bande verticale (strip) au format 10x15cm."""
|
"""Cree une bande portrait style pellicule (4 photos de gauche a droite).
|
||||||
|
|
||||||
|
La bande fait la moitie d'un 10x15 paysage (1800 x 570px environ).
|
||||||
|
Style pellicule : bords avec perforations.
|
||||||
|
"""
|
||||||
nb = len(photos)
|
nb = len(photos)
|
||||||
images = [Image.open(p) for p in photos]
|
images = [Image.open(p) for p in photos]
|
||||||
|
|
||||||
# Calculer la taille de chaque cellule dans le format 10x15
|
# Dimensions de la bande : largeur d'un 10x15 paysage, hauteur = moitie
|
||||||
espace_disponible_h = HAUTEUR_10x15 - marge * (nb + 1)
|
bande_largeur = LARGEUR_10x15_L
|
||||||
hauteur_cellule = espace_disponible_h // nb
|
bande_hauteur = (HAUTEUR_10x15_L - marge * 3) // 2 # 2 bandes par page
|
||||||
largeur_cellule = LARGEUR_10x15 - marge * 2
|
|
||||||
|
# Zone perforations pellicule (haut et bas)
|
||||||
|
perf_hauteur = 30
|
||||||
|
zone_photos_hauteur = bande_hauteur - perf_hauteur * 2
|
||||||
|
|
||||||
|
# Taille de chaque photo dans la bande
|
||||||
|
espace_photos = bande_largeur - marge * (nb + 1)
|
||||||
|
largeur_cellule = espace_photos // nb
|
||||||
|
hauteur_cellule = zone_photos_hauteur
|
||||||
ratio_cellule = largeur_cellule / hauteur_cellule
|
ratio_cellule = largeur_cellule / hauteur_cellule
|
||||||
|
|
||||||
# Recadrer et redimensionner chaque photo
|
# Recadrer et redimensionner
|
||||||
images_redim = []
|
images_redim = []
|
||||||
for img in images:
|
for img in images:
|
||||||
img_crop = _crop_center(img, ratio_cellule)
|
img_crop = _crop_center(img, ratio_cellule)
|
||||||
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
|
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
|
||||||
images_redim.append(img_redim)
|
images_redim.append(img_redim)
|
||||||
|
|
||||||
# Assembler sur le canvas 10x15
|
# Creer la bande
|
||||||
strip = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
|
strip = Image.new("RGB", (bande_largeur, bande_hauteur), couleur_fond)
|
||||||
y = marge
|
draw = ImageDraw.Draw(strip)
|
||||||
|
|
||||||
|
# Perforations pellicule (haut)
|
||||||
|
for px in range(marge, bande_largeur - marge, 40):
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
[px, 6, px + 20, perf_hauteur - 6],
|
||||||
|
radius=4, fill=(30, 30, 30)
|
||||||
|
)
|
||||||
|
# Perforations pellicule (bas)
|
||||||
|
for px in range(marge, bande_largeur - marge, 40):
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
[px, bande_hauteur - perf_hauteur + 6, px + 20, bande_hauteur - 6],
|
||||||
|
radius=4, fill=(30, 30, 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bords pellicule
|
||||||
|
draw.rectangle([0, 0, bande_largeur - 1, perf_hauteur], fill=(40, 40, 40))
|
||||||
|
draw.rectangle([0, bande_hauteur - perf_hauteur, bande_largeur - 1, bande_hauteur - 1],
|
||||||
|
fill=(40, 40, 40))
|
||||||
|
|
||||||
|
# Re-dessiner les perforations par-dessus les bords
|
||||||
|
for px in range(marge, bande_largeur - marge, 40):
|
||||||
|
draw.rounded_rectangle([px, 6, px + 20, perf_hauteur - 6], radius=4, fill=couleur_fond)
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
[px, bande_hauteur - perf_hauteur + 6, px + 20, bande_hauteur - 6],
|
||||||
|
radius=4, fill=couleur_fond
|
||||||
|
)
|
||||||
|
|
||||||
|
# Coller les photos
|
||||||
|
x = marge
|
||||||
for img in images_redim:
|
for img in images_redim:
|
||||||
strip.paste(img, (marge, y))
|
strip.paste(img, (x, perf_hauteur))
|
||||||
y += hauteur_cellule + marge
|
x += largeur_cellule + marge
|
||||||
|
|
||||||
nom = f"strip_{photos[0].stem}.jpg"
|
nom = f"strip_{photos[0].stem}.jpg"
|
||||||
chemin = DOSSIER_EXPORTS / nom
|
chemin = DOSSIER_EXPORTS / nom
|
||||||
strip.save(chemin, "JPEG", quality=95, dpi=(300, 300))
|
strip.save(chemin, "JPEG", quality=95, dpi=(300, 300))
|
||||||
log.info(f"Strip 10x15 cree : {chemin} ({nb} photos)")
|
log.info(f"Strip pellicule cree : {chemin} ({nb} photos)")
|
||||||
|
return chemin
|
||||||
|
|
||||||
|
|
||||||
|
def creer_impression_strip(chemin_strip: Path, marge: int = 20,
|
||||||
|
couleur_fond: str = "#ffffff") -> Path:
|
||||||
|
"""Cree un 10x15 paysage avec 2 bandes identiques superposees pour impression."""
|
||||||
|
strip = Image.open(chemin_strip)
|
||||||
|
|
||||||
|
# Canvas 10x15 paysage
|
||||||
|
page = Image.new("RGB", (LARGEUR_10x15_L, HAUTEUR_10x15_L), couleur_fond)
|
||||||
|
|
||||||
|
# Redimensionner la bande pour qu'elle tienne dans la moitie
|
||||||
|
bande_largeur = LARGEUR_10x15_L - marge * 2
|
||||||
|
bande_hauteur = (HAUTEUR_10x15_L - marge * 3) // 2
|
||||||
|
strip_redim = strip.resize((bande_largeur, bande_hauteur), Image.LANCZOS)
|
||||||
|
|
||||||
|
# Coller 2 fois
|
||||||
|
page.paste(strip_redim, (marge, marge))
|
||||||
|
page.paste(strip_redim, (marge, marge + bande_hauteur + marge))
|
||||||
|
|
||||||
|
nom = f"print_{chemin_strip.stem}.jpg"
|
||||||
|
chemin = DOSSIER_EXPORTS / nom
|
||||||
|
page.save(chemin, "JPEG", quality=95, dpi=(300, 300))
|
||||||
|
log.info(f"Page impression 10x15 creee : {chemin}")
|
||||||
return chemin
|
return chemin
|
||||||
|
|
||||||
|
|
||||||
@@ -67,19 +133,16 @@ def creer_collage(photos: list[Path], colonnes: int = 2, marge: int = 25,
|
|||||||
|
|
||||||
lignes = (nb + colonnes - 1) // colonnes
|
lignes = (nb + colonnes - 1) // colonnes
|
||||||
|
|
||||||
# Calculer la taille de chaque cellule
|
|
||||||
largeur_cellule = (LARGEUR_10x15 - marge * (colonnes + 1)) // colonnes
|
largeur_cellule = (LARGEUR_10x15 - marge * (colonnes + 1)) // colonnes
|
||||||
hauteur_cellule = (HAUTEUR_10x15 - marge * (lignes + 1)) // lignes
|
hauteur_cellule = (HAUTEUR_10x15 - marge * (lignes + 1)) // lignes
|
||||||
ratio_cellule = largeur_cellule / hauteur_cellule
|
ratio_cellule = largeur_cellule / hauteur_cellule
|
||||||
|
|
||||||
# Recadrer et redimensionner
|
|
||||||
images_redim = []
|
images_redim = []
|
||||||
for img in images:
|
for img in images:
|
||||||
img_crop = _crop_center(img, ratio_cellule)
|
img_crop = _crop_center(img, ratio_cellule)
|
||||||
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
|
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
|
||||||
images_redim.append(img_redim)
|
images_redim.append(img_redim)
|
||||||
|
|
||||||
# Assembler sur le canvas 10x15
|
|
||||||
collage = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
|
collage = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
|
||||||
|
|
||||||
for i, img in enumerate(images_redim):
|
for i, img in enumerate(images_redim):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from backend.config import (
|
|||||||
from backend.camera import camera
|
from backend.camera import camera
|
||||||
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
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.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES
|
||||||
from backend.collage import creer_strip, creer_collage
|
from backend.collage import creer_strip, creer_collage, creer_impression_strip
|
||||||
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur
|
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur
|
||||||
from backend.printer import lister_imprimantes, imprimer
|
from backend.printer import lister_imprimantes, imprimer
|
||||||
from backend.mailer import envoyer_photo
|
from backend.mailer import envoyer_photo
|
||||||
@@ -192,8 +192,15 @@ async def api_strip(donnees: dict):
|
|||||||
for c in chemins:
|
for c in chemins:
|
||||||
if not c.exists():
|
if not c.exists():
|
||||||
return JSONResponse({"erreur": f"Photo introuvable : {c.name}"}, status_code=404)
|
return JSONResponse({"erreur": f"Photo introuvable : {c.name}"}, status_code=404)
|
||||||
chemin = creer_strip(chemins)
|
chemin_strip = creer_strip(chemins)
|
||||||
return {"nom": chemin.name, "chemin": f"/data/exports/{chemin.name}"}
|
# Creer aussi la page d'impression (2 bandes sur 10x15 paysage)
|
||||||
|
chemin_print = creer_impression_strip(chemin_strip)
|
||||||
|
return {
|
||||||
|
"nom": chemin_strip.name,
|
||||||
|
"chemin": f"/data/exports/{chemin_strip.name}",
|
||||||
|
"impression": chemin_print.name,
|
||||||
|
"chemin_impression": f"/data/exports/{chemin_print.name}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/collage")
|
@app.post("/api/collage")
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async function lancerCapture() {
|
|||||||
|
|
||||||
function getNombrePhotos() {
|
function getNombrePhotos() {
|
||||||
if (modeActuel === 'simple') return 1;
|
if (modeActuel === 'simple') return 1;
|
||||||
if (modeActuel === 'multi') return config.multi_shot?.nombre_photos || 3;
|
if (modeActuel === 'multi') return config.multi_shot?.nombre_photos || 4;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,17 +172,24 @@ function afficherFlash() {
|
|||||||
setTimeout(() => flash.classList.add('cache'), 400);
|
setTimeout(() => flash.classList.add('cache'), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let photoImpression = null; // Nom du fichier d'impression (2 bandes sur 10x15)
|
||||||
|
|
||||||
async function traiterCapture() {
|
async function traiterCapture() {
|
||||||
if (photosSession.length === 0) {
|
if (photosSession.length === 0) {
|
||||||
allerA('accueil');
|
allerA('accueil');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
photoImpression = null;
|
||||||
|
|
||||||
if (modeActuel === 'multi') {
|
if (modeActuel === 'multi') {
|
||||||
const mode = config.multi_shot?.mode || 'strip';
|
const mode = config.multi_shot?.mode || 'strip';
|
||||||
let resultat;
|
let resultat;
|
||||||
if (mode === 'strip') {
|
if (mode === 'strip') {
|
||||||
resultat = await apiPost('/api/strip', { photos: photosSession });
|
resultat = await apiPost('/api/strip', { photos: photosSession });
|
||||||
|
if (resultat.impression) {
|
||||||
|
photoImpression = resultat.impression;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resultat = await apiPost('/api/collage', { photos: photosSession });
|
resultat = await apiPost('/api/collage', { photos: photosSession });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ function changerExemplaires(delta) {
|
|||||||
async function lancerImpression() {
|
async function lancerImpression() {
|
||||||
if (!photoFinale) return;
|
if (!photoFinale) return;
|
||||||
fermerImpression();
|
fermerImpression();
|
||||||
|
// Pour les strips, imprimer la version 2 bandes sur 10x15
|
||||||
|
const fichierImpression = photoImpression || photoFinale;
|
||||||
afficherStatut(`Impression de ${nbExemplaires} exemplaire(s)...`, 'succes');
|
afficherStatut(`Impression de ${nbExemplaires} exemplaire(s)...`, 'succes');
|
||||||
const resultat = await apiPost('/api/imprimer', { photo: photoFinale, copies: nbExemplaires });
|
const resultat = await apiPost('/api/imprimer', { photo: fichierImpression, copies: nbExemplaires });
|
||||||
if (resultat.succes) {
|
if (resultat.succes) {
|
||||||
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
|
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user