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 Psr\Container\ContainerInterface
;
16 use Rapsys\PackBundle\RapsysPackBundle
;
18 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
19 use Symfony\Component\Filesystem\Filesystem
;
20 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
21 use Symfony\Component\Routing\RouterInterface
;
24 * Helps manage facebook images
30 protected string $alias;
35 protected array $config;
38 * Creates a new facebook util
40 * @param ContainerInterface $container The container instance
41 * @param RouterInterface $router The RouterInterface instance
42 * @param SluggerUtil $slugger The SluggerUtil instance
44 public function __construct(protected ContainerInterface
$container, protected RouterInterface
$router, protected SluggerUtil
$slugger) {
46 $this->config
= $container->getParameter($this->alias
= RapsysPackBundle
::getAlias());
50 * Return the facebook image
52 * Generate simple image in jpeg format or load it from cache
54 * @TODO: move to a svg merging system ?
56 * @param string $path The request path info
57 * @param array $texts The image texts
58 * @param int $updated The updated timestamp
59 * @param ?string $source The image source
60 * @param ?int $height The height
61 * @param ?int $width The width
62 * @return array The image array
64 public function getImage(string $path, array $texts, int $updated, ?string $source = null, ?int $height = null, ?int $width = null): array {
66 if ($source === null && $this->config
['facebook']['source'] === null) {
67 //Return empty image data
69 //Without local source
70 } elseif ($source === null) {
72 $source = $this->config
['facebook']['source'];
76 if ($width === null) {
77 //Set width from config
78 $width = $this->config
['facebook']['width'];
82 if ($height === null) {
83 //Set height from config
84 $height = $this->config
['facebook']['height'];
88 $facebook = $this->config
['cache'].'/'.$this->config
['prefixes']['facebook'].$path.'.jpeg';
90 //Without existing path
91 if (!is_dir($dir = dirname($facebook))) {
92 //Create filesystem object
93 $filesystem = new Filesystem();
97 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
98 $filesystem->mkdir($dir, 0775);
99 } catch (IOExceptionInterface
$e) {
101 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
106 if (is_file($facebook) && ($mtime = stat($facebook)['mtime']) && $mtime >= $updated) {
107 #XXX: we used to drop texts with $data['canonical'] === true !!!
110 $short = $this->slugger
->short($path);
113 $hash = $this->slugger
->serialize([$short, $height, $width]);
117 'og:image' => $this->router
->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => $mtime], UrlGeneratorInterface
::ABSOLUTE_URL
),
118 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
119 'og:image:height' => $height,
120 'og:image:width' => $width
125 $cache = $this->config
['cache'].'/'.$this->config
['prefixes']['facebook'].$path.'.png';
128 if (!is_dir($dir = dirname($cache))) {
129 //Create filesystem object
130 $filesystem = new Filesystem();
134 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
135 $filesystem->mkdir($dir, 0775);
136 } catch (IOExceptionInterface
$e) {
138 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
142 //Create image object
143 $image = new \
Imagick();
145 //Without cache image
146 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
147 //Check target directory
148 if (!is_dir($dir = dirname($cache))) {
149 //Create filesystem object
150 $filesystem = new Filesystem();
154 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
155 $filesystem->mkdir($dir, 0775);
156 } catch (IOExceptionInterface
$e) {
158 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
163 if (!is_file($source)) {
165 throw new \
Exception(sprintf('Source file "%s" do not exists', $source));
168 //Convert to absolute path
169 $source = realpath($source);
172 //XXX: Imagick::readImage only supports absolute path
173 $image->readImage($source);
175 //Crop using aspect ratio
176 //XXX: for better result upload image directly in aspect ratio :)
177 $image->cropThumbnailImage($width, $height);
179 //Strip image exif data and properties
180 $image->stripImage();
183 if (!$image->writeImage($cache)) {
185 throw new \
Exception(sprintf('Unable to write image "%s"', $cache));
190 $image->readImage($cache);
194 $draw = new \
ImagickDraw();
196 //Set stroke antialias
197 $draw->setStrokeAntialias(true);
200 $draw->setTextAntialias(true);
204 'left' => \Imagick
::ALIGN_LEFT
,
205 'center' => \Imagick
::ALIGN_CENTER
,
206 'right' => \Imagick
::ALIGN_RIGHT
213 $count = count($texts);
215 //Draw each text stroke
216 foreach($texts as $text => $data) {
218 $draw->setFont($this->config
['fonts'][$data['font']??$this->config
['facebook']['font']]);
221 $draw->setFontSize($data['size']??$this->config
['facebook']['size']);
224 $draw->setStrokeWidth($data['thickness']??$this->config
['facebook']['thickness']);
227 $draw->setTextAlignment($align = ($aligns[$data['align']??$this->config
['facebook']['align']]));
230 $metrics = $image->queryFontMetrics($draw, $text);
233 if (empty($data['y'])) {
234 //Position verticaly each text evenly
235 $texts[$text]['y'] = $data['y'] = (($height +
100) / (count($texts) +
1) * $i) - 50;
239 if (empty($data['x'])) {
240 if ($align == \Imagick
::ALIGN_CENTER
) {
241 $texts[$text]['x'] = $data['x'] = $width/2;
242 } elseif ($align == \Imagick
::ALIGN_LEFT
) {
243 $texts[$text]['x'] = $data['x'] = 50;
244 } elseif ($align == \Imagick
::ALIGN_RIGHT
) {
245 $texts[$text]['x'] = $data['x'] = $width - 50;
250 //XXX: add ascender part then center it back by half of textHeight
251 //TODO: maybe add a boundingbox ???
252 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2;
255 $draw->setStrokeColor(new \
ImagickPixel($data['border']??$this->config
['facebook']['border']));
258 $draw->setFillColor(new \
ImagickPixel($data['fill']??$this->config
['facebook']['fill']));
261 $draw->annotation($data['x'], $data['y'], $text);
267 //Create stroke object
268 $stroke = new \
Imagick();
271 $stroke->newImage($width, $height, new \
ImagickPixel('transparent'));
274 $stroke->drawImage($draw);
277 //XXX: blur the stroke canvas only
278 $stroke->blurImage(5,3);
281 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
282 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
);
285 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0);
297 $draw->setTextAntialias(true);
300 foreach($texts as $text => $data) {
302 $draw->setFont($this->config
['fonts'][$data['font']??$this->config
['facebook']['font']]);
305 $draw->setFontSize($data['size']??$this->config
['facebook']['size']);
308 $draw->setTextAlignment($aligns[$data['align']??$this->config
['facebook']['align']]);
311 $draw->setFillColor(new \
ImagickPixel($data['fill']??$this->config
['facebook']['fill']));
314 $draw->annotation($data['x'], $data['y'], $text);
316 //With canonical text
317 if (!empty($data['canonical'])) {
318 //Prevent canonical to finish in alt
319 unset($texts[$text]);
324 $image->drawImage($draw);
326 //Strip image exif data and properties
327 $image->stripImage();
330 $image->setImageFormat('jpeg');
332 //Set progressive jpeg
333 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
336 if (!$image->writeImage($facebook)) {
338 throw new \
Exception(sprintf('Unable to write image "%s"', $facebook));
342 $short = $this->slugger
->short($path);
345 $hash = $this->slugger
->serialize([$short, $height, $width]);
349 'og:image' => $this->router
->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => stat($facebook)['mtime']], UrlGeneratorInterface
::ABSOLUTE_URL
),
350 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
351 'og:image:height' => $height,
352 'og:image:width' => $width