Files
photobooth/backend/collage.py
Jules e3d1d9cc07 Impression Mitsubishi K60 + cadres + interface admin distante
- Intégration selphy_print (backend CUPS dyesub Mitsubishi CP-K60DW-S)
- Formats impression : 15x20, 10x15, 10x15 2-strips avec cadre et orientation portrait/paysage
- Cadres d'impression par format (strip/10x15/15x20) : upload, sélection, aperçu live
- Cadres démo générés : pellicule noir/vintage/couleurs + bordures classique/doré/rose gold
- Écran de choix de cadre avant capture (clic direct, aperçu grand format)
- Filtres réduits à 3 (Couleur, N&B, Sépia) et appliqués sur tous les strips
- Port 80 via authbind, route /admin avec redirection auto depuis poste distant
- Autologin LightDM corrigé (pam-autologin-service)
- Bouton évacuer bourrage papier dans admin
- Fix : _gp_lock manquant dans Camera.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 21:38:31 +02:00

157 lines
5.7 KiB
Python

import logging
from pathlib import Path
from PIL import Image, ImageDraw
from backend.config import DOSSIER_EXPORTS, charger_config
from backend.effects import appliquer_cadre_impression
log = logging.getLogger("photobooth.collage")
# Format 10x15cm a 300dpi
LARGEUR_10x15 = 1200
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:
"""Recadre une image au centre pour correspondre au ratio cible."""
ratio_img = img.width / img.height
if ratio_img > ratio_cible:
nouvelle_largeur = int(img.height * ratio_cible)
offset = (img.width - nouvelle_largeur) // 2
return img.crop((offset, 0, offset + nouvelle_largeur, img.height))
else:
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 = 10, couleur_fond: str = "#000000",
cadre: str | None = None) -> Path:
"""Cree une bande portrait style pellicule (4 photos de gauche a droite).
Pellicule noire avec perforations, photos separees par des bandes noires.
Plus large : utilise toute la hauteur d'un 10x15 paysage divise en 2.
"""
nb = len(photos)
images = [Image.open(p) for p in photos]
# Dimensions de la bande : largeur d'un 10x15 paysage, hauteur = moitie
bande_largeur = LARGEUR_10x15_L
bande_hauteur = (HAUTEUR_10x15_L - marge) // 2
# Zone perforations pellicule (haut et bas)
perf_hauteur = 34
zone_photos_hauteur = bande_hauteur - perf_hauteur * 2
# Taille de chaque photo dans la bande (separees par bandes noires)
sep = 12 # separateur noir entre photos
espace_photos = bande_largeur - marge * 2 - sep * (nb - 1)
largeur_cellule = espace_photos // nb
hauteur_cellule = zone_photos_hauteur
ratio_cellule = largeur_cellule / hauteur_cellule
# Recadrer et redimensionner
images_redim = []
for img in images:
img_crop = _crop_center(img, ratio_cellule)
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
images_redim.append(img_redim)
# Creer la bande (fond noir = pellicule)
strip = Image.new("RGB", (bande_largeur, bande_hauteur), (20, 20, 20))
draw = ImageDraw.Draw(strip)
# Bords pellicule noirs (deja le fond)
# Perforations pellicule (haut) - trous blancs dans le bord noir
for px in range(marge + 10, bande_largeur - marge, 44):
draw.rounded_rectangle(
[px, 8, px + 22, perf_hauteur - 8],
radius=5, fill=(60, 60, 60)
)
# Perforations pellicule (bas)
for px in range(marge + 10, bande_largeur - marge, 44):
draw.rounded_rectangle(
[px, bande_hauteur - perf_hauteur + 8, px + 22, bande_hauteur - 8],
radius=5, fill=(60, 60, 60)
)
# Coller les photos (separees par des bandes noires verticales)
x = marge
for img in images_redim:
strip.paste(img, (x, perf_hauteur))
x += largeur_cellule + sep
if cadre:
strip = appliquer_cadre_impression(strip, "strip", cadre)
nom = f"strip_{photos[0].stem}.jpg"
chemin = DOSSIER_EXPORTS / nom
strip.save(chemin, "JPEG", quality=95, dpi=(300, 300))
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
def creer_collage(photos: list[Path], colonnes: int = 2, marge: int = 25,
couleur_fond: str = "#ffffff", cadre: str | None = None) -> Path:
"""Cree un collage en grille au format 10x15cm."""
nb = len(photos)
images = [Image.open(p) for p in photos]
lignes = (nb + colonnes - 1) // colonnes
largeur_cellule = (LARGEUR_10x15 - marge * (colonnes + 1)) // colonnes
hauteur_cellule = (HAUTEUR_10x15 - marge * (lignes + 1)) // lignes
ratio_cellule = largeur_cellule / hauteur_cellule
images_redim = []
for img in images:
img_crop = _crop_center(img, ratio_cellule)
img_redim = img_crop.resize((largeur_cellule, hauteur_cellule), Image.LANCZOS)
images_redim.append(img_redim)
collage = Image.new("RGB", (LARGEUR_10x15, HAUTEUR_10x15), couleur_fond)
for i, img in enumerate(images_redim):
col = i % colonnes
lig = i // colonnes
x = marge + col * (largeur_cellule + marge)
y = marge + lig * (hauteur_cellule + marge)
collage.paste(img, (x, y))
if cadre:
collage = appliquer_cadre_impression(collage, "10x15", cadre)
nom = f"collage_{photos[0].stem}.jpg"
chemin = DOSSIER_EXPORTS / nom
collage.save(chemin, "JPEG", quality=95, dpi=(300, 300))
log.info(f"Collage 10x15 cree : {chemin} ({nb} photos, {colonnes}x{lignes})")
return chemin