На сегодняшний день практически в любом веб-приложении использующем изображения существует потребность формировать уменьшенные копии этих изображений с возможной некоторой дополнительной модификацией, например: водяной знак, оттенки серого, сепия и т.д.
Для детализации обозначим такой список требований:
- ресайз изображений под любые размеры (добавление новых размеров не должно вызывать головную боль)
- модификация изображений: добавление водяного знака, применение эффектов оттенки серого, сепия и вообще добавление новых эффектов не должно быть трудной задачей
- обработка изображения не должна влиять на основной поток (скорость загрузки страницы)
- для ускорения загрузки изображений на странице решение должно позволять обойти лимит одновременных соединений в браузерах, детальнее о лимите (рус)
- избежать возможность засорения сервера явной передачей параметров ресайза в url
- кешировать результаты работы
Решение
Для начала аргументирую выбор средств для решения данной задачи. Среди рассмотренных библиотек для работы с изображениями на PHP я выделил 3 основных: Wideimage, PHPThumb, Imagine. На последнюю и пал мой выбор, так как первые две очень давно не обновлялись и непонятно будут ли. Twig я выбрал в качестве шаблонизатора под которое я адаптировал данное решение. Ну и в качестве веб-серверов я выбрал Nginx и Apache. Nginx — для выделенных серверов, а Apache для работоспособности на шаровом
Готовое решение выгладит так:
на 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 BazaltThumbsExtension());
1. Ресайз изображений под любые размеры
Как видно второй параметр функции thumb
принимает размер изображения, по умолчанию действует алгоритм обрезания краев под заданный размер, если после пропорционального уменьшение размеров одна из сторон вылазит. Если один из параметров поставить 0, то изображение пропорционально уменьшится, а размер второй стороны будет пропорционально высчитан, например, изображение размером 400x300 с параметром размера '200x0' на выходе будет иметь размер 200х150, а с размером '0x200' — 266x200.
2. Модификация изображений
Расширение функционала модификации изображений сделано очень просто. Есть класс Operations, который в базовой версии имеет только функцию size
, чтобы добавить свой функционал его просто нужно унаследовать и описать в нем нужные функции. Третий параметр функции thumb
отвечает как раз за вызов этих дополнительных модификаторов, это массив ключ которого название функции, а значение опции, которые будут переданы в эту функции.
class Operations extends BazaltThumbsOperations
{
public function watermark(ImagineImageImageInterface $image, $options, $allOptions)
{
$imagine = new ImagineGdImagine();
$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 ImagineImagePoint($x, $y);
return $image->paste($wm, $point);
}
public function grayscale(ImagineImageImageInterface $image, $options, $allOptions)
{
$image->effects()->grayscale();
return $image;
}
public function sepia(ImagineImageImageInterface $image, $options, $allOptions)
{
$image->effects()
->grayscale()
->colorize(new ImagineImageColor('#643200'));
return $image;
}
}
3. Вынос обработки изображения из основной поток
Обрабатывать изображение сразу же в том месте, где оно встречается в коде — это плохо, потому что с ростом количества изображений растет и скорость загрузки страницы. Решение простое — генерация обработанного изображение только при запросе к нему. В интернете очень много готовых решений, чем моё отличается от уже созданных так это тем, что я предлагаю сохранять параметры обработки изображение не в url в виде параметров GET запроса или частей самого url, а создавать некий файл конфигурации и записывать туда путь к изображению, размеры миниатюры и все остальные опции. Забегая наперед сразу хочу заметить, что это также решает 5 пункт поставленных требований.
4. Обход лимита одновременных соединений в браузерах
По стандарту HTTP 1.1 браузер не может грузить одновременно более 2 запросов с одного и того же домена. Решение? Сделать несколько поддоменов для статики (cookieless domain) заодно и трафик будет экономится за счет того, что не будут передаваться лишние куки.
В коде можно использовать один из 3 вариантов настроек:
// данный вариант я оставил для того чтобы можно было потестировать
// локально без поднятия веб-сервера, через `php -S localhost:8080`
BazaltThumbsImage::initStorage(__DIR__ . '/static', '/thumb.php?file=/static');
// этот вариант подходит если коректно настроет веб-сервер
BazaltThumbsImage::initStorage(__DIR__ . '/static', '/static');
// данный вариант для коректно настроеного веб-сервера и cookieless доменов
BazaltThumbsImage::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, который создаст миниатюру и сохранит её на диск
Эти настройки не идеальны, просто чтобы показать идею.
В итоге мы имеем простое решение такой частой задачи, я пытался описать все подводные камни с которыми сталкивался, если что-то упустил буду благодарен за сдельные комментарии.
Автор: esvit