Django: автоматически создаем миниатюры для изображений в ImageField

Создание миниатюр изображений (они же thumbnails, они же previews) часто используется при работе с графическими файлами. Это позволяет, например, сильно сократить время загрузки страницы со списком фотографий, отображая их уменьшенные копии и загружая полноразмерную фотографию только при клике на нее. 

В этом посте рассмотрим способ, которым можно воспользоваться при работе с Django ImageField для автоматического создания миниатюр изображений при сохранении модели, не требующего установки каких-либо дополнительных приложений-"батареек".

 

Изначально наша модель выглядит так:

models.py

from datetime import datetime
from slugify import slugify     # тут используется awesome-slugify


def some_model_file_name(instance, filename):
    ext = filename.split('.')[-1]
    filename = "%s.%s" % (slugify(instance.name, to_lower=True), ext)
    now = datetime.now()
    return os.path.join('somemodel/{year}/{month}/{day}/'.format(year=now.year,
                                                                 month=now.month,
                                                                 day=now.day), filename)

class SomeModel(models.Model):
    name = models.CharField(_('Name'), max_length=120)
    image = models.ImageField(upload_to=some_model_file_name)

При загрузке изображения происходит его сохранение в директорию somemodel/<год>/<месяц>/<день> с именем, приведенным к нижнему регистру и к латинским символам.

Мы же хотим, чтобы в момент сохранения модели создавались еще и копии загруженного изображения в заданном нами масштабе. Для этого установим библиотеку pillow для обработки изображений.

$ pip install Pillow

И дополним код следующим образом:

models.py

from datetime import datetime
from slugify import slugify     # тут используется awesome-slugify
from PIL import Image           # для обработки изображения нам нужен pillow


def some_model_file_name(instance, filename):
    ext = filename.split('.')[-1]
    filename = "%s.%s" % (slugify(instance.name, to_lower=True), ext)
    now = datetime.now()
    return os.path.join('somemodel/{year}/{month}/{day}/'.format(year=now.year,
                                                                 month=now.month,
                                                                 day=now.day), filename)

def some_model_thumb_name(instance, filename):
    original_image_path = str(instance.image).rsplit('/', 1)[0]
    return os.path.join(original_image_path, filename)

class SomeModel(models.Model):
    name = models.CharField('Name', max_length=120)
    image = models.ImageField(upload_to=some_model_file_name)
    image_thumb = models.CharField('Thumbnail image', max_length=255, blank=True)

    def get_thumb_image_url(self):
        return MEDIA_URL + self.image_thumb

    def save(self, *args, **kwargs):
        size = {'height': 60, 'width': 60}
        super(SomeModel, self).save(*args, **kwargs)
        extension = str(self.image.path).rsplit('.', 1)[1]  # получаем расширение загруженного файла
        filename = str(self.image.path).rsplit(os.sep, 1)[1].rsplit('.', 1)[0]  # получаем имя загруженного файла (без пути к нему и расширения)
        fullpath = str(self.image.path).rsplit(os.sep, 1)[0]  # получаем путь к файлу (без имени и расширения)

        if extension in ['jpg', 'jpeg', 'png']:    # если расширение входит в разрешенный список
            im = Image.open(str(self.image.path))  # открываем изображение
            im.thumbnail((size['width'], size['height'])) # создаем миниатюру указанной ширины и высоты (важно - im.thumbnail сохраняет пропорции изображения!)
            thumbname = filename + "_" + str(size['width']) + "x" + str(size['height']) + '.' + extension # имя нового изображения в формате oldname_60x60.jpg
            im.save(fullpath + os.sep + thumbname) # сохраняем полученную миниатюру
            self.image_thumb = some_model_thumb_name(self, thumbname) # записываем путь к ней в поле image_thumb в модели
            super(SomeModel, self).save(*args, **kwargs)

Используя добавленный метод get_thumb_image_url, мы можем легко отобразить миниатюру в коде шаблона.

your_template.html

...
{% for some_model in some_models %}
    <img src="{{ some_model.get_thumb_image_url }}" />
{% endfor %}
...

Небольшая тонкость: приведенный фрагмент кода

im.thumbnail((size['width'], size['height']))

осуществить изменение размеров изображения с сохранением его пропорций. То есть нет гарантии, что будет получено изображение с шириной и высотой, точно соответствующих указанным в  переменной

 size = {'height': 60, 'width': 60}

В случае, когда нам важнее получить точно заданные ширину и высоту, чем сохранить пропорции изображения, стоит использовать такой вариант:

im = im.resize((size['width'], size['height']))

В приведенном коде есть один значительный недостаток - создание миниатюр будет происходить каждый раз при сохранении модели. Постараемся с этим побороться, добавив в модель следующий метод:

    def __init__(self, *args, **kwargs):
        super(SomeModel, self).__init__(*args, **kwargs)
        self.__original_image = self.image.url

А осуществлять все дальнейшие действия будем только в случае, если url картинки изменился:

    def save(self, *args, **kwargs):
        if self.image.url != self.__original_image:
            ...
        else:
            super(SomeModel, self).save(*args, **kwargs)

Ниже привожу конечный получившийся код:

models.py

from datetime import datetime
from slugify import slugify     # тут используется awesome-slugify
from PIL import Image           # для обработки изображения нам нужен pillow


def some_model_file_name(instance, filename):
    ext = filename.split('.')[-1]
    filename = "%s.%s" % (slugify(instance.name, to_lower=True), ext)
    now = datetime.now()
    return os.path.join('somemodel/{year}/{month}/{day}/'.format(year=now.year,
                                                                 month=now.month,
                                                                 day=now.day), filename)

def some_model_thumb_name(instance, filename):
    original_image_path = str(instance.image).rsplit('/', 1)[0]
    return os.path.join(original_image_path, filename)

class SomeModel(models.Model):
    name = models.CharField('Name', max_length=120)
    image = models.ImageField(upload_to=some_model_file_name)
    image_thumb = models.CharField('Thumbnail image', max_length=255, blank=True)

    def __init__(self, *args, **kwargs):
        super(SomeModel, self).__init__(*args, **kwargs)
        self.__original_image = self.image.url

    def get_thumb_image_url(self):
        return MEDIA_URL + self.image_thumb

    def save(self, *args, **kwargs):
        if self.image.url != self.__original_image:
            size = {'height': 60, 'width': 60}
            super(SomeModel, self).save(*args, **kwargs)
            extension = str(self.image.path).rsplit('.', 1)[1]  # получаем расширение загруженного файла
            filename = str(self.image.path).rsplit(os.sep, 1)[1].rsplit('.', 1)[0]  # получаем имя загруженного файла (без пути к нему и расширения)
            fullpath = str(self.image.path).rsplit(os.sep, 1)[0]  # получаем путь к файлу (без имени и расширения)

            if extension in ['jpg', 'jpeg', 'png']:    # если расширение входит в разрешенный список
                im = Image.open(str(self.image.path))  # открываем изображение
                im.thumbnail((size['width'], size['height'])) # создаем миниатюру указанной ширины и высоты (важно - im.thumbnail сохраняет пропорции изображения!)
                thumbname = filename + "_" + str(size['width']) + "x" + str(size['height']) + '.' + extension # имя нового изображения в формате oldname_60x60.jpg
                im.save(fullpath + os.sep + thumbname) # сохраняем полученную миниатюру
                self.image_thumb = some_model_thumb_name(self, thumbname) # записываем путь к ней в поле image_thumb в модели
                super(SomeModel, self).save(*args, **kwargs)
         else:
             super(SomeModel, self).save(*args, **kwargs)

 

Оглавление