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: orientation: str = "paysage", event_id: str | None = None) -> Path:
""" """
Pivote si nécessaire, recadre aux dimensions exactes (cover crop), 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: try:
from PIL import Image, ImageOps from PIL import Image, ImageOps
@@ -91,12 +91,10 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
return chemin return chemin
img = Image.open(chemin).convert("RGB") 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: if orientation == "portrait" and img.width > img.height:
img = img.rotate(90, expand=True) 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: elif orientation == "paysage" and img.height > img.width:
img = img.rotate(-90, expand=True) img = img.rotate(-90, expand=True)
@@ -105,12 +103,52 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
if cadre and format_papier: if cadre and format_papier:
img = appliquer_cadre_impression(img, format_papier, cadre, event_id=event_id) 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) tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300)) img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300))
log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}") log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}")
return Path(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: def _dupliquer_page(chemin: Path, taille_page: tuple[int, int]) -> Path:
"""Colle la bande 2 fois cote a cote pour remplir la feuille entiere. """Colle la bande 2 fois cote a cote pour remplir la feuille entiere.