110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
#!/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()
|