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