Как стать автором
Обновить

Django, ImageField и .webp. Ещё немного про ускорение web приложения и экономию дискового пространства

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров7.3K
Всего голосов 4: ↑3 и ↓1+2
Комментарии42

Комментарии 42

В чём преимущество get над filter в случаях, когда мы не уверены, что объект есть? Мне кажется, можно сократить.

    def save(self, *args, **kwargs):
        try:
            this = CompanyLogo.objects.filter(id=self.id).first()
            if this and this.logo != self.logo:
                self.logo = self.compress_logo(self.logo)
                this.logo.delete(save=False)                
        except ValueError:
            pass
        super(CompanyLogo, self).save(*args, **kwargs)

Идея с first() мне нравится. А вот остальной код сократить не получится, если допустим вы захотите удалить изображение из БД, но оставить запись, скажем у вас ещё есть поле title в модели и вы хотите его оставить, то изображение с сервера не удалится.

Не очень понял. А в вашем коде за этот случай что отвечает?

И потом, если в модели удалили изображение, то self.logo будет None (или что-то в этом роде) и не будет совпадать с this.logo, и удаление запустится.

Да, согласен, отлично работает. Надо вместо pass добавить super(CompanyLogo, self).save(*args, **kwargs)

Добрый день. Можно ли таким же образом сконвертировать PNG в SVG?

Добрый день. Думаю таким образом преобразовать не выйдет, Pillow не поддерживает подобных преобразований. Можно посмотреть в сторону другой библиотеки, допустим https://pypi.org/project/pypotrace/

пиксели в вектора? Вам векторизатор нужен, а не конвертор.

1) хранить картинки в базе - ужасная практика. Никогда так не делайте.

2) добавлять мусор к изображению для ....... для чего, чтобы потом опять обрезать?

3) конвертировать картинки на событии загрузки файла - плохая практика. Нужны другие размеры, форматы завтра, перезаливать будете, велосипедить конвертер?

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

Здравствуйте!

  1. Строго говоря изображения хранятся в папке указанной в settings.py (MEDIA_ROOT ), или Вы о чём то другом говорите, возможно есть другой способ хранения изображений?

  2. Согласен, если допустим человек, который будет загружать контент будет сразу обрезать (подгонять) изображения под нужный формат то это конечно не требуется, а если допустим у нас окно прямоугольное, а изображение квадратное и мы его просто 'ресайзим' под наши пиксели, то будет искажение. Разве нет?

  3. Полностью с Вами согласен. Зачастую информации мало по этому поводу.

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

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

Правила обрезки очень быстрой библиотеки
https://sharp.pixelplumbing.com/api-resize
Вообще можете это апи брать за инструкцию.

Один из микросервисов для изображений. Можно найти аналоги.
https://imgproxy.net/
Куда эффективнее картинки вынести на отдельный сервер а хранить только урлы, где нужно. Велосипеды с картинками сейчас не нужны. Лучше брать готовое из опенсорса.

Понял, спасибо за информацию.

какие есть микросервисы опенсорсные хорошие?

imgproxy - MIT
я не особо ковырялся искал сравнивал, но этот марсианский

А смысл платить $499 в год за функционал, который можно получить локально абсолютно бесплатно, используя тот же pillow + ffmpeg?

Наличие платного тарифа предполагает ограничение бесплатного. Сервер-то надо оплачивать. Да и вообще не понятно, зачем использовать удалённый сервис для такой простой задачи? Коннект может отвалиться, апи поменяться, изменятся условия... Ладно, там переводчик языковой. Но тут-то pillow + sorl.thumbnail или imagekit - и это всё уже есть готовое.

Дополнительный функционал не является ограничением.

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

Апи для того и существует, чтобы менялось.

Django и так умеет хранить картинки на другом сервере.

И это Ваш Апи может не меняться. Чужой - может отвалиться, поменяться, закрыться. Не понимаю, зачем закладывать в систему лишний риск.

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

Когда это 2 ваших сервера - не вопрос.

Первое решается достаточно просто.

Необходимо создать функцию, которая будет возвращать путь+id(uuid4 лучше если ожидается огромное количество логотипов) и дать ее в атрибут upload_to.

def get_image_path(instance, filename):
    return 'logo/{0}_{1}'.format(instance.id, filename)
...
    logo = models.ImageField(upload_to=get_image_path,...)

В таком случае можно не волноваться при удалении что удалится что то не то.

А вообще, даже если использовать простой id, то, учитывая специфику инкрементации id(при удалении элемента инкрементация не сбрасывает счетчик), можно даже с простым id не беспокоиться что удалится не то.

UPD

Это верно для O2O связи. Для FK потребуется разветвление по папкам.

вы немного не поняли меня, если вообще операция delete есть в логике, вы увеличваете вероятность отказа всей системы. Не сегодня, завтра, после завтра кто-то что-то пропустит (как яндекс диск забыл поставить точку перед удалением и сносил винду от корня) и будут проблемы неясные. Прилетит не тот id или не от туда, всякое.
Если нужно удалять, лучше делать пометку в бд и удалять заданием по списку большому под контролем.

Заниматься конвертацией картинки в том же потоке не очень эффективно, да и хранить на сервере тоже не очень. Но если речь просто о пет проекте на дешевом впс, то тогда лучше вынести форматирование + сохранение картинки для модели в отдельную таску селери :)

Согласен с Вами. Что же будет являться лучшей практикой в этом случае?

Отложенная задача - нет возможности сразу посмотреть результат. В одном случае Вы загрузили большую картинку 3000х4000, django ресайзнула её до 1500х2000. И клиент\администратор сразу видит результат. А если процесс обработки отложен на 10 мин? Что там получится? А администратор уже следующую карточку товара пишет...

Там получится, что 1 клиент видит превью сразу, другие 100 не могут попасть на сайт, потому что ждут первого.

При типовой нагрузке в 100 RPS один ресайз картинки в секунду ничего не усложнит - это быстрая операция. Если добавлений много, то запись (куда картинка добавляется) не активна, админу показывается свёрстанный вариант, и только потом кнопка Опубликовать. Так, например, на drive2 делается. И никаких затыков.

При конвертации видео - там да, надо фоновым процессом, ибо операция длинная по времени.

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

Нет, не будут ждать. Nginx получит новый запрос, из пула web-воркеров выделится свободный процесс, отработает запрос и отдаст обратно.

Более того, отресайзить картинку локально будет быстрее, чем отправить её в удалённый сервис, подождать, загрузить обратно и сохранить. И это потребует не только процессорного времени, но и сети и дополнительного трафика. Тестировать всё это сложнее, а получить Too Many Requests или 502 ошибку очень легко.

выделится свободный процесс

конечно выделится, а если они заняты ресайзом и ресайз еще есть в очереди?

отресайзить картинку локально будет быстрее, чем отправить её в удалённый сервис

Работает это не так. Изображение сразу отправляется с клиента на апи, там и остаться. Ответ - только id изображения, а параметры как,урл, размер, формат, качество, вы хардкодите в html шаблон. Изображение ресайзится на лету по запросу.

Тестировать всё это сложнее

Наоборот проще, связности никакой с основным сервером. Пилите сервер и пилите, картинки не слетят.

> а получить Too Many Requests или 502 ошибку очень легко.

Ну как сделаете так и будет. Вы же сами контролируете частоту запросов, ваш же сервер.

Изображение сразу отправляется с клиента на апи, там и остаться. Ответ - только id изображения

Понял Вас. Но это уже выбивается из темы топика про django+imageField.

В целом, нужно хранить оригиналы фото, чтобы, при необходимости, пережать варианты. Попробуйте django-stdimage (или один из форков), там можно писать как-то так:

image = StdImageField(variations={'thumbnail': (100, 75), 'medium': (800,800)})

Для получения url: object.image.medium.url

И если нужно будет изменить размеры, то есть manage.py rendervariations

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

def save(self, *args, **kwargs):
    try:
        this = CompanyLogo.objects.get(id=self.id)
    except CompanyLogo.DoesNotExist:
        self.logo = self.compress_logo(self.logo)
    else:
        if this.logo != self.logo:
            this.logo.delete(save=False)
            self.logo = self.compress_logo(self.logo)
    finally:
        super(CompanyLogo, self).save(*args, **kwargs)

Я думаю, подобный код будет понятней и короче (не запускал, может в чем-то не прав).
Обработку ValueError лучше оставить на стороне compress_logo.

Возможно, стоит смотреть в сторону celery с пост сейв обработкой.

django-imagekit решает множество проблем.

А с форматом webp работает? Мне кажется, что нет.

Почему вам так кажется?

preview = ImageSpecField(source='file',
                             processors=[ResizeToFit(150, 150)],
                             format='WEBP',
                             options={'quality': 80})

Он работает со всеми форматами, с которыми работает pillow. Плюс, вы получается возможность управлять стратегией генерации изображений - во время загрузки или по требованию.

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

  1. Писать код ресайза в save() -- сомнительная идея.

  2. Батарейка django_resized позволяет ресайзить большие картинки при загрузке:
    image = ResizedImageField(size=[100, 100], crop=['top', 'left'], upload_to='whatever', force_format='PNG', scale=0.5)

  3. Принудительно кропить при загрузке -- обрежете голову. Уж лучше потом в шаблонах использовать sorl.thumbnail


  1. А куда надо писать, как вы считаете?

  2. Батарейки, вариант, но они с webp не работают.

  3. Вы вероятно говорите, про эту библиотек? Она с webp не работает, на сколько мне известно. А подрезать в шаблонах, в любом случае вы будете получать снижение производительности приложения. Ведь картинка будет подгружаться целиком, а потом сжимать в шаблоне, малоэффективно.

  1. Посмотрите, как написаны штатные (ImageField) и нештатные (как пример, ResizedImageField) - они сами берут на себя ответственность за обработку картинок. Простая мысль: Модель "Компания" должна отвечать за Компанию. Т.е. сделать self.slug = slugify(self.name) - уместно, поскольку это относится к Компании. А обработку картинок надо выносить. Вы же не будете писать 1-3-5 методов, если помимо лого будут ещё картинки загружаться.

  2. Батарейки работают. Смотрите зависимости. Большинство используют Pillow для картинок. Если Pillow умеет сохранять в webp - и их потомки тоже умеют.

  3. Нет. Не не так. Картинка не будет подгружаться целиком.

    {% thumbnail obj.image "400x400" crop="center" quality=95 as pixthumb %}
    <img ... src="{{ pixthumb }}">

    {% endthumbnail %}


    Тут при первом обращении сделается превью (ресайз, кроп, все дела), сохраниться на диск (в папку cache). Потом именно этот файл будет отдаваться по этому запросу.


    Проверьте - сами увидите.

Обязательно посмотрю. Спасибо.

В Django вы также можете сделать это внутри модели:

image = models.ImageField("Image", upload_to='img/', blank=True)
image_thumbnail_width_750 = ImageSpecField(source='image', processors=[Transpose(), ResizeToFill(width=750, height=450, upscale=False)], format='WEBP', options={'quality': 100})

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации