]> Raphaël G. Git Repositories - packbundle/blob - Controller.php
Add download action
[packbundle] / Controller.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;
13
14 use Rapsys\PackBundle\Util\ImageUtil;
15 use Rapsys\PackBundle\Util\SluggerUtil;
16
17 use Psr\Container\ContainerInterface;
18
19 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
20 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
21 use Symfony\Component\Filesystem\Filesystem;
22 use Symfony\Component\HttpFoundation\BinaryFileResponse;
23 use Symfony\Component\HttpFoundation\HeaderUtils;
24 use Symfony\Component\HttpFoundation\Request;
25 use Symfony\Component\HttpFoundation\Response;
26 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
27 use Symfony\Component\Routing\RequestContext;
28 use Symfony\Contracts\Service\ServiceSubscriberInterface;
29
30 /**
31 * {@inheritdoc}
32 */
33 class Controller extends AbstractController implements ServiceSubscriberInterface {
34 /**
35 * Alias string
36 */
37 protected string $alias;
38
39 /**
40 * Config array
41 */
42 protected array $config;
43
44 /**
45 * Stream context
46 */
47 protected mixed $ctx;
48
49 /**
50 * Version string
51 */
52 protected string $version;
53
54 /**
55 * Creates a new image controller
56 *
57 * @param ContainerInterface $container The ContainerInterface instance
58 * @param ImageUtil $image The MapUtil instance
59 * @param SluggerUtil $slugger The SluggerUtil instance
60 */
61 function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected SluggerUtil $slugger) {
62 //Retrieve config
63 $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
64
65 //Set ctx
66 $this->ctx = stream_context_create($this->config['context']);
67 }
68
69 /**
70 * Return captcha image
71 *
72 * @param Request $request The Request instance
73 * @param string $hash The hash
74 * @param string $equation The shorted equation
75 * @param int $height The height
76 * @param int $width The width
77 * @return Response The rendered image
78 */
79 public function captcha(Request $request, string $hash, string $equation, int $height, int $width, string $_format): Response {
80 //Without matching hash
81 if ($hash !== $this->slugger->serialize([$equation, $height, $width])) {
82 //Throw new exception
83 throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
84 //Without valid format
85 } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
86 //Throw new exception
87 throw new NotFoundHttpException('Invalid thumb format');
88 }
89
90 //Unshort equation
91 $equation = $this->slugger->unshort($short = $equation);
92
93 //Set hashed tree
94 $hashed = str_split(strval($equation));
95
96 //Set captcha
97 $captcha = $this->config['cache'].'/'.$this->config['prefixes']['captcha'].'/'.$hashed[0].'/'.$hashed[4].'/'.$hashed[8].'/'.$short.'.'.$_format;
98
99 //Without captcha up to date file
100 if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < (new \DateTime('-1 hour'))->getTimestamp()) {
101 //Without existing captcha path
102 if (!is_dir($dir = dirname($captcha))) {
103 //Create filesystem object
104 $filesystem = new Filesystem();
105
106 try {
107 //Create path
108 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
109 //XXX: on CoW filesystems execute a chattr +C before filling
110 $filesystem->mkdir($dir, 0775);
111 } catch (IOExceptionInterface $e) {
112 //Throw error
113 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
114 }
115 }
116
117 //Create image instance
118 $image = new \Imagick();
119
120 //Add imagick draw instance
121 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
122 $draw = new \ImagickDraw();
123
124 //Set text antialias
125 $draw->setTextAntialias(true);
126
127 //Set stroke antialias
128 $draw->setStrokeAntialias(true);
129
130 //Set text alignment
131 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
132
133 //Set gravity
134 $draw->setGravity(\Imagick::GRAVITY_CENTER);
135
136 //Set fill color
137 $draw->setFillColor($this->config['captcha']['fill']);
138
139 //Set stroke color
140 $draw->setStrokeColor($this->config['captcha']['border']);
141
142 //Set font size
143 $draw->setFontSize($this->config['captcha']['size'] / 1.5);
144
145 //Set stroke width
146 $draw->setStrokeWidth($this->config['captcha']['thickness'] / 3);
147
148 //Set rotation
149 $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
150
151 //Get font metrics
152 $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
153
154 //Add annotation
155 $draw->annotation($width / 2 - ceil(rand(intval(-$metrics2['textWidth']), intval($metrics2['textWidth'])) / 2) - abs($rotate), ceil($metrics2['textHeight'] + $metrics2['descender'] + $metrics2['ascender']) / 2 - $this->config['captcha']['thickness'] - $rotate, strval('stop spam'));
156
157 //Set rotation
158 $draw->rotate(-$rotate);
159
160 //Set font size
161 $draw->setFontSize($this->config['captcha']['size']);
162
163 //Set stroke width
164 $draw->setStrokeWidth($this->config['captcha']['thickness']);
165
166 //Set rotation
167 $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
168
169 //Get font metrics
170 $metrics = $image->queryFontMetrics($draw, strval($equation));
171
172 //Add annotation
173 $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->config['captcha']['thickness'], strval($equation));
174
175 //Set rotation
176 $draw->rotate(-$rotate);
177
178 //Add new image
179 #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->config['captcha']['background']), 'jpeg');
180 $image->newImage($width, $height, new \ImagickPixel($this->config['captcha']['background']), $_format);
181
182 //Draw on image
183 $image->drawImage($draw);
184
185 //Strip image exif data and properties
186 $image->stripImage();
187
188 //Set compression quality
189 $image->setImageCompressionQuality(70);
190
191 //Save captcha
192 if (!$image->writeImage($captcha)) {
193 //Throw error
194 throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
195 }
196
197 //Set mtime
198 $mtime = stat($captcha)['mtime'];
199 }
200
201 //Read captcha from cache
202 $response = new BinaryFileResponse($captcha);
203
204 //Set file name
205 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'.'.$_format);
206
207 //Set etag
208 $response->setEtag(md5($hash));
209
210 //Set last modified
211 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
212
213 //Set as public
214 $response->setPublic();
215
216 //Return 304 response if not modified
217 $response->isNotModified($request);
218
219 //Return response
220 return $response;
221 }
222
223 /**
224 * Return download file
225 *
226 * @param Request $request The Request instance
227 * @param string $hash The hash
228 * @param string $path The image path
229 * @return Response The rendered image
230 */
231 public function download(Request $request, string $hash, string $path/*, string $_format*/): Response {
232 //Without matching hash
233 if ($hash !== $this->slugger->hash($path)) {
234 //Throw new exception
235 throw new NotFoundHttpException('Invalid download hash');
236 //Without valid format
237 #} elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
238 # //Throw new exception
239 # throw new NotFoundHttpException('Invalid download format');
240 }
241
242 //Unshort path
243 $path = $this->slugger->unshort($short = $path);
244
245 //Without file
246 if (!is_file($path) || !($mtime = stat($path)['mtime'])) {
247 //Throw new exception
248 throw new NotFoundHttpException('Unable to get thumb file');
249 }
250
251 //Read thumb from cache
252 $response = new BinaryFileResponse($path);
253
254 //Set file name
255 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($path));
256
257 //Set etag
258 //TODO: set etag to file content md5 ? cache it ?
259 $response->setEtag(md5($hash));
260
261 //Set last modified
262 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
263
264 //Set as public
265 $response->setPublic();
266
267 //Return 304 response if not modified
268 $response->isNotModified($request);
269
270 //Return response
271 return $response;
272 }
273
274 /**
275 * Return facebook image
276 *
277 * @param Request $request The Request instance
278 * @param string $hash The hash
279 * @param string $path The image path
280 * @param int $height The height
281 * @param int $width The width
282 * @return Response The rendered image
283 */
284 public function facebook(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
285 //Without matching hash
286 if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
287 //Throw new exception
288 throw new NotFoundHttpException(sprintf('Unable to match facebook hash: %s', $hash));
289 //Without matching format
290 } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
291 //Throw new exception
292 throw new NotFoundHttpException(sprintf('Invalid facebook format: %s', $_format));
293 }
294
295 //Unshort path
296 $path = $this->slugger->unshort($short = $path);
297
298 //Without facebook file
299 if (!is_file($facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.'.$_format)) {
300 //Throw new exception
301 throw new NotFoundHttpException('Unable to get facebook file');
302 }
303
304 //Read facebook from cache
305 $response = new BinaryFileResponse($facebook);
306
307 //Set file name
308 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format);
309
310 //Set etag
311 //TODO: set etag to file content md5 ? cache it ?
312 $response->setEtag(md5($hash));
313
314 //Set last modified
315 $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($facebook)['mtime'])));
316
317 //Set as public
318 $response->setPublic();
319
320 //Return 304 response if not modified
321 $response->isNotModified($request);
322
323 //Return response
324 return $response;
325 }
326
327 /**
328 * Return map image
329 *
330 * @param Request $request The Request instance
331 * @param string $hash The hash
332 * @param int $updated The updated timestamp
333 * @param float $latitude The latitude
334 * @param float $longitude The longitude
335 * @param int $zoom The zoom
336 * @param int $width The width
337 * @param int $height The height
338 * @return Response The rendered image
339 */
340 public function map(Request $request, string $hash, float $latitude, float $longitude, int $height, int $width, int $zoom, string $_format): Response {
341 //Without matching hash
342 if ($hash !== $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude])) {
343 //Throw new exception
344 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
345 }
346
347 //Set map
348 $map = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
349
350 //Without map file
351 //TODO: refresh after config modification ?
352 if (!is_file($map)) {
353 //Without existing map path
354 if (!is_dir($dir = dirname($map))) {
355 //Create filesystem object
356 $filesystem = new Filesystem();
357
358 try {
359 //Create path
360 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
361 //XXX: on CoW filesystems execute a chattr +C before filling
362 $filesystem->mkdir($dir, 0775);
363 } catch (IOExceptionInterface $e) {
364 //Throw error
365 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
366 }
367 }
368
369 //Create image instance
370 $image = new \Imagick();
371
372 //Add new image
373 $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
374
375 //Create tile instance
376 $tile = new \Imagick();
377
378 //Get tile xy
379 $centerX = $this->image->longitudeToX($longitude, $zoom);
380 $centerY = $this->image->latitudeToY($latitude, $zoom);
381
382 //Calculate start xy
383 $startX = floor(floor($centerX) - $width / $this->config['map']['tz']);
384 $startY = floor(floor($centerY) - $height / $this->config['map']['tz']);
385
386 //Calculate end xy
387 $endX = ceil(ceil($centerX) + $width / $this->config['map']['tz']);
388 $endY = ceil(ceil($centerY) + $height / $this->config['map']['tz']);
389
390 for($x = $startX; $x <= $endX; $x++) {
391 for($y = $startY; $y <= $endY; $y++) {
392 //Set cache path
393 $cache = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
394
395 //Without cache image
396 if (!is_file($cache)) {
397 //Set tile url
398 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['map']['server']]);
399
400 //Without cache path
401 if (!is_dir($dir = dirname($cache))) {
402 //Create filesystem object
403 $filesystem = new Filesystem();
404
405 try {
406 //Create path
407 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
408 $filesystem->mkdir($dir, 0775);
409 } catch (IOExceptionInterface $e) {
410 //Throw error
411 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
412 }
413 }
414
415 //Store tile in cache
416 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
417 }
418
419 //Set dest x
420 $destX = intval(floor($width / 2 - $this->config['map']['tz'] * ($centerX - $x)));
421
422 //Set dest y
423 $destY = intval(floor($height / 2 - $this->config['map']['tz'] * ($centerY - $y)));
424
425 //Read tile from cache
426 $tile->readImage($cache);
427
428 //Compose image
429 $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
430
431 //Clear tile
432 $tile->clear();
433 }
434 }
435
436 //Add imagick draw instance
437 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
438 $draw = new \ImagickDraw();
439
440 //Set text antialias
441 $draw->setTextAntialias(true);
442
443 //Set stroke antialias
444 $draw->setStrokeAntialias(true);
445
446 //Set text alignment
447 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
448
449 //Set gravity
450 $draw->setGravity(\Imagick::GRAVITY_CENTER);
451
452 //Set fill color
453 $draw->setFillColor($this->config['map']['fill']);
454
455 //Set stroke color
456 $draw->setStrokeColor($this->config['map']['border']);
457
458 //Set stroke width
459 $draw->setStrokeWidth($this->config['map']['thickness']);
460
461 //Draw circle
462 $draw->circle($width/2 - $this->config['map']['radius'], $height/2 - $this->config['map']['radius'], $width/2 + $this->config['map']['radius'], $height/2 + $this->config['map']['radius']);
463
464 //Draw on image
465 $image->drawImage($draw);
466
467 //Strip image exif data and properties
468 $image->stripImage();
469
470 //Add latitude
471 //XXX: not supported by imagick :'(
472 $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
473
474 //Add longitude
475 //XXX: not supported by imagick :'(
476 $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
477
478 //Set progressive jpeg
479 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
480
481 //Set compression quality
482 $image->setImageCompressionQuality($this->config['map']['quality']);
483
484 //Save image
485 if (!$image->writeImage($map)) {
486 //Throw error
487 throw new \Exception(sprintf('Unable to write image "%s"', $map));
488 }
489 }
490
491 //Read map from cache
492 $response = new BinaryFileResponse($map);
493
494 //Set file name
495 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
496
497 //Set etag
498 //TODO: set etag to file content md5 ? cache it ?
499 $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude])));
500
501 //Set last modified
502 $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
503
504 //Disable robot index
505 $response->headers->set('X-Robots-Tag', 'noindex');
506
507 //Set as public
508 $response->setPublic();
509
510 //Return 304 response if not modified
511 $response->isNotModified($request);
512
513 //Return response
514 return $response;
515 }
516
517 /**
518 * Return multi map image
519 *
520 * @param Request $request The Request instance
521 * @param string $hash The hash
522 * @param int $updated The updated timestamp
523 * @param float $latitude The latitude
524 * @param float $longitude The longitude
525 * @param string $coordinates The coordinates
526 * @param int $zoom The zoom
527 * @param int $width The width
528 * @param int $height The height
529 * @return Response The rendered image
530 */
531 public function multi(Request $request, string $hash, string $coordinate, int $height, int $width, int $zoom, string $_format): Response {
532 //Without matching hash
533 if ($hash !== $this->slugger->hash([$height, $width, $zoom, $coordinate])) {
534 //Throw new exception
535 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
536 }
537
538 //Set latitudes and longitudes array
539 $latitudes = $longitudes = [];
540
541 //Set coordinates
542 $coordinates = array_map(
543 function ($v) use (&$latitudes, &$longitudes) {
544 list($latitude, $longitude) = explode(',', $v);
545 $latitudes[] = $latitude;
546 $longitudes[] = $longitude;
547 return [ $latitude, $longitude ];
548 },
549 explode('-', $coordinate)
550 );
551
552 //Set latitude
553 $latitude = round((min($latitudes)+max($latitudes))/2, 6);
554
555 //Set longitude
556 $longitude = round((min($longitudes)+max($longitudes))/2, 6);
557
558 //Set map
559 $map = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$coordinate.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
560
561 //Without map file
562 if (!is_file($map)) {
563 //Without existing multi path
564 if (!is_dir($dir = dirname($map))) {
565 //Create filesystem object
566 $filesystem = new Filesystem();
567
568 try {
569 //Create path
570 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
571 //XXX: on CoW filesystems execute a chattr +C before filling
572 $filesystem->mkdir($dir, 0775);
573 } catch (IOExceptionInterface $e) {
574 //Throw error
575 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
576 }
577 }
578
579 //Create image instance
580 $image = new \Imagick();
581
582 //Add new image
583 $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
584
585 //Create tile instance
586 $tile = new \Imagick();
587
588 //Get tile xy
589 $centerX = $this->image->longitudeToX($longitude, $zoom);
590 $centerY = $this->image->latitudeToY($latitude, $zoom);
591
592 //Calculate start xy
593 $startX = floor(floor($centerX) - $width / $this->config['multi']['tz']);
594 $startY = floor(floor($centerY) - $height / $this->config['multi']['tz']);
595
596 //Calculate end xy
597 $endX = ceil(ceil($centerX) + $width / $this->config['multi']['tz']);
598 $endY = ceil(ceil($centerY) + $height / $this->config['multi']['tz']);
599
600 for($x = $startX; $x <= $endX; $x++) {
601 for($y = $startY; $y <= $endY; $y++) {
602 //Set cache path
603 $cache = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
604
605 //Without cache image
606 if (!is_file($cache)) {
607 //Set tile url
608 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['multi']['server']]);
609
610 //Without cache path
611 if (!is_dir($dir = dirname($cache))) {
612 //Create filesystem object
613 $filesystem = new Filesystem();
614
615 try {
616 //Create path
617 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
618 $filesystem->mkdir($dir, 0775);
619 } catch (IOExceptionInterface $e) {
620 //Throw error
621 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
622 }
623 }
624
625 //Store tile in cache
626 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
627 }
628
629 //Set dest x
630 $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $x)));
631
632 //Set dest y
633 $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $y)));
634
635 //Read tile from cache
636 $tile->readImage($cache);
637
638 //Compose image
639 $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
640
641 //Clear tile
642 $tile->clear();
643 }
644 }
645
646 //Add imagick draw instance
647 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
648 $draw = new \ImagickDraw();
649
650 //Set text antialias
651 $draw->setTextAntialias(true);
652
653 //Set stroke antialias
654 $draw->setStrokeAntialias(true);
655
656 //Set text alignment
657 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
658
659 //Set gravity
660 $draw->setGravity(\Imagick::GRAVITY_CENTER);
661
662 //Iterate on locations
663 foreach($coordinates as $id => $coordinate) {
664 //Get coordinates
665 list($clatitude, $clongitude) = $coordinate;
666
667 //Set dest x
668 $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $this->image->longitudeToX(floatval($clongitude), $zoom))));
669
670 //Set dest y
671 $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $this->image->latitudeToY(floatval($clatitude), $zoom))));
672
673 //Set fill color
674 $draw->setFillColor($this->config['multi']['fill']);
675
676 //Set font size
677 $draw->setFontSize($this->config['multi']['size']);
678
679 //Set stroke color
680 $draw->setStrokeColor($this->config['multi']['border']);
681
682 //Set circle radius
683 $radius = $this->config['multi']['radius'];
684
685 //Set stroke width
686 $stroke = $this->config['multi']['thickness'];
687
688 //With matching position
689 if ($clatitude === $latitude && $clongitude == $longitude) {
690 //Set fill color
691 $draw->setFillColor($this->config['multi']['highfill']);
692
693 //Set font size
694 $draw->setFontSize($this->config['multi']['highsize']);
695
696 //Set stroke color
697 $draw->setStrokeColor($this->config['multi']['highborder']);
698
699 //Set circle radius
700 $radius = $this->config['multi']['highradius'];
701
702 //Set stroke width
703 $stroke = $this->config['multi']['highthickness'];
704 }
705
706 //Set stroke width
707 $draw->setStrokeWidth($stroke);
708
709 //Draw circle
710 $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
711
712 //Set fill color
713 $draw->setFillColor($draw->getStrokeColor());
714
715 //Set stroke width
716 $draw->setStrokeWidth($stroke / 4);
717
718 //Get font metrics
719 #$metrics = $image->queryFontMetrics($draw, strval($id));
720
721 //Add annotation
722 $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
723 }
724
725 //Draw on image
726 $image->drawImage($draw);
727
728 //Strip image exif data and properties
729 $image->stripImage();
730
731 //Add latitude
732 //XXX: not supported by imagick :'(
733 $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
734
735 //Add longitude
736 //XXX: not supported by imagick :'(
737 $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
738
739 //Add description
740 //XXX: not supported by imagick :'(
741 #$image->setImageProperty('exif:Description', $caption);
742
743 //Set progressive jpeg
744 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
745
746 //Set compression quality
747 $image->setImageCompressionQuality($this->config['multi']['quality']);
748
749 //Save image
750 if (!$image->writeImage($map)) {
751 //Throw error
752 throw new \Exception(sprintf('Unable to write image "%s"', $path));
753 }
754 }
755
756 //Read map from cache
757 $response = new BinaryFileResponse($map);
758
759 //Set file name
760 #$response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
761 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
762
763 //Set etag
764 //TODO: set etag to file content md5 ? cache it ?
765 $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate])));
766
767 //Set last modified
768 $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
769
770 //Disable robot index
771 $response->headers->set('X-Robots-Tag', 'noindex');
772
773 //Set as public
774 $response->setPublic();
775
776 //Return 304 response if not modified
777 $response->isNotModified($request);
778
779 //Return response
780 return $response;
781 }
782
783 /**
784 * Return thumb image
785 *
786 * @param Request $request The Request instance
787 * @param string $hash The hash
788 * @param string $path The image path
789 * @param int $height The height
790 * @param int $width The width
791 * @return Response The rendered image
792 */
793 public function thumb(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
794 //Without matching hash
795 if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
796 //Throw new exception
797 throw new NotFoundHttpException('Invalid thumb hash');
798 //Without valid format
799 } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
800 //Throw new exception
801 throw new NotFoundHttpException('Invalid thumb format');
802 }
803
804 //Unshort path
805 $path = $this->slugger->unshort($short = $path);
806
807 //Set thumb
808 $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$width.'x'.$height.'.'.$_format;
809
810 //Without file
811 if (!is_file($path) || !($mtime = stat($path)['mtime'])) {
812 //Throw new exception
813 throw new NotFoundHttpException('Unable to get thumb file');
814 }
815
816 //Without thumb up to date file
817 if (!is_file($thumb) || !($updated = stat($thumb)['mtime']) || $updated < $mtime) {
818 //Without existing thumb path
819 if (!is_dir($dir = dirname($thumb))) {
820 //Create filesystem object
821 $filesystem = new Filesystem();
822
823 try {
824 //Create path
825 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
826 //XXX: on CoW filesystems execute a chattr +C before filling
827 $filesystem->mkdir($dir, 0775);
828 } catch (IOExceptionInterface $e) {
829 //Throw error
830 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
831 }
832 }
833
834 //Create image instance
835 $image = new \Imagick();
836
837 //Read image
838 $image->readImage(realpath($path));
839
840 //Crop using aspect ratio
841 //XXX: for better result upload image directly in aspect ratio :)
842 $image->cropThumbnailImage($width, $height);
843
844 //Strip image exif data and properties
845 $image->stripImage();
846
847 //Set compression quality
848 //TODO: ajust that
849 $image->setImageCompressionQuality(70);
850
851 //Set image format
852 #$image->setImageFormat($_format);
853
854 //Save thumb
855 if (!$image->writeImage($thumb)) {
856 //Throw error
857 throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
858 }
859
860 //Set updated
861 $updated = stat($thumb)['mtime'];
862 }
863
864 //Read thumb from cache
865 $response = new BinaryFileResponse($thumb);
866
867 //Set file name
868 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format);
869
870 //Set etag
871 //TODO: set etag to file content md5 ? cache it ?
872 $response->setEtag(md5($hash));
873
874 //Set last modified
875 $response->setLastModified(\DateTime::createFromFormat('U', strval($updated)));
876
877 //Set as public
878 $response->setPublic();
879
880 //Return 304 response if not modified
881 $response->isNotModified($request);
882
883 //Return response
884 return $response;
885 }
886 }