1 <?php
declare(strict_types
=1);
4 * This file is part of the Rapsys PackBundle package.
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Rapsys\PackBundle\Controller
;
14 use Symfony\Component\HttpFoundation\HeaderUtils
;
15 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
;
16 use Symfony\Component\DependencyInjection\ContainerInterface
;
17 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
18 use Symfony\Component\Filesystem\Filesystem
;
19 use Symfony\Component\HttpFoundation\BinaryFileResponse
;
20 use Symfony\Component\HttpFoundation\Request
;
21 use Symfony\Component\HttpFoundation\Response
;
22 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException
;
23 use Symfony\Component\Routing\RequestContext
;
24 use Symfony\Contracts\Service\ServiceSubscriberInterface
;
26 use Rapsys\PackBundle\Util\MapUtil
;
27 use Rapsys\PackBundle\Util\SluggerUtil
;
32 class CaptchaController
extends AbstractController
implements ServiceSubscriberInterface
{
36 protected string $cache;
39 * The stream context instance
44 * The MapUtil instance
46 protected MapUtil
$map;
51 protected string $path;
54 * The SluggerUtil instance
56 protected SluggerUtil
$slugger;
61 protected string $url;
64 * Creates a new captcha controller
66 * @param ContainerInterface $container The ContainerInterface instance
67 * @param SluggerUtil $slugger The SluggerUtil instance
68 * @param string $cache The cache path
69 * @param string $path The public path
70 * @param string $prefix The prefix
72 function __construct(ContainerInterface
$container, SluggerUtil
$slugger, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'captcha') {
74 $this->cache
= $cache.'/'.$prefix;
77 $this->container
= $container;
80 $this->path
= $path.'/'.$prefix;
83 $this->slugger
= $slugger;
89 * @param Request $request The Request instance
90 * @param string $hash The hash
91 * @param int $updated The updated timestamp
92 * @param float $latitude The latitude
93 * @param float $longitude The longitude
94 * @param int $zoom The zoom
95 * @param int $width The width
96 * @param int $height The height
97 * @return Response The rendered image
99 public function map(Request
$request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response
{
100 //Without matching hash
101 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
102 //Throw new exception
103 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
107 $map = $this->path
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
109 //Without multi up to date file
110 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
111 //Without existing map path
112 if (!is_dir($dir = dirname($map))) {
113 //Create filesystem object
114 $filesystem = new Filesystem();
118 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
119 //XXX: on CoW filesystems execute a chattr +C before filling
120 $filesystem->mkdir($dir, 0775);
121 } catch (IOExceptionInterface
$e) {
123 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
127 //Create image instance
128 $image = new \
Imagick();
131 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
133 //Create tile instance
134 $tile = new \
Imagick();
137 $centerX = $this->map
->longitudeToX($longitude, $zoom);
138 $centerY = $this->map
->latitudeToY($latitude, $zoom);
141 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
142 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
145 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
146 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
148 for($x = $startX; $x <= $endX; $x++
) {
149 for($y = $startY; $y <= $endY; $y++
) {
151 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
153 //Without cache image
154 if (!is_file($cache)) {
156 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
159 if (!is_dir($dir = dirname($cache))) {
160 //Create filesystem object
161 $filesystem = new Filesystem();
165 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
166 $filesystem->mkdir($dir, 0775);
167 } catch (IOExceptionInterface
$e) {
169 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
173 //Store tile in cache
174 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
178 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
181 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
183 //Read tile from cache
184 $tile->readImage($cache);
187 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
194 //Add imagick draw instance
195 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
196 $draw = new \
ImagickDraw();
199 $draw->setTextAntialias(true);
201 //Set stroke antialias
202 $draw->setStrokeAntialias(true);
205 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
208 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
211 $draw->setFillColor('#cff');
214 $draw->setStrokeColor('#00c3f9');
217 $draw->setStrokeWidth(2);
220 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 +
5, $height/2 +
5);
223 $image->drawImage($draw);
225 //Strip image exif data and properties
226 $image->stripImage();
229 //XXX: not supported by imagick :'(
230 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
233 //XXX: not supported by imagick :'(
234 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
237 //XXX: not supported by imagick :'(
238 #$image->setImageProperty('exif:Description', $caption);
240 //Set progressive jpeg
241 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
243 //Set compression quality
245 $image->setImageCompressionQuality(70);
248 if (!$image->writeImage($map)) {
250 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
254 $mtime = stat($map)['mtime'];
257 //Read map from cache
258 $response = new BinaryFileResponse($map);
261 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
264 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
267 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
270 $response->setPublic();
272 //Return 304 response if not modified
273 $response->isNotModified($request);
280 * Return multi map image
282 * @param Request $request The Request instance
283 * @param string $hash The hash
284 * @param int $updated The updated timestamp
285 * @param float $latitude The latitude
286 * @param float $longitude The longitude
287 * @param string $coordinates The coordinates
288 * @param int $zoom The zoom
289 * @param int $width The width
290 * @param int $height The height
291 * @return Response The rendered image
293 public function multiMap(Request
$request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response
{
294 //Without matching hash
295 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger
->hash($coordinates), $zoom, $width, $height])) {
296 //Throw new exception
297 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
301 $map = $this->path
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
303 //Without multi up to date file
304 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
305 //Without existing multi path
306 if (!is_dir($dir = dirname($map))) {
307 //Create filesystem object
308 $filesystem = new Filesystem();
312 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
313 //XXX: on CoW filesystems execute a chattr +C before filling
314 $filesystem->mkdir($dir, 0775);
315 } catch (IOExceptionInterface
$e) {
317 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
321 //Create image instance
322 $image = new \
Imagick();
325 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
327 //Create tile instance
328 $tile = new \
Imagick();
331 $centerX = $this->map
->longitudeToX($longitude, $zoom);
332 $centerY = $this->map
->latitudeToY($latitude, $zoom);
335 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
336 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
339 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
340 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
342 for($x = $startX; $x <= $endX; $x++
) {
343 for($y = $startY; $y <= $endY; $y++
) {
345 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
347 //Without cache image
348 if (!is_file($cache)) {
350 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
353 if (!is_dir($dir = dirname($cache))) {
354 //Create filesystem object
355 $filesystem = new Filesystem();
359 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
360 $filesystem->mkdir($dir, 0775);
361 } catch (IOExceptionInterface
$e) {
363 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
367 //Store tile in cache
368 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
372 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
375 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
377 //Read tile from cache
378 $tile->readImage($cache);
381 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
388 //Add imagick draw instance
389 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
390 $draw = new \
ImagickDraw();
393 $draw->setTextAntialias(true);
395 //Set stroke antialias
396 $draw->setStrokeAntialias(true);
399 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
402 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
405 $coordinates = array_reverse(array_map(function ($v) { $p
= strpos($v
, ','); return ['latitude' => floatval(substr($v
, 0, $p
)), 'longitude' => floatval(substr($v
, $p +
1))]; }, explode('-', $coordinates)), true);
407 //Iterate on locations
408 foreach($coordinates as $id => $coordinate) {
410 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $this->map
->longitudeToX(floatval($coordinate['longitude']), $zoom))));
413 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $this->map
->latitudeToY(floatval($coordinate['latitude']), $zoom))));
416 $draw->setFillColor($this->map
->fill
);
419 $draw->setFontSize($this->map
->fontSize
);
422 $draw->setStrokeColor($this->map
->stroke
);
425 $radius = $this->map
->radius
;
428 $stroke = $this->map
->strokeWidth
;
430 //With matching position
431 if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
433 $draw->setFillColor($this->map
->highFill
);
436 $draw->setFontSize($this->map
->highFontSize
);
439 $draw->setStrokeColor($this->map
->highStroke
);
442 $radius = $this->map
->highRadius
;
445 $stroke = $this->map
->highStrokeWidth
;
449 $draw->setStrokeWidth($stroke);
452 $draw->circle($destX - $radius, $destY - $radius, $destX +
$radius, $destY +
$radius);
455 $draw->setFillColor($draw->getStrokeColor());
458 $draw->setStrokeWidth($stroke / 4);
461 $metrics = $image->queryFontMetrics($draw, strval($id));
464 $draw->annotation($destX - $radius, $destY +
$stroke, strval($id));
468 $image->drawImage($draw);
470 //Strip image exif data and properties
471 $image->stripImage();
474 //XXX: not supported by imagick :'(
475 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
478 //XXX: not supported by imagick :'(
479 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
482 //XXX: not supported by imagick :'(
483 #$image->setImageProperty('exif:Description', $caption);
485 //Set progressive jpeg
486 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
488 //Set compression quality
490 $image->setImageCompressionQuality(70);
493 if (!$image->writeImage($map)) {
495 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
499 $mtime = stat($map)['mtime'];
502 //Read map from cache
503 $response = new BinaryFileResponse($map);
506 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
509 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
512 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
515 $response->setPublic();
517 //Return 304 response if not modified
518 $response->isNotModified($request);