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.
169 lines
6.6 KiB
Python
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
|