]> Raphaël G. Git Repositories - packbundle/blob - Util/FacebookUtil.php
Fix deprecated implicit conversion from float to int loses precision message
[packbundle] / Util / FacebookUtil.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys PackBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\PackBundle\Util;
13
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;
18
19 /**
20 * Helps manage facebook images
21 */
22 class FacebookUtil {
23 /**
24 * The align
25 *
26 * @var string
27 */
28 protected string $align;
29
30 /**
31 * The cache directory
32 *
33 * @var string
34 */
35 protected string $cache;
36
37 /**
38 * The fill
39 *
40 * @var string
41 */
42 protected string $fill;
43
44 /**
45 * The font
46 *
47 * @var string
48 */
49 protected string $font;
50
51 /**
52 * The fonts array
53 *
54 * @var array
55 */
56 protected array $fonts;
57
58 /**
59 * The public path
60 *
61 * @var string
62 */
63 protected string $path;
64
65 /**
66 * The prefix
67 *
68 * @var string
69 */
70 protected string $prefix;
71
72 /**
73 * The RouterInterface instance
74 */
75 protected RouterInterface $router;
76
77 /**
78 * The size
79 *
80 * @var int
81 */
82 protected int $size;
83
84 /**
85 * The source
86 *
87 * @var ?string
88 */
89 protected ?string $source;
90
91 /**
92 * The stroke
93 *
94 * @var string
95 */
96 protected string $stroke;
97
98 /**
99 * The width
100 *
101 * @var int
102 */
103 protected int $width;
104
105 /**
106 * Creates a new facebook util
107 *
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
120 */
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') {
122 //Set align
123 $this->align = $align;
124
125 //Set cache
126 $this->cache = $cache.'/'.$prefix;
127
128 //Set fill
129 $this->fill = $fill;
130
131 //Set font
132 $this->font = $font;
133
134 //Set fonts
135 $this->fonts = $fonts;
136
137 //Set path
138 $this->path = $path.'/'.$prefix;
139
140 //Set prefix key
141 $this->prefix = $prefix;
142
143 //Set router
144 $this->router = $router;
145
146 //Set size
147 $this->size = $size;
148
149 //Set source
150 $this->source = $source;
151
152 //Set stroke
153 $this->stroke = $stroke;
154
155 //Set width
156 $this->width = $width;
157 }
158
159 /**
160 * Return the facebook image
161 *
162 * Generate simple image in jpeg format or load it from cache
163 *
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
171 */
172 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
173 //Without source
174 if ($source === null && $this->source === null) {
175 //Return empty image data
176 return [];
177 //Without local source
178 } elseif ($source === null) {
179 //Set local source
180 $source = $this->source;
181 }
182
183 //Set path file
184 $path = $this->path.$pathInfo.'.jpeg';
185
186 //Without existing path
187 if (!is_dir($dir = dirname($path))) {
188 //Create filesystem object
189 $filesystem = new Filesystem();
190
191 try {
192 //Create path
193 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
194 $filesystem->mkdir($dir, 0775);
195 } catch (IOExceptionInterface $e) {
196 //Throw error
197 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
198 }
199 }
200
201 //With path file
202 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
203 #XXX: we used to drop texts with $data['canonical'] === true !!!
204
205 //Return image data
206 return [
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
211 ];
212 }
213
214 //Set cache path
215 $cache = $this->cache.$pathInfo.'.png';
216
217 //Without cache path
218 if (!is_dir($dir = dirname($cache))) {
219 //Create filesystem object
220 $filesystem = new Filesystem();
221
222 try {
223 //Create path
224 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
225 $filesystem->mkdir($dir, 0775);
226 } catch (IOExceptionInterface $e) {
227 //Throw error
228 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
229 }
230 }
231
232 //Create image object
233 $image = new \Imagick();
234
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();
241
242 try {
243 //Create dir
244 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
245 $filesystem->mkdir($dir, 0775);
246 } catch (IOExceptionInterface $e) {
247 //Throw error
248 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
249 }
250 }
251
252 //Without source
253 if (!is_file($source)) {
254 //Throw error
255 throw new \Exception(sprintf('Source file "%s" do not exists', $this->source));
256 }
257
258 //Convert to absolute path
259 $source = realpath($source);
260
261 //Read image
262 //XXX: Imagick::readImage only supports absolute path
263 $image->readImage($source);
264
265 //Crop using aspect ratio
266 //XXX: for better result upload image directly in aspect ratio :)
267 $image->cropThumbnailImage($width, $height);
268
269 //Strip image exif data and properties
270 $image->stripImage();
271
272 //Save cache image
273 if (!$image->writeImage($cache)) {
274 //Throw error
275 throw new \Exception(sprintf('Unable to write image "%s"', $cache));
276 }
277 //With cache
278 } else {
279 //Read image
280 $image->readImage($cache);
281 }
282
283 //Create draw
284 $draw = new \ImagickDraw();
285
286 //Set stroke antialias
287 $draw->setStrokeAntialias(true);
288
289 //Set text antialias
290 $draw->setTextAntialias(true);
291
292 //Set align aliases
293 $aligns = [
294 'left' => \Imagick::ALIGN_LEFT,
295 'center' => \Imagick::ALIGN_CENTER,
296 'right' => \Imagick::ALIGN_RIGHT
297 ];
298
299 //Init counter
300 $i = 1;
301
302 //Set text count
303 $count = count($texts);
304
305 //Draw each text stroke
306 foreach($texts as $text => $data) {
307 //Set font
308 $draw->setFont($this->fonts[$data['font']??$this->font]);
309
310 //Set font size
311 $draw->setFontSize($data['size']??$this->size);
312
313 //Set stroke width
314 $draw->setStrokeWidth($data['width']??$this->width);
315
316 //Set text alignment
317 $draw->setTextAlignment($align = ($aligns[$data['align']??$this->align]));
318
319 //Get font metrics
320 $metrics = $image->queryFontMetrics($draw, $text);
321
322 //Without y
323 if (empty($data['y'])) {
324 //Position verticaly each text evenly
325 $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
326 }
327
328 //Without x
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;
336 }
337 }
338
339 //Center verticaly
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;
343
344 //Set stroke color
345 $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$this->stroke));
346
347 //Set fill color
348 $draw->setFillColor(new \ImagickPixel($data['stroke']??$this->stroke));
349
350 //Add annotation
351 $draw->annotation($data['x'], $data['y'], $text);
352
353 //Increase counter
354 $i++;
355 }
356
357 //Create stroke object
358 $stroke = new \Imagick();
359
360 //Add new image
361 $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
362
363 //Draw on image
364 $stroke->drawImage($draw);
365
366 //Blur image
367 //XXX: blur the stroke canvas only
368 $stroke->blurImage(5,3);
369
370 //Set opacity to 0.5
371 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
372 $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
373
374 //Compose image
375 $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
376
377 //Clear stroke
378 $stroke->clear();
379
380 //Destroy stroke
381 unset($stroke);
382
383 //Clear draw
384 $draw->clear();
385
386 //Set text antialias
387 $draw->setTextAntialias(true);
388
389 //Draw each text
390 foreach($texts as $text => $data) {
391 //Set font
392 $draw->setFont($this->fonts[$data['font']??$this->font]);
393
394 //Set font size
395 $draw->setFontSize($data['size']??$this->size);
396
397 //Set text alignment
398 $draw->setTextAlignment($aligns[$data['align']??$this->align]);
399
400 //Set fill color
401 $draw->setFillColor(new \ImagickPixel($data['fill']??$this->fill));
402
403 //Add annotation
404 $draw->annotation($data['x'], $data['y'], $text);
405
406 //With canonical text
407 if (!empty($data['canonical'])) {
408 //Prevent canonical to finish in alt
409 unset($texts[$text]);
410 }
411 }
412
413 //Draw on image
414 $image->drawImage($draw);
415
416 //Strip image exif data and properties
417 $image->stripImage();
418
419 //Set image format
420 $image->setImageFormat('jpeg');
421
422 //Set progressive jpeg
423 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
424
425 //Save image
426 if (!$image->writeImage($path)) {
427 //Throw error
428 throw new \Exception(sprintf('Unable to write image "%s"', $path));
429 }
430
431 //Return image data
432 return [
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
437 ];
438 }
439 }