1 <?php
declare(strict_types
=1);
4 * This file is part of the Rapsys PackBundle package.
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Rapsys\PackBundle\Util
;
14 use Symfony\Component\Asset\PackageInterface
;
15 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
16 use Symfony\Component\Filesystem\Filesystem
;
19 * Helps manage facebook images
51 * The package instance
53 * @var PackageInterface
72 * Creates a new osm util
74 * @param PackageInterface $package The package instance
75 * @param string $cache The cache directory
76 * @param string $path The public path
77 * @param string $url The public url
78 * @param string $prefix The prefix
80 function __construct(PackageInterface
$package, string $cache, string $path, string $url, string $prefix = 'facebook', string $source = 'png/facebook.png', array $fonts = [ 'default' => 'ttf/default.ttf' ]) {
82 $this->cache
= $cache.'/'.$prefix;
85 $this->fonts
= $fonts;
88 $this->path
= $path.'/'.$prefix;
91 $this->url
= $url.'/'.$prefix;
93 //Set package instance
94 $this->package
= $package;
97 $this->prefix
= $prefix;
100 $this->source
= $source;
104 * Return the facebook image
106 * Generate simple image in jpeg format or load it from cache
108 * @param string $pathInfo The request path info
109 * @param array $texts The image texts
110 * @param int $updated The updated timestamp
111 * @param ?string $source The image source
112 * @param int $width The width
113 * @param int $height The height
114 * @return array The image array
116 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
118 $path = $this->path
.$pathInfo.'.jpeg';
120 //Without existing path
121 if (!is_dir($dir = dirname($path))) {
122 //Create filesystem object
123 $filesystem = new Filesystem();
127 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
128 $filesystem->mkdir($dir, 0775);
129 } catch (IOExceptionInterface
$e) {
131 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
136 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
137 #XXX: we used to drop texts with $data['canonical'] === true !!!
141 #'og:image' => $this->package->getAbsoluteUrl('@RapsysAir/facebook/'.$mtime.$pathInfo.'.jpeg'),
142 'og:image' => $this->package
->getAbsoluteUrl($this->url
.'/'.$mtime.$pathInfo.'.jpeg'),
143 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
144 'og:image:height' => $height,
145 'og:image:width' => $width
150 $cache = $this->cache
.$pathInfo.'.png';
153 if (!is_dir($dir = dirname($cache))) {
154 //Create filesystem object
155 $filesystem = new Filesystem();
159 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
160 $filesystem->mkdir($dir, 0775);
161 } catch (IOExceptionInterface
$e) {
163 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
168 if ($source === null) {
170 $source = realpath($this->source
);
173 //Create image object
174 $image = new \
Imagick();
176 //Without cache image
177 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
178 //Check target directory
179 if (!is_dir($dir = dirname($cache))) {
180 //Create filesystem object
181 $filesystem = new Filesystem();
185 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
186 $filesystem->mkdir($dir, 0775);
187 } catch (IOExceptionInterface
$e) {
189 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
194 $image->readImage($source);
196 //Crop using aspect ratio
197 //XXX: for better result upload image directly in aspect ratio :)
198 $image->cropThumbnailImage($width, $height);
200 //Strip image exif data and properties
201 $image->stripImage();
204 if (!$image->writeImage($cache)) {
206 throw new \
Exception(sprintf('Unable to write image "%s"', $cache));
211 $image->readImage($cache);
215 $draw = new \
ImagickDraw();
217 //Set stroke antialias
218 $draw->setStrokeAntialias(true);
221 $draw->setTextAntialias(true);
225 'left' => \Imagick
::ALIGN_LEFT
,
226 'center' => \Imagick
::ALIGN_CENTER
,
227 'right' => \Imagick
::ALIGN_RIGHT
231 $defaultFont = 'dejavusans';
234 $defaultAlign = 'center';
240 $defaultStroke = '#00c3f9';
246 $defaultFill = 'white';
252 $count = count($texts);
254 //Draw each text stroke
255 foreach($texts as $text => $data) {
257 $draw->setFont($this->fonts
[$data['font']??$defaultFont]);
260 $draw->setFontSize($data['size']??$defaultSize);
263 $draw->setStrokeWidth($data['width']??$defaultWidth);
266 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
269 $metrics = $image->queryFontMetrics($draw, $text);
272 if (empty($data['y'])) {
273 //Position verticaly each text evenly
274 $texts[$text]['y'] = $data['y'] = (($height +
100) / (count($texts) +
1) * $i) - 50;
278 if (empty($data['x'])) {
279 if ($align == \Imagick
::ALIGN_CENTER
) {
280 $texts[$text]['x'] = $data['x'] = $width/2;
281 } elseif ($align == \Imagick
::ALIGN_LEFT
) {
282 $texts[$text]['x'] = $data['x'] = 50;
283 } elseif ($align == \Imagick
::ALIGN_RIGHT
) {
284 $texts[$text]['x'] = $data['x'] = $width - 50;
289 //XXX: add ascender part then center it back by half of textHeight
290 //TODO: maybe add a boundingbox ???
291 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2;
294 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$defaultStroke));
297 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$defaultStroke));
300 $draw->annotation($data['x'], $data['y'], $text);
306 //Create stroke object
307 $stroke = new \
Imagick();
310 $stroke->newImage($width, $height, new \
ImagickPixel('transparent'));
313 $stroke->drawImage($draw);
316 //XXX: blur the stroke canvas only
317 $stroke->blurImage(5,3);
320 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
321 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
);
324 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0);
336 $draw->setTextAntialias(true);
339 foreach($texts as $text => $data) {
341 $draw->setFont($this->fonts
[$data['font']??$defaultFont]);
344 $draw->setFontSize($data['size']??$defaultSize);
347 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
350 $draw->setFillColor(new \
ImagickPixel($data['fill']??$defaultFill));
353 $draw->annotation($data['x'], $data['y'], $text);
355 //With canonical text
356 if (!empty($data['canonical'])) {
357 //Prevent canonical to finish in alt
358 unset($texts[$text]);
363 $image->drawImage($draw);
365 //Strip image exif data and properties
366 $image->stripImage();
369 $image->setImageFormat('jpeg');
371 //Set progressive jpeg
372 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
375 if (!$image->writeImage($path)) {
377 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
382 'og:image' => $this->package
->getAbsoluteUrl($this->url
.'/'.stat($path)['mtime'].$pathInfo.'.jpeg'),
383 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
384 'og:image:height' => $height,
385 'og:image:width' => $width