Статьи

Динамическая доставка изображений с CloudFront

Изображения могут стать основной головной болью для разработчиков веб-приложений, особенно если ваше приложение позволяет пользователям загружать свои собственные изображения. Они проблематичны по двум причинам:

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

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

В прошлом я разрабатывал всевозможные решения для решения этой проблемы. Моим оригинальным решением было хранить 10 разных размеров каждого изображения, каждое из которых было создано во время загрузки. Это работало хорошо, но это было убийственно на диске и не всегда было так гибко, как мне было нужно. Если мне нужен был новый размер изображения, я должен был изменить размер каждого существующего изображения в системе. Вот это боль.

Лучшее решение

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

Вот сценарий: вы загружаете на свой сервер изображение «Penguins.jpg» с разрешением 1024 x 768 пикселей. Вы хотите использовать это изображение в качестве миниатюры размером 80 пикселей — что вы делаете?

Использование исходного изображения, очевидно, не сработает. Это слишком много.

С решением, которое я продемонстрирую, все, что вам нужно сделать, это указать следующий URL в вашем источнике:

<img src="//dynamiccdn.example.com/maxD_80/imagestore/Penguins.jpg" alt="" />

Это было легко! Простое добавление префикса к имени изображения с помощью «maxD_80» автоматически изменяет его размер до максимальной ширины 80 пикселей с самой длинной стороны.

Это нормально, но что, если вы хотите использовать квадратный эскиз в качестве фонового изображения и хотите, чтобы на широких или высоких изображениях не было белых пробелов по краю? Простое решение:

 <img src="//dynamiccdn.example.com/minD_80/imagestore/Penguins.jpg" alt="" />

Сейчас мы говорим! Использование «minD_80» вместо «maxD_80» приводит к получению максимально маленького изображения, но с наименьшей стороной не менее 80 пикселей. Теперь это выглядит идеально!

Круто … но как?

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

  • Учетная запись веб-служб Amazon с включенным CloudFront
  • Веб-сервер под управлением Apache (вы можете сделать это и в Windows, выполнив дополнительную работу)
  • Немного навыков PHP (Вы можете копировать и вставлять?)

Настройка веб-сервера

Каталог: imagestore
Начните с создания каталога с именем imagestore в корневом каталоге вашего веб-сайта. Здесь вы будете хранить все «оригинальные» изображения. Вы можете организовать их любым способом в этой папке. А пока, поместите туда несколько больших файлов jpg для тестирования.

Файл .htaccess
Вам нужно добавить несколько строк в файл .htaccess в корне вашей сети (просто создайте файл, если он не существует):

 RewriteEngine On 
RewriteCond %{QUERY_STRING} (.*) 
RewriteRule ^([^/]*)/imagestore/(.*).(jpg|jpeg)$ image.php?format=$1&amp;path=imagestore/$2.$3

Этот код сообщает серверу отправлять любой запрос изображений jpg из каталога «imagestore» (или любого из его подкаталогов) в файл «image.php» (который мы создадим далее).

Файл PHP: image.php
Создайте этот файл в корне вашей сети. Этот файл будет выполнять всю работу по обработке запросов на форматированные изображения.

Код

Давайте пройдемся по нему шаг за шагом. Для простоты мы предполагаем, что все ваши изображения в формате jpg. Все следующие фрагменты кода идут прямо в файл image.php, который вы только что создали.

Давайте начнем с выбора переменных строки запроса (формат и путь), представленных правилом перезаписи, который мы ранее добавили в наш файл .htaccess:

 $path = $_GET['path'];
$formats = explode(',', $_GET['format']);

Мы создаем массив (используя разнесение) из формата. Это позволит нам объединить любое количество команд форматирования на одном изображении, разделяя каждую из них запятой в URL.

Теперь создайте объект изображения, используя переменную $ path, которую мы только что создали:

 $image = imagecreatefromjpeg($path);

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

 header("Content-Type: image/jpeg"); 
header("Cache-control: Public"); 
$headerTimeOffset = 60 * 60 * 24 * 30; 
$headeExpire = "Expires: ".gmdate("D, d M Y H:i:s",time()+$headerTimeOffset)." GMT"; 
header ($headeExpire);

Далее мы пройдемся по массиву форматов, который мы создали ранее, и применим функцию форматирования изображения (formatImage), которую мы скоро создадим.

 foreach ($formats as $format) { 
	$image = formatImage($format, $image); 
}

Наконец, выведите изображение и уничтожьте объект:

 imagejpeg($image, Null, 100); 
imagedestroy($image);

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

 // Translates commands (like maxD_80 or minD_40) from your URL
// into the correct resampling functions.
function formatImage($format, $image) {
	// Get the original image sizes.
	$curW = imagesx($image);
	$curH = imagesy($image);
	// Split the format into parts separeted by "_"
	$formatParts = explode("_", $format);
	switch ($formatParts[0]) {
		// If the format command is maxD, resize the image
		// so that the largest side is the specified
		// number of pixels ($formatParts[1])
		case "maxD":
			if ($curW &gt; $curH) {
				$ratio = $curH / $curW;
				$newW = (int) maxProtect($formatParts[1]);
				$newH = (int) (maxProtect($formatParts[1]) * $ratio);
			} else {
				$ratio = $curW / $curH;
				$newW = (int) (maxProtect($formatParts[1]) * ratio);
				$newH = (int) maxProtect($formatParts[1]);
			}
			$image = resizeImage($image, $newW, $newH);
			break;
		// If the format command is minD, resize the image
		// so that the smallest side is the specified
		// number of pixels ($formatParts[1])
		case "minD":
			if ($curW  800) $value = 800;
	return $value;
}

Уф! Мы уже закончили писать код? Да.

Время тестировать

Просто перейдите к изображениям на вашем веб-сайте в папке imagestore. Теперь в своем URL добавьте префикс с помощью команды maxD или minD. Помните, что эти команды требуют значения после них, разделенного подчеркиванием. Пример: maxD_55

Так что, если ваш оригинальный URL-адрес изображения выглядит следующим образом:
http://yourownwebsite.com/imagestore/Penguins.jpg

Ваш новый URL будет выглядеть так:
http://yourownwebsite.com/maxD_55/imagestore/Penguins.jpg

А как насчет облака?

Хотя интересно динамически создавать размеры изображений с простыми изменениями URL-адреса вашего изображения, на этом останавливаться не практично. Изменение размера изображения требует очень много ресурсов процессора, и обработка всех запросов на все ваши изображения все время действительно приводит к сбоям в работе вашего сервера.

CloudFront предоставляет чрезвычайно простое решение. Он предназначен для того, чтобы вы могли легко распространять контент с низкой задержкой через глобальную сеть периферийных местоположений. Другими словами, он автоматически снимает нагрузку с вашего сервера и приближает ваш контент (географически) к вашим клиентам.

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

Начните с входа в консоль управления Amazon Web Services:

Найдите вкладку CloudFront в консоли управления и нажмите «Создать рассылку»:

Откроется мастер создания рассылки. Записи, которые вы здесь делаете, важны. Сначала выберите «Custom Origin». В поле «Исходное DNS-имя» введите имя своего домена, в котором находятся каталог imagestore и файл image.php. В соответствии с политикой протокола я рекомендую выбрать Match Viewer.

Вы можете просмотреть следующие два раздела (Детали распространения и Обзор). После этого вам нужно подождать около 15 минут, пока AWS создаст ваш дистрибутив.

Создав свой дистрибутив, вы получите DNS-имя для использования. Это будет выглядеть так: abcdefg1234.cloudfront.net

Теперь пришло время завершить решение. Вместо создания таких изображений:

Теперь вы будете ссылаться на ваши изображения следующим образом:

 <img src="//abcdefg1234.cloudfront.net/minD_80/imagestore/Penguins.jpg" alt="" />

Вот и все — вы сделали! Теперь всякий раз, когда запрашивается это изображение, запрос переходит к CloudFront. При первом запросе изображения CloudFront запросит его с вашего сервера. После этого он будет хранить его в своем кэше и мгновенно доставлять для новых запросов. Когда он истекает, он просто запросит его снова с вашего сервера.

Что дальше?

Еще многое можно сделать, чтобы эта система работала лучше для вашего приложения. Вот несколько идей:

Ручка 404с
Убедитесь, что ваш скрипт настроен на обработку случаев, когда запрашиваются изображения, которых нет на вашем сервере.

Обрабатывать другие форматы изображения
Настройте систему для работы с JPEG, PNG, GIF и любым другим форматом изображений, который вы используете.

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

Водяные знаки, обрезка и многое другое!
Этот тип системы имеет так много возможностей. Легко настроить его, чтобы добавить водяной знак для каждого изображения или обрезать изображения с определенным соотношением размеров.

Интеграция с Amazon S3
Начать можно с сохранения оригиналов на вашем сервере, но на самом деле вы должны использовать Amazon S3 или другой сервис облачного хранения для хранения ваших оригиналов. Внесение этого изменения гарантирует, что вам никогда не придется беспокоиться о резервном копировании или масштабируемости.

Используйте CNAME
Не нравится обслуживание изображений с доменного имени cloudfront.net? Нет проблем, вы можете настроить CloudFront на использование своего собственного домена, такого как cdn.yourdomain.com.

Добавить безопасность
Этот тип системы позволяет людям «поиграть» с вашими изображениями. Если кто-то является злоумышленником, он может использовать эту систему, чтобы действительно заглушить ваш сервер (атака DoS) Вы можете предотвратить это разными способами. Мой любимый способ — использовать кодирование и шифрование для части формата URL-адреса изображения. Это добавляет немного работы, но не дает людям возможности поразить ваш сервер неограниченными запросами на изображения неограниченного размера. Есть много других способов предотвратить злоупотребления … главное — помнить, что вам нужно что-то поставить на место.

Веселитесь с этим!

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

Если у вас есть предложения по улучшению или улучшению системы, пожалуйста, оставьте свои комментарии ниже. И, конечно, если у вас есть вопросы — пожалуйста, задавайте!


Вопросы и ответы

Что если мне нужно получить доступ к исходной версии моего изображения?
Нет проблем! Просто используйте любой префикс. Мне нравится использовать «о» для оригинала. Так что мой оригинальный URL-адрес изображения будет выглядеть так:
http://dynamiccdn.example.com/o/imagestore/Penguins.jpg

Почему я не могу просто использовать переменные строки запроса после своего изображения, чтобы указать форматирование?
Это было бы круто … но Amazon CloudFront игнорирует эти переменные.

Этот пример работает только для файла JPG. Я люблю PNG больше. Что дает??
Чтобы сделать код максимально простым, этот пример обрабатывает только файлы jpg. Но вы можете настроить скрипт для обработки любого формата изображения, который вы можете использовать.

Почему вы ссылаетесь на ваши изображения, начинающиеся с «//» вместо «http: //»?
Хороший вопрос. Использование «//» заставляет URL ссылаться на тот же протокол (http или https), что и страница, которую пользователь просматривает в данный момент. Это действительно удобно и предотвращает появление в некоторых браузерах предупреждений о «небезопасном контенте».

Пример полного сценария

Вот полный рабочий скрипт image.php. Он включает в себя множество дополнительных удобных опций формата. Все это менее 100 строк кода!

 $path = $_GET['path'];
$formats = explode(',', $_GET['format']);
$image = imagecreatefromjpeg($path);
header('Content-Type: image/jpeg');
header('Cache-control: Public');
$headerTimeOffset = 60 * 60 * 24 * 30;
$headeExpire = "Expires: " . gmdate ("D, d M Y H:i:s", time() + $headerTimeOffset) . " GMT";
header ($headeExpire);

foreach ($formats as $format) {
    $image = formatImage($format, $image);
}

imagejpeg($image, Null, 100);
imagedestroy($image);

function resizeImage($originalImage,$toWidth,$toHeight) {
    $width = imagesx($originalImage);
    $height = imagesy($originalImage);
    $imageResized = imagecreatetruecolor($toWidth, $toHeight);
    imagecopyresampled($imageResized, $originalImage, 0, 0, 0, 0, $toWidth, $toHeight, $width, $height);
    return $imageResized;
}

function formatImage($format, $image) {
    $curW = imagesx($image);
    $curH = imagesy($image);
    $formatParts = explode('_', $format);
    switch ($formatParts[0]) {
        case 'filterGray':
            #### Make it Grayscale
            imagefilter($image, IMG_FILTER_GRAYSCALE);
            break;
        case 'filterBlur':
            #### Blur It Up
            imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
            break;
        case 'maxD':
            #### Maximum Either Dimension - maxD_100
            if ($curW &gt; $curH) {
$ratio = $curH / $curW;
$newW = (int) maxProtect($formatParts[1]);
$newH = (int) (maxProtect($formatParts[1]) * $ratio);
            } else {
$ratio = $curW / $curH;
$newW = (int) (maxProtect($formatParts[1]) * ratio);
$newH = (int) maxProtect($formatParts[1]);
            }
            $image = resizeImage($image, $newW, $newH);
            break;
        case 'minD':
            #### Minimum Smallest Dimension - maxD_100
            if ($curW  800) $value = 800;
    return $value;
}

Изображение через Марк Дитрих / Shutterstock