]> Raphaël G. Git Repositories - packbundle/blob - Util/FacebookUtil.php
Add missing licence header
[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 cache directory
25 *
26 * @var string
27 */
28 protected $cache;
29
30 /**
31 * The fonts array
32 *
33 * @var array
34 */
35 protected $fonts;
36
37 /**
38 * The public path
39 *
40 * @var string
41 */
42 protected $path;
43
44 /**
45 * The prefix
46 *
47 * @var string
48 */
49 protected $prefix;
50
51 /**
52 * The RouterInterface instance
53 */
54 protected RouterInterface $router;
55
56 /**
57 * The source
58 *
59 * @var string
60 */
61 protected $source;
62
63 /**
64 * Creates a new facebook util
65 *
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
70 */
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' ]) {
72 //Set cache
73 $this->cache = $cache.'/'.$prefix;
74
75 //Set fonts
76 $this->fonts = $fonts;
77
78 //Set path
79 $this->path = $path.'/'.$prefix;
80
81 //Set router
82 $this->router = $router;
83
84 //Set prefix key
85 $this->prefix = $prefix;
86
87 //Set source
88 $this->source = $source;
89 }
90
91 /**
92 * Return the facebook image
93 *
94 * Generate simple image in jpeg format or load it from cache
95 *
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
103 */
104 public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
105 //Set path file
106 $path = $this->path.$pathInfo.'.jpeg';
107
108 //Without existing path
109 if (!is_dir($dir = dirname($path))) {
110 //Create filesystem object
111 $filesystem = new Filesystem();
112
113 try {
114 //Create path
115 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
116 $filesystem->mkdir($dir, 0775);
117 } catch (IOExceptionInterface $e) {
118 //Throw error
119 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
120 }
121 }
122
123 //With path file
124 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
125 #XXX: we used to drop texts with $data['canonical'] === true !!!
126
127 //Return image data
128 return [
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
133 ];
134 }
135
136 //Set cache path
137 $cache = $this->cache.$pathInfo.'.png';
138
139 //Without cache path
140 if (!is_dir($dir = dirname($cache))) {
141 //Create filesystem object
142 $filesystem = new Filesystem();
143
144 try {
145 //Create path
146 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
147 $filesystem->mkdir($dir, 0775);
148 } catch (IOExceptionInterface $e) {
149 //Throw error
150 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
151 }
152 }
153
154 //Without source
155 if ($source === null) {
156 //Set source
157 $source = realpath($this->source);
158 }
159
160 //Create image object
161 $image = new \Imagick();
162
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();
169
170 try {
171 //Create dir
172 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
173 $filesystem->mkdir($dir, 0775);
174 } catch (IOExceptionInterface $e) {
175 //Throw error
176 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
177 }
178 }
179
180 //Read image
181 $image->readImage($source);
182
183 //Crop using aspect ratio
184 //XXX: for better result upload image directly in aspect ratio :)
185 $image->cropThumbnailImage($width, $height);
186
187 //Strip image exif data and properties
188 $image->stripImage();
189
190 //Save cache image
191 if (!$image->writeImage($cache)) {
192 //Throw error
193 throw new \Exception(sprintf('Unable to write image "%s"', $cache));
194 }
195 //With cache
196 } else {
197 //Read image
198 $image->readImage($cache);
199 }
200
201 //Create draw
202 $draw = new \ImagickDraw();
203
204 //Set stroke antialias
205 $draw->setStrokeAntialias(true);
206
207 //Set text antialias
208 $draw->setTextAntialias(true);
209
210 //Set align aliases
211 $aligns = [
212 'left' => \Imagick::ALIGN_LEFT,
213 'center' => \Imagick::ALIGN_CENTER,
214 'right' => \Imagick::ALIGN_RIGHT
215 ];
216
217 //Set default font
218 $defaultFont = 'dejavusans';
219
220 //Set default align
221 $defaultAlign = 'center';
222
223 //Set default size
224 $defaultSize = 60;
225
226 //Set default stroke
227 $defaultStroke = '#00c3f9';
228
229 //Set default width
230 $defaultWidth = 15;
231
232 //Set default fill
233 $defaultFill = 'white';
234
235 //Init counter
236 $i = 1;
237
238 //Set text count
239 $count = count($texts);
240
241 //Draw each text stroke
242 foreach($texts as $text => $data) {
243 //Set font
244 $draw->setFont($this->fonts[$data['font']??$defaultFont]);
245
246 //Set font size
247 $draw->setFontSize($data['size']??$defaultSize);
248
249 //Set stroke width
250 $draw->setStrokeWidth($data['width']??$defaultWidth);
251
252 //Set text alignment
253 $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign]));
254
255 //Get font metrics
256 $metrics = $image->queryFontMetrics($draw, $text);
257
258 //Without y
259 if (empty($data['y'])) {
260 //Position verticaly each text evenly
261 $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
262 }
263
264 //Without x
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;
272 }
273 }
274
275 //Center verticaly
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;
279
280 //Set stroke color
281 $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke));
282
283 //Set fill color
284 $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke));
285
286 //Add annotation
287 $draw->annotation($data['x'], $data['y'], $text);
288
289 //Increase counter
290 $i++;
291 }
292
293 //Create stroke object
294 $stroke = new \Imagick();
295
296 //Add new image
297 $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
298
299 //Draw on image
300 $stroke->drawImage($draw);
301
302 //Blur image
303 //XXX: blur the stroke canvas only
304 $stroke->blurImage(5,3);
305
306 //Set opacity to 0.5
307 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
308 $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
309
310 //Compose image
311 $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
312
313 //Clear stroke
314 $stroke->clear();
315
316 //Destroy stroke
317 unset($stroke);
318
319 //Clear draw
320 $draw->clear();
321
322 //Set text antialias
323 $draw->setTextAntialias(true);
324
325 //Draw each text
326 foreach($texts as $text => $data) {
327 //Set font
328 $draw->setFont($this->fonts[$data['font']??$defaultFont]);
329
330 //Set font size
331 $draw->setFontSize($data['size']??$defaultSize);
332
333 //Set text alignment
334 $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]);
335
336 //Set fill color
337 $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill));
338
339 //Add annotation
340 $draw->annotation($data['x'], $data['y'], $text);
341
342 //With canonical text
343 if (!empty($data['canonical'])) {
344 //Prevent canonical to finish in alt
345 unset($texts[$text]);
346 }
347 }
348
349 //Draw on image
350 $image->drawImage($draw);
351
352 //Strip image exif data and properties
353 $image->stripImage();
354
355 //Set image format
356 $image->setImageFormat('jpeg');
357
358 //Set progressive jpeg
359 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
360
361 //Save image
362 if (!$image->writeImage($path)) {
363 //Throw error
364 throw new \Exception(sprintf('Unable to write image "%s"', $path));
365 }
366
367 //Return image data
368 return [
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
373 ];
374 }
375 }