Cuando permitimos subir imágenes subidas por un usuario, para posteriormente mostrarlas en un sitio web, encontraremos un problema de rendimiento. Los usuarios subirán imágenes de gran tamaño, y mostrarlas en la web sin optimizarlas ralentizará la carga de la página. Por ejemplo, si un usuario sube un selfie hecho con su smartphone puede pesar varios megabytes, pero a continuación se mostrará en un espacio muy pequeño. Nos suben una imagen de 3000px pero a continuación para tal vez mostrarla en un espacio de 100px. ¡Esto es un desperdicio de ancho de banda y de recursos del servidor!
Si trabajas con Django puedes optimizar automáticamente todas las imágenes subidas. O, en otras palabras, crear una copia de menor tamaño de una imagen añadida a campo de un modelo. Y no solo eso, sino que también puedes transformarla a un formato como AVIF, limpiar los las imágenes no usadas, y mucho más.
Nota 1: AVIF es un formato de imagen moderno y eficiente.Ofrece una compresión de imagen superior a la de JPEG y WebP. Además admite transparencias y animaciones. Si desarrollas aplicaciones o creas sitios web, no lo dudes. ¡Pásate ya! Es compatible con todos los navegadores actuales.
Nota 2: Si lo que deseas es crear una miniatura de un vídeo, puedes visitar este artículo.
Empezaremos instalando 3 dependencias:
Pillow
para trabajar con imágenes.pillow-avif-plugin
para convertir imágenes a AVIF.django-cleanup
para limpiar las imágenes no usadas.
Añade en tu archivo requirements.txt
las siguientes líneas:
Pillow===10.4.0
pillow-avif-plugin===1.4.6
django-cleanup===9.0.0
Revisa que estas usando las últimas versiones, las que se muestran aquí son las que se usaron en el momento de escribir este artículo.
A continuación, añade en tu archivo settings.py
las dirección del dominio que estas usando con el protocolo adecuado (http o https):
DOMAIN_URL = "https://www.tudominio.com"
Además incluimos django_cleanup
en la lista de aplicaciones instaladas:
INSTALLED_APPS = [
...
'django_cleanup',
...
]
En el artículo usaré para el ejemplo un modelo llamado Publication
con un campo de imagen llamado image
. Añade el siguiente código en tu archivo models.py
:
from django.db import models
from django.utils import timezone
class Publication(models.Model):
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='photos/', blank=True, null=True)
image_thumbnail = models.ImageField(upload_to='photos/thumbnails', blank=True, null=True)
pub_date = models.DateTimeField(default=timezone.now)
description
es un campo de texto opcional. Lo usaremos para el texto alternativo de la imagen (atributoalt
en HTML).image
es el campo de imagen que el usuario subirá. La que trataremos y nunca será mostrada directamente.image_thumbnail
es la miniatura de la imagen que mostraremos en la web.pub_date
es la fecha de publicación de la imagen. ¿Por qué no usarauto_now_add=True
? Es una buena práctica, ya que te permite poder editar la fecha si fuera necesario. Si usamosauto_now_add=True
la fecha no se podrá modificar.
Ahora vamos a añadir un método en el modelo Publication
para crear la miniatura de la imagen. Añade el siguiente código en tu archivo models.py
:
import os
from django.db import models
from django.conf import settings
from django.utils import timezone
import subprocess
import tempfile
import shutil
from PIL import Image, ImageOps
import pillow_avif
class Publication(models.Model):
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='photos/', blank=True, null=True)
image_thumbnail = models.ImageField(upload_to='photos/thumbnails', blank=True, null=True)
pub_date = models.DateTimeField(default=timezone.now)
def make_thumbnail(self):
if self.image:
width = 800
extension = "avif"
with Image.open(self.image.path) as image_raw:
# Rotate image if needed
image = ImageOps.exif_transpose(image_raw)
# Create thumbnail
image.thumbnail([width, width])
# Save thumbnail
dirpath = tempfile.mkdtemp()
output_file = os.path.join(dirpath, self.image.name.replace("/", "_")) + f".{extension}"
image.save(output_file, "AVIF")
image_name = self.image.name.split(".")[0] + f".{extension}"
# Save thumbnail to model
self.image_thumbnail.save(image_name, open(output_file, 'rb'))
self.save()
# Clean up
shutil.rmtree(dirpath)
En el ejemplo estamos creando una miniatura de 800px de ancho. Además conservamos la relación de aspecto de la imagen original, por lo que la altura se ajustará automáticamente.
Los pasos que se siguen son los siguientes:
- Si hay una imagen presente en el campo
image
, se abre la imagen. - Se rota la imagen si es necesario. Algunas cámaras guardan la orientación de la imagen en los metadatos EXIF, por lo que es posible que la imagen se muestre en una orientación incorrecta. Arreglamos el problema.
- Se crea la miniatura de la imagen.
- Creamos una carpeta temporal para guardar la imagen.
- Creamos la imagen en formato AVIF.
- Guardamos la imagen en el campo
image_thumbnail
del modelo. - Guardamos el modelo.
- Limpiamos la carpeta temporal.
Si queda alguna imagen perdida, se limpiará automáticamente gracias a la librería django-cleanup
. ¡Un problema menos!
¿Cuando creamos la minuatura? ¿Cómo automatizamos el proceso? Usando señales.
Añade el siguiente código en tu archivo models.py
:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Publication)
def make_thumbnail(sender, instance, **kwargs):
if instance.image:
post_save.disconnect(make_thumbnail, sender=Publication)
instance.make_thumbnail()
post_save.connect(make_thumbnail, sender=Publication)
Cuando se guarda un objeto Publication
, se ejecuta la señal post_save
. Si te fijas aquí hay un bucle infinito. Cuando guardamos la imagen, lanzamos una señal que crea la miniatura y vuelve a guardar el modelo, que a su vez vuelve a lanzar la señal. ¡No es buena idea guardar el modelo en el post_save
. Para evitar este bucle infinito, desconectamos la señal antes de crear la miniatura y la volvemos a conectar a su finalización. Truco sencillo pero efectivo.
Para terminar podríamos incluir una propertie
en el modelo para obtener la URL de la imagen miniatura. Añade el siguiente código en tu archivo models.py
:
from django.conf import settings
class Publication(models.Model):
...
@property
def public_image_url_thumbnail(self):
if not self.image_thumbnail:
self.make_thumbnail()
if self.image:
return settings.DOMAIN_URL + (self.image_thumbnail.url if self.image_thumbnail else self.image.url)
return None
Con esta propiedad, si la imagen miniatura no existe, se creará automáticamente. Otra automatización más.
¡Ya hemos terminado! Ahora cada vez que subas una imagen a un objeto Publication
, se creará una miniatura de la imagen en formato AVIF. Además, la imagen se limpiará automáticamente si no se usa.
El código completo quedaría de la siguiente forma:
import os
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.db.models.signals import post_save
from django.dispatch import receiver
import subprocess
import tempfile
import shutil
from PIL import Image, ImageOps
import pillow_avif
class Publication(models.Model):
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='photos/', blank=True, null=True)
image_thumbnail = models.ImageField(upload_to='photos/thumbnails', blank=True, null=True)
pub_date = models.DateTimeField(default=timezone.now)
def make_thumbnail(self):
if self.image:
width = 800
extension = "avif"
with Image.open(self.image.path) as image_raw:
# Rotate image if needed
image = ImageOps.exif_transpose(image_raw)
# Create thumbnail
image.thumbnail([width, width])
# Save thumbnail
dirpath = tempfile.mkdtemp()
output_file = os.path.join(dirpath, self.image.name.replace("/", "_")) + f".{extension}"
image.save(output_file, "AVIF")
image_name = self.image.name.split(".")[0] + f".{extension}"
# Save thumbnail to model
self.image_thumbnail.save(image_name, open(output_file, 'rb'))
self.save()
# Clean up
shutil.rmtree(dirpath)
@property
def public_image_url_thumbnail(self):
if not self.image_thumbnail:
self.make_thumbnail()
if self.image:
return settings.DOMAIN_URL + (self.image_thumbnail.url if self.image_thumbnail else self.image.url)
return None
@receiver(post_save, sender=Publication)
def make_thumbnail(sender, instance, **kwargs):
if instance.image:
post_save.disconnect(make_thumbnail, sender=Publication)
instance.make_thumbnail()
post_save.connect(make_thumbnail, sender=Publication)
Para terminar ejecutamos las migraciones.
python manage.py makemigrations
python manage.py migrate
Si tienes alguna duda, no dudes en preguntar en los comentarios. ¡Buen hacking!
{{ comments.length }} comentarios