Calibration impression : offsets coupe (mm) + rotation 180° configurable

- impression.offsets_coupe_mm : {haut, bas, gauche, droite} compense la coupe asymétrique
- impression.rotation_180 : retourne l'image si le driver inverse le sens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:29:40 +02:00
parent e83d5c8d58
commit 182042350d

View File

@@ -82,7 +82,7 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
orientation: str = "paysage", event_id: str | None = None) -> Path:
"""
Pivote si nécessaire, recadre aux dimensions exactes (cover crop),
applique le cadre, sauvegarde en temp.
applique le cadre, compense les marges de coupe, sauvegarde en temp.
"""
try:
from PIL import Image, ImageOps
@@ -91,12 +91,10 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
return chemin
img = Image.open(chemin).convert("RGB")
img = ImageOps.exif_transpose(img) # respecte l'orientation EXIF
img = ImageOps.exif_transpose(img)
# Portrait demandé mais DSLR paysage → rotation 90°
if orientation == "portrait" and img.width > img.height:
img = img.rotate(90, expand=True)
# Paysage demandé mais image portrait (ex. prise avec DSLR tourné) → rotation -90°
elif orientation == "paysage" and img.height > img.width:
img = img.rotate(-90, expand=True)
@@ -105,12 +103,52 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
if cadre and format_papier:
img = appliquer_cadre_impression(img, format_papier, cadre, event_id=event_id)
img = _appliquer_offsets_coupe(img)
config_imp = charger_config().get("impression", {})
if config_imp.get("rotation_180", False):
img = img.rotate(180)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300))
log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}")
return Path(tmp.name)
def _appliquer_offsets_coupe(img):
"""Décale le contenu de l'image pour compenser les marges de coupe asymétriques.
Config impression.offsets_coupe_mm : {haut, bas, gauche, droite} en mm."""
from PIL import Image
config = charger_config()
offsets = config.get("impression", {}).get("offsets_coupe_mm", {})
if not offsets:
return img
DPI = 300
mm_px = DPI / 25.4
haut = int(offsets.get("haut", 0) * mm_px)
bas = int(offsets.get("bas", 0) * mm_px)
gauche = int(offsets.get("gauche", 0) * mm_px)
droite = int(offsets.get("droite", 0) * mm_px)
if haut == 0 and bas == 0 and gauche == 0 and droite == 0:
return img
w, h = img.size
nouvelle = Image.new("RGB", (w, h), (255, 255, 255))
# Décale : offset positif = l'imprimante coupe trop de ce côté, on décale le contenu vers l'opposé
# crop la source pour que le contenu tienne malgré le décalage
src_x = max(0, droite - gauche)
src_y = max(0, bas - haut)
dst_x = max(0, gauche - droite)
dst_y = max(0, haut - bas)
crop_w = w - abs(gauche - droite)
crop_h = h - abs(haut - bas)
region = img.crop((src_x, src_y, src_x + crop_w, src_y + crop_h))
nouvelle.paste(region, (dst_x, dst_y))
return nouvelle
def _dupliquer_page(chemin: Path, taille_page: tuple[int, int]) -> Path:
"""Colle la bande 2 fois cote a cote pour remplir la feuille entiere.