29 июл. 2020 г.
Всем привет! Сегодня хотел бы поделиться опытом создания сервиса для генерации заглушек изображений(Placeholder).
Placeholder изображения могут использоваться при разработке макета. Когда реальной картинки нет, но при этом нужно что-то поставить на ее место. Вот пример такого шаблона.
Выглядит примерно так:
Обычно для генерации таких изображений используются сторонние сервисы, например https://placeholder.com/ и др.
Понятно, что, скорее всего, placeholder не должны использоваться в реальных проектах. В этом случае достаточно использовать сторонний сервис. Но можно представить ситуацию, что заглушки понадобится использовать в production. Например, это может понадобится при разработке конструктора сайтов. В этом случае зависимость от стороннего сервиса может быть проблемой.
В результате возникла идея написать небольшой сервис, который можно будет развернуть на собственном сервере. За основу был взять сервис https://placeholder.com/. Функционал практически полностью совпадает, однако API отличается. Решил сделать параметры более однозначными.
Сервис написан на PHP. Для генерации картинок используется библиотека PHP GD. Была идея сделать сервис в виде одного php файла, который можно будет добавить в любой проект. Также была идея оформить в виде библиотеки и установки через composer.
Но, в итоге, решил сделать в виде самостоятельного сервиса на Symfony 5.
Поддерживаются следующие опции:
Изображение отдается в формате png. Поддержку других форматов делать пока не стал.
Для запуска сервиса локально необходимо клонировать репозиторий
git clone https://github.com/antonshell/placeholder-service.git
Установить зависимости
cd placeholder-service
composer install
И запустить локальный сервер(либо настроить apache/nginx).
php -S 127.0.0.1:8000 public/index.php
Добавил 2 роута: index - для проверки состояния сервиса, image - непосредственно генерации картинки.
index:
path: /
controller: App\Controller\MainController::index
image:
path: /img
controller: App\Controller\MainController::image
В контроллере используется сервис placeholderGenerator.
public function image(Request $request): Response
{
$imageRequest = ImageRequest::create($request);
$resolution = $this->resolutionService->createFromRequest($imageRequest);
return $this->placeholderGenerator->generate(
$resolution->getWidth(),
$resolution->getHeight(),
$imageRequest->getText(),
$imageRequest->getTextSize(),
$imageRequest->getColorText(),
$imageRequest->getColorBg()
);
}
Генерация изображения реализована в PlaceholderGenerator. Изображение создается функцией imagecreatetruecolor, цвета фона и текста создаются с помощью imagecolorallocate. Затем изображение закрашивается с помощью imagefilledrectangle.
Для центрирования текста сначала высчитывается размер текстового блока с помощью функции imagettfbbox. Затем высчитываются координаты размещения текста. После этого текст создается и размещается с помощью функции imagettftext. Для использования вместе с Symfony Response изображение нужно сначала сохранить во временную директорию. После создания Response изображение можно удалить.
public function generate(
int $width,
int $height,
?string $text = null,
int $textSize = self::DEFAULT_TEXT_SIZE,
string $colorText = self::COLOR_WHITE,
string $colorBg = self::COLOR_GREY
): Response {
if($text === null) {
$text = sprintf('%sx%s', $width, $height);
}
// generate image
$im = imagecreatetruecolor($width, $height);
// create colors
$colorText = $this->hex2rgb($colorText);
$colorText = imagecolorallocate($im, $colorText->getRed(), $colorText->getGreen(), $colorText->getBlue());
$colorBg = $this->hex2rgb($colorBg);
$colorBg = imagecolorallocate($im, $colorBg->getRed(), $colorBg->getGreen(), $colorBg->getBlue());
// fill image with bg color
imagefilledrectangle($im, 0, 0, $width - 1, $height - 1, $colorBg);
//create text
$angle = 0;
$font = __DIR__ . '/../../resources/fonts/ArialRegular.ttf';
$points = imagettfbbox($textSize, $angle, $font, $text);
$textWidth = abs($points[2]);
$textHeight = abs($points[5]);
$textStartX = $width / 2 - $textWidth / 2;
$textStartY = $height / 2 + $textHeight / 2;
imagettftext($im, $textSize, $angle, $textStartX, $textStartY, $colorText, $font, $text);
// save image to temp folder
$hash = md5(sprintf('%s_%s_%s_%s_%s_%s', $width, $height, $text, $textSize, $colorText, $colorBg));
$filepath = sprintf(__DIR__ . '/../../temp/%s.png', $hash);
imagepng($im, $filepath);
imagedestroy($im);
// create response, remove image
$response = $this->createResponse($filepath);
unlink($filepath);
return $response;
}
Symfony Response создается таким образом:
private function createResponse(string $filepath): Response
{
$response = new Response();
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE, 'image.png');
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', 'image/png');
$response->headers->set('Content-length', filesize($filepath));
$response->sendHeaders();
$response->setContent(file_get_contents($filepath));
return $response;
}
Конвертация цветов из HEX в RGB:
private function hex2rgb(string $hex): ColorRgb
{
$hex = str_replace("#", "", $hex);
if(strlen($hex) == 3) {
$r = hexdec(substr($hex,0,1).substr($hex,0,1));
$g = hexdec(substr($hex,1,1).substr($hex,1,1));
$b = hexdec(substr($hex,2,1).substr($hex,2,1));
} else {
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
}
return new ColorRgb($r, $g, $b);
}
При желании, PlaceholderGenerator можно использовать отдельно от сервиса.
Для проверки генерации картинок используется md5 хэш. Хэш сгенернированной картинки сравнивается с хэшем существующего файла. Существующие файлы хранятся в resources/test_images. Тест выглядит таким образом:
public function testImage()
{
$configuration = [
[
'url' => '/img', // 300x300
'file' => 'img.png',
],
[
'url' => 'http://127.0.0.1:8000/img?width=500', // 500x500
'file' => 'img_width=500.png',
],
[
'url' => 'http://127.0.0.1:8000/img?height=400', // 400x400
'file' => 'img_height=400.png',
],
[
'url' => 'http://127.0.0.1:8000/img?width=320&height=240', // 320x240
'file' => 'img_width=320_height=240.png',
],
[
'url' => 'http://127.0.0.1:8000/img?text=Hello', // custom text
'file' => 'img_text=Hello.png',
],
[
'url' => 'http://127.0.0.1:8000/img?width=800&text_size=40', // text size
'file' => 'img_width=800_text_size=40.png',
],
[
'url' => 'http://127.0.0.1:8000/img?color_text=000', // text color
'file' => 'img_color_text=000.png',
],
[
'url' => 'http://127.0.0.1:8000/img?color_bg=000', // background color
'file' => 'img_color_bg=000.png',
],
];
$expectedFilesDir = __DIR__ . '/../../resources/test_images';
$client = static::createClient();
foreach ($configuration as $row) {
$client->request('GET', $row['url']);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$expectedFilePath = sprintf('%s/%s', $expectedFilesDir, $row['file']);
$expectedFileHash = md5(file_get_contents($expectedFilePath));
$contentHash = md5($client->getResponse()->getContent());
$this->assertEquals($expectedFileHash, $contentHash);
}
}
Код сервиса доступен на github: https://github.com/antonshell/placeholder-service. Также доступно demo.
На этом пока все. Спасибо за внимание!