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