Для KinsburgTV нужно было сделать ресайз картинок. Сначала я скопировал с одного из своих проектов шаблонный тег с ресайзингом (берущий корни от этого сниппета), но для продакшена столь медленное решение казалось неприемлемым. Поэтому я написал другой шаблонный тег, который строит нужный URL для Nginx, и далее его обрабатывает ngx_http_image_filter_module и proxy_cache.


У меня есть график для сравнения, который я сделал через JMeter, но он к сожелению оказался неинформативен. Если кому всётаки интересно, то вот этот график. Слева вариант на PIL, справа Nginx. Выборка на 1000 реквестов.


Получился быстрый, прозрачный для приложения ресайзер и кроппер с возможностью кешировать результаты, а также с командой для конвертирования картинок в подходящий формат. Итак, встречайте django-nginx-image!

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

Если вас волнует вопрос перебора ширины и высоты в URL, то можно указать нужные правила для ограничения размеров непосредственно в Nginx.

Установка и настройка

Ставим с PyPI:

pip install django-nginx-image

Далее необходимо настроить Nginx (приведены настройки, относящиеся только к теме обсуждения):

  • <STORAGE_ROOT> - путь до каталога с "media/statiс" (например, у меня это "/storage/kinsburg_tv");
  • <CACHE_NAME> - произвольное имя для кеша (например, "kinsburg_tv_thumbnails_cache").
http {

    # Укажите необходимый путь до каталога кеша, имя кеша и максимальный размер кеша
    proxy_cache_path <STORAGE_ROOT>/nginx/cache levels=1:2 keys_zone=<CACHE_NAME>:10m max_size=1G;

    # Теперь настроим сервер, который будет кешировать результаты
    server {
        listen 80;
        server_name www.example.org;

        location ~* ^/(resize|crop)/ {
            proxy_pass http://image.example.org$request_uri;
            proxy_cache <CACHE_NAME>;
            proxy_cache_key "$host$document_uri";
            proxy_cache_valid 200 1d;
            proxy_cache_valid any 1m;
            proxy_cache_use_stale error timeout invalid_header updating;
        }
    }

    # И сервер, который будет выполнять resize и crop
    server {
        listen 80;
        server_name image.example.org;

        location ~* ^/resize/([\d\-]+)/([\d\-]+)/(.+)$ {
            alias <STORAGE_ROOT>/$3;
            image_filter resize $1 $2;
            image_filter_buffer 2M;
            error_page 415 = /empty;
        }

        location ~* ^/crop/([\d\-]+)/([\d\-]+)/(.+)$ {
            alias <STORAGE_ROOT>/$3;
            image_filter crop $1 $2;
            image_filter_buffer 2M;
            error_page 415 = /empty;
        }

        location = /empty {
            empty_gif;
        }
    }
}

Если вам не нужно кеширование, то просто удалите локейшен из первого "server" и перенесите локейшены из второго "server" в первый. Кеширование мне дало прирост производительности на 10-20%.

Схема, иллюстрирующая работу:

Схема, иллюстрирующая работу

Использование

В любом своём шаблоне подключите модуль nginx_image с шаблонным тегом thumbnail, передайте ему путь до картинки, ширину, высоту и опционально флаг crop, чтобы картинка обрезалась, а не ресайзилась:

{% load nginx_image %}

Пропорциональный ресайз картинки, основанный на её ширине и высоте (доминирует высота):
    <img src="{% thumbnail user.profile.avatar 130 130 %}" />

Пропорциональный ресайз картинки, основанный на её ширине:
    <img src="{% thumbnail user.profile.avatar 130 '-' %}" />
    <img src="{% thumbnail user.profile.avatar 130 0 %}" />
    <img src="{% thumbnail user.profile.avatar 130 %}" />

Пропорциональный ресайз картинки, основанный на её высоте:
    <img src="{% thumbnail user.profile.avatar '-' 130 %}" />
    <img src="{% thumbnail user.profile.avatar 0 130 %}" />

Обрезать картинку:
    <img src="{% thumbnail user.profile.avatar 130 130 crop=1 %}" />
    <img src="{% thumbnail user.profile.avatar 130 0 crop=1 %}" />
    <img src="{% thumbnail user.profile.avatar 0 130 crop=1 %}" />

В общем этот тег только и делает, что формирует URL вида:

  • /resize/130/-/media/users/avatars/12345.jpg
  • /resize/-/130/media/users/avatars/12345.jpg
  • /crop/130/-/media/users/avatars/12345.jpg

Этот тег можно использовать вне шаблона, например:

class Film(models.Model):
    thumbnail = models.ImageField(upload_to='films/thumbnails', null=True)

    def thumbnail_preview_url(self):
        from nginx_image.templatetags.nginx_image import thumbnail
        return thumbnail(self.thumbnail, 160, 140, crop=True)

Команда nginx_image_converter

К сожалению (или к счастью), ngx_http_image_filter_module поддерживает только JPEG, GIF и PNG, поэтому пришлось писать конвертер, преобразующий форматы, отличные от вышеназванных, в JPEG.

./manage.py nginx_image_converter -i /storage/project/media -o /storage/project/newmedia

Дополнительные опции этой команды смотреть тут.

Проблема с Nginx resolver

Если вам встретилась примерно такая ошибка в nginx/error.log:

... no resolver defined to resolve image.example.org ...

то укажите resolver в nginx/nginx.conf, например так:

resolver 127.0.0.1 8.8.8.8;

Поддержка image_filter в Nginx

Чтобы добавить поддержку этого модуля в ваш Nginx, необходимо пересобрать Nginx. В Debian надо просто доустановить nginx-extras:

sudo apt-get install nginx-extras

Во FreeBSD надо пересобрать порт:

cd /usr/ports/www/nginx
make config

Выбираем опцию HTTP_IMAGE_FILTER и переустанавливаем:

make deinstall && make install clean

Всё.