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\Filesystem\Exception\IOExceptionInterface
;
15 use Symfony\Component\Filesystem\Filesystem
;
16 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
17 use Symfony\Component\Routing\RouterInterface
;
20 * Helps manage facebook images
28 protected string $align;
35 protected string $cache;
42 protected string $fill;
49 protected string $font;
56 protected array $fonts;
63 protected string $path;
70 protected string $prefix;
73 * The RouterInterface instance
75 protected RouterInterface
$router;
89 protected ?string $source;
96 protected string $stroke;
103 protected int $width;
106 * Creates a new facebook util
108 * @param RouterInterface $router The RouterInterface instance
109 * @param string $cache The cache directory
110 * @param string $path The public path
111 * @param string $prefix The prefix
112 * @param ?string $source The source
113 * @param array $fonts The fonts
114 * @param string $font The font
115 * @param int $size The size
116 * @param int $width The width
117 * @param string $fill The fill
118 * @param string $stroke The stroke
119 * @param string $align The align
121 function __construct(RouterInterface
$router, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'facebook', ?string $source = null, array $fonts = [ 'default' => 'ttf/default.ttf' ], string $font = 'default', int $size = 60, int $width = 15, string $fill = 'white', string $stroke = '#00c3f9', string $align = 'center') {
123 $this->align
= $align;
126 $this->cache
= $cache.'/'.$prefix;
135 $this->fonts
= $fonts;
138 $this->path
= $path.'/'.$prefix;
141 $this->prefix
= $prefix;
144 $this->router
= $router;
150 $this->source
= $source;
153 $this->stroke
= $stroke;
156 $this->width
= $width;
160 * Return the facebook image
162 * Generate simple image in jpeg format or load it from cache
164 * @param string $pathInfo The request path info
165 * @param array $texts The image texts
166 * @param int $updated The updated timestamp
167 * @param ?string $source The image source
168 * @param int $width The width
169 * @param int $height The height
170 * @return array The image array
172 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
174 if ($source === null && $this->source
=== null) {
175 //Return empty image data
177 //Without local source
178 } elseif ($source === null) {
180 $source = $this->source
;
184 $path = $this->path
.$pathInfo.'.jpeg';
186 //Without existing path
187 if (!is_dir($dir = dirname($path))) {
188 //Create filesystem object
189 $filesystem = new Filesystem();
193 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
194 $filesystem->mkdir($dir, 0775);
195 } catch (IOExceptionInterface
$e) {
197 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
202 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
203 #XXX: we used to drop texts with $data['canonical'] === true !!!
207 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => $mtime, 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
208 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
209 'og:image:height' => $height,
210 'og:image:width' => $width
215 $cache = $this->cache
.$pathInfo.'.png';
218 if (!is_dir($dir = dirname($cache))) {
219 //Create filesystem object
220 $filesystem = new Filesystem();
224 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
225 $filesystem->mkdir($dir, 0775);
226 } catch (IOExceptionInterface
$e) {
228 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
232 //Create image object
233 $image = new \
Imagick();
235 //Without cache image
236 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
237 //Check target directory
238 if (!is_dir($dir = dirname($cache))) {
239 //Create filesystem object
240 $filesystem = new Filesystem();
244 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
245 $filesystem->mkdir($dir, 0775);
246 } catch (IOExceptionInterface
$e) {
248 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
253 if (!is_file($source)) {
255 throw new \
Exception(sprintf('Source file "%s" do not exists', $this->source
));
258 //Convert to absolute path
259 $source = realpath($source);
262 //XXX: Imagick::readImage only supports absolute path
263 $image->readImage($source);
265 //Crop using aspect ratio
266 //XXX: for better result upload image directly in aspect ratio :)
267 $image->cropThumbnailImage($width, $height);
269 //Strip image exif data and properties
270 $image->stripImage();
273 if (!$image->writeImage($cache)) {
275 throw new \
Exception(sprintf('Unable to write image "%s"', $cache));
280 $image->readImage($cache);
284 $draw = new \
ImagickDraw();
286 //Set stroke antialias
287 $draw->setStrokeAntialias(true);
290 $draw->setTextAntialias(true);
294 'left' => \Imagick
::ALIGN_LEFT
,
295 'center' => \Imagick
::ALIGN_CENTER
,
296 'right' => \Imagick
::ALIGN_RIGHT
303 $count = count($texts);
305 //Draw each text stroke
306 foreach($texts as $text => $data) {
308 $draw->setFont($this->fonts
[$data['font']??$this->font
]);
311 $draw->setFontSize($data['size']??$this->size
);
314 $draw->setStrokeWidth($data['width']??$this->width
);
317 $draw->setTextAlignment($align = ($aligns[$data['align']??$this->align
]));
320 $metrics = $image->queryFontMetrics($draw, $text);
323 if (empty($data['y'])) {
324 //Position verticaly each text evenly
325 $texts[$text]['y'] = $data['y'] = (($height +
100) / (count($texts) +
1) * $i) - 50;
329 if (empty($data['x'])) {
330 if ($align == \Imagick
::ALIGN_CENTER
) {
331 $texts[$text]['x'] = $data['x'] = $width/2;
332 } elseif ($align == \Imagick
::ALIGN_LEFT
) {
333 $texts[$text]['x'] = $data['x'] = 50;
334 } elseif ($align == \Imagick
::ALIGN_RIGHT
) {
335 $texts[$text]['x'] = $data['x'] = $width - 50;
340 //XXX: add ascender part then center it back by half of textHeight
341 //TODO: maybe add a boundingbox ???
342 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2;
345 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$this->stroke
));
348 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$this->stroke
));
351 $draw->annotation($data['x'], $data['y'], $text);
357 //Create stroke object
358 $stroke = new \
Imagick();
361 $stroke->newImage($width, $height, new \
ImagickPixel('transparent'));
364 $stroke->drawImage($draw);
367 //XXX: blur the stroke canvas only
368 $stroke->blurImage(5,3);
371 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
372 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
);
375 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0);
387 $draw->setTextAntialias(true);
390 foreach($texts as $text => $data) {
392 $draw->setFont($this->fonts
[$data['font']??$this->font
]);
395 $draw->setFontSize($data['size']??$this->size
);
398 $draw->setTextAlignment($aligns[$data['align']??$this->align
]);
401 $draw->setFillColor(new \
ImagickPixel($data['fill']??$this->fill
));
404 $draw->annotation($data['x'], $data['y'], $text);
406 //With canonical text
407 if (!empty($data['canonical'])) {
408 //Prevent canonical to finish in alt
409 unset($texts[$text]);
414 $image->drawImage($draw);
416 //Strip image exif data and properties
417 $image->stripImage();
420 $image->setImageFormat('jpeg');
422 //Set progressive jpeg
423 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
426 if (!$image->writeImage($path)) {
428 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
433 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => stat($path)['mtime'], 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
434 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
435 'og:image:height' => $height,
436 'og:image:width' => $width