#!/usr/bin/env python3 """Collage del banner: pics/ -> site/img/banner.webp, banner-movil.webp y og.jpg. Cada celda es (archivo, x, y, ancho, alto[, centro]) en píxeles de la salida. La foto se recorta para cubrir la celda (como object-fit: cover); `centro` es (cx, cy) entre 0 y 1, por defecto (0.5, 0.5). Entre celdas hay una línea negra a juego con el borde de la web. La salida no lleva metadatos. """ import sys from pathlib import Path from PIL import Image, ImageOps RAIZ = Path(__file__).resolve().parent.parent PICS = RAIZ / "pics" IMG = RAIZ / "site" / "img" NEGRO = (0, 0, 0) CALIDAD = 80 # Las cuatro fotos son apaisadas (ratios 1.02 a 1.53), así que las celdas también. # Escritorio: 2200x660 (se muestra a 1100x330). Una fila, línea de 6px (3px a 2x). ESCRITORIO = { "archivo": "banner.webp", "tamano": (2200, 660), "linea": 6, "celdas": [ ("banner4", 0, 0, 620, 660, (0.45, 0.55)), ("banner1", 620, 0, 600, 660), ("banner3", 1220, 0, 550, 660), ("banner5", 1770, 0, 430, 660, (0.5, 0.4)), ], } # Móvil: 800x640. Dos filas de 320, línea de 4px. MOVIL = { "archivo": "banner-movil.webp", "tamano": (800, 640), "linea": 4, "celdas": [ ("banner4", 0, 0, 400, 320, (0.45, 0.55)), ("banner1", 400, 0, 400, 320), ("banner5", 0, 320, 400, 320, (0.5, 0.35)), ("banner3", 400, 320, 400, 320), ], } # Para compartir en redes (og:image): 1200x630, el tamaño que esperan las plataformas. OG = { "archivo": "og.jpg", "tamano": (1200, 630), "linea": 4, "celdas": [ ("banner4", 0, 0, 600, 315, (0.45, 0.55)), ("banner1", 600, 0, 600, 315), ("banner5", 0, 315, 600, 315, (0.5, 0.35)), ("banner3", 600, 315, 600, 315), ], } SALIDAS = (ESCRITORIO, MOVIL, OG) def foto(nombre): for ext in ("png", "jpg", "jpeg", "webp"): p = PICS / f"{nombre}.{ext}" if p.exists(): im = Image.open(p) im = ImageOps.exif_transpose(im) return im.convert("RGB") sys.exit(f"falta pics/{nombre}.(png|jpg|jpeg|webp)") def compone(spec): ancho, alto = spec["tamano"] lienzo = Image.new("RGB", (ancho, alto), NEGRO) linea = spec["linea"] for nombre, x, y, w, h, *resto in spec["celdas"]: centro = resto[0] if resto else (0.5, 0.5) # Línea negra: media a cada lado de la celda, y en los bordes exteriores nada. x0 = x + (linea // 2 if x > 0 else 0) y0 = y + (linea // 2 if y > 0 else 0) x1 = x + w - (linea - linea // 2 if x + w < ancho else 0) y1 = y + h - (linea - linea // 2 if y + h < alto else 0) trozo = ImageOps.fit(foto(nombre), (x1 - x0, y1 - y0), Image.LANCZOS, centering=centro) lienzo.paste(trozo, (x0, y0)) salida = IMG / spec["archivo"] if salida.suffix == ".jpg": lienzo.save(salida, "JPEG", quality=85, optimize=True, progressive=True) else: lienzo.save(salida, "WEBP", quality=CALIDAD, method=6) return salida def limpio(ruta): im = Image.open(ruta) if im.getexif(): return False return not any(k in im.info for k in ("exif", "xmp", "icc_profile")) def main(): IMG.mkdir(parents=True, exist_ok=True) for spec in SALIDAS: salida = compone(spec) estado = "sin metadatos" if limpio(salida) else "OJO: con metadatos" print(f"{salida.relative_to(RAIZ)} {salida.stat().st_size // 1024} KB {estado}") if __name__ == "__main__": main()