Estandarizar tamaño de imagenes WooCommerce con Python
1) Introducción
Un problema frecuente en tiendas online es que el tamaño de imagen en WooCommerce se vea inconsistente: algunos productos aparecen más grandes y otros más chicos en el mismo grid, aunque en teoría todas las imágenes tengan el mismo tamaño en píxeles.
La causa real casi nunca es WooCommerce. El problema suele estar en el archivo de imagen: distinto encuadre, distinto espacio en blanco alrededor del producto, diferencias de inclinación y fondos que cambian la percepción del tamaño.
La solución correcta es estandarizar las imágenes de producto con un criterio fijo, de forma masiva y controlada.
Nota: Regenerar miniaturas ayuda cuando cambias tamaños o recortes del tema, pero no corrige la escala visual interna del producto.
2) Objetivo y estándar visual del catálogo
Para corregir de verdad el tamaño de imagen en WooCommerce, define un estándar visual único para todo el catálogo:
- Tamaño final: 1200x1500 (relación 4:5).
- Fondo: blanco puro #FFFFFF.
- Centrado: vertical y horizontal, con márgenes equilibrados.
- Escala visual: el producto ocupa 86% a 88% del alto total.
- Sombra: una sombra de contacto sutil y natural debajo del producto.
Nota: Elige 1 producto como referencia y replica ese criterio para todo el catálogo.
3) Por qué no editar todo wp-content/uploads/
La carpeta uploads contiene todo el sitio, no solo productos: imágenes de posts, banners, sliders, logos, miniaturas y otros adjuntos. Editar todo masivamente es riesgoso.
Nota: Trabaja solo con imágenes asociadas a productos WooCommerce (imagen destacada y galería).
4) Extraer solo imágenes de productos desde la base de datos
4.1) Detectar el prefijo de tablas
En tu caso, el prefijo de las tablas es wp_, por eso se usa wp_posts y wp_postmeta.
4.2) Consulta SQL para obtener URL de la imagen destacada
Esta consulta devuelve product_id, image_id e image_url:
SELECT
p.ID AS product_id,
img.ID AS image_id,
img.guid AS image_url
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
JOIN wp_posts img ON pm.meta_value = img.ID
WHERE p.post_type = 'product'
AND pm.meta_key = '_thumbnail_id';Nota: Exporta el resultado a CSV desde phpMyAdmin y guárdalo como imagenes_productos.csv.
5) Preparar el proyecto en Windows
Crea una carpeta estable (no Descargas). Por ejemplo:
Documentos/Proyectos/woocommerce_image_standardizer/Estructura recomendada:
- procesar_desde_csv.py
- imagenes_productos.csv
- descargas/
- procesadas/
Nota: Evitar Descargas reduce errores de rutas, desorden y facilita backups.
6) Automatizar con Python: descargar y procesar en lote
El flujo automático hace lo siguiente:
- Lee el CSV con las URLs.
- Descarga cada imagen.
- Quita el fondo con rembg.
- Crea canvas 1200x1500 con fondo blanco.
- Escala por altura (86% a 88%) para uniformidad visual.
- Centra el producto.
- Exporta JPG.
Nota: Prueba primero con 5 imágenes antes de procesar todo el catálogo.
Código del script (descarga + proceso masivo a 1200x1500 JPG)
import csv
from io import BytesIO
from pathlib import Path
import requests
from PIL import Image, ImageFilter
from rembg import remove
CSV_FILE = Path("imagenes_productos.csv")
DOWNLOAD_DIR = Path("descargas")
OUTPUT_DIR = Path("procesadas")
TARGET_W = 1200
TARGET_H = 1500
FILL_RATIO = 0.87
TARGET_PRODUCT_H = int(TARGET_H * FILL_RATIO)
BACKGROUND_RGB = (255, 255, 255)
SHADOW_OPACITY = 45
SHADOW_BLUR = 18
SHADOW_Y_OFFSET = 10
JPG_QUALITY = 95
REQUEST_TIMEOUT = 40
def get_alpha_bbox(rgba: Image.Image):
alpha = rgba.split()[-1]
return alpha.getbbox()
def add_contact_shadow(canvas_rgba: Image.Image, bbox):
x1, y1, x2, y2 = bbox
pw = max(1, x2 - x1)
ph = max(1, y2 - y1)
shadow_w = int(pw * 0.70)
shadow_h = max(10, int(ph * 0.10))
shadow = Image.new("RGBA", (shadow_w, shadow_h), (0, 0, 0, 0))
mask = Image.new("L", (shadow_w, shadow_h), 0)
from PIL import ImageDraw
d = ImageDraw.Draw(mask)
d.ellipse((0, 0, shadow_w, shadow_h), fill=SHADOW_OPACITY)
shadow.putalpha(mask)
shadow = shadow.filter(ImageFilter.GaussianBlur(SHADOW_BLUR))
sx = x1 + (pw - shadow_w) // 2
sy = y2 - shadow_h // 2 + SHADOW_Y_OFFSET
layer = Image.new("RGBA", canvas_rgba.size, (0, 0, 0, 0))
layer.paste(shadow, (sx, sy), shadow)
return Image.alpha_composite(canvas_rgba, layer)
def safe_filename_from_url(url: str) -> str:
name = url.split("?")[0].split("/")[-1].strip()
if not name:
name = "imagen.jpg"
if not name.lower().endswith((".jpg", ".jpeg")):
name = Path(name).with_suffix(".jpg").name
return name
def download_image(url: str) -> bytes:
r = requests.get(url, timeout=REQUEST_TIMEOUT)
r.raise_for_status()
return r.content
def process_image_to_standard(image_bytes: bytes) -> Image.Image:
img = Image.open(BytesIO(image_bytes)).convert("RGBA")
cut = remove(img)
if isinstance(cut, (bytes, bytearray)):
cut = Image.open(BytesIO(cut)).convert("RGBA")
else:
cut = cut.convert("RGBA")
bbox = get_alpha_bbox(cut)
if not bbox:
base = Image.new("RGB", (TARGET_W, TARGET_H), BACKGROUND_RGB)
temp = img.convert("RGB").resize((TARGET_W, TARGET_H), Image.LANCZOS)
base.paste(temp, (0, 0))
return base
product = cut.crop(bbox)
pw, ph = product.size
scale = TARGET_PRODUCT_H / max(1, ph)
new_w = max(1, int(pw * scale))
new_h = max(1, int(ph * scale))
product_resized = product.resize((new_w, new_h), Image.LANCZOS)
canvas = Image.new("RGBA", (TARGET_W, TARGET_H), (*BACKGROUND_RGB, 255))
px = (TARGET_W - new_w) // 2
py = (TARGET_H - new_h) // 2
canvas.paste(product_resized, (px, py), product_resized)
canvas = add_contact_shadow(canvas, (px, py, px + new_w, py + new_h))
return canvas.convert("RGB")
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
if not CSV_FILE.exists():
raise FileNotFoundError(f"No se encontró el CSV: {CSV_FILE.resolve()}")
with open(CSV_FILE, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
if not reader.fieldnames:
raise ValueError("El CSV no tiene encabezados.")
if "image_url" not in reader.fieldnames:
raise ValueError(
"El CSV debe tener una columna llamada image_url. "
f"Encabezados encontrados: {reader.fieldnames}"
)
rows = list(reader)
if not rows:
print("El CSV está vacío. No hay imágenes para procesar.")
return
print(f"Imágenes a procesar: {len(rows)}")
ok = 0
fail = 0
for i, row in enumerate(rows, start=1):
url = (row.get("image_url") or "").strip()
if not url:
fail += 1
print(f"[{i}] URL vacía. Saltando.")
continue
out_name = safe_filename_from_url(url)
out_path = OUTPUT_DIR / out_name
dl_path = DOWNLOAD_DIR / out_name
try:
print(f"[{i}] Descargando: {out_name}")
content = download_image(url)
dl_path.write_bytes(content)
result = process_image_to_standard(content)
result.save(out_path, "JPEG", quality=JPG_QUALITY, optimize=True)
ok += 1
print(f"[{i}] OK -> {out_path}")
except Exception as e:
fail += 1
print(f"[{i}] ERROR -> {out_name} | {e}")
print(f"Listo. OK: {ok} | ERROR: {fail}")
print(f"Salida final en: {OUTPUT_DIR.resolve()}")
if __name__ == "__main__":
main()7) El gran problema: librerías y entornos (por qué pasó)
Errores típicos:
- ModuleNotFoundError: no module named rembg.
- Conflictos por numpy al instalar paquetes pesados en el entorno base.
Nota: Para tareas de imágenes, lo correcto es usar un entorno separado y no instalar librerías pesadas en base.
8) Solución profesional: crear un entorno nuevo
conda create -n wooimg python=3.9
conda activate wooimg
pip install rembg onnxruntime pillow requests
python -c "import rembg; print("rembg OK")"Nota: Este entorno se puede reutilizar para otros trabajos de imágenes.
9) Conectar Spyder al entorno (y el tema spyder-kernels)
En Spyder cambia el intérprete en Tools > Preferences > Python Interpreter. Si aparece un error indicando que falta spyder-kernels, instálalo dentro del entorno:
conda activate wooimg
pip install spyder-kernels==3.0.*Verifica el intérprete real:
import sys
print(sys.executable)10) Ejecución y salida
Ejecuta el script y revisa la salida en la carpeta procesadas:
conda activate wooimg
python procesar_desde_csv.py- Resultados en: procesadas/
- Dimensión: 1200x1500
- Formato: JPG
11) Reemplazo en WordPress sin romper enlaces
Sube por FTP o File Manager y reemplaza solo los archivos de producto, manteniendo el mismo nombre y ruta. Así no cambian URLs ni SEO.
Nota: Haz backup de wp-content/uploads antes.
12) Checklist final
- CSV OK (columna image_url y datos correctos).
- Spyder apunta a envs/wooimg/python.exe.
- rembg OK.
- Salida OK (1200x1500 JPG).
- Reemplazo OK (mismo nombre y ruta).