Pillow permite leer, transformar y guardar imágenes. Para uploads, limita bytes y píxeles antes de consumir CPU y memoria: el nombre del archivo no demuestra su contenido.
from pathlib import Path
from PIL import Image, ImageOps
def crear_miniatura(origen: Path, destino: Path) -> None:
with Image.open(origen) as imagen:
imagen = ImageOps.exif_transpose(imagen)
imagen.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
if imagen.mode not in ("RGB", "L"):
imagen = imagen.convert("RGB")
imagen.save(destino, format="JPEG", quality=85, optimize=True)
thumbnail conserva la proporción y modifica el objeto. exif_transpose aplica la orientación de cámara. Convertir a RGB evita modos incompatibles con JPEG, pero eliminar transparencia debe ser intencional.
No sobrescribas el original antes de validar el resultado. Mantén Pillow actualizado y guarda uploads fuera de rutas ejecutables. La guía de pathlib en Python ayuda con rutas.
La documentación oficial de Pillow, consultada el 22 de julio de 2026, cubre formatos, resampling y seguridad. Prueba orientación, transparencia, perfiles de color e imágenes grandes con muestras controladas.
Instalar e inspeccionar una imagen
Instala Pillow en un entorno virtual con python -m pip install Pillow. La distribución mantenida se llama Pillow, pero los imports conservan el namespace histórico PIL. Después de abrir un archivo, revisa format, size y mode: describen el contenedor detectado, las dimensiones y la representación de los píxeles.
from PIL import Image
with Image.open("entrada.png") as imagen:
print(imagen.format)
print(imagen.size)
print(imagen.mode)
imagen.load()
Image.open() identifica el archivo y normalmente aplaza la decodificación. load() fuerza ese trabajo mientras el archivo permanece abierto. Si el objeto debe sobrevivir al bloque, crea una copia dentro. Ni la extensión ni el tipo HTTP demuestran el formato; Pillow inspecciona bytes, pero reconocerlos tampoco vuelve segura una subida.
Validar antes de transformar
verify() comprueba la estructura sin decodificar todos los píxeles y deja el objeto inutilizable. Reabre la fuente para procesarla. Captura errores de identificación, rechaza la entrada y no intentes reparar silenciosamente datos desconocidos.
from PIL import Image, UnidentifiedImageError
def validar_imagen(ruta):
try:
with Image.open(ruta) as imagen:
if imagen.format not in {"JPEG", "PNG", "WEBP"}:
raise ValueError("Formato no permitido")
ancho, alto = imagen.size
if ancho < 1 or alto < 1 or ancho * alto > 25_000_000:
raise ValueError("Dimensiones no permitidas")
imagen.verify()
except UnidentifiedImageError as error:
raise ValueError("El archivo no es una imagen reconocida") from error
Pillow emite DecompressionBombWarning sobre su umbral de píxeles y puede lanzar DecompressionBombError para entradas mayores. No desactives globalmente esta protección. Ajusta Image.MAX_IMAGE_PIXELS solo por requisitos medidos y limita también los bytes, porque tamaño comprimido y cantidad de píxeles representan riesgos distintos.
Redimensionar, crear miniatura o recortar
resize() produce dimensiones exactas y puede deformar cuando cambia la proporción. thumbnail() cabe dentro de una caja, conserva proporción, modifica el objeto y normalmente no amplía. ImageOps.fit() llena una caja y recorta el exceso, útil para tarjetas y avatares.
from PIL import Image, ImageOps
with Image.open("foto.jpg") as original:
corregida = ImageOps.exif_transpose(original)
tarjeta = ImageOps.fit(
corregida,
(1200, 675),
method=Image.Resampling.LANCZOS,
centering=(0.5, 0.4),
)
tarjeta.save("tarjeta.jpg", quality=85, optimize=True)
centering desplaza la región conservada. Pillow no detecta por sí solo el sujeto, así que encuadrar rostros u objetos requiere coordenadas de otra etapa. Un recorte central no debe presentarse como inteligente.
Los filtros de remuestreo equilibran coste y apariencia. LANCZOS es una buena base al reducir fotografías; NEAREST conserva píxeles duros en pixel art. Inspecciona el resultado al tamaño real de visualización, donde halos o bordes demasiado nítidos pueden ser evidentes.
Orientación, transparencia y modos
Las cámaras pueden guardar píxeles con una orientación y registrar la rotación en EXIF. ImageOps.exif_transpose() aplica la orientación y elimina el marcador correspondiente. Hazlo antes de calcular recortes, o el ancho y el alto aparentes pueden estar invertidos.
JPEG no admite canal alfa. Convertir RGBA directamente a RGB no permite controlar el fondo y puede producir zonas negras. Compón sobre un color explícito.
from PIL import Image
def quitar_alpha(imagen, fondo=(255, 255, 255)):
rgba = imagen.convert("RGBA")
base = Image.new("RGBA", rgba.size, fondo + (255,))
compuesta = Image.alpha_composite(base, rgba)
return compuesta.convert("RGB")
Los modos 1, L, P, RGB, RGBA y CMYK tienen semánticas distintas. RGB y RGBA cubren la mayoría de publicaciones web. Imágenes CMYK de impresión pueden cambiar de color al convertirlas sin más; si la fidelidad importa, considera perfiles ICC e inspecciona con herramientas apropiadas.
Elegir el formato de salida
JPEG conviene a fotografías sin transparencia. PNG conserva alfa y bordes exactos, pero puede pesar mucho en fotos. WebP puede comprimir bien cuando todos los destinos lo aceptan. Decide según contenido, compatibilidad y política del producto.
quality no es un porcentaje objetivo de fidelidad. Valores muy altos aumentan bastante un JPEG con poca ganancia visible. optimize=True dedica trabajo a una codificación eficiente y progressive=True permite una carga en pasadas. Compara muestras y artefactos en vez de imponer un ajuste universal.
imagen.save(
"salida.jpg",
format="JPEG",
quality=85,
optimize=True,
progressive=True,
exif=b"",
)
Los metadatos pueden revelar ubicación, cámara y datos privados. Eliminar EXIF de derivados públicos suele ser sensato, aunque quizá deban conservarse datos editoriales o de autoría. Guarda el original bajo una política explícita y no lo sobrescribas.
Memoria y procesamiento por lotes
BytesIO permite consumir un stream en memoria, pero no evita limitar el cuerpo HTTP antes de reservarlo.
from io import BytesIO
from PIL import Image
def dimensiones(data: bytes) -> tuple[int, int]:
if len(data) > 10 * 1024 * 1024:
raise ValueError("El archivo supera 10 MB")
with Image.open(BytesIO(data)) as imagen:
imagen.load()
return imagen.size
En lotes, escribe en un destino separado o temporal del mismo volumen y reemplaza solo después de guardar con éxito. No captures cualquier excepción para continuar sin informe. Registra origen, destino, formato, dimensiones y error, nunca el contenido binario.
La decodificación consume CPU. En un servidor asíncrono, no ejecutes trabajos grandes en el loop de eventos. Envíalos a una cola o executor con concurrencia limitada. Procesos ilimitados multiplican el pico de memoria.
Operaciones útiles y calidad predecible
crop() recibe (izquierda, arriba, derecha, abajo). rotate() puede ampliar el lienzo con expand=True. ImageOps.contain() conserva la imagen completa dentro del límite; ImageOps.pad() rellena espacio con un color. Elige según la regla editorial, no solo por las dimensiones.
Pillow ofrece ajustes con ImageEnhance y filtros con ImageFilter. Documenta cada parámetro destructivo y conserva fixtures de regresión. Guardar repetidamente como JPEG acumula pérdidas; transforma en memoria y codifica una sola vez al final.
Pruebas y lista de producción
Crea fixtures pequeñas de paisaje, retrato, transparencia, animación, rotación EXIF, CMYK, archivo truncado y dimensiones excesivas. Comprueba tamaño, modo, formato y proporción. La inspección humana sigue siendo necesaria porque las métricas no detectan todos los artefactos visibles.
GIF y WebP pueden tener varios frames. Procesar solo el primero elimina la animación sin aviso. Decide si el producto rechaza animaciones, conserva frames o genera conscientemente una miniatura estática, y comunica ese comportamiento.
Antes de desplegar, confirma límites de bytes y píxeles, formatos permitidos, nombres generados por la aplicación, almacenamiento no ejecutable, Pillow actualizado y limpieza de temporales. Verifica orientación, fondo para alfa, metadatos, memoria máxima y entradas corruptas. Un pipeline confiable es predecible tanto al producir resultados como al rechazar archivos.
Si almacenas derivados en caché, incluye todos los parámetros de transformación y la revisión de la fuente en la clave. De lo contrario, cambiar el recorte o la calidad puede seguir entregando una versión antigua. Reabre el archivo guardado para comprobarlo antes de publicarlo y actualiza la base de datos o manifiesto únicamente cuando el asset final ya exista.
Las dimensiones de salida forman parte del contrato. Un recorte inesperado puede romper el diseño aunque la decodificación haya funcionado. Registra la versión del pipeline junto al derivado; así una migración futura puede regenerar selectivamente resultados antiguos sin recomprimir toda la biblioteca ni degradar imágenes que ya estaban correctas.