Avanzando despliegue

This commit is contained in:
omgslinux 2026-09-30 02:29:58 +02:00
parent 6e1dd63d5a
commit 095025ce87
11 changed files with 167 additions and 12 deletions

110
tools/collage.py Normal file
View file

@ -0,0 +1,110 @@
#!/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()