От автора: для чего нужно изменение размера изображения по требованию? Мы создаем шаблонное приложение — блог галереи с несколькими изображениями — для тестирования производительности и оптимизации. На этом этапе наше приложение обслуживает одно и то же изображение независимо от разрешения и размера экрана, на котором оно выполняется. В этом уроке мы изменим приложение, чтобы оно обслуживало версию изображения с другим размером в зависимости от размера экрана.
Статья входит в серию по созданию шаблонного приложения — блога галереи с несколькими изображениями — для бенчмаркинга и оптимизации производительности. (Репозиторий по ссылке).
Задача
Для этого улучшения необходимо:
Нам нужно, чтобы все изображения были адаптивными везде, где это было бы полезно. Одно место — это миниатюры на домашней странице и на страницах галереи, а другое — полноразмерное изображение, когда в галерее кликается отдельное изображение.
Нам нужно добавить в приложение логику изменения размера. Суть в том, чтобы создавать измененное изображение «на лету», поскольку оно требуется. Это приведет к тому, что непопулярные изображения засорят наш жесткий диск, и он будет следить за тем, чтобы популярные изображения при последующих запросах удовлетворялись в оптимальных размерах.
Адаптивные изображения?
Вместо простого старого <img src=»mypic.jpg»> у нас теперь есть что-то сумасшедшее:
<picture> <source media="(max-width: 700px)" sizes="(max-width: 500px) 50vw, 10vw" srcset="stick-figure-narrow.png 138w, stick-figure-hd-narrow.png 138w"> <source media="(max-width: 1400px)" sizes="(max-width: 1000px) 100vw, 50vw" srcset="stick-figure.png 416w, stick-figure-hd.png 416w"> <img src="stick-original.png" alt="Human"> </picture>
Комбинация srcset, picture и sizes необходима в сценарии, в котором вы сомневаетесь, если вы используете одно и то же изображение для меньшего размера экрана, основной объект изображения может стать слишком маленьким по размеру. Вы хотите отобразить другое изображение (более ориентированное на основной объект) в другом размере экрана, но все же хотите отображать отдельные файлы одного и того же изображения на основе соотношения пикселей устройства и хотите настроить высоту и ширину изображения на основе вьюпорта.
Поскольку наши изображения — это фотографии, и мы всегда хотим, чтобы они находились в заданной по умолчанию позиции DOM, заполняющей максимум своего родительского контейнера, нам не нужен picture (который позволяет нам определить альтернативный источник для другого разрешения или поддержки браузера — например, пытаться отобразить SVG, затем PNG, если SVG не поддерживается) или sizes (который позволяет нам определить, какую часть вьюпорта должно занимать изображение). Мы можем избавиться от использования только srcset, который загружает версию другого размера одного и того же изображения в зависимости от размера экрана.
Добавление srcset
Первое место, где мы сталкиваемся с изображениями, находится в home-galleries-lazy-load.html.twig , частичном шаблоне, который отображает список галерей домашнего экрана.
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}" class="gallery__leading-image card-img-top"> </a>
Мы видим здесь, что ссылка изображения извлекается из фильтра Twig, который можно найти в файле src/Twig/ImageRendererExtension.php. Он принимает идентификатор изображения и имя маршрута (определенное в аннотации в маршруте serveImageAction ImageController) и генерирует URL-адрес на основе этой формулы: /image/{id}/raw -> замените {id} указанным идентификатором:
public function getImageUrl(Image $image) { return $this->router->generate('image.serve', [ 'id' => $image->getId(), ], RouterInterface::ABSOLUTE_URL); }
Давайте изменим это на следующее:
public function getImageUrl(Image $image, $size = null) { return $this->router->generate('image.serve', [ 'id' => $image->getId() . (($size) ? '--' . $size : ''), ], RouterInterface::ABSOLUTE_URL); }
Теперь все URL-адреса изображений будут иметь —x как суффикс, где x — их размер. Это изменение мы применим и к нашему тегу img, в форме srcset. Давайте изменим его на:
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}" srcset=" {{ gallery.images.first|getImageUrl('1120') }} 1120w, {{ gallery.images.first|getImageUrl('720') }} 720w, {{ gallery.images.first|getImageUrl('400') }} 400w" class="gallery__leading-image card-img-top"> </a>
Если мы обновим домашнюю страницу сейчас, мы заметим, что новые размеры srcset перечислены:
Однако это не поможет нам. Если у нас широкий вьюпорт, это потребует полноразмерных изображений, несмотря на то, что они являются эскизами. Поэтому вместо srcset лучше использовать фиксированный размер миниатюр:
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl('250') }}" alt="{{ gallery.name }}" class="gallery__leading-image card-img-top"> </a>
Теперь у нас есть миниатюры по запросу, но они кэшируются и извлекаются, когда они уже созданы. Давайте теперь будем искать другие местоположения srcset.
В templates/gallery/single-gallery.html.twig мы применяем те же исправления, что и раньше. Мы имеем дело с эскизами, поэтому давайте просто сжимаем файл, добавляя параметр размера в наш фильтр getImageUrl:
<img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}" class="single-gallery__item-image card-img-top">
И теперь перейдем к реализации srcset, наконец! Представления отдельных изображений отображаются с помощью модального окна JavaScript в нижней части одного и того же вида галереи:
{% block javascripts %} {{ parent() }} <script> $(function () { $('.single-gallery__item-image').on('click', function () { var src = $(this).attr('src'); var $modal = $('.single-gallery__modal'); var $modalBody = $modal.find('.modal-body'); $modalBody.html(''); $modalBody.append($('<img src="' + src + '" class="single-gallery__modal-image">')); $modal.modal({}); }); }) </script> {% endblock %}
Есть вызов append который добавляет элемент img в тело модального окна. Именно здесь будет наш атрибут srcset. Но поскольку наши графические URL-адреса динамически генерируются, мы не можем действительно вызвать фильтр Twig из script. Один из вариантов заключается в том, чтобы добавить srcset в миниатюры, а затем использовать его в JS, скопировав его из элементов превью. Но это не только заставит загружать полноразмерные изображения на фоне эскизов (из-за широкого вьюпорта), но будет также вызываться фильтр 4 раза для каждой миниатюры, замедляя работу. Вместо этого давайте создадим новый фильтр Twig в src/Twig/ImageRendererExtension.php, который будет генерировать полный атрибут srcset для каждого изображения.
public function getImageSrcset(Image $image) { $id = $image->getId(); $sizes = [1120, 720, 400]; $string = ''; foreach ($sizes as $size) { $string .= $this->router->generate('image.serve', [ 'id' => $image->getId() . '--' . $size, ], RouterInterface::ABSOLUTE_URL).' '.$size.'w, '; } $string = trim($string, ', '); return html_entity_decode($string); }
Не нужно забывать регистрировать этот фильтр:
public function getFilters() { return [ new Twig_SimpleFilter('getImageUrl', [$this, 'getImageUrl']), new Twig_SimpleFilter('getImageSrcset', [$this, 'getImageSrcset']), ]; }
Мы должны добавить эти значения в пользовательский атрибут, который мы будем называть data-srcset для каждой отдельной миниатюры:
<img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}" data-srcset=" {{ image|getImageSrcset }}" class="single-gallery__item-image card-img-top">
Теперь каждый отдельный эскиз имеет атрибут data-srcset с требуемыми значениями srcset, но это не срабатывает, потому что он находится в пользовательском атрибуте, данные будут использоваться позже.
Последним шагом является обновление JS, чтобы воспользоваться этим:
{% block javascripts %} {{ parent() }} <script> $(function () { $('.single-gallery__item-image').on('click', function () { var src = $(this).attr('src'); var srcset = $(this).attr('data-srcset'); var $modal = $('.single-gallery__modal'); var $modalBody = $modal.find('.modal-body'); $modalBody.html(''); $modalBody.append($('<img src="' + src + '" srcset="" + srcset + '" class="single-gallery__modal-image">')); $modal.modal({}); }); }) </script> {% endblock %}
Добавление Glide
Glide — это библиотека, которая делает то, что мы хотим — изменение размера изображения по требованию. Давайте установим его.
composer require league/glide
Затем давайте зарегистрируем его в приложении. Мы делаем это, добавляя новую услугу в src/Services со следующим содержимым:
<?php namespace App\Service; use League\Glide; class GlideServer { private $server; public function __construct(FileManager $fm) { $this->server = $server = Glide\ServerFactory::create([ 'source' => $fm->getUploadsDirectory(), 'cache' => $fm->getUploadsDirectory().'/cache', ]); } public function getGlide() { return $this->server; } }
Служба расходует уже заявленную службу FileManager, которая автоматически вводится из-за нового подхода к автоматической подстройке Symfony. Мы объявляем как входной, так и выходной путь в качестве uploads dir, даем выходному каталогу суффикс cache и добавляем метод для возврата сервера. Сервер — это в основном экземпляр Glide, который выполняет изменение размера и возвращает изображение с измененным размером.
Нам нужно сделать метод getUploadsDirectory в FileManager общедоступным, так как он в настоящее время private:
public function getUploadsDirectory() { return $this->path; }
Наконец, давайте изменим метод serveImageAction в ImageController, чтобы он выглядел так:
/** * @Route("/image/{id}/raw", name="image.serve") */ public function serveImageAction(Request $request, $id, GlideServer $glide) { $idFragments = explode('--', $id); $id = $idFragments[0]; $size = $idFragments[1] ?? null; $image = $this->em->getRepository(Image::class)->find($id); if (empty($image)) { throw new NotFoundHttpException('Image not found'); } $fullPath = $this->fileManager->getFilePath($image->getFilename()); if ($size) { $info = pathinfo($fullPath); $file = $info['filename'] . '.' . $info['extension']; $newfile = $info['filename'] . '-' . $size . '.' . $info['extension']; $fullPathNew = str_replace($file, $newfile, $fullPath); if (file_exists($fullPath) && ! file_exists($fullPathNew)) { $fullPath = $fullPathNew; $img = $glide->getGlide()->getImageAsBase64($file, ['w' => $size]); $ifp = fopen($fullPath, 'wb'); $data = explode(',', $img); fwrite($ifp, base64_decode($data[1])); fclose($ifp); } } $response = new BinaryFileResponse($fullPath); $response->headers->set('Content-type', mime_content_type($fullPath)); $response->headers->set('Content-Disposition', 'attachment; filename="' . $image->getOriginalFilename() . '";'); return $response; }
Этот метод теперь разбивает идентификатор изображения двойным тире, отделяя размер от идентификатора изображения. Когда Doctrine извлекает путь к файлу изображения из базы данных, размер повторно прикрепляется к имени файла, если он был передан, в противном случае используется исходное изображение. Если это изображение не существует, оно создается из исходного пути и сохраняется для последующего использования.
Для демонстрационных целей мы используем более длинный путь и генерируем файлы вручную, добавляя их размер и сохраняя их в папке uploads. Следует отметить, что вы также можете использовать метод outputImage из Glide для прямого вывода изображения, и он будет обслуживаться непосредственно из папки cache, а не сохранять его с суффиксом в основной папке upload. Вы также можете использовать метод makeImage, чтобы просто создать изображение и позволить старой логике получать изображение. Это подход, который мы выбрали ниже:
/** * @Route("/image/{id}/raw", name="image.serve") */ public function serveImageAction(Request $request, $id, GlideServer $glide) { $idFragments = explode('--', $id); $id = $idFragments[0]; $size = $idFragments[1] ?? null; $image = $this->em->getRepository(Image::class)->find($id); if (empty($image)) { throw new NotFoundHttpException('Image not found'); } $fullPath = $this->fileManager->getFilePath($image->getFilename()); if ($size) { $info = pathinfo($fullPath); $file = $info['filename'] . '.' . $info['extension']; $cachePath = $glide->getGlide()->makeImage($file, ['w' => $size]); $fullPath = str_replace($file, '/cache/' . $cachePath, $fullPath); } $response = new BinaryFileResponse($fullPath); $response->headers->set('Content-type', mime_content_type($fullPath)); $response->headers->set('Content-Disposition', 'attachment; filename="' . $image->getOriginalFilename() . '";'); return $response; }
Наш логика по изменению размера изображения по требованию работает. Теперь все, что нам нужно сделать, это проверки.
Тестирование
Как только мы обновим домашнюю страницу, которая будет немного медленнее, изображения начнут генерироваться в папке var/uploads. Давайте проверим, не прокручивая вторую страницу.
Разумеется, теперь у нас есть крошечная миниатюрная версия каждого изображения на домашней странице, и ниже показано это изображение. Обратите внимание на размер небольших файлов. Теперь давайте посмотрим галерею и нажимаем на изображение, чтобы получить большую версию.
Да, наше изображение сформировано из оригинала. Но как насчет мобильных устройств? В современных браузерах достаточно легко включить мобильный режим. Попробуем открыть изображение галереи в мобильном режиме и затем проверить папку с изображением.
Что, если мы изменим ориентацию и проверим папку?
Успех, мобильный размер нашего изображения был успешно сгенерирован, а полноэкранное изображение было повторно использовано, потому что экран нашего «мобильного устройства» в альбомном режиме большой. srcset по требованию был успешно реализован! Приложение с этими обновлениями было помечено как этот релиз.
Заключение
В этой статье мы рассмотрели процесс оптимизации изображений для доставки на фото-ориентированный сайт. Мы сохранили миниатюры с фиксированным размером для достижения наилучших результатов, и с полноэкранными изображениями мы сосредоточились на внедрении srcset — простого дополнения к любому современному веб-сайту — в тандеме с Glide, пакетом для изменения размера изображения по требованию, который может выполнять тяжелую работу за нас.
Но пока мы изменяем размеры изображений, разве было бы разумно также автоматически оптимизировать их по качеству и размеру, удалив метаданные? И действительно ли это лучший вариант для их изменения по требованию, когда пользователь ждет или есть другой, более практичный подход? Узнаете в следующей части.
Автор: Bruno Skvorc
Источник: https://www.sitepoint.com/
Редакция: Команда webformyself.