Estandarizar tamaño de imagenes WooCommerce con Python

tamano-de-imagen-en-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).

Cómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)Cómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)

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ómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)Cómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)
Los mejores detectores de texto con IA probados en 2025Los mejores detectores de texto con IA probados en 2025

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.

Cómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)Cómo convertir un script Python en .EXE con PyInstaller (Guía completa paso a paso)
Los mejores detectores de texto con IA probados en 2025Los mejores detectores de texto con IA probados en 2025
WhatsApp para ciegos: fácil en computadoras y Atajos de Teclado OcultosWhatsApp para ciegos: fácil en computadoras y Atajos de Teclado Ocultos

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).