Сервис генерации заглушек изображений

29 Jul 2020

Всем привет! Сегодня хотел бы поделиться опытом создания сервиса для генерации заглушек изображений(Placeholder).

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

Выглядит примерно так:

Обычно для генерации таких изображений используются сторонние сервисы, например https://placeholder.com/ и др.

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

В результате возникла идея написать небольшой сервис, который можно будет развернуть на собственном сервере. За основу был взять сервис https://placeholder.com/. Функционал практически полностью совпадает, однако API отличается. Решил сделать параметры более однозначными.

Технологии

Сервис написан на PHP. Для генерации картинок используется библиотека PHP GD. Была идея сделать сервис в виде одного php файла, который можно будет добавить в любой проект. Также была идея оформить в виде библиотеки и установки через composer.

Но, в итоге, решил сделать в виде самостоятельного сервиса на Symfony 5.

Функционал

Поддерживаются следующие опции:

  • width / height - задание ширины/высоты картинки
  • text - указание текста картинки
  • text_size - размер текста
  • color_text/color_bg - цвет текста/фона, задается в виде hex параметров

Изображение отдается в формате 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.

На этом пока все. Спасибо за внимание!

Теги: PHP, Images,