29 Jul 2020
Hello! Today I would like to share experience of creating placeholder image placeholder service.
Placeholder images can be used for website template development. In case there is no real image, but we need to use something instead of it. There is an example of such template.
It looks like that:
External services are ususally used for this, for example https://placeholder.com/ etc.
It's clear, that in most cases placeholders shouldn't be used in real projects. Then it's ok to use external service for placeholders. But it could be such case, when placeholders need to be used in production. For example, it can be website constructor. In this case external service for placeholders could be a problem.
Then I decided to implement small self-hosted web service for placeholders generation. Service inspired by https://placeholder.com/. Functional is almost completely the same. But API is different, because I decided to make it more clear.
Service built with PHP. PHP GD library used for images generation. Initially there was idea to make it as a single php file, that can be easily added to any project. Another option was to create library that can be installed with composer.
But finally I decided to make it as a separate service, built with Symfony 5.
There are following options:
Image always created in png format for now.
For local install need to clone repository
git clone https://github.com/antonshell/placeholder-service.git
Install dependencies
cd placeholder-service
composer install
Start PHP local server(or configure apache/nginx).
php -S 127.0.0.1:8000 public/index.php
Added 2 routes: index - for service health check, image - for image generation.
index:
path: /
controller: App\Controller\MainController::index
image:
path: /img
controller: App\Controller\MainController::image
Then PlaceholderGenerator service is used in MainController.
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()
);
}
Image generation happens in PlaceholderGenerator. Image created by imagecreatetruecolor function, text and background colors are created with imagecolorallocate. Then image filled with imagefilledrectangle.
For text centering need to do some calculations. Text block size calculated with imagettfbbox function, Then text coordinates are calculated and text created with imagettftext function. For Symfony Response capability image should be stored in the temporary directory. Image can be deleted after Response created.
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 created with such code:
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;
}
Colors need to be converted from HEX to 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);
}
Probably, PlaceholderGenerator can be used separately from placeholder-service.
Md5 hash is used for testing correct image generation. Hash of created image compared with hash of existing file. Existing files stored in resources/test_images. Test looks like that:
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);
}
}
Placeholder service is available on github: https://github.com/antonshell/placeholder-service. There is also working demo.
That's all for today. Thank you for your attention!