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