Простое решение для обработки изображений на лету с кешированием результата

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

    1. ресайз изображений под любые размеры (добавление новых размеров не должно вызывать головную боль)
    2. модификация изображений: добавление водяного знака, применение эффектов оттенки серого, сепия и вообще добавление новых эффектов не должно быть трудной задачей
    3. обработка изображения не должна влиять на основной поток (скорость загрузки страницы)
    4. для ускорения загрузки изображений на странице решение должно позволять обойти лимит одновременных соединений в браузерах, детальнее о лимите (рус)
    5. избежать возможность засорения сервера явной передачей параметров ресайза в url
    6. кешировать результаты работы


    Решение


    Для начала аргументирую выбор средств для решения данной задачи. Среди рассмотренных библиотек для работы с изображениями на PHP я выделил 3 основных: Wideimage, PHPThumb, Imagine. На последнюю и пал мой выбор, так как первые две очень давно не обновлялись и непонятно будут ли. Twig я выбрал в качестве шаблонизатора под которое я адаптировал данное решение. Ну и в качестве веб-серверов я выбрал Nginx и Apache. Nginx — для выделенных серверов, а Apache для работоспособности на шаровом хостинге.

    Репозиторий на GitHub

    Готовое решение выглядит так:
    на PHP
    echo thumb(__DIR__ . '/images/Chrysanthemum.jpg', '200x100', [ "watermark" => "right top" ]);
    

    на Twig
    <img src="{{ image|thumb("200x200", { "watermark": "right bottom", "grayscale": true }) }}" />
    

    Чтобы этот фильтр был доступен нужно подключить расширение для Twig.
    $twig->addExtension(new \Bazalt\Thumbs\Extension());
    


    1. Ресайз изображений под любые размеры

    Как видно второй параметр функции thumb принимает размер изображения, по умолчанию действует алгоритм обрезания краев под заданный размер, если после пропорционального уменьшение размеров одна из сторон вылазит. Если один из параметров поставить 0, то изображение пропорционально уменьшится, а размер второй стороны будет пропорционально высчитан, например, изображение размером 400x300 с параметром размера '200x0' на выходе будет иметь размер 200х150, а с размером '0x200' — 266x200.

    2. Модификация изображений

    Расширение функционала модификации изображений сделано очень просто. Есть класс Operations, который в базовой версии имеет только функцию size, чтобы добавить свой функционал его просто нужно унаследовать и описать в нем нужные функции. Третий параметр функции thumb отвечает как раз за вызов этих дополнительных модификаторов, это массив ключ которого название функции, а значение опции, которые будут переданы в эту функции.

    Пример нескольких модификаторов
    class Operations extends \Bazalt\Thumbs\Operations
    {
        public function watermark(\Imagine\Image\ImageInterface $image, $options, $allOptions)
        {
            $imagine = new \Imagine\Gd\Imagine();
            $wm = $imagine->open(__DIR__ . '/images/watermark.png');
    
            $size = $image->getSize();
            $wmSize = $wm->getSize();
            list($x, $y) = explode(' ', $options);
            if (!is_numeric($x)) {
                $x = ($x == 'right') ? ($size->getWidth() - $wmSize->getWidth()) : 0;
                if ($x < 0) $x = 0;
            }
            if (!is_numeric($y)) {
                $y = ($y == 'bottom') ? ($size->getHeight() - $wmSize->getHeight()) : 0;
                if ($y < 0) $y = 0;
            }
    
            $point = new \Imagine\Image\Point($x, $y);
            return $image->paste($wm, $point);
        }
    
        public function grayscale(\Imagine\Image\ImageInterface $image, $options, $allOptions)
        {
            $image->effects()->grayscale();
            return $image;
        }
    
        public function sepia(\Imagine\Image\ImageInterface $image, $options, $allOptions)
        {
            $image->effects()
                  ->grayscale()
                  ->colorize(new \Imagine\Image\Color('#643200'));
            return $image;
        }
    }
    



    3. Вынос обработки изображения из основной поток

    Обрабатывать изображение сразу же в том месте, где оно встречается в коде — это плохо, потому что с ростом количества изображений растет и скорость загрузки страницы. Решение простое — генерация обработанного изображение только при запросе к нему. В интернете очень много готовых решений, чем моё отличается от уже созданных так это тем, что я предлагаю сохранять параметры обработки изображение не в url в виде параметров GET запроса или частей самого url, а создавать некий файл конфигурации и записывать туда путь к изображению, размеры миниатюры и все остальные опции. Забегая наперед сразу хочу заметить, что это также решает 5 пункт поставленных требований.

    4. Обход лимита одновременных соединений в браузерах

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

    В коде можно использовать один из 3 вариантов настроек:
    // данный вариант я оставил для того чтобы можно было потестировать
    // локально без поднятия веб-сервера, через `php -S localhost:8080`
    \Bazalt\Thumbs\Image::initStorage(__DIR__ . '/static', '/thumb.php?file=/static');
    
    // этот вариант подходит если коректно настроет веб-сервер
    \Bazalt\Thumbs\Image::initStorage(__DIR__ . '/static', '/static');
    
    // данный вариант для коректно настроеного веб-сервера и cookieless доменов
    \Bazalt\Thumbs\Image::initStorage(__DIR__ . '/static', 'http://img%s.example.com/static');
    // на место %s подставляется значение в пределах 0x0-0xF в шестнадцатеричном представлении
    // img0.example.com, img1.example.com, ..., imge.example.com, imgf.example.com
    


    На крупном проекте рассматривается вариант CDN, но это выходит за рамки данной статьи.

    5. Избежание возможность засорения сервера явной передачей параметров ресайза в url

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

    6. Кеширование результатов работы

    Функция thumb просто генерирует конфигурационный файл, если такого еще нет. Имя этого файла вычисляется по хеш параметров обработки и имени файла изображения. Имя конфигурационного файла совпадает с именем миниатюры плюс некое расширение. Например, если по адресу "/static/d1/7e/d17e248758722c42d8c88d21d8b538d7.jpg" должна быть миниатюра, то конфигурационный файл будет называться "/static/d1/7e/d17e248758722c42d8c88d21d8b538d7.jpg.pre".
    Обработка выполняется уже когда браузер посылает второй запрос на получение миниатюры.

    Настройки для nginx:
    location /static/ {
        root /www/public;
    
        try_files $uri /thumb.php?file=$uri;
    }
    


    Для Apache:
    RewriteCond %{REQUEST_URI} ^(/static/)
    RewriteCond %{SCRIPT_FILENAME} !-f
    RewriteRule ^(.*)$ thumb.php?file=$1 [L]
    


    Как видно из настроек если файл уже существует, то веб-сервер отдает его напрямую, а если нет, то будет вызван файл thumb.php, который создаст миниатюру и сохранит её на диск
    Эти настройки не идеальны, просто чтобы показать идею.

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

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

      0
      Интересно насколько отличается по скорости проверка существования файла через file_exists и %{SCRIPT_FILENAME} !-f
        0
        Если Вы о file_exists который в php то тут и сравнивать не нужно.
        Проверка на уровне веб сервера и проверка с запуском php. Вроде как само должно говорить за себя.
          0
          Я имею ввиду выполнение самой функции, не учитывая запуск php
            –1
            Я думаю, это больше зависит от скорости файловой системы (размера кеша и т.д.). Просто по-моему в этом решении экономия по скорости не настолько ощутима, что бы применять эти непонятные файлы конфигов и настройки сервера. Почему бы не создавать миниатюры на этапе обработки шаблона?
              0
              Если создавать миниатюры на этапе обработки шаблона, то время генерации страницы будет расти в зависимости от количества изображений на ней и их качества. Легко добиться реальной ситуации когда пользователь просто может не дождаться загрузки страницы
              0
              Что позволит вам «не учитывать запуск php»? На это реально уйдет на порядок или два больше времени, чем на саму проверку.
                +1
                Ну так страницу же php и создает. он в любом случае запускается. Может вы просто не совсем поняли мою мысль. Я хотел бы понять насколько будет дольше работать иной алгоритм и есть ли смысл вообще делать так как автор.

                То что я имел ввиду:
                В шаблоне пишем {{ image|thumb(«200x200», { «watermark»: «right bottom», «grayscale»: true }) }} (это пример из статьи)
                На основе этих параметров обработчик создает хеш (можно приплюсовать timestamp для гарантированной уникальности). Получается имя файла превью. Если этого файла не существует (вот тут проверка file_exists), он создается. При следующем выводе этого файла ничего создаваться не будет, а просто подставится хеш в src. Клиент же получает ссылку на уже готовый созданный файл и пропадает необходимость в доп настройках сервера.

                Конечно, загрузка сайта будет медленнее при появлении новых картинок, но только один раз и только у того пользователя кто первым загружает страницу с новыми картинками. А на сайтах где картинки добавляются массово и нагрузки огромные, ни мой, ни автора вариант не прокатит. Там либо превью создается еще на этапе загрузки картинки, либо php вообще не участвует в этом.
                  0
                  Действительно, не так понял. Как вы предлагаете, тоже имеет право на жизнь и реализация в целом будет проще. Но вы правильно описали минусы: при большом кол-ве картинок пользователь увидит страницу немного позже.
          0
          /static/d1/7e/d17e248758722c42d8c88d21d8b538d7.jpg

          Не маловато ли будет 2х уровневой структуры? У меня /d/1/7/d17e248758722c42d8c88d21d8b538d7.jpg и некоторые папки имеют больше сотен файлов. Хотя тут конечно от проекта зависит.
            +3
            У вас файлы раскиданы по 16^3 папкам, у esvit по 16^4. Уровень влажности не причем.
              0
              Точно. Спасибо. От уровня зависит только количество папок в папке. У меня оно 16 у автора 16^2.
                0
                Было бы странно, если бы уровень влажности влиял на размещение файлов.
              0
              Прошу прощение за глупый вопрос, ибо темень я деревенская.
              В коде на гитхабе есть запись
              function thumb($image, $size, $options = [])
              

              И собственно вопрос
              $options = []
              

              что это за запись. Ведь php 5.3.25 выдает ошибку
              Parse error: syntax error, unexpected '[' in
                +3
                Это php 5.4, короткое объявление массива, '[]' это аналогия 'array()' на php 5.3

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

              Самое читаемое