Crear miniaturas de imágenes en Django | Programador Web Valencia

Crear miniaturas de imágenes en Django

5 minutos

Django

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 (atributo alt 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 usar auto_now_add=True? Es una buena práctica, ya que te permite poder editar la fecha si fuera necesario. Si usamos auto_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:

  1. Si hay una imagen presente en el campo image, se abre la imagen.
  2. 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.
  3. Se crea la miniatura de la imagen.
  4. Creamos una carpeta temporal para guardar la imagen.
  5. Creamos la imagen en formato AVIF.
  6. Guardamos la imagen en el campo image_thumbnail del modelo.
  7. Guardamos el modelo.
  8. 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!

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me invitas a un café? ☕

Puedes hacerlo usando el terminal.

ssh customer@andros.dev -p 5555

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...