Универсальная анимация средствами nginx

Автор: | 24.08.2015

Стандартом де-факто в Интернете для анимированных изображений является Graphics Interchange Format, более известный как GIF. Формат достаточно хороший: его понимает любой браузер и есть тысячи инструментов для работы с ним. Но это всё ничто, когда тебе надо запихнуть в один кадр более 256 цветов, да ещё и с полупрозрачностью. И вроде бы ерунда — думаешь ты — есть же ещё APNG, WebP, BPG и MNG…

Но про последние два можно забыть, так как пока ни один браузер их не поддерживает. Проверяем какие современные браузеры поддерживают APNG… Не густо — только Mozilla Firefox и Apple Safari. С форматом WebP ситуация не сильно лучше — Google Chrome и его производные. В целом забавная ситуация получается. Если формат GIF поддерживают все браузеры, то множество браузеров с поддержкой APNG никак не пересекается с множеством браузеров с поддержкой WebP. А Microsoft IE/Edge вообще ничего из этого не знает. Поэтому тут просто обязана быть эта картинка:
0f239
Тут можно опустить руки и вернуться к использованию старого доброго GIF. Можно попытаться заставить работать APNG в Chrome с помощью apng-canvas или наоборот, заставить работать WebP в Firefox с помощью WebPJS. Но было решено пойти другим путём и использовать все три формата: для Firefox и Safari — APNG, для Chrome — WebP, а для всех остальных — GIF. То есть если браузер поддерживает APNG или WebP, то отдаём ему анимированную картинку в хорошем качестве. Если не поддерживает — отдаём GIF, уж какой есть. А так как приоритет у задачи смещён на скорость, а не на точность, то определять какой же у пользователя браузер будет web-сервер, а именно nginx.


Очевидное решение, которое просто плавает на поверхности, это парсить HTTP-заголовок User-Agent, и пытать определить браузер и его версию. Поэтому в нужном нам location просто прописываем if ($http_user_agent ~ MSIE) Стоп! Делать это через директиву if как-то громоздко будет, да и вообще If Is Evil. У nginx для решения такой задачи есть другой инструмент, который позволит реализовать задуманное намного эффективней и элегантней. Но про это чуть позже, а для начала надо разобраться с User-Agent, а точнее на что конкретно надо обращать внимание. Взяв за основу информацию с Mozilla Developer Network и изучив различные User-Agent на сайте Udger team, сделал для себя некоторые выводы:

  • Internet Explorer не такой как все. Его UA до 10 версии включительно обязательно содержит «MSIE», а в 11 версии просто «IE». Так же можно искать по «Trident».
  • Edge не такой, как Internet Explorer. Его UA содержит «Edge», но при этом так же содержит «Chrome» и «Safari» (см. ниже).
  • У Firefox всё просто — его UA, как это не странно, содержит «Firefox».
  • UA Chrome, помимо очевидного «Chrome», содержит ещё и «Safari».
  • У Safari, как и у Firefox, всё просто — надо искать «Safari»… Ну почти просто.
  • С Opera всё сложно — в юности UA содержал «Opera» и «Presto», а более ранние версии вообще имели клеймо «MSIE». У современных версий содержится уникальный «OPR», но так же есть уже традиционные «Chrome» и «Safari».
  • С мобильными браузерами ситуация, в целом, аналогичная.

Очевидно, что ключевые слова «Chrome» и «Safari» содержаться в User-Agent чуть менее, чем всех браузеров. На практике с этим могут быть проблемы, так что надо быть готовым оперативно вносить корректировки и исключения в регулярные выражения. К слову, приведённые ниже регулярные выражения намеренно упрощены в целях удобства восприятия и последующего администрирования.

И вот теперь, когда мы знаем что и как надо искать, пора приступить к настройке nginx. Как я уже говорил — директиву if не стоит использовать. Для этих целей куда удобнее директива map. Данная директива является мощным инструментом, который позволяет присвоить значение одной переменной в зависимости от значения другой. В попытке собрать всё вышесказанное воедино, у меня появился вот такой конфигурационный файл nginx (приведён только изменённый фрагмент):

http {
    map $http_user_agent $browser {
        default                           "legacy";
        "~(Edge|MSIE|Trident)"            "legacy";
        "~Android [2-4]\."                "legacy";
        "~Opera Mini"                     "legacy";
        "~Firefox/([3-9]|[1-5][0-9])\."   "gecko";
        "~Opera/9\.[5-9]"                 "gecko";
        "~OPR/(19|[2-4][0-9])\."          "blink";
        "~Chrome/(3[2-9]|[4-5][0-9])\."   "blink";
        "~Version/[8-9]\..*Safari/"       "gecko";
    }

    server {
        location /images/ {
            try_files $uri @$browser;
        }

        location @blink {
            default_type image/webp;
            try_files /path/to/images/for/blink/$arg_img.webp =404;
        }

        location @gecko {
            default_type image/png;
            try_files /path/to/images/for/gecko/$arg_img.png =404;
        }

        location @legacy {
            default_type image/gif;
            try_files /path/to/images/for/legacy/$arg_img.gif =404;
        }
    }
}

Как видно, благодаря директиве map, переменная $browser принимает одно из трёх значений («blink», «gecko» или «legacy») в зависимости от значения переменной $http_user_agent, то есть в зависимости от User-Agent браузера пользователя. Директива поддерживает специальный параметр default, который позволяет задать значение по умолчанию — «legacy». Так же записываем в «legacy» все продукты Microsoft и Opera Mini. Согласно данным Can I Use, Android полноценно поддерживает WebP ещё с версии 4.3, но на имеющихся в моём распоряжении устройствах с Android KitKat, штатный браузер не хотел работать с этим форматом. Поэтому все устройства на Android ниже Lollipop тоже «legacy», на всякий случай. С Firefox проще всего, ведь его User-Agent достаточно уникален и он поддерживает формат APNG ещё с версии 3.0 (2008 год). Достаточно просто искать «Firefox» в UA, но можно и сделать выборку по версиям — в моём примере это версии с 3 по 59.

Остались три самых проблемных браузера. Opera, за время своего развития, успела сменить движок с Presto на Blink, так что по сути имеем два разных браузера, которые поддерживают две разные технологии. Кстати, приверженцев старой Opera ещё достаточно много, хотя она устарела 3 года назад. Версии Opera с 9.5 по 12 поддерживают формат APNG, а Opera на движке Blink поддерживает формат WebP. Первую группу определяем по ключевому слову «Opera» и версии 9.5+. Дело в том, что даже 12 версия представляется как «Opera/9.80», а уже конкретная версия браузера записана отдельно. Поэтому достаточно проверить, чтобы major=9, а minor>5. Новая версия со своего рождения понимает WebP, однако анимированный WebP только с 19 версии.

Проверку на Google Chrome делаем предпоследней, т.к. ключевое слово «Chrome» достаточно популярно, то есть сначала надо отфильтровать другие браузеры. Chrome поддерживает анимированный WebP-формат начиная с 32 версии, поэтому в примере ищутся версии с 32 по 59. И только в самом конце осуществляем проверку на Apple Safari, ибо ключевое слово «Safari» не использует только ленивый и Mozilla Firefox. Тут есть особенность — у Safari версия браузера записана отдельно, поэтому регулярное выражение немного отличается от остальных браузеров. В примере определяются версии 8 и 9.

Обновление от 05.11.2017. Если дело касается только форматов APNG и WebP, то можно поступить проще:

map $http_accept $browser {
    default "legacy";
    "~*image/apng" "gecko";
    "~*image/webp" "blink";
}

С определением браузера разобрались. Следующим шагом надо решить, как воспользоваться этим знанием. В моём примере используется такой подход:

  • Есть некий сайт http://example.com/.
  • Картинки у этого сайта доступны по адресу http://example.com/images/<filename>.<ext>.
  • Но если обратиться по адресу http://example.com/images/?img=filename, то будет запущена процедура выдачи изображения в том формате, который поддерживает браузер.

На самом деле можно сделать как угодно, ведь nginx очень мощный инструмент. Например, не менять систему адресации на сайте и для картинок по адресу http://example.com/images/boobs.gif отдавать APNG для Firefox и WebP для Chrome.

Разберём подробнее, что же у нас происходит в контексте server. В location /images/ с помощью директивы try_files пытаемся отдать запрошенный файл и если таковой не находим, то передаём запрос в именованный location, а в какой именно определяет переменная $browser. В соответствующем именованном location задаём правильный заголовок Content-Type и отдаём файл нужного типа с именем filename (переменная $arg_img), а если такого файла нет, то возвращаем 404 ошибку.

На этом собственно всё. В качестве заключения приведу пример практической реализации вышесказанного. Перед вами две статичные обезьяны. Первая в GIF, а вторая в формате PNG. Обратите внимание на разницу в качестве из-за потери полупрозрачности:

monkey1monkey2

А теперь эти же обезьяны, но уже анимированные. Первая в формате GIF — её должно быть видно в любом случае, а вот вторая будет в одном из трёх форматов, в зависимости от вашего браузера:

monkey

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *