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
52 * The RouterInterface instance
54 protected RouterInterface
$router;
64 * Creates a new facebook util
66 * @param RouterInterface $router The RouterInterface instance
67 * @param string $cache The cache directory
68 * @param string $path The public path
69 * @param string $prefix The prefix
71 function __construct(RouterInterface
$router, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'facebook', string $source = 'png/facebook.png', array $fonts = [ 'default' => 'ttf/default.ttf' ]) {
73 $this->cache
= $cache.'/'.$prefix;
76 $this->fonts
= $fonts;
79 $this->path
= $path.'/'.$prefix;
82 $this->router
= $router;
85 $this->prefix
= $prefix;
88 $this->source
= $source;
92 * Return the facebook image
94 * Generate simple image in jpeg format or load it from cache
96 * @param string $pathInfo The request path info
97 * @param array $texts The image texts
98 * @param int $updated The updated timestamp
99 * @param ?string $source The image source
100 * @param int $width The width
101 * @param int $height The height
102 * @return array The image array
104 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
106 $path = $this->path
.$pathInfo.'.jpeg';
108 //Without existing path
109 if (!is_dir($dir = dirname($path))) {
110 //Create filesystem object
111 $filesystem = new Filesystem();
115 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
116 $filesystem->mkdir($dir, 0775);
117 } catch (IOExceptionInterface
$e) {
119 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
124 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
125 #XXX: we used to drop texts with $data['canonical'] === true !!!
129 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => $mtime, 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
130 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
131 'og:image:height' => $height,
132 'og:image:width' => $width
137 $cache = $this->cache
.$pathInfo.'.png';
140 if (!is_dir($dir = dirname($cache))) {
141 //Create filesystem object
142 $filesystem = new Filesystem();
146 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
147 $filesystem->mkdir($dir, 0775);
148 } catch (IOExceptionInterface
$e) {
150 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
155 if ($source === null) {
157 $source = realpath($this->source
);
160 //Create image object
161 $image = new \
Imagick();
163 //Without cache image
164 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
165 //Check target directory
166 if (!is_dir($dir = dirname($cache))) {
167 //Create filesystem object
168 $filesystem = new Filesystem();
172 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
173 $filesystem->mkdir($dir, 0775);
174 } catch (IOExceptionInterface
$e) {
176 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
181 $image->readImage($source);
183 //Crop using aspect ratio
184 //XXX: for better result upload image directly in aspect ratio :)
185 $image->cropThumbnailImage($width, $height);
187 //Strip image exif data and properties
188 $image->stripImage();
191 if (!$image->writeImage($cache)) {
193 throw new \
Exception(sprintf('Unable to write image "%s"', $cache));
198 $image->readImage($cache);
202 $draw = new \
ImagickDraw();
204 //Set stroke antialias
205 $draw->setStrokeAntialias(true);
208 $draw->setTextAntialias(true);
212 'left' => \Imagick
::ALIGN_LEFT
,
213 'center' => \Imagick
::ALIGN_CENTER
,
214 'right' => \Imagick
::ALIGN_RIGHT
218 $defaultFont = 'dejavusans';
221 $defaultAlign = 'center';
227 $defaultStroke = '#00c3f9';
233 $defaultFill = 'white';
239 $count = count($texts);
241 //Draw each text stroke
242 foreach($texts as $text => $data) {
244 $draw->setFont($this->fonts
[$data['font']??$defaultFont]);
247 $draw->setFontSize($data['size']??$defaultSize);
250 $draw->setStrokeWidth($data['width']??$defaultWidth);
253 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
256 $metrics = $image->queryFontMetrics($draw, $text);
259 if (empty($data['y'])) {
260 //Position verticaly each text evenly
261 $texts[$text]['y'] = $data['y'] = (($height +
100) / (count($texts) +
1) * $i) - 50;
265 if (empty($data['x'])) {
266 if ($align == \Imagick
::ALIGN_CENTER
) {
267 $texts[$text]['x'] = $data['x'] = $width/2;
268 } elseif ($align == \Imagick
::ALIGN_LEFT
) {
269 $texts[$text]['x'] = $data['x'] = 50;
270 } elseif ($align == \Imagick
::ALIGN_RIGHT
) {
271 $texts[$text]['x'] = $data['x'] = $width - 50;
276 //XXX: add ascender part then center it back by half of textHeight
277 //TODO: maybe add a boundingbox ???
278 $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2;
281 $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$defaultStroke));
284 $draw->setFillColor(new \
ImagickPixel($data['stroke']??$defaultStroke));
287 $draw->annotation($data['x'], $data['y'], $text);
293 //Create stroke object
294 $stroke = new \
Imagick();
297 $stroke->newImage($width, $height, new \
ImagickPixel('transparent'));
300 $stroke->drawImage($draw);
303 //XXX: blur the stroke canvas only
304 $stroke->blurImage(5,3);
307 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
308 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
);
311 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0);
323 $draw->setTextAntialias(true);
326 foreach($texts as $text => $data) {
328 $draw->setFont($this->fonts
[$data['font']??$defaultFont]);
331 $draw->setFontSize($data['size']??$defaultSize);
334 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
337 $draw->setFillColor(new \
ImagickPixel($data['fill']??$defaultFill));
340 $draw->annotation($data['x'], $data['y'], $text);
342 //With canonical text
343 if (!empty($data['canonical'])) {
344 //Prevent canonical to finish in alt
345 unset($texts[$text]);
350 $image->drawImage($draw);
352 //Strip image exif data and properties
353 $image->stripImage();
356 $image->setImageFormat('jpeg');
358 //Set progressive jpeg
359 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
362 if (!$image->writeImage($path)) {
364 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
369 'og:image' => $this->router
->generate('rapsys_pack_facebook', ['mtime' => stat($path)['mtime'], 'path' => $pathInfo], UrlGeneratorInterface
::ABSOLUTE_URL
),
370 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
371 'og:image:height' => $height,
372 'og:image:width' => $width