Scripts y automatización de archivos

Redimensionar una carpeta de imágenes con Pillow conservando los originales

Redimensiona varias imágenes en una carpeta nueva sin estirarlas ni sustituir los archivos de origen. Un pequeño conjunto de imágenes sintéticas permite calcular fácilmente a mano las dimensiones esperadas de salida.

Ver el índice

La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. English

Para quién esEsta guía está dirigida a personas que necesitan copias más pequeñas de muchas imágenes conservando las proporciones de cada una y dejando intactos los originales.

Preparación
  • Python 3.12 y un comando de terminal que inicie esa versión.
  • Pillow instalado con python -m pip install Pillow.
  • Una carpeta de trabajo donde los scripts puedan crear nuevas carpetas dentro de outputs.
  • Espacio suficiente en disco para las imágenes sintéticas originales y las copias redimensionadas.

01Definir primero la regla de redimensionado

El objetivo es ajustar cada imagen dentro de un cuadro de 300 by 300 pixel conservando su relación de aspecto. Las imágenes que ya son más pequeñas que ese cuadro no se amplían. Las imágenes de origen permanecen sin cambios y cada copia redimensionada se escribe en una carpeta de salida independiente.

Para una imagen con ancho W y alto H, calcula scale como el menor valor entre 300/W, 300/H y 1. Después calcula el nuevo ancho y alto a partir de ese único scale factor. Usar un solo factor para ambas dimensiones evita el estiramiento.

text
scale = min(300 / width, 300 / height, 1)
new_width = int(width * scale)
new_height = int(height * scale)

02Crear una carpeta de imágenes sintéticas

Los siguientes archivos son sintéticos y fueron creados específicamente para este artículo. El script de preparación crea cuatro imágenes PNG sencillas con dimensiones conocidas. Su contenido visual no es importante; las dimensiones se eligieron para que todos los resultados esperados del redimensionado puedan comprobarse manualmente.

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
IMAGES = {
    "landscape.png": ((800, 600), (220, 80, 80)),
    "portrait.png": ((600, 800), (80, 120, 220)),
    "small.png": ((200, 100), (80, 180, 100)),
    "wide.png": ((1200, 300), (180, 120, 60)),
}

SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir()  # Stop if this synthetic source already exists.

for filename, (size, color) in IMAGES.items():
    image = Image.new("RGB", size, color)
    target = SOURCE / filename
    image.save(target, format="PNG")
text
python create_resize_demo.py
ArchivoAncho originalAlto originalRelación de aspecto
landscape.png8006004:3
portrait.png6008003:4
small.png2001002:1
wide.png12003004:1

Los cuatro originales contienen un total de 1,240,000 pixels: 480,000 + 480,000 + 20,000 + 360,000. El número de pixels es útil para comprender el ejemplo, pero no equivale al tamaño del archivo en bytes porque la compresión PNG depende del contenido de la imagen.

03Calcular manualmente las cuatro dimensiones esperadas

Para landscape.png, la dimensión limitante es el ancho: 300/800 = 0.375. Multiplicar ambas dimensiones por 0.375 da 300 by 225. Para portrait.png, la altura es la limitante: 300/800 = 0.375, produciendo 225 by 300.

small.png ya cabe porque ambas dimensiones son iguales o inferiores a 300. El scale está limitado a 1, por lo que permanece en 200 by 100. Para wide.png, el ancho limita el resultado: 300/1200 = 0.25, dando 300 by 75.

ArchivoScaleSalida esperadaPixels de salida
landscape.png0.375300 × 22567,500
portrait.png0.375225 × 30067,500
small.png1200 × 10020,000
wide.png0.25300 × 7522,500

Por tanto, las copias redimensionadas contienen un total de 177,500 pixels. Los originales siguen presentes por separado, así que esta cifra no representa un ahorro de espacio en disco.

04Redimensionar la carpeta hacia un nuevo destino

Guarda el siguiente script como batch_image_resize.py. Procesa archivos PNG, JPEG y WebP directamente dentro de SOURCE. No busca subcarpetas de forma recursiva. El destino se crea con mkdir sin exist_ok, por lo que un directorio de salida ya existente hace que el script se detenga en lugar de mezclar los resultados nuevos con los de una ejecución anterior.

python
from pathlib import Path
from PIL import Image, ImageOps

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"
MAX_WIDTH = 300
MAX_HEIGHT = 300
SUPPORTED = {".png", ".jpg", ".jpeg", ".webp"}


def target_size(width: int, height: int) -> tuple[int, int]:
    scale = min(MAX_WIDTH / width, MAX_HEIGHT / height, 1)
    new_width = max(1, int(width * scale))
    new_height = max(1, int(height * scale))
    return new_width, new_height


def main() -> None:
    if not SOURCE.is_dir():
        raise FileNotFoundError(f"Source folder not found: {SOURCE}")
    if MAX_WIDTH < 1 or MAX_HEIGHT < 1:
        raise ValueError("Maximum dimensions must be positive integers.")

    source_resolved = SOURCE.resolve()
    output_resolved = OUTPUT_DIR.resolve()
    if source_resolved == output_resolved:
        raise ValueError("SOURCE and OUTPUT_DIR must be different.")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()  # Refuse to reuse an existing destination.

    candidates = sorted(
        path for path in SOURCE.iterdir()
        if path.is_file() and path.suffix.lower() in SUPPORTED
    )

    resized_count = 0
    unchanged_size_count = 0

    for source_path in candidates:
        output_path = OUTPUT_DIR / source_path.name
        if output_path.exists():
            raise FileExistsError(f"Output already exists: {output_path}")

        with Image.open(source_path) as opened:
            image = ImageOps.exif_transpose(opened)
            image.load()
            old_size = image.size
            new_size = target_size(*old_size)

            if new_size == old_size:
                result = image.copy()
                unchanged_size_count += 1
            else:
                result = image.resize(new_size, Image.Resampling.LANCZOS)
                resized_count += 1

            save_options = {}
            if source_path.suffix.lower() in {".jpg", ".jpeg"}:
                save_options["quality"] = 90
            result.save(output_path, **save_options)
            result.close()

        print(f"{source_path.name}: {old_size[0]}x{old_size[1]} -> {new_size[0]}x{new_size[1]}")

    print(f"Processed {len(candidates)} images.")
    print(f"Resized: {resized_count}; already within limit: {unchanged_size_count}.")
    print(f"Output folder: {OUTPUT_DIR.as_posix()}")


if __name__ == "__main__":
    main()

ImageOps.exif_transpose aplica la orientación EXIF de una imagen antes del cálculo de redimensionado cuando existen esos datos de orientación. Los archivos PNG sintéticos no tienen ese requisito, por lo que sus dimensiones mostradas siguen siendo las indicadas anteriormente.

05Comparar la salida esperada

Como los nombres de archivo se procesan en orden, las líneas esperadas de consola son landscape.png, portrait.png, small.png y wide.png. Tres imágenes se hacen más pequeñas y una permanece con sus dimensiones originales.

text
landscape.png: 800x600 -> 300x225
portrait.png: 600x800 -> 225x300
small.png: 200x100 -> 200x100
wide.png: 1200x300 -> 300x75
Processed 4 images.
Resized: 3; already within limit: 1.
Output folder: outputs/image_resize_result

Esta salida esperada se obtuvo manualmente y no se capturó de una ejecución. Las comprobaciones importantes son las dimensiones, el número de archivos y el hecho de que los originales permanezcan en outputs/image_resize_demo.

ComprobaciónValor esperado
Archivos en la carpeta de origen4
Archivos en la carpeta de salida4
Archivos redimensionados3
Archivos que ya estaban dentro del límite1
Mayor ancho de salida300
Mayor alto de salida300

06Verificar personalmente las imágenes generadas

No dependas únicamente de los nombres de archivo o del texto de consola. Abre los archivos resultantes e inspecciona sus dimensiones. El siguiente comprobador opcional lee ambas carpetas e informa de las dimensiones. No realiza ninguna escritura.

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"

for source_path in sorted(SOURCE.glob("*.png")):
    output_path = OUTPUT_DIR / source_path.name
    with Image.open(source_path) as original, Image.open(output_path) as resized:
        print(
            f"{source_path.name}: "
            f"original={original.size}, output={resized.size}"
        )
  • Confirma que cada ancho de salida sea como máximo 300 y cada alto de salida sea como máximo 300.
  • Confirma que landscape.png siga siendo 4:3, portrait.png siga siendo 3:4 y wide.png siga siendo 4:1.
  • Confirma que small.png permanezca en 200 by 100 en lugar de ampliarse a 300 by 150.
  • Compara las fechas de modificación o los hashes si necesitas evidencia adicional de que los archivos de origen no fueron reescritos.
  • Ejecuta el script de redimensionado por segunda vez sin cambiar OUTPUT_DIR. Debería detenerse con FileExistsError antes de procesar otro lote.

07Reconocer errores comunes

SíntomaQué comprobar
ModuleNotFoundError: No module named PILInstala Pillow en el mismo entorno de Python utilizado para ejecutar el script con python -m pip install Pillow.
FileNotFoundErrorComprueba SOURCE y el directorio de trabajo actual de la terminal. Para este ejemplo, ejecuta primero el script de preparación sintético.
FileExistsErrorLa carpeta de salida ya existe. Revísala y selecciona un destino nuevo en lugar de permitir una sobrescritura automática.
Image cannot be identifiedLa extensión puede parecer compatible aunque el contenido esté dañado o no sea realmente una imagen. Considera incompleto el lote fallido.
Unexpected rotationLos metadatos de orientación y la orientación de pixels pueden diferir. ImageOps.exif_transpose se utiliza antes del redimensionado, pero inspecciona visualmente las imágenes importantes.
Unexpected output file size in bytesLas dimensiones en pixels por sí solas no determinan el tamaño del archivo comprimido. También influyen el formato, el contenido de la imagen, los metadatos y la calidad JPEG.

Si el procesamiento falla después de haber escrito algunos archivos, el script no elimina esos resultados parciales. Conserva el destino para investigarlo o utiliza un nuevo directorio de salida después de corregir el problema.

08Comprender los límites antes de utilizar imágenes reales

Este ejemplo solo maneja archivos directamente dentro de una carpeta y admite extensiones PNG, JPEG y WebP. No reproduce un árbol de directorios anidado, no redimensiona individualmente los frames animados, no conserva todas las formas de metadatos ni garantiza que la información de gestión del color permanezca sin cambios.

El remuestreo modifica los datos de pixels. Incluso cuando una imagen conserva las mismas dimensiones, abrirla y guardarla puede cambiar sus bytes codificados o sus metadatos. La salida JPEG tiene pérdida y este script guarda las copias JPEG con quality 90; eso no conserva el bitstream JPEG original ni la configuración de compresión original.

El cálculo con int redondea hacia abajo las dimensiones fraccionarias. Para tamaños de origen arbitrarios, la relación de pixels resultante puede diferir ligeramente de la relación matemática porque las dimensiones de una imagen deben ser números enteros. Las dimensiones sintéticas de esta guía se dividen exactamente, por lo que los cuatro resultados esperados no requieren un redondeo ambiguo.

Registro de ejecución y verificación

2026-09-20 · ejemplo revisado manualmente · objetivo: Python 3.12 · se requiere Pillow · sin ejecución

  • Se comprobaron manualmente las cuatro dimensiones sintéticas de origen: 800x600, 600x800, 200x100 y 1200x300.
  • Se calcularon manualmente los scale factors 0.375, 0.375, 1 y 0.25.
  • Se obtuvieron manualmente las dimensiones de salida esperadas 300x225, 225x300, 200x100 y 300x75.
  • Se calculó manualmente el total de pixels de origen como 1,240,000 y el total de pixels de salida como 177,500.
  • Se revisó el código para confirmar que los archivos de origen se abren para lectura, los resultados se guardan en una carpeta separada y una carpeta de destino ya existente hace que la ejecución se detenga.
  • Se obtuvieron manualmente la salida esperada de consola y los recuentos.
Límites de la verificación
  • El código no fue ejecutado por el autor de esta respuesta; aquí no se probaron el comportamiento de Pillow ni las escrituras en el sistema de archivos.
  • JPEG, WebP, orientación EXIF, archivos dañados, perfiles de color, animación y conservación de metadatos se comentaron, pero no se probaron.
  • No se predijeron tamaños de archivo comprimido en bytes porque dependen del contenido de la imagen y de la codificación.
  • Las URL de la documentación oficial se proporcionaron a partir de ubicaciones de documentación conocidas, pero no se comprobaron en línea.

Criterios de redacción y verificación de todo el sitio

Fuentes de referencia

Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.