Files
photobooth/backend/collage.py
Jules f49d5511ef Pellicule : marge de coupe plus large + warmup LiveView apres capture
La coupe physique du K60 n'est jamais parfaitement centree, donc une marge
noire trop fine fait perdre la pellicule d'un cote du strip dedouble. Marge
de coupe (perforations) elargie de 34 a 55px (~4.6mm) de chaque cote.

Ajoute aussi un warmup LiveView (_chauffer_liveview) juste apres la remontee
du miroir post-capture : consomme les ~1-2s de stabilisation pendant le
spinner de traitement plutot que de laisser le preview du countdown suivant
figé sur l'ancienne photo.
2026-06-21 23:47:06 +02:00

169 lines
6.6 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 (photos empilees de haut en bas).
Pellicule noire avec perforations sur les cotes, photos separees par des
bandes noires. Taille fixe 600x1800px (5x15cm a 300dpi) : c'est le format
attendu par l'imprimante en mode "10x15-2up" (PageSize ...-div2), qui
duplique et decoupe automatiquement la bande en 2 exemplaires sur une
feuille 10x15. Doit rester strictement portrait (largeur < hauteur) et a
cette taille exacte pour eviter toute rotation/etirement a l'impression.
"""
nb = len(photos)
images = [Image.open(p) for p in photos]
# Dimensions de la bande : format fixe attendu par printer.py (FORMATS["10x15-2up"])
bande_largeur = 600
bande_hauteur = 1800
# Zone perforations pellicule (gauche et droite). Cette zone noire est aussi
# la marge de securite pour la coupe physique du K60 : la feuille pleine
# (2 bandes cote a cote) est coupee au centre apres impression, et la coupe
# n'est jamais parfaitement precise. Une marge trop fine fait que la coupe
# tombe dans la photo d'un cote (pellicule absente d'un cote, visible de
# l'autre). Elargie a 55px (~4.6mm) pour absorber le jeu mecanique du cutter.
perf_largeur = 55
zone_photos_largeur = bande_largeur - perf_largeur * 2
# Taille de chaque photo dans la bande (separees par bandes noires)
sep = 12 # separateur noir entre photos
espace_photos = bande_hauteur - marge * 2 - sep * (nb - 1)
hauteur_cellule = espace_photos // nb
largeur_cellule = zone_photos_largeur
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 (gauche) - trous gris dans le bord noir, centrees
# dans la marge (laisse du noir plein des 2 cotes pour la marge de coupe)
trou_largeur = 26
trou_x = (perf_largeur - trou_largeur) // 2
for py in range(marge + 10, bande_hauteur - marge, 44):
draw.rounded_rectangle(
[trou_x, py, trou_x + trou_largeur, py + 22],
radius=5, fill=(60, 60, 60)
)
# Perforations pellicule (droite)
for py in range(marge + 10, bande_hauteur - marge, 44):
draw.rounded_rectangle(
[bande_largeur - perf_largeur + trou_x, py, bande_largeur - perf_largeur + trou_x + trou_largeur, py + 22],
radius=5, fill=(60, 60, 60)
)
# Coller les photos (separees par des bandes noires horizontales)
y = marge
for img in images_redim:
strip.paste(img, (perf_largeur, y))
y += hauteur_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