]> Raphaël G. Git Repositories - packbundle/commitdiff
Import contact form master 0.5.4
authorRaphaël Gertz <git@rapsys.eu>
Sun, 8 Dec 2024 07:04:40 +0000 (08:04 +0100)
committerRaphaël Gertz <git@rapsys.eu>
Sun, 8 Dec 2024 07:04:40 +0000 (08:04 +0100)
28 files changed:
.gitignore
Command.php
Command/RangeCommand.php
Controller.php [new file with mode: 0644]
Controller/ImageController.php [deleted file]
Controller/MapController.php [deleted file]
DependencyInjection/Configuration.php
DependencyInjection/RapsysPackExtension.php
Extension/PackExtension.php
Form/CaptchaType.php
Form/ContactType.php [new file with mode: 0644]
Parser/TokenParser.php
RapsysPackBundle.php
Resources/config/routes/rapsyspack.yaml [deleted file]
Util/FacebookUtil.php
Util/ImageUtil.php
Util/IntlUtil.php
Util/MapUtil.php
Util/SluggerUtil.php
config/packages/rapsyspack.yaml [moved from Resources/config/packages/rapsyspack.yaml with 65% similarity]
config/routes/rapsyspack.yaml [new file with mode: 0644]
public/css/.keep [moved from Resources/public/css/.keep with 100% similarity]
public/facebook/.keep [moved from Resources/public/facebook/.keep with 100% similarity]
public/facebook/source.php [new file with mode: 0644]
public/facebook/source.png [new file with mode: 0644]
public/image/.keep [moved from Resources/public/image/.keep with 100% similarity]
public/js/.keep [moved from Resources/public/js/.keep with 100% similarity]
public/map/.keep [moved from Resources/public/map/.keep with 100% similarity]

index 0c7e3e9ce8ed3dae04e6b078f6cc59bd1b6cd4f3..ad3242e4cf053dbd044755c1ad4d4e956f655fae 100644 (file)
@@ -1,3 +1,3 @@
 .*.un~
 *~
-Resources/public/*
+public/*
index 0097d1246ebdde9e6a6ea9d870ca85f8e5f426b9..ca0e3cc0bc0edcc55acc98185c48ac2c8a14d881 100644 (file)
@@ -20,12 +20,39 @@ use Symfony\Component\DependencyInjection\Container;
  * {@inheritdoc}
  */
 class Command extends BaseCommand {
+       /**
+        * Alias string
+        */
+       protected string $alias = '';
+
+       /**
+        * Bundle string
+        */
+       protected string $bundle = '';
+
        /**
         * {@inheritdoc}
         */
        public function __construct(protected ?string $name = null) {
+               //Get class
+               $class = strrchr(static::class, '\\', true);
+
+               //Without command name
+               if (substr(static::class, -strlen('\\Command')) !== '\\Command') {
+                       $class = strrchr($class, '\\', true);
+               }
+
+               //Set bundle
+               $this->bundle = strtolower($class);
+
+               //With full class name
+               if (class_exists($class .= '\\'.str_replace('\\', '', $class)) && method_exists($class, 'getAlias')) {
+                       //Set alias
+                       $this->alias = call_user_func([$class, 'getAlias']);
+               }
+
                //Fix name
-               $this->name = $this->name ?? static::getName();
+               $this->name = $this->name ?? static::getName($this->alias);
 
                //Call parent constructor
                parent::__construct($this->name);
@@ -44,11 +71,13 @@ class Command extends BaseCommand {
        }
 
        /**
+        * Return the command name
+        *
         * {@inheritdoc}
         *
-        * Return the command name
+        * @param ?string $alias The bundle alias
         */
-       public function getName(): string {
+       public function getName(?string $alias = null): string {
                //With namespace
                if ($npos = strrpos(static::class, '\\')) {
                        //Set name pos
@@ -58,17 +87,33 @@ class Command extends BaseCommand {
                        $npos = 0;
                }
 
+               //Set bundle pos
+               $bpos = strlen(static::class) - $npos;
+
                //With trailing command
                if (substr(static::class, -strlen('Command'), strlen('Command')) === 'Command') {
-                       //Set bundle pos
-                       $bpos = strlen(static::class) - $npos - strlen('Command');
-               //Without bundle
-               } else {
-                       //Set bundle pos
-                       $bpos = strlen(static::class) - $npos;
+                       //Fix bundle pos
+                       $bpos -= strlen('Command');
+               }
+
+               //Without alias
+               if ($alias === null) {
+                       //Get class
+                       $class = strrchr(static::class, '\\', true);
+
+                       //Without command name
+                       if (substr(static::class, -strlen('\\Command')) !== '\\Command') {
+                               $class = strrchr($class, '\\', true);
+                       }
+
+                       //With full class name
+                       if (class_exists($class .= '\\'.str_replace('\\', '', $class)) && method_exists($class, 'getAlias')) {
+                               //Set alias
+                               $alias = call_user_func([$class, 'getAlias']);
+                       }
                }
 
                //Return command alias
-               return RapsysPackBundle::getAlias().':'.strtolower(substr(static::class, $npos, $bpos));
+               return ($alias?$alias.':':'').strtolower(substr(static::class, $npos, $bpos));
        }
 }
index e4f15daf80a9f015f302e8365aeb860657b7be1f..059713825f9dee11b696d853a88b269017e4f908 100644 (file)
@@ -12,7 +12,6 @@
 namespace Rapsys\PackBundle\Command;
 
 use Rapsys\PackBundle\Command;
-use Rapsys\PackBundle\RapsysPackBundle;
 
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
@@ -29,19 +28,19 @@ class RangeCommand extends Command {
         *
         * Shown with bin/console list
         */
-       protected string $description = 'Outputs a shuffled printable characters range';
+       protected string $description = 'Generates a shuffled printable characters range';
 
        /**
         * Set help
         *
         * Shown with bin/console --help rapsyspack:range
         */
-       protected string $help = 'This command outputs a shuffled printable characters range';
+       protected string $help = 'This command generates a shuffled printable characters range';
 
        /**
         * {@inheritdoc}
         */
-       public function __construct(protected string $file = '.env.local', protected ?string $name = null) {
+       public function __construct(protected string $file, protected ?string $name = null) {
                //Call parent constructor
                parent::__construct($this->name);
 
@@ -99,7 +98,7 @@ class RangeCommand extends Command {
                        //Without match
                        } else {
                                //Append string
-                               $content .= (strlen($content)?"\n\n":'').'###> '.RapsysPackBundle::getBundleAlias().' ###'."\n".$string."\n".'###< '.RapsysPackBundle::getBundleAlias().' ###';
+                               $content .= (strlen($content)?"\n\n":'').'###> '.$this->bundle.' ###'."\n".$string."\n".'###< '.$this->bundle.' ###';
                        }
 
                        //Write file content
@@ -119,7 +118,7 @@ class RangeCommand extends Command {
                        echo '# Add to '.$file."\n";
 
                        //Print rapsys pack range variable
-                       echo '###> '.RapsysPackBundle::getBundleAlias().' ###'."\n".$string."\n".'###< '.RapsysPackBundle::getBundleAlias().' ###';
+                       echo '###> '.$this->bundle.' ###'."\n".$string."\n".'###< '.$this->bundle.' ###';
 
                        //Add trailing line
                        echo "\n";
diff --git a/Controller.php b/Controller.php
new file mode 100644 (file)
index 0000000..77a3e14
--- /dev/null
@@ -0,0 +1,831 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle;
+
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+use Psr\Container\ContainerInterface;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\HeaderUtils;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Contracts\Service\ServiceSubscriberInterface;
+
+/**
+ * {@inheritdoc}
+ */
+class Controller extends AbstractController implements ServiceSubscriberInterface {
+       /**
+        * Alias string
+        */
+       protected string $alias;
+
+       /**
+        * Config array
+        */
+       protected array $config;
+
+       /**
+        * Stream context
+        */
+       protected mixed $ctx;
+
+       /**
+        * Version string
+        */
+       protected string $version;
+
+       /**
+        * Creates a new image controller
+        *
+        * @param ContainerInterface $container The ContainerInterface instance
+        * @param ImageUtil $image The MapUtil instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
+        */
+       function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected SluggerUtil $slugger) {
+               //Retrieve config
+               $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
+
+               //Set ctx
+               $this->ctx = stream_context_create($this->config['context']);
+       }
+
+       /**
+        * Return captcha image
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param string $equation The shorted equation
+        * @param int $height The height
+        * @param int $width The width
+        * @return Response The rendered image
+        */
+       public function captcha(Request $request, string $hash, string $equation, int $height, int $width, string $_format): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->serialize([$equation, $height, $width])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
+               //Without valid format
+               } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Invalid thumb format');
+               }
+
+               //Unshort equation
+               $equation = $this->slugger->unshort($short = $equation);
+
+               //Set hashed tree
+               $hashed = str_split(strval($equation));
+
+               //Set captcha
+               $captcha = $this->config['cache'].'/'.$this->config['prefixes']['captcha'].'/'.$hashed[0].'/'.$hashed[4].'/'.$hashed[8].'/'.$short.'.'.$_format;
+
+               //Without captcha up to date file
+               if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < (new \DateTime('-1 hour'))->getTimestamp()) {
+                       //Without existing captcha path
+                       if (!is_dir($dir = dirname($captcha))) {
+                               //Create filesystem object
+                               $filesystem = new Filesystem();
+
+                               try {
+                                       //Create path
+                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                       //XXX: on CoW filesystems execute a chattr +C before filling
+                                       $filesystem->mkdir($dir, 0775);
+                               } catch (IOExceptionInterface $e) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
+                               }
+                       }
+
+                       //Create image instance
+                       $image = new \Imagick();
+
+                       //Add imagick draw instance
+                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+                       $draw = new \ImagickDraw();
+
+                       //Set text antialias
+                       $draw->setTextAntialias(true);
+
+                       //Set stroke antialias
+                       $draw->setStrokeAntialias(true);
+
+                       //Set text alignment
+                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+                       //Set gravity
+                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+                       //Set fill color
+                       $draw->setFillColor($this->config['captcha']['fill']);
+
+                       //Set stroke color
+                       $draw->setStrokeColor($this->config['captcha']['border']);
+
+                       //Set font size
+                       $draw->setFontSize($this->config['captcha']['size'] / 1.5);
+
+                       //Set stroke width
+                       $draw->setStrokeWidth($this->config['captcha']['thickness'] / 3);
+
+                       //Set rotation
+                       $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
+
+                       //Get font metrics
+                       $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
+
+                       //Add annotation
+                       $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'));
+
+                       //Set rotation
+                       $draw->rotate(-$rotate);
+
+                       //Set font size
+                       $draw->setFontSize($this->config['captcha']['size']);
+
+                       //Set stroke width
+                       $draw->setStrokeWidth($this->config['captcha']['thickness']);
+
+                       //Set rotation
+                       $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
+
+                       //Get font metrics
+                       $metrics = $image->queryFontMetrics($draw, strval($equation));
+
+                       //Add annotation
+                       $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->config['captcha']['thickness'], strval($equation));
+
+                       //Set rotation
+                       $draw->rotate(-$rotate);
+
+                       //Add new image
+                       #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->config['captcha']['background']), 'jpeg');
+                       $image->newImage($width, $height, new \ImagickPixel($this->config['captcha']['background']), $_format);
+
+                       //Draw on image
+                       $image->drawImage($draw);
+
+                       //Strip image exif data and properties
+                       $image->stripImage();
+
+                       //Set compression quality
+                       $image->setImageCompressionQuality(70);
+
+                       //Save captcha
+                       if (!$image->writeImage($captcha)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
+                       }
+
+                       //Set mtime
+                       $mtime = stat($captcha)['mtime'];
+               }
+
+               //Read captcha from cache
+               $response = new BinaryFileResponse($captcha);
+
+               //Set file name
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'.'.$_format);
+
+               //Set etag
+               $response->setEtag(md5($hash));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+
+       /**
+        * Return facebook image
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param string $path The image path
+        * @param int $height The height
+        * @param int $width The width
+        * @return Response The rendered image
+        */
+       public function facebook(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException(sprintf('Unable to match facebook hash: %s', $hash));
+               //Without matching format
+               } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+                       //Throw new exception
+                       throw new NotFoundHttpException(sprintf('Invalid facebook format: %s', $_format));
+               }
+
+               //Unshort path
+               $path = $this->slugger->unshort($short = $path);
+
+               //Without facebook file
+               if (!is_file($facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.'.$_format)) {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Unable to get facebook file');
+               }
+
+               //Read facebook from cache
+               $response = new BinaryFileResponse($facebook);
+
+               //Set file name
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format);
+
+               //Set etag
+               $response->setEtag(md5($hash));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($facebook)['mtime'])));
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+
+       /**
+        * Return map image
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param int $updated The updated timestamp
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @param int $zoom The zoom
+        * @param int $width The width
+        * @param int $height The height
+        * @return Response The rendered image
+        */
+       public function map(Request $request, string $hash, float $latitude, float $longitude, int $height, int $width, int $zoom, string $_format): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
+               }
+
+               //Set map
+               $map = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
+
+               //Without map file
+               //TODO: refresh after config modification ?
+               if (!is_file($map)) {
+                       //Without existing map path
+                       if (!is_dir($dir = dirname($map))) {
+                               //Create filesystem object
+                               $filesystem = new Filesystem();
+
+                               try {
+                                       //Create path
+                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                       //XXX: on CoW filesystems execute a chattr +C before filling
+                                       $filesystem->mkdir($dir, 0775);
+                               } catch (IOExceptionInterface $e) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                               }
+                       }
+
+                       //Create image instance
+                       $image = new \Imagick();
+
+                       //Add new image
+                       $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
+
+                       //Create tile instance
+                       $tile = new \Imagick();
+
+                       //Get tile xy
+                       $centerX = $this->image->longitudeToX($longitude, $zoom);
+                       $centerY = $this->image->latitudeToY($latitude, $zoom);
+
+                       //Calculate start xy
+                       $startX = floor(floor($centerX) - $width / $this->config['map']['tz']);
+                       $startY = floor(floor($centerY) - $height / $this->config['map']['tz']);
+
+                       //Calculate end xy
+                       $endX = ceil(ceil($centerX) + $width / $this->config['map']['tz']);
+                       $endY = ceil(ceil($centerY) + $height / $this->config['map']['tz']);
+
+                       for($x = $startX; $x <= $endX; $x++) {
+                               for($y = $startY; $y <= $endY; $y++) {
+                                       //Set cache path
+                                       $cache = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
+
+                                       //Without cache image
+                                       if (!is_file($cache)) {
+                                               //Set tile url
+                                               $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['map']['server']]);
+
+                                               //Without cache path
+                                               if (!is_dir($dir = dirname($cache))) {
+                                                       //Create filesystem object
+                                                       $filesystem = new Filesystem();
+
+                                                       try {
+                                                               //Create path
+                                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                                               $filesystem->mkdir($dir, 0775);
+                                                       } catch (IOExceptionInterface $e) {
+                                                               //Throw error
+                                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                                                       }
+                                               }
+
+                                               //Store tile in cache
+                                               file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
+                                       }
+
+                                       //Set dest x
+                                       $destX = intval(floor($width / 2 - $this->config['map']['tz'] * ($centerX - $x)));
+
+                                       //Set dest y
+                                       $destY = intval(floor($height / 2 - $this->config['map']['tz'] * ($centerY - $y)));
+
+                                       //Read tile from cache
+                                       $tile->readImage($cache);
+
+                                       //Compose image
+                                       $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
+
+                                       //Clear tile
+                                       $tile->clear();
+                               }
+                       }
+
+                       //Add imagick draw instance
+                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+                       $draw = new \ImagickDraw();
+
+                       //Set text antialias
+                       $draw->setTextAntialias(true);
+
+                       //Set stroke antialias
+                       $draw->setStrokeAntialias(true);
+
+                       //Set text alignment
+                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+                       //Set gravity
+                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+                       //Set fill color
+                       $draw->setFillColor($this->config['map']['fill']);
+
+                       //Set stroke color
+                       $draw->setStrokeColor($this->config['map']['border']);
+
+                       //Set stroke width
+                       $draw->setStrokeWidth($this->config['map']['thickness']);
+
+                       //Draw circle
+                       $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']);
+
+                       //Draw on image
+                       $image->drawImage($draw);
+
+                       //Strip image exif data and properties
+                       $image->stripImage();
+
+                       //Add latitude
+                       //XXX: not supported by imagick :'(
+                       $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
+
+                       //Add longitude
+                       //XXX: not supported by imagick :'(
+                       $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
+
+                       //Set progressive jpeg
+                       $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
+
+                       //Set compression quality
+                       $image->setImageCompressionQuality($this->config['map']['quality']);
+
+                       //Save image
+                       if (!$image->writeImage($map)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Unable to write image "%s"', $map));
+                       }
+               }
+
+               //Read map from cache
+               $response = new BinaryFileResponse($map);
+
+               //Set file name
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
+
+               //Set etag
+               $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude])));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
+
+               //Disable robot index
+               $response->headers->set('X-Robots-Tag', 'noindex');
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+
+       /**
+        * Return multi map image
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param int $updated The updated timestamp
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @param string $coordinates The coordinates
+        * @param int $zoom The zoom
+        * @param int $width The width
+        * @param int $height The height
+        * @return Response The rendered image
+        */
+       public function multi(Request $request, string $hash, string $coordinate, int $height, int $width, int $zoom, string $_format): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->hash([$height, $width, $zoom, $coordinate])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
+               }
+
+               //Set latitudes and longitudes array
+               $latitudes = $longitudes = [];
+
+               //Set coordinates
+               $coordinates = array_map(
+                       function ($v) use (&$latitudes, &$longitudes) {
+                               list($latitude, $longitude) = explode(',', $v);
+                               $latitudes[] = $latitude;
+                               $longitudes[] = $longitude;
+                               return [ $latitude, $longitude ];
+                       },
+                       explode('-', $coordinate)
+               );
+
+               //Set latitude
+               $latitude = round((min($latitudes)+max($latitudes))/2, 6);
+
+               //Set longitude
+               $longitude = round((min($longitudes)+max($longitudes))/2, 6);
+
+               //Set map
+               $map = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$coordinate.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
+
+               //Without map file
+               if (!is_file($map)) {
+                       //Without existing multi path
+                       if (!is_dir($dir = dirname($map))) {
+                               //Create filesystem object
+                               $filesystem = new Filesystem();
+
+                               try {
+                                       //Create path
+                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                       //XXX: on CoW filesystems execute a chattr +C before filling
+                                       $filesystem->mkdir($dir, 0775);
+                               } catch (IOExceptionInterface $e) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                               }
+                       }
+
+                       //Create image instance
+                       $image = new \Imagick();
+
+                       //Add new image
+                       $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
+
+                       //Create tile instance
+                       $tile = new \Imagick();
+
+                       //Get tile xy
+                       $centerX = $this->image->longitudeToX($longitude, $zoom);
+                       $centerY = $this->image->latitudeToY($latitude, $zoom);
+
+                       //Calculate start xy
+                       $startX = floor(floor($centerX) - $width / $this->config['multi']['tz']);
+                       $startY = floor(floor($centerY) - $height / $this->config['multi']['tz']);
+
+                       //Calculate end xy
+                       $endX = ceil(ceil($centerX) + $width / $this->config['multi']['tz']);
+                       $endY = ceil(ceil($centerY) + $height / $this->config['multi']['tz']);
+
+                       for($x = $startX; $x <= $endX; $x++) {
+                               for($y = $startY; $y <= $endY; $y++) {
+                                       //Set cache path
+                                       $cache = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
+
+                                       //Without cache image
+                                       if (!is_file($cache)) {
+                                               //Set tile url
+                                               $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['multi']['server']]);
+
+                                               //Without cache path
+                                               if (!is_dir($dir = dirname($cache))) {
+                                                       //Create filesystem object
+                                                       $filesystem = new Filesystem();
+
+                                                       try {
+                                                               //Create path
+                                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                                               $filesystem->mkdir($dir, 0775);
+                                                       } catch (IOExceptionInterface $e) {
+                                                               //Throw error
+                                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                                                       }
+                                               }
+
+                                               //Store tile in cache
+                                               file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
+                                       }
+
+                                       //Set dest x
+                                       $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $x)));
+
+                                       //Set dest y
+                                       $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $y)));
+
+                                       //Read tile from cache
+                                       $tile->readImage($cache);
+
+                                       //Compose image
+                                       $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
+
+                                       //Clear tile
+                                       $tile->clear();
+                               }
+                       }
+
+                       //Add imagick draw instance
+                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+                       $draw = new \ImagickDraw();
+
+                       //Set text antialias
+                       $draw->setTextAntialias(true);
+
+                       //Set stroke antialias
+                       $draw->setStrokeAntialias(true);
+
+                       //Set text alignment
+                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+                       //Set gravity
+                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+                       //Iterate on locations
+                       foreach($coordinates as $id => $coordinate) {
+                               //Get coordinates
+                               list($clatitude, $clongitude) = $coordinate;
+
+                               //Set dest x
+                               $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $this->image->longitudeToX(floatval($clongitude), $zoom))));
+
+                               //Set dest y
+                               $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $this->image->latitudeToY(floatval($clatitude), $zoom))));
+
+                               //Set fill color
+                               $draw->setFillColor($this->config['multi']['fill']);
+
+                               //Set font size
+                               $draw->setFontSize($this->config['multi']['size']);
+
+                               //Set stroke color
+                               $draw->setStrokeColor($this->config['multi']['border']);
+
+                               //Set circle radius
+                               $radius = $this->config['multi']['radius'];
+
+                               //Set stroke width
+                               $stroke = $this->config['multi']['thickness'];
+
+                               //With matching position
+                               if ($clatitude === $latitude && $clongitude == $longitude) {
+                                       //Set fill color
+                                       $draw->setFillColor($this->config['multi']['highfill']);
+
+                                       //Set font size
+                                       $draw->setFontSize($this->config['multi']['highsize']);
+
+                                       //Set stroke color
+                                       $draw->setStrokeColor($this->config['multi']['highborder']);
+
+                                       //Set circle radius
+                                       $radius = $this->config['multi']['highradius'];
+
+                                       //Set stroke width
+                                       $stroke = $this->config['multi']['highthickness'];
+                               }
+
+                               //Set stroke width
+                               $draw->setStrokeWidth($stroke);
+
+                               //Draw circle
+                               $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
+
+                               //Set fill color
+                               $draw->setFillColor($draw->getStrokeColor());
+
+                               //Set stroke width
+                               $draw->setStrokeWidth($stroke / 4);
+
+                               //Get font metrics
+                               #$metrics = $image->queryFontMetrics($draw, strval($id));
+
+                               //Add annotation
+                               $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
+                       }
+
+                       //Draw on image
+                       $image->drawImage($draw);
+
+                       //Strip image exif data and properties
+                       $image->stripImage();
+
+                       //Add latitude
+                       //XXX: not supported by imagick :'(
+                       $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
+
+                       //Add longitude
+                       //XXX: not supported by imagick :'(
+                       $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
+
+                       //Add description
+                       //XXX: not supported by imagick :'(
+                       #$image->setImageProperty('exif:Description', $caption);
+
+                       //Set progressive jpeg
+                       $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
+
+                       //Set compression quality
+                       $image->setImageCompressionQuality($this->config['multi']['quality']);
+
+                       //Save image
+                       if (!$image->writeImage($map)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Unable to write image "%s"', $path));
+                       }
+               }
+
+               //Read map from cache
+               $response = new BinaryFileResponse($map);
+
+               //Set file name
+               #$response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
+
+               //Set etag
+               $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate])));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
+
+               //Disable robot index
+               $response->headers->set('X-Robots-Tag', 'noindex');
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+
+       /**
+        * Return thumb image
+        *
+        * @param Request $request The Request instance
+        * @param string $hash The hash
+        * @param string $path The image path
+        * @param int $height The height
+        * @param int $width The width
+        * @return Response The rendered image
+        */
+       public function thumb(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
+               //Without matching hash
+               if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Invalid thumb hash');
+               //Without valid format
+               } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Invalid thumb format');
+               }
+
+               //Unshort path
+               $path = $this->slugger->unshort($short = $path);
+
+               //Set thumb
+               $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$_format;
+
+               //Without file
+               if (!is_file($path) || !($updated = stat($path)['mtime'])) {
+                       //Throw new exception
+                       throw new NotFoundHttpException('Unable to get thumb file');
+               }
+
+               //Without thumb up to date file
+               if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
+                       //Without existing thumb path
+                       if (!is_dir($dir = dirname($thumb))) {
+                               //Create filesystem object
+                               $filesystem = new Filesystem();
+
+                               try {
+                                       //Create path
+                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                       //XXX: on CoW filesystems execute a chattr +C before filling
+                                       $filesystem->mkdir($dir, 0775);
+                               } catch (IOExceptionInterface $e) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
+                               }
+                       }
+
+                       //Create image instance
+                       $image = new \Imagick();
+
+                       //Read image
+                       $image->readImage(realpath($path));
+
+                       //Crop using aspect ratio
+                       //XXX: for better result upload image directly in aspect ratio :)
+                       $image->cropThumbnailImage($width, $height);
+
+                       //Strip image exif data and properties
+                       $image->stripImage();
+
+                       //Set compression quality
+                       //TODO: ajust that
+                       $image->setImageCompressionQuality(70);
+
+                       //Set image format
+                       #$image->setImageFormat($_format);
+
+                       //Save thumb
+                       if (!$image->writeImage($thumb)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
+                       }
+
+                       //Set mtime
+                       $mtime = stat($thumb)['mtime'];
+               }
+
+               //Read thumb from cache
+               $response = new BinaryFileResponse($thumb);
+
+               //Set file name
+               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format);
+
+               //Set etag
+               $response->setEtag(md5($hash));
+
+               //Set last modified
+               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+
+               //Set as public
+               $response->setPublic();
+
+               //Return 304 response if not modified
+               $response->isNotModified($request);
+
+               //Return response
+               return $response;
+       }
+}
diff --git a/Controller/ImageController.php b/Controller/ImageController.php
deleted file mode 100644 (file)
index 13865a7..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-<?php declare(strict_types=1);
-
-/*
- * This file is part of the Rapsys PackBundle package.
- *
- * (c) Raphaël Gertz <symfony@rapsys.eu>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Rapsys\PackBundle\Controller;
-
-use Rapsys\PackBundle\Util\ImageUtil;
-use Rapsys\PackBundle\Util\SluggerUtil;
-
-use Psr\Container\ContainerInterface;
-
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
-use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
-use Symfony\Component\HttpFoundation\HeaderUtils;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\Routing\RequestContext;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
-
-/**
- * {@inheritdoc}
- */
-class ImageController extends AbstractController implements ServiceSubscriberInterface {
-       /**
-        * Creates a new image controller
-        *
-        * @param ContainerInterface $container The ContainerInterface instance
-        * @param ImageUtil $image The MapUtil instance
-        * @param SluggerUtil $slugger The SluggerUtil instance
-        * @param string $cache The cache path
-        * @param string $path The public path
-        * @param string $prefix The prefix
-        */
-       function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'image') {
-       }
-
-       /**
-        * Return captcha image
-        *
-        * @param Request $request The Request instance
-        * @param string $hash The hash
-        * @param int $updated The updated timestamp
-        * @param string $equation The shorted equation
-        * @param int $width The width
-        * @param int $height The height
-        * @return Response The rendered image
-        */
-       public function captcha(Request $request, string $hash, int $updated, string $equation, int $width, int $height): Response {
-               //Without matching hash
-               if ($hash !== $this->slugger->serialize([$updated, $equation, $width, $height])) {
-                       //Throw new exception
-                       throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
-               }
-
-               //Set hashed tree
-               $hashed = array_reverse(str_split(strval($updated)));
-
-               //Set captcha
-               $captcha = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$equation.'/'.$width.'x'.$height.'.jpeg';
-
-               //Unshort equation
-               $equation = $this->slugger->unshort($equation);
-
-               //Without captcha up to date file
-               if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < $updated) {
-                       //Without existing captcha path
-                       if (!is_dir($dir = dirname($captcha))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
-
-                               try {
-                                       //Create path
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       //XXX: on CoW filesystems execute a chattr +C before filling
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
-
-                       //Create image instance
-                       $image = new \Imagick();
-
-                       //Add imagick draw instance
-                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
-                       $draw = new \ImagickDraw();
-
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
-
-                       //Set stroke antialias
-                       $draw->setStrokeAntialias(true);
-
-                       //Set text alignment
-                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
-                       //Set gravity
-                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
-                       //Set fill color
-                       $draw->setFillColor($this->image->getFill());
-
-                       //Set stroke color
-                       $draw->setStrokeColor($this->image->getStroke());
-
-                       //Set font size
-                       $draw->setFontSize($this->image->getFontSize() / 1.5);
-
-                       //Set stroke width
-                       $draw->setStrokeWidth($this->image->getStrokeWidth() / 3);
-
-                       //Set rotation
-                       $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
-
-                       //Get font metrics
-                       $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
-
-                       //Add annotation
-                       $draw->annotation($width / 2 - ceil(rand(intval(-$metrics2['textWidth']), intval($metrics2['textWidth'])) / 2) - abs($rotate), ceil($metrics2['textHeight'] + $metrics2['descender'] + $metrics2['ascender']) / 2 - $this->image->getStrokeWidth() - $rotate, strval('stop spam'));
-
-                       //Set rotation
-                       $draw->rotate(-$rotate);
-
-                       //Set font size
-                       $draw->setFontSize($this->image->getFontSize());
-
-                       //Set stroke width
-                       $draw->setStrokeWidth($this->image->getStrokeWidth());
-
-                       //Set rotation
-                       $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
-
-                       //Get font metrics
-                       $metrics = $image->queryFontMetrics($draw, strval($equation));
-
-                       //Add annotation
-                       $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->image->getStrokeWidth(), strval($equation));
-
-                       //Set rotation
-                       $draw->rotate(-$rotate);
-
-                       //Add new image
-                       #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->image->getBackground()), 'jpeg');
-                       $image->newImage($width, $height, new \ImagickPixel($this->image->getBackground()), 'jpeg');
-
-                       //Draw on image
-                       $image->drawImage($draw);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Set compression quality
-                       $image->setImageCompressionQuality(70);
-
-                       //Save captcha
-                       if (!$image->writeImage($captcha)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
-                       }
-
-                       //Set mtime
-                       $mtime = stat($captcha)['mtime'];
-               }
-
-               //Read captcha from cache
-               $response = new BinaryFileResponse($captcha);
-
-               //Set file name
-               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'-'.$width.'x'.$height.'.jpeg');
-
-               //Set etag
-               $response->setEtag(md5($hash));
-
-               //Set last modified
-               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
-               //Set as public
-               $response->setPublic();
-
-               //Return 304 response if not modified
-               $response->isNotModified($request);
-
-               //Return response
-               return $response;
-       }
-
-       /**
-        * Return thumb image
-        *
-        * @param Request $request The Request instance
-        * @param string $hash The hash
-        * @param int $updated The updated timestamp
-        * @param string $path The image path
-        * @param int $width The width
-        * @param int $height The height
-        * @return Response The rendered image
-        */
-       public function thumb(Request $request, string $hash, int $updated, string $path, int $width, int $height): Response {
-               //Without matching hash
-               if ($hash !== $this->slugger->serialize([$updated, $path, $width, $height])) {
-                       //Throw new exception
-                       throw new NotFoundHttpException(sprintf('Unable to match thumb hash: %s', $hash));
-               }
-
-               //Set hashed tree
-               $hashed = array_reverse(str_split(strval($updated)));
-
-               //Set thumb
-               $thumb = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$path.'/'.$width.'x'.$height.'.jpeg';
-
-               //Unshort path
-               $path = $this->slugger->unshort($path);
-
-               //Without thumb up to date file
-               if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
-                       //Without existing thumb path
-                       if (!is_dir($dir = dirname($thumb))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
-
-                               try {
-                                       //Create path
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       //XXX: on CoW filesystems execute a chattr +C before filling
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
-
-                       //Create image instance
-                       $image = new \Imagick();
-
-                       //Read image
-                       $image->readImage(realpath($path));
-
-                       //Crop using aspect ratio
-                       //XXX: for better result upload image directly in aspect ratio :)
-                       $image->cropThumbnailImage($width, $height);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Set compression quality
-                       //TODO: ajust that
-                       $image->setImageCompressionQuality(70);
-
-                       //Save thumb
-                       if (!$image->writeImage($thumb)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
-                       }
-
-                       //Set mtime
-                       $mtime = stat($thumb)['mtime'];
-               }
-
-               //Read thumb from cache
-               $response = new BinaryFileResponse($thumb);
-
-               //Set file name
-               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.str_replace('/', '_', $path).'-'.$width.'x'.$height.'.jpeg');
-
-               //Set etag
-               $response->setEtag(md5($hash));
-
-               //Set last modified
-               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
-               //Set as public
-               $response->setPublic();
-
-               //Return 304 response if not modified
-               $response->isNotModified($request);
-
-               //Return response
-               return $response;
-       }
-}
diff --git a/Controller/MapController.php b/Controller/MapController.php
deleted file mode 100644 (file)
index 8d38aa6..0000000
+++ /dev/null
@@ -1,507 +0,0 @@
-<?php declare(strict_types=1);
-
-/*
- * This file is part of the Rapsys PackBundle package.
- *
- * (c) Raphaël Gertz <symfony@rapsys.eu>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Rapsys\PackBundle\Controller;
-
-use Rapsys\PackBundle\Util\MapUtil;
-use Rapsys\PackBundle\Util\SluggerUtil;
-
-use Psr\Container\ContainerInterface;
-
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
-use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
-use Symfony\Component\HttpFoundation\HeaderUtils;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\Routing\RequestContext;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
-
-/**
- * {@inheritdoc}
- */
-class MapController extends AbstractController implements ServiceSubscriberInterface {
-       /**
-        * The stream context instance
-        */
-       protected mixed $ctx;
-
-       /**
-        * Creates a new osm controller
-        *
-        * @param ContainerInterface $container The ContainerInterface instance
-        * @param MapUtil $map The MapUtil instance
-        * @param SluggerUtil $slugger The SluggerUtil instance
-        * @param string $cache The cache path
-        * @param string $path The public path
-        * @param string $prefix The prefix
-        * @param string $url The tile server url
-        */
-       function __construct(protected ContainerInterface $container, protected MapUtil $map, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'map', protected string $url = MapUtil::osm) {
-               //Set ctx
-               $this->ctx = stream_context_create(
-                       [
-                               'http' => [
-                                       #'header' => ['Referer: https://www.openstreetmap.org/'],
-                                       'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20,
-                                       'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== "" ? (float)$timeout : 60),
-                                       'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== "" ? (string)$agent : RapsysPackBundle::getAlias().'/'.RapsysPackBundle::getVersion())
-                               ]
-                       ]
-               );
-       }
-
-       /**
-        * Return map image
-        *
-        * @param Request $request The Request instance
-        * @param string $hash The hash
-        * @param int $updated The updated timestamp
-        * @param float $latitude The latitude
-        * @param float $longitude The longitude
-        * @param int $zoom The zoom
-        * @param int $width The width
-        * @param int $height The height
-        * @return Response The rendered image
-        */
-       public function map(Request $request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response {
-               //Without matching hash
-               if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
-                       //Throw new exception
-                       throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
-               }
-
-               //Set map
-               $map = $this->path.'/'.$this->prefix.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
-
-               //Without multi up to date file
-               if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
-                       //Without existing map path
-                       if (!is_dir($dir = dirname($map))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
-
-                               try {
-                                       //Create path
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       //XXX: on CoW filesystems execute a chattr +C before filling
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
-
-                       //Create image instance
-                       $image = new \Imagick();
-
-                       //Add new image
-                       $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
-                       //Create tile instance
-                       $tile = new \Imagick();
-
-                       //Get tile xy
-                       $centerX = $this->map->longitudeToX($longitude, $zoom);
-                       $centerY = $this->map->latitudeToY($latitude, $zoom);
-
-                       //Calculate start xy
-                       $startX = floor(floor($centerX) - $width / MapUtil::tz);
-                       $startY = floor(floor($centerY) - $height / MapUtil::tz);
-
-                       //Calculate end xy
-                       $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
-                       $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
-
-                       for($x = $startX; $x <= $endX; $x++) {
-                               for($y = $startY; $y <= $endY; $y++) {
-                                       //Set cache path
-                                       $cache = $this->cache.'/'.$this->prefix.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
-                                       //Without cache image
-                                       if (!is_file($cache)) {
-                                               //Set tile url
-                                               $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
-
-                                               //Without cache path
-                                               if (!is_dir($dir = dirname($cache))) {
-                                                       //Create filesystem object
-                                                       $filesystem = new Filesystem();
-
-                                                       try {
-                                                               //Create path
-                                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                                               $filesystem->mkdir($dir, 0775);
-                                                       } catch (IOExceptionInterface $e) {
-                                                               //Throw error
-                                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                                                       }
-                                               }
-
-                                               //Store tile in cache
-                                               file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
-                                       }
-
-                                       //Set dest x
-                                       $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
-
-                                       //Set dest y
-                                       $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
-
-                                       //Read tile from cache
-                                       $tile->readImage($cache);
-
-                                       //Compose image
-                                       $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
-                                       //Clear tile
-                                       $tile->clear();
-                               }
-                       }
-
-                       //Add imagick draw instance
-                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
-                       $draw = new \ImagickDraw();
-
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
-
-                       //Set stroke antialias
-                       $draw->setStrokeAntialias(true);
-
-                       //Set text alignment
-                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
-                       //Set gravity
-                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
-                       //Set fill color
-                       $draw->setFillColor('#cff');
-
-                       //Set stroke color
-                       $draw->setStrokeColor('#00c3f9');
-
-                       //Set stroke width
-                       $draw->setStrokeWidth(2);
-
-                       //Draw circle
-                       $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 5, $height/2 + 5);
-
-                       //Draw on image
-                       $image->drawImage($draw);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Add latitude
-                       //XXX: not supported by imagick :'(
-                       $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
-
-                       //Add longitude
-                       //XXX: not supported by imagick :'(
-                       $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
-
-                       //Add description
-                       //XXX: not supported by imagick :'(
-                       #$image->setImageProperty('exif:Description', $caption);
-
-                       //Set progressive jpeg
-                       $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
-                       //Set compression quality
-                       //TODO: ajust that
-                       $image->setImageCompressionQuality(70);
-
-                       //Save image
-                       if (!$image->writeImage($map)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $path));
-                       }
-
-                       //Set mtime
-                       $mtime = stat($map)['mtime'];
-               }
-
-               //Read map from cache
-               $response = new BinaryFileResponse($map);
-
-               //Set file name
-               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
-
-               //Set etag
-               $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
-
-               //Set last modified
-               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
-               //Disable robot index
-               $response->headers->set('X-Robots-Tag', 'noindex');
-
-               //Set as public
-               $response->setPublic();
-
-               //Return 304 response if not modified
-               $response->isNotModified($request);
-
-               //Return response
-               return $response;
-       }
-
-       /**
-        * Return multi map image
-        *
-        * @param Request $request The Request instance
-        * @param string $hash The hash
-        * @param int $updated The updated timestamp
-        * @param float $latitude The latitude
-        * @param float $longitude The longitude
-        * @param string $coordinates The coordinates
-        * @param int $zoom The zoom
-        * @param int $width The width
-        * @param int $height The height
-        * @return Response The rendered image
-        */
-       public function multiMap(Request $request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response {
-               //Without matching hash
-               if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger->hash($coordinates), $zoom, $width, $height])) {
-                       //Throw new exception
-                       throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
-               }
-
-               //Set multi
-               $map = $this->path.'/'.$this->prefix.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
-
-               //Without multi up to date file
-               if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
-                       //Without existing multi path
-                       if (!is_dir($dir = dirname($map))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
-
-                               try {
-                                       //Create path
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       //XXX: on CoW filesystems execute a chattr +C before filling
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
-
-                       //Create image instance
-                       $image = new \Imagick();
-
-                       //Add new image
-                       $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
-                       //Create tile instance
-                       $tile = new \Imagick();
-
-                       //Get tile xy
-                       $centerX = $this->map->longitudeToX($longitude, $zoom);
-                       $centerY = $this->map->latitudeToY($latitude, $zoom);
-
-                       //Calculate start xy
-                       $startX = floor(floor($centerX) - $width / MapUtil::tz);
-                       $startY = floor(floor($centerY) - $height / MapUtil::tz);
-
-                       //Calculate end xy
-                       $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
-                       $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
-
-                       for($x = $startX; $x <= $endX; $x++) {
-                               for($y = $startY; $y <= $endY; $y++) {
-                                       //Set cache path
-                                       $cache = $this->cache.'/'.$this->prefix.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
-                                       //Without cache image
-                                       if (!is_file($cache)) {
-                                               //Set tile url
-                                               $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
-
-                                               //Without cache path
-                                               if (!is_dir($dir = dirname($cache))) {
-                                                       //Create filesystem object
-                                                       $filesystem = new Filesystem();
-
-                                                       try {
-                                                               //Create path
-                                                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                                               $filesystem->mkdir($dir, 0775);
-                                                       } catch (IOExceptionInterface $e) {
-                                                               //Throw error
-                                                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                                                       }
-                                               }
-
-                                               //Store tile in cache
-                                               file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
-                                       }
-
-                                       //Set dest x
-                                       $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
-
-                                       //Set dest y
-                                       $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
-
-                                       //Read tile from cache
-                                       $tile->readImage($cache);
-
-                                       //Compose image
-                                       $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
-                                       //Clear tile
-                                       $tile->clear();
-                               }
-                       }
-
-                       //Add imagick draw instance
-                       //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
-                       $draw = new \ImagickDraw();
-
-                       //Set text antialias
-                       $draw->setTextAntialias(true);
-
-                       //Set stroke antialias
-                       $draw->setStrokeAntialias(true);
-
-                       //Set text alignment
-                       $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
-                       //Set gravity
-                       $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
-                       //Convert to array
-                       $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);
-
-                       //Iterate on locations
-                       foreach($coordinates as $id => $coordinate) {
-                               //Set dest x
-                               $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $this->map->longitudeToX(floatval($coordinate['longitude']), $zoom))));
-
-                               //Set dest y
-                               $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $this->map->latitudeToY(floatval($coordinate['latitude']), $zoom))));
-
-                               //Set fill color
-                               $draw->setFillColor($this->map->getFill());
-
-                               //Set font size
-                               $draw->setFontSize($this->map->getFontSize());
-
-                               //Set stroke color
-                               $draw->setStrokeColor($this->map->getStroke());
-
-                               //Set circle radius
-                               $radius = $this->map->getRadius();
-
-                               //Set stroke width
-                               $stroke = $this->map->getStrokeWidth();
-
-                               //With matching position
-                               if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
-                                       //Set fill color
-                                       $draw->setFillColor($this->map->getHighFill());
-
-                                       //Set font size
-                                       $draw->setFontSize($this->map->getHighFontSize());
-
-                                       //Set stroke color
-                                       $draw->setStrokeColor($this->map->getHighStroke());
-
-                                       //Set circle radius
-                                       $radius = $this->map->getHighRadius();
-
-                                       //Set stroke width
-                                       $stroke = $this->map->getHighStrokeWidth();
-                               }
-
-                               //Set stroke width
-                               $draw->setStrokeWidth($stroke);
-
-                               //Draw circle
-                               $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
-
-                               //Set fill color
-                               $draw->setFillColor($draw->getStrokeColor());
-
-                               //Set stroke width
-                               $draw->setStrokeWidth($stroke / 4);
-
-                               //Get font metrics
-                               #$metrics = $image->queryFontMetrics($draw, strval($id));
-
-                               //Add annotation
-                               $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
-                       }
-
-                       //Draw on image
-                       $image->drawImage($draw);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Add latitude
-                       //XXX: not supported by imagick :'(
-                       $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
-
-                       //Add longitude
-                       //XXX: not supported by imagick :'(
-                       $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
-
-                       //Add description
-                       //XXX: not supported by imagick :'(
-                       #$image->setImageProperty('exif:Description', $caption);
-
-                       //Set progressive jpeg
-                       $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
-                       //Set compression quality
-                       //TODO: ajust that
-                       $image->setImageCompressionQuality(70);
-
-                       //Save image
-                       if (!$image->writeImage($map)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $path));
-                       }
-
-                       //Set mtime
-                       $mtime = stat($map)['mtime'];
-               }
-
-               //Read map from cache
-               $response = new BinaryFileResponse($map);
-
-               //Set file name
-               $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
-
-               //Set etag
-               $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
-
-               //Set last modified
-               $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
-               //Disable robot index
-               $response->headers->set('X-Robots-Tag', 'noindex');
-
-               //Set as public
-               $response->setPublic();
-
-               //Return 304 response if not modified
-               $response->isNotModified($request);
-
-               //Return response
-               return $response;
-       }
-}
index 60b68b6ecee1cb99749c3d7559db583bf49966e3..2f292095b8bbbc76efa7377dcab18eb8e6345e47 100644 (file)
@@ -38,6 +38,38 @@ class Configuration implements ConfigurationInterface {
 
                //The bundle default values
                $defaults = [
+                       //XXX: use a path relative to __DIR__ as console and index do not have the same execution directory
+                       //XXX: use realpath on var/cache only as alias subdirectory may not yet exists
+                       'cache' => realpath(dirname(__DIR__).'/../../../var/cache').'/'.$alias,
+                       'captcha' => [
+                               'background' => 'white',
+                               'fill' => '#cff',
+                               'format' => 'jpeg',
+                               'height' => 52,
+                               'size' => 45,
+                               'border' => '#00c3f9',
+                               'thickness' => 2,
+                               'width' => 192
+                       ],
+                       'context' => [
+                               'http' => [
+                                       'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20,
+                                       'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== '' ? (float)$timeout : 60),
+                                       'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== '' ? (string)$agent : $alias.'/'.($version = RapsysPackBundle::getVersion()))
+                               ]
+                       ],
+                       'facebook' => [
+                               'align' => 'center',
+                               'fill' => 'white',
+                               'font' => 'default',
+                               'format' => 'jpeg',
+                               'height' => 630,
+                               'size' => 60,
+                               'source' => dirname(__DIR__).'/public/facebook/source.png',
+                               'border' => '#00c3f9',
+                               'thickness' => 15,
+                               'width' => 1200
+                       ],
                        'filters' => [
                                'css' => [
                                        0 => [
@@ -64,15 +96,84 @@ class Configuration implements ConfigurationInterface {
                                        ]
                                ]
                        ],
-                       #TODO: migrate to public.path, public.url and router->generateUrl ?
-                       #XXX: that would means dropping the PathPackage stuff and use static route like rapsyspack_facebook
-                       'output' => [
-                               'css' => '@RapsysPack/css/*.pack.css',
-                               'img' => '@RapsysPack/img/*.pack.jpg',
-                               'js' =>  '@RapsysPack/js/*.pack.js'
+                       'fonts' => [
+                               'default' => '/usr/share/fonts/TTF/dejavu/DejaVuSans.ttf',
+                               #TODO: move these in veranda config ? with *: %rapsyspack.public%/woff2/*.woff2 ?
+                               'droidsans' => dirname(__DIR__).'/public/woff2/droidsans.regular.woff2',
+                               'droidsansb' => dirname(__DIR__).'/public/woff2/droidsans.bold.woff2',
+                               'droidsansi' => dirname(__DIR__).'/public/woff2/droidserif.italic.woff2',
+                               'droidsansm' => dirname(__DIR__).'/public/woff2/droidsansmono.regular.woff2',
+                               'droidserif' => dirname(__DIR__).'/public/woff2/droidserif.regular.woff2',
+                               'droidserifb' => dirname(__DIR__).'/public/woff2/droidserif.bold.woff2',
+                               'droidserifbi' => dirname(__DIR__).'/public/woff2/droidserif.bolditalic.woff2',
+                               'irishgrover' => dirname(__DIR__).'/public/woff2/irishgrover.v10.woff2',
+                               'lemon' => dirname(__DIR__).'/public/woff2/lemon.woff2',
+                               'notoemoji' => dirname(__DIR__).'/public/woff2/notoemoji.woff2'
+                       ],
+                       'map' => [
+                               'border' => '#00c3f9',
+                               'fill' => '#cff',
+                               'format' => 'jpeg',
+                               'height' => 640,
+                               'quality' => 70,
+                               'radius' => 5,
+                               'server' => 'osm',
+                               'thickness' => 2,
+                               'tz' => 256,
+                               'width' => 640,
+                               'zoom' => 17
+                       ],
+                       'multi' => [
+                               'border' => '#00c3f9',
+                               'fill' => '#cff',
+                               'format' => 'jpeg',
+                               'height' => 640,
+                               'highborder' => '#3333c3',
+                               'highfill' => '#c3c3f9',
+                               'highradius' => 6,
+                               'highsize' => 30,
+                               'highthickness' => 4,
+                               'quality' => 70,
+                               'radius' => 5,
+                               'server' => 'osm',
+                               'size' => 20,
+                               'thickness' => 2,
+                               'tz' => 256,
+                               'width' => 640,
+                               'zoom' => 17
                        ],
-                       'path' => dirname(__DIR__).'/Resources/public',
-                       'token' => 'asset_url'
+                       'prefixes' => [
+                               'captcha' => 'captcha',
+                               'css' => 'css',
+                               'facebook' => 'facebook',
+                               'img' => 'img',
+                               'map' => 'map',
+                               'multi' => 'multi',
+                               'pack' => 'pack',
+                               'thumb' => 'thumb',
+                               'js' => 'js'
+                       ],
+                       //XXX: use a path relative to __DIR__ as console and index do not have the same execution directory
+                       'public' => dirname(__DIR__).'/public',
+                       'routes' => [
+                               'css' => 'rapsyspack_css',
+                               'img' => 'rapsyspack_img',
+                               'js' => 'rapsyspack_js'
+                       ],
+                       'servers' => [
+                               'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png',
+                               'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png',
+                               'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
+                       ],
+                       'thumb' => [
+                               'height' => 128,
+                               'width' => 128
+                       ],
+                       'tokens' => [
+                               'css' => 'asset',
+                               'img' => 'asset',
+                               'js' => 'asset'
+                       ]
                ];
 
                /**
@@ -90,6 +191,48 @@ class Configuration implements ConfigurationInterface {
                        ->getRootNode()
                                ->addDefaultsIfNotSet()
                                ->children()
+                                       ->scalarNode('cache')->cannotBeEmpty()->defaultValue($defaults['cache'])->end()
+                                       ->arrayNode('captcha')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('background')->cannotBeEmpty()->defaultValue($defaults['captcha']['background'])->end()
+                                                       ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['captcha']['fill'])->end()
+                                                       ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['captcha']['format'])->end()
+                                                       ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['captcha']['height'])->end()
+                                                       ->scalarNode('size')->cannotBeEmpty()->defaultValue($defaults['captcha']['size'])->end()
+                                                       ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['captcha']['border'])->end()
+                                                       ->scalarNode('thickness')->cannotBeEmpty()->defaultValue($defaults['captcha']['thickness'])->end()
+                                                       ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['captcha']['width'])->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('context')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->arrayNode('http')
+                                                               ->addDefaultsIfNotSet()
+                                                               ->children()
+                                                                       ->scalarNode('max_redirects')->defaultValue($defaults['context']['http']['max_redirects'])->end()
+                                                                       ->scalarNode('timeout')->defaultValue($defaults['context']['http']['timeout'])->end()
+                                                                       ->scalarNode('user_agent')->cannotBeEmpty()->defaultValue($defaults['context']['http']['user_agent'])->end()
+                                                               ->end()
+                                                       ->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('facebook')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('align')->cannotBeEmpty()->defaultValue($defaults['facebook']['align'])->end()
+                                                       ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['facebook']['fill'])->end()
+                                                       ->scalarNode('font')->cannotBeEmpty()->defaultValue($defaults['facebook']['font'])->end()
+                                                       ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['facebook']['format'])->end()
+                                                       ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['facebook']['height'])->end()
+                                                       ->scalarNode('size')->cannotBeEmpty()->defaultValue($defaults['facebook']['size'])->end()
+                                                       ->scalarNode('source')->cannotBeEmpty()->defaultValue($defaults['facebook']['source'])->end()
+                                                       ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['facebook']['border'])->end()
+                                                       ->scalarNode('thickness')->cannotBeEmpty()->defaultValue($defaults['facebook']['thickness'])->end()
+                                                       ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['facebook']['width'])->end()
+                                               ->end()
+                                       ->end()
                                        ->arrayNode('filters')
                                                ->addDefaultsIfNotSet()
                                                ->children()
@@ -162,16 +305,83 @@ class Configuration implements ConfigurationInterface {
                                                        ->end()
                                                ->end()
                                        ->end()
-                                       ->arrayNode('output')
+                                       ->arrayNode('fonts')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['fonts'])
+                                               ->scalarPrototype()->end()
+                                       ->end()
+                                       ->arrayNode('map')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['map']['border'])->end()
+                                                       ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['map']['fill'])->end()
+                                                       ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['facebook']['format'])->end()
+                                                       ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['map']['height'])->end()
+                                                       ->scalarNode('quality')->cannotBeEmpty()->defaultValue($defaults['map']['quality'])->end()
+                                                       ->scalarNode('radius')->cannotBeEmpty()->defaultValue($defaults['map']['radius'])->end()
+                                                       ->scalarNode('server')->cannotBeEmpty()->defaultValue($defaults['map']['server'])->end()
+                                                       ->scalarNode('thickness')->cannotBeEmpty()->defaultValue($defaults['map']['thickness'])->end()
+                                                       ->scalarNode('tz')->cannotBeEmpty()->defaultValue($defaults['map']['tz'])->end()
+                                                       ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['map']['width'])->end()
+                                                       ->scalarNode('zoom')->cannotBeEmpty()->defaultValue($defaults['map']['zoom'])->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('multi')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['multi']['border'])->end()
+                                                       ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['multi']['fill'])->end()
+                                                       ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['facebook']['format'])->end()
+                                                       ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['multi']['height'])->end()
+                                                       ->scalarNode('highborder')->cannotBeEmpty()->defaultValue($defaults['multi']['highborder'])->end()
+                                                       ->scalarNode('highfill')->cannotBeEmpty()->defaultValue($defaults['multi']['highfill'])->end()
+                                                       ->scalarNode('highradius')->cannotBeEmpty()->defaultValue($defaults['multi']['highradius'])->end()
+                                                       ->scalarNode('highsize')->cannotBeEmpty()->defaultValue($defaults['multi']['highsize'])->end()
+                                                       ->scalarNode('highthickness')->cannotBeEmpty()->defaultValue($defaults['multi']['highthickness'])->end()
+                                                       ->scalarNode('quality')->cannotBeEmpty()->defaultValue($defaults['multi']['quality'])->end()
+                                                       ->scalarNode('radius')->cannotBeEmpty()->defaultValue($defaults['multi']['radius'])->end()
+                                                       ->scalarNode('server')->cannotBeEmpty()->defaultValue($defaults['multi']['server'])->end()
+                                                       ->scalarNode('size')->cannotBeEmpty()->defaultValue($defaults['multi']['size'])->end()
+                                                       ->scalarNode('thickness')->cannotBeEmpty()->defaultValue($defaults['multi']['thickness'])->end()
+                                                       ->scalarNode('tz')->cannotBeEmpty()->defaultValue($defaults['multi']['tz'])->end()
+                                                       ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['multi']['width'])->end()
+                                                       ->scalarNode('zoom')->cannotBeEmpty()->defaultValue($defaults['multi']['zoom'])->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('prefixes')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['prefixes'])
+                                               ->scalarPrototype()->end()
+                                       ->end()
+                                       ->scalarNode('public')->cannotBeEmpty()->defaultValue($defaults['public'])->end()
+                                       ->arrayNode('routes')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('css')->cannotBeEmpty()->defaultValue($defaults['routes']['css'])->end()
+                                                       ->scalarNode('img')->cannotBeEmpty()->defaultValue($defaults['routes']['img'])->end()
+                                                       ->scalarNode('js')->cannotBeEmpty()->defaultValue($defaults['routes']['js'])->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('servers')
+                                               ->treatNullLike([])
+                                               ->defaultValue($defaults['servers'])
+                                               ->scalarPrototype()->end()
+                                       ->end()
+                                       ->arrayNode('thumb')
+                                               ->addDefaultsIfNotSet()
+                                               ->children()
+                                                       ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['thumb']['height'])->end()
+                                                       ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['thumb']['width'])->end()
+                                               ->end()
+                                       ->end()
+                                       ->arrayNode('tokens')
                                                ->addDefaultsIfNotSet()
                                                ->children()
-                                                       ->scalarNode('css')->cannotBeEmpty()->defaultValue($defaults['output']['css'])->end()
-                                                       ->scalarNode('img')->cannotBeEmpty()->defaultValue($defaults['output']['img'])->end()
-                                                       ->scalarNode('js')->cannotBeEmpty()->defaultValue($defaults['output']['js'])->end()
+                                                       ->scalarNode('css')->cannotBeEmpty()->defaultValue($defaults['tokens']['css'])->end()
+                                                       ->scalarNode('img')->cannotBeEmpty()->defaultValue($defaults['tokens']['img'])->end()
+                                                       ->scalarNode('js')->cannotBeEmpty()->defaultValue($defaults['tokens']['js'])->end()
                                                ->end()
                                        ->end()
-                                       ->scalarNode('path')->cannotBeEmpty()->defaultValue($defaults['path'])->end()
-                                       ->scalarNode('token')->cannotBeEmpty()->defaultValue($defaults['token'])->end()
                                ->end()
                        ->end();
 
index fc9efcebcb19db954d30711c3c7682d1248af9e4..3a6ed53cbf716ca0ed208484b06661217ba0df0c 100644 (file)
@@ -49,8 +49,11 @@ class RapsysPackExtension extends Extension {
                //Set rapsyspack.alias key
                $container->setParameter($alias.'.alias', $alias);
 
-               //Set rapsyspack.path key
-               $container->setParameter($alias.'.path', $config['path']);
+               //Set rapsyspack.cache key
+               $container->setParameter($alias.'.cache', $config['cache']);
+
+               //Set rapsyspack.public key
+               $container->setParameter($alias.'.public', $config['public']);
 
                //Set rapsyspack.version key
                $container->setParameter($alias.'.version', RapsysPackBundle::getVersion());
index aa1168889e9b43395163fde3b718ea019ee3456a..5c890996301310a8c36a1e086ebba48de1d67f95 100644 (file)
 
 namespace Rapsys\PackBundle\Extension;
 
+use Psr\Container\ContainerInterface;
+
 use Rapsys\PackBundle\Parser\TokenParser;
 use Rapsys\PackBundle\RapsysPackBundle;
 use Rapsys\PackBundle\Util\IntlUtil;
 use Rapsys\PackBundle\Util\SluggerUtil;
 
-use Symfony\Component\Asset\PackageInterface;
+use Symfony\Component\Routing\RouterInterface;
 use Symfony\Component\HttpKernel\Config\FileLocator;
 
 use Twig\Extension\AbstractExtension;
@@ -26,11 +28,34 @@ use Twig\Extension\AbstractExtension;
  */
 class PackExtension extends AbstractExtension {
        /**
+        * Config array
+        */
+       protected array $config;
+
+       /**
+        * The stream context instance
+        */
+       protected mixed $ctx;
+
+       /**
+        * Creates pack extension
+        *
         * {@inheritdoc}
         *
         * @link https://twig.symfony.com/doc/2.x/advanced.html
+        *
+        * @param ContainerInterface $container The ContainerInterface instance
+        * @param IntlUtil $intl The IntlUtil instance
+        * @param FileLocator $locator The FileLocator instance
+        * @param RouterInterface $router The RouterInterface instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
         */
-       public function __construct(protected IntlUtil $intl, protected FileLocator $locator, protected PackageInterface $package, protected SluggerUtil $slugger, protected array $parameters) {
+       public function __construct(protected ContainerInterface $container, protected IntlUtil $intl, protected FileLocator $locator, protected RouterInterface $router, protected SluggerUtil $slugger) {
+               //Retrieve config
+               $this->config = $container->getParameter(RapsysPackBundle::getAlias());
+
+               //Set ctx
+               $this->ctx = stream_context_create($this->config['context']);
        }
 
        /**
@@ -40,9 +65,9 @@ class PackExtension extends AbstractExtension {
         */
        public function getTokenParsers(): array {
                return [
-                       new TokenParser($this->locator, $this->package, $this->parameters['token'], 'stylesheet', $this->parameters['output']['css'], $this->parameters['filters']['css']),
-                       new TokenParser($this->locator, $this->package, $this->parameters['token'], 'javascript', $this->parameters['output']['js'], $this->parameters['filters']['js']),
-                       new TokenParser($this->locator, $this->package, $this->parameters['token'], 'image', $this->parameters['output']['img'], $this->parameters['filters']['img'])
+                       new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'css', 'stylesheet'),
+                       new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'js', 'javascript'),
+                       new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'img', 'image')
                ];
        }
 
@@ -60,7 +85,6 @@ class PackExtension extends AbstractExtension {
                        new \Twig\TwigFilter('intlcurrency', [$this->intl, 'currency']),
                        new \Twig\TwigFilter('intldate', [$this->intl, 'date'], ['needs_environment' => true]),
                        new \Twig\TwigFilter('intlnumber', [$this->intl, 'number']),
-                       new \Twig\TwigFilter('intlsize', [$this->intl, 'size']),
                        new \Twig\TwigFilter('lcfirst', 'lcfirst'),
                        new \Twig\TwigFilter('short', [$this->slugger, 'short']),
                        new \Twig\TwigFilter('slug', [$this->slugger, 'slug']),
index c3a5f69c4559e8d588ad40d62f5453dcc8f8c0a2..f8cf3d024deba7465841d157d49a34981c7947e4 100644 (file)
@@ -11,6 +11,7 @@
 
 namespace Rapsys\PackBundle\Form;
 
+use Rapsys\PackBundle\RapsysPackBundle;
 use Rapsys\PackBundle\Util\ImageUtil;
 use Rapsys\PackBundle\Util\SluggerUtil;
 
@@ -49,7 +50,7 @@ class CaptchaType extends AbstractType {
                //With image, slugger and translator
                if (!empty($options['captcha']) && $this->image !== null && $this->slugger !== null && $this->translator !== null) {
                        //Set captcha
-                       $captcha = $this->image->getCaptcha((new \DateTime('-1 year'))->getTimestamp());
+                       $captcha = $this->image->getCaptcha();
 
                        //Add captcha token
                        $builder->add('_captcha_token', HiddenType::class, ['data' => $captcha['token'], 'empty_data' => $captcha['token'], 'mapped' => false]);
@@ -70,7 +71,7 @@ class CaptchaType extends AbstractType {
                parent::configureOptions($resolver);
 
                //Set defaults
-               $resolver->setDefaults(['captcha' => false]);
+               $resolver->setDefaults(['captcha' => false, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]);
 
                //Add extra captcha option
                $resolver->setAllowedTypes('captcha', 'boolean');
diff --git a/Form/ContactType.php b/Form/ContactType.php
new file mode 100644 (file)
index 0000000..3d70bfb
--- /dev/null
@@ -0,0 +1,60 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle\Form;
+
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Validator\Constraints\Email;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+/**
+ * {@inheritdoc}
+ */
+class ContactType extends CaptchaType {
+       /**
+        * {@inheritdoc}
+        */
+       public function buildForm(FormBuilderInterface $builder, array $options): void {
+               //Add fields
+               $builder
+                       ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'constraints' => [new NotBlank(['message' => 'Please provide your name'])]])
+                       ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'constraints' => [new NotBlank(['message' => 'Please provide your subject'])]])
+                       ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'constraints' => [new NotBlank(['message' => 'Please provide a valid mail']), new Email(['message' => 'Your mail doesn\'t seems to be valid'])]])
+                       ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'constraints' => [new NotBlank(['message' => 'Please provide your message'])]])
+                       ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+               //Call parent
+               parent::buildForm($builder, $options);
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function configureOptions(OptionsResolver $resolver): void {
+               //Call parent configure options
+               parent::configureOptions($resolver);
+
+               //Set defaults
+               $resolver->setDefaults(['captcha' => true]);
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getName() {
+               return 'contact_form';
+       }
+}
index 67e87eb52f6bad193e0add9d6b8fc9f1a8aac6d5..7d73117b432129a70fbfbfcb6f2972530846de40 100644 (file)
 
 namespace Rapsys\PackBundle\Parser;
 
+use Psr\Container\ContainerInterface;
+
 use Rapsys\PackBundle\RapsysPackBundle;
+use Rapsys\PackBundle\Util\SluggerUtil;
 
 use Symfony\Component\Asset\PackageInterface;
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\HttpKernel\Config\FileLocator;
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\RouterInterface;
 
 use Twig\Error\Error;
 use Twig\Node\Expression\AssignNameExpression;
@@ -32,32 +39,49 @@ use Twig\TokenParser\AbstractTokenParser;
  */
 class TokenParser extends AbstractTokenParser {
        /**
-        * The stream context instance
+        * Filters array
+        */
+       protected array $filters;
+
+       /**
+        * Output string
+        */
+       protected string $output;
+
+       /**
+        * Route string
+        */
+       protected string $route;
+
+       /**
+        * Token string
         */
-       protected mixed $ctx;
+       protected string $token;
 
        /**
         * Constructor
         *
+        * @param ContainerInterface $container The ContainerInterface instance
         * @param FileLocator $locator The FileLocator instance
-        * @param PackageInterface $package The Assets Package instance
-        * @param string $token The token name
+        * @param RouterInterface $router The RouterInterface instance
+        * @param SluggerUtil $slugger The SluggerUtil instance
+        * @param array $config The config
+        * @param mixed $ctx The context stream instance
+        * @param string $prefix The output prefix
         * @param string $tag The tag name
-        * @param string $output The default output string
-        * @param array $filters The default filter array
         */
-       public function __construct(protected FileLocator $locator, protected PackageInterface $package, protected string $token, protected string $tag, protected string $output, protected array $filters) {
-               //Set ctx
-               $this->ctx = stream_context_create(
-                       [
-                               'http' => [
-                                       #'header' => ['Referer: https://www.openstreetmap.org/'],
-                                       'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20,
-                                       'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== "" ? (float)$timeout : 60),
-                                       'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== "" ? (string)$agent : RapsysPackBundle::getAlias().'/'.RapsysPackBundle::getVersion())
-                               ]
-                       ]
-               );
+       public function __construct(protected ContainerInterface $container, protected FileLocator $locator, protected RouterInterface $router, protected SluggerUtil $slugger, protected array $config, protected mixed $ctx, protected string $prefix, protected string $tag) {
+               //Set filters
+               $this->filters = $config['filters'][$prefix];
+
+               //Set output
+               $this->output = $config['public'].'/'.$config['prefixes']['pack'].'/'.$config['prefixes'][$prefix].'/*.'.$prefix;
+
+               //Set route
+               $this->route = $config['routes'][$prefix];
+
+               //Set token
+               $this->token = $config['tokens'][$prefix];
        }
 
        /**
@@ -96,7 +120,7 @@ class TokenParser extends AbstractTokenParser {
                while (!$stream->test(Token::BLOCK_END_TYPE)) {
                        //The files to process
                        if ($stream->test(Token::STRING_TYPE)) {
-                               //'somewhere/somefile.(css,img,js)' 'somewhere/*' '@jquery'
+                               //'somewhere/somefile.(css|img|js)' 'somewhere/*' '@jquery'
                                $inputs[] = $stream->next()->getValue();
                        //The filters token
                        } elseif ($stream->test(Token::NAME_TYPE, 'filters')) {
@@ -104,12 +128,19 @@ class TokenParser extends AbstractTokenParser {
                                $stream->next();
                                $stream->expect(Token::OPERATOR_TYPE, '=');
                                $this->filters = array_merge($this->filters, array_filter(array_map('trim', explode(',', $stream->expect(Token::STRING_TYPE)->getValue()))));
+                       //The route token
+                       } elseif ($stream->test(Token::NAME_TYPE, 'route')) {
+                               //output='rapsyspack_css' OR output='rapsyspack_js' OR output='rapsyspack_img'
+                               $stream->next();
+                               $stream->expect(Token::OPERATOR_TYPE, '=');
+                               $this->route = $stream->expect(Token::STRING_TYPE)->getValue();
                        //The output token
                        } elseif ($stream->test(Token::NAME_TYPE, 'output')) {
                                //output='js/packed/*.js' OR output='js/core.js'
                                $stream->next();
                                $stream->expect(Token::OPERATOR_TYPE, '=');
                                $this->output = $stream->expect(Token::STRING_TYPE)->getValue();
+                       //TODO: add format ? jpeg|png|gif|webp|webm ???
                        //The token name
                        } elseif ($stream->test(Token::NAME_TYPE, 'token')) {
                                //name='core_js'
@@ -119,6 +150,7 @@ class TokenParser extends AbstractTokenParser {
                        //Unexpected token
                        } else {
                                $token = $stream->getCurrent();
+                               //Throw error
                                throw new Error(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext());
                        }
                }
@@ -132,12 +164,25 @@ class TokenParser extends AbstractTokenParser {
                //Process end block
                $stream->expect(Token::BLOCK_END_TYPE);
 
-               //Replace star with sha1
-               if (($pos = strpos($this->output, '*')) !== false) {
-                       //XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7)
-                       $this->output = substr($this->output, 0, $pos).sha1(serialize($inputs).serialize($this->filters)).substr($this->output, $pos + 1);
+               //Without valid output
+               if (($pos = strpos($this->output, '*')) === false || $pos !== strrpos($this->output, '*')) {
+                       //Throw error
+                       throw new Error(sprintf('Invalid output "%s"', $this->output), $token->getLine(), $stream->getSourceContext());
+               }
+
+               //Without existing route
+               if ($this->router->getRouteCollection()->get($this->route) === null) {
+                       //Throw error
+                       throw new Error(sprintf('Invalid route "%s"', $this->route), $token->getLine(), $stream->getSourceContext());
                }
 
+               //Set file
+               //XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7)
+               $file = $this->slugger->hash([$inputs, $this->filters, $this->output, $this->route, $this->token]);
+
+               //Replace star by file
+               $this->output = substr($this->output, 0, $pos).$file.substr($this->output, $pos + 1);
+
                //Process inputs
                for($k = 0; $k < count($inputs); $k++) {
                        //Deal with generic url
@@ -161,6 +206,7 @@ class TokenParser extends AbstractTokenParser {
                                        foreach($replacement as $input) {
                                                //Check that it's a file
                                                if (!is_file($input)) {
+                                                       //Throw error
                                                        throw new Error(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext());
                                                }
                                        }
@@ -172,17 +218,21 @@ class TokenParser extends AbstractTokenParser {
                                        $k += count($replacement) - 1;
                                //Check that it's a file
                                } elseif (!is_file($inputs[$k])) {
+                                       //Throw error
                                        throw new Error(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext());
                                }
                        }
                }
 
+               #TODO: move the inputs reading from here to inside the filters ?
+
                //Check inputs
                if (!empty($inputs)) {
                        //Retrieve files content
                        foreach($inputs as $input) {
                                //Try to retrieve content
                                if (($data = file_get_contents($input, false, $this->ctx)) === false) {
+                                       //Throw error
                                        throw new Error(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext());
                                }
 
@@ -229,12 +279,6 @@ class TokenParser extends AbstractTokenParser {
                        #throw new Error('Empty filters token', $token->getLine(), $stream->getSourceContext());
                }
 
-               //Retrieve asset uri
-               //XXX: this path is the merge of services.assets.path_package.arguments[0] and rapsyspack.output.(css,img,js)
-               if (($outputUrl = $this->package->getUrl($this->output)) === false) {
-                       throw new Error(sprintf('Unable to get url for asset: %s', $this->output), $token->getLine(), $stream->getSourceContext());
-               }
-
                //Check if we have a bundle path
                if ($this->output[0] == '@') {
                        //Resolve it
@@ -264,14 +308,31 @@ class TokenParser extends AbstractTokenParser {
                        $filesystem->dumpFile($this->output, $content);
                } catch (IOExceptionInterface $e) {
                        //Throw error
-                       throw new Error(sprintf('Unable to write to: %s', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+                       throw new Error(sprintf('Unable to write "%s"', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+               }
+
+               //Without output file mtime
+               if (($mtime = filemtime($this->output)) === false) {
+                       //Throw error
+                       throw new Error(sprintf('Unable to get "%s" mtime', $this->output), $token->getLine(), $stream->getSourceContext(), $e);
+               }
+
+               //TODO: get mimetype for images ? and set _format ?
+
+               try {
+                       //Generate asset url
+                       $asset = $this->router->generate($this->route, [ 'file' => $file, 'u' => $mtime ]);
+               //Catch router exceptions
+               } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
+                       //Throw error
+                       throw new Error(sprintf('Unable to generate asset route "%s"', $this->route), $token->getLine(), $stream->getSourceContext(), $e);
                }
 
                //Set name in context key
                $ref = new AssignNameExpression($this->token, $token->getLine());
 
                //Set output in context value
-               $value = new TextNode($outputUrl, $token->getLine());
+               $value = new TextNode($asset, $token->getLine());
 
                //Send body with context set
                return new Node([
@@ -312,62 +373,50 @@ class TokenParser extends AbstractTokenParser {
                        return $this->config['jquery'];
                }*/
 
-               //Check that we have a / separator between bundle name and path
-               if (($pos = strpos($file, '/')) === false) {
-                       throw new Error(sprintf('Invalid path "%s"', $file), $lineno, $source);
+               //Extract bundle
+               if (($bundle = strstr($file, '/', true)) === false) {
+                       throw new Error(sprintf('Invalid bundle "%s"', $file), $lineno, $source);
                }
 
-               //Set bundle
-               $bundle = substr($file, 0, $pos);
-
-               //Set path
-               $path = substr($file, $pos + 1);
-
-               //Check for bundle suffix presence
-               //XXX: use "bundle templates automatic namespace" mimicked behaviour to find intended bundle and/or path
-               //XXX: see https://symfony.com/doc/4.3/templates.html#bundle-templates
-               if (strlen($bundle) < strlen('Bundle') || substr($bundle, -strlen('Bundle')) !== 'Bundle') {
-                       //Append Bundle in an attempt to fix it's naming for locator
-                       $bundle .= 'Bundle';
-
-                       //Check for public resource prefix presence
-                       if (strlen($path) < strlen('Resources/public') || substr($path, 0, strlen('Resources/public')) != 'Resources/public') {
-                               //Prepend standard public path
-                               $path = 'Resources/public/'.$path;
-                       }
+               //Extract path
+               if (($path = strstr($file, '/')) === false) {
+                       throw new Error(sprintf('Invalid path "%s"', $file), $lineno, $source);
                }
 
-               //Resolve bundle prefix
-               try {
-                       $prefix = $this->locator->locate($bundle);
-               //Catch bundle does not exist or is not enabled exception
-               } catch(\InvalidArgumentException $e) {
-                       //Fix lowercase first bundle character
-                       if ($bundle[1] > 'Z' || $bundle[1] < 'A') {
-                               $bundle[1] = strtoupper($bundle[1]);
-                       }
-
-                       //Detect double bundle suffix
-                       if (strlen($bundle) > strlen('_bundleBundle') && substr($bundle, -strlen('_bundleBundle')) == '_bundleBundle') {
-                               //Strip extra bundle
-                               $bundle = substr($bundle, 0, -strlen('Bundle'));
-                       }
+               //Extract alias
+               $alias = strtolower(substr($bundle, 1));
 
-                       //Convert snake case in camel case
-                       if (strpos($bundle, '_') !== false) {
-                               //Fix every first character following a _
-                               while(($cur = strpos($bundle, '_')) !== false) {
-                                       $bundle = substr($bundle, 0, $cur).ucfirst(substr($bundle, $cur + 1));
-                               }
+               //With public parameter
+               if ($this->container->hasParameter($alias.'.public')) {
+                       //Set prefix
+                       $prefix = $this->container->getParameter($alias.'.public');
+               //Without public parameter
+               } else {
+                       //Without bundle suffix presence
+                       //XXX: use "bundle templates automatic namespace" mimicked behaviour to find intended bundle and/or path
+                       //XXX: see https://symfony.com/doc/4.3/templates.html#bundle-templates
+                       if (strlen($bundle) < strlen('@Bundle') || substr($bundle, -strlen('Bundle')) !== 'Bundle') {
+                               //Append Bundle
+                               $bundle .= 'Bundle';
                        }
 
-                       //Resolve fixed bundle prefix
+                       //Try to resolve bundle prefix
                        try {
                                $prefix = $this->locator->locate($bundle);
-                               //Catch bundle does not exist or is not enabled exception again
+                       //Catch bundle does not exist or is not enabled exception
                        } catch(\InvalidArgumentException $e) {
-                               //Bail out as bundle or path is invalid and we have no way to know what was meant
-                               throw new Error(sprintf('Invalid bundle name "%s" in path "%s". Maybe you meant "%s"', substr($file, 1, $pos - 1), $file, $bundle.'/'.$path), $lineno, $source, $e);
+                               throw new Error(sprintf('Unlocatable bundle "%s"', $bundle), $lineno, $source, $e);
+                       }
+
+                       //With Resources/public subdirectory
+                       if (is_dir($prefix.'Resources/public')) {
+                               $prefix .= 'Resources/public';
+                       //With public subdirectory
+                       } elseif (is_dir($prefix.'public')) {
+                               $prefix .= 'public';
+                       //Without any public subdirectory
+                       } else {
+                               throw new Error(sprintf('Bundle "%s" lacks a public subdirectory', $bundle), $lineno, $source, $e);
                        }
                }
 
index 8de9fa3a280b61b8082044ba826e22e2ffac5fda..08360b08cb3f0623ec668f6de293c6916ee3a98a 100644 (file)
@@ -28,45 +28,6 @@ class RapsysPackBundle extends Bundle {
                return $this->createContainerExtension();
        }
 
-       /**
-        * Return bundle alias
-        *
-        * @return string The bundle alias
-        */
-       public static function getBundleAlias(): string {
-               //With namespace
-               if ($npos = strrpos(static::class, '\\')) {
-                       //Set name pos
-                       $npos++;
-
-                       //With single namespace
-                       $nspos = strpos(static::class, '\\');
-                       //Without namespace
-               } else {
-                       //Set name pos
-                       $npos = 0;
-               }
-
-               //With trailing bundle
-               if (substr(static::class, -strlen('Bundle'), strlen('Bundle')) === 'Bundle') {
-                       //Set bundle pos
-                       $bpos = strlen(static::class) - $npos - strlen('Bundle');
-                       //Without bundle
-               } else {
-                       //Set bundle pos
-                       $bpos = strlen(static::class) - $npos;
-               }
-
-               //With namespace
-               if ($npos) {
-                       //Return prefixed class name
-                       return strtolower(substr(static::class, 0, $nspos).'/'.substr(static::class, $npos, $bpos));
-               }
-
-               //Return class name
-               return strtolower(substr(static::class, $npos, $bpos));
-       }
-
        /**
         * Return alias
         *
@@ -103,6 +64,6 @@ class RapsysPackBundle extends Bundle {
         */
        public static function getVersion(): string {
                //Return version
-               return '0.5.2';
+               return '0.5.4';
        }
 }
diff --git a/Resources/config/routes/rapsyspack.yaml b/Resources/config/routes/rapsyspack.yaml
deleted file mode 100644 (file)
index aa0a953..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-#Routes configuration
-rapsyspack_captcha:
-    path: '/captcha/{hash<[a-zA-Z0-9=_-]+>}/{updated<\d+>}/{equation<[a-zA-Z0-9=_-]+>}/{width<\d+>?120}/{height<\d+>?36}.{!_format?jpeg}'
-    controller: Rapsys\PackBundle\Controller\ImageController::captcha
-    methods: GET
-
-#TODO: replace this url with a redirection route ???
-#XXX: we don't need the mtime, maybe we can drop it in this redirect instead of apache ?
-rapsyspack_facebook:
-    path: '/bundles/rapsyspack/facebook/{mtime<\d+>}{path</.*>}.{!_format?jpeg}'
-    methods: GET
-
-rapsyspack_map:
-    path: '/map/{hash<[a-zA-Z0-9=_-]+>}/{updated<\d+>}/{latitude<\d+(\.?\d+)?>}/{longitude<\d+(\.?\d+)?>}/{zoom<\d+>?17}/{width<\d+>?640}/{height<\d+>?640}.{!_format?jpeg}'
-    controller: Rapsys\PackBundle\Controller\MapController::map
-    methods: GET
-
-rapsyspack_multimap:
-    path: '/multimap/{hash<[a-zA-Z0-9=_-]+>}/{updated<\d+>}/{latitude<\d+(\.?\d+)?>}/{longitude<\d+(\.?\d+)?>}/{coordinates<(?:\d+(\.\d+)?,\d+(\.\d+)?(-\d+(\.\d+)?,\d+(\.\d+)?)*)?>}/{zoom<\d+>?15}/{width<\d+>?640}/{height<\d+>?640}.{!_format?jpeg}'
-    controller: Rapsys\PackBundle\Controller\MapController::multimap
-    methods: GET
-
-rapsyspack_thumb:
-    path: '/thumb/{hash<[a-zA-Z0-9=_-]+>}/{updated<\d+>}/{path<[a-zA-Z0-9=_-]+>}/{width<\d+>?640}/{height<\d+>?640}.{!_format?jpeg}'
-    controller: Rapsys\PackBundle\Controller\ImageController::thumb
-    methods: GET
index 91664d1525b6b0e4e63ffccb2b0a3345408163c2..633702b61574a2bd2542cea9d89847d753891487 100644 (file)
 
 namespace Rapsys\PackBundle\Util;
 
+use Psr\Container\ContainerInterface;
+
+use Rapsys\PackBundle\RapsysPackBundle;
+
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -21,337 +25,24 @@ use Symfony\Component\Routing\RouterInterface;
  */
 class FacebookUtil {
        /**
-        * The default fonts
-        */
-       const fonts = [ 'default' => 'ttf/default.ttf' ];
-
-       /**
-        * The default font
-        */
-       const font = 'default';
-
-       /**
-        * The default font size
-        */
-       const size = 60;
-
-       /**
-        * The default width
-        */
-       const width = 15;
-
-       /**
-        * The default fill
-        */
-       const fill = 'white';
-
-       /**
-        * The default stroke
+        * Alias string
         */
-       const stroke = '#00c3f9';
+       protected string $alias;
 
        /**
-        * The default align
+        * Config array
         */
-       const align = 'center';
+       protected array $config;
 
        /**
         * Creates a new facebook util
         *
+        * @param ContainerInterface $container The container instance
         * @param RouterInterface $router The RouterInterface instance
-        * @param string $cache The cache directory
-        * @param string $path The public path
-        * @param string $prefix The prefix
-        * @param ?string $source The source
-        * @param array $fonts The fonts
-        * @param string $font The font
-        * @param int $size The size
-        * @param int $width The width
-        * @param string $fill The fill
-        * @param string $stroke The stroke
-        * @param string $align The align
-        */
-       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) {
-       }
-
-       /**
-        * Return the facebook image
-        *
-        * Generate simple image in jpeg format or load it from cache
-        *
-        * @param string $pathInfo The request path info
-        * @param array $texts The image texts
-        * @param int $updated The updated timestamp
-        * @param ?string $source The image source
-        * @param int $width The width
-        * @param int $height The height
-        * @return array The image array
+        * @param SluggerUtil $slugger The SluggerUtil instance
         */
-       public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array {
-               //Without source
-               if ($source === null && $this->source === null) {
-                       //Return empty image data
-                       return [];
-               //Without local source
-               } elseif ($source === null) {
-                       //Set local source
-                       $source = $this->source;
-               }
-
-               //Set path file
-               $path = $this->path.'/'.$this->prefix.$pathInfo.'.jpeg';
-
-               //Without existing path
-               if (!is_dir($dir = dirname($path))) {
-                       //Create filesystem object
-                       $filesystem = new Filesystem();
-
-                       try {
-                               //Create path
-                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                               $filesystem->mkdir($dir, 0775);
-                       } catch (IOExceptionInterface $e) {
-                               //Throw error
-                               throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
-                       }
-               }
-
-               //With path file
-               if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) {
-                       #XXX: we used to drop texts with $data['canonical'] === true !!!
-
-                       //Return image data
-                       return [
-                               'og:image' => $this->router->generate('rapsyspack_facebook', ['mtime' => $mtime, 'path' => $pathInfo], UrlGeneratorInterface::ABSOLUTE_URL),
-                               'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                               'og:image:height' => $height,
-                               'og:image:width' => $width
-                       ];
-               }
-
-               //Set cache path
-               $cache = $this->cache.'/'.$this->prefix.$pathInfo.'.png';
-
-               //Without cache path
-               if (!is_dir($dir = dirname($cache))) {
-                       //Create filesystem object
-                       $filesystem = new Filesystem();
-
-                       try {
-                               //Create path
-                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                               $filesystem->mkdir($dir, 0775);
-                       } catch (IOExceptionInterface $e) {
-                               //Throw error
-                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                       }
-               }
-
-               //Create image object
-               $image = new \Imagick();
-
-               //Without cache image
-               if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
-                       //Check target directory
-                       if (!is_dir($dir = dirname($cache))) {
-                               //Create filesystem object
-                               $filesystem = new Filesystem();
-
-                               try {
-                                       //Create dir
-                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
-                                       $filesystem->mkdir($dir, 0775);
-                               } catch (IOExceptionInterface $e) {
-                                       //Throw error
-                                       throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
-                               }
-                       }
-
-                       //Without source
-                       if (!is_file($source)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Source file "%s" do not exists', $this->source));
-                       }
-
-                       //Convert to absolute path
-                       $source = realpath($source);
-
-                       //Read image
-                       //XXX: Imagick::readImage only supports absolute path
-                       $image->readImage($source);
-
-                       //Crop using aspect ratio
-                       //XXX: for better result upload image directly in aspect ratio :)
-                       $image->cropThumbnailImage($width, $height);
-
-                       //Strip image exif data and properties
-                       $image->stripImage();
-
-                       //Save cache image
-                       if (!$image->writeImage($cache)) {
-                               //Throw error
-                               throw new \Exception(sprintf('Unable to write image "%s"', $cache));
-                       }
-               //With cache
-               } else {
-                       //Read image
-                       $image->readImage($cache);
-               }
-
-               //Create draw
-               $draw = new \ImagickDraw();
-
-               //Set stroke antialias
-               $draw->setStrokeAntialias(true);
-
-               //Set text antialias
-               $draw->setTextAntialias(true);
-
-               //Set align aliases
-               $aligns = [
-                       'left' => \Imagick::ALIGN_LEFT,
-                       'center' => \Imagick::ALIGN_CENTER,
-                       'right' => \Imagick::ALIGN_RIGHT
-               ];
-
-               //Init counter
-               $i = 1;
-
-               //Set text count
-               $count = count($texts);
-
-               //Draw each text stroke
-               foreach($texts as $text => $data) {
-                       //Set font
-                       $draw->setFont($this->fonts[$data['font']??$this->font]);
-
-                       //Set font size
-                       $draw->setFontSize($data['size']??$this->size);
-
-                       //Set stroke width
-                       $draw->setStrokeWidth($data['width']??$this->width);
-
-                       //Set text alignment
-                       $draw->setTextAlignment($align = ($aligns[$data['align']??$this->align]));
-
-                       //Get font metrics
-                       $metrics = $image->queryFontMetrics($draw, $text);
-
-                       //Without y
-                       if (empty($data['y'])) {
-                               //Position verticaly each text evenly
-                               $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
-                       }
-
-                       //Without x
-                       if (empty($data['x'])) {
-                               if ($align == \Imagick::ALIGN_CENTER) {
-                                       $texts[$text]['x'] = $data['x'] = $width/2;
-                               } elseif ($align == \Imagick::ALIGN_LEFT) {
-                                       $texts[$text]['x'] = $data['x'] = 50;
-                               } elseif ($align == \Imagick::ALIGN_RIGHT) {
-                                       $texts[$text]['x'] = $data['x'] = $width - 50;
-                               }
-                       }
-
-                       //Center verticaly
-                       //XXX: add ascender part then center it back by half of textHeight
-                       //TODO: maybe add a boundingbox ???
-                       $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
-
-                       //Set stroke color
-                       $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$this->stroke));
-
-                       //Set fill color
-                       $draw->setFillColor(new \ImagickPixel($data['stroke']??$this->stroke));
-
-                       //Add annotation
-                       $draw->annotation($data['x'], $data['y'], $text);
-
-                       //Increase counter
-                       $i++;
-               }
-
-               //Create stroke object
-               $stroke = new \Imagick();
-
-               //Add new image
-               $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
-
-               //Draw on image
-               $stroke->drawImage($draw);
-
-               //Blur image
-               //XXX: blur the stroke canvas only
-               $stroke->blurImage(5,3);
-
-               //Set opacity to 0.5
-               //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
-               $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
-
-               //Compose image
-               $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
-
-               //Clear stroke
-               $stroke->clear();
-
-               //Destroy stroke
-               unset($stroke);
-
-               //Clear draw
-               $draw->clear();
-
-               //Set text antialias
-               $draw->setTextAntialias(true);
-
-               //Draw each text
-               foreach($texts as $text => $data) {
-                       //Set font
-                       $draw->setFont($this->fonts[$data['font']??$this->font]);
-
-                       //Set font size
-                       $draw->setFontSize($data['size']??$this->size);
-
-                       //Set text alignment
-                       $draw->setTextAlignment($aligns[$data['align']??$this->align]);
-
-                       //Set fill color
-                       $draw->setFillColor(new \ImagickPixel($data['fill']??$this->fill));
-
-                       //Add annotation
-                       $draw->annotation($data['x'], $data['y'], $text);
-
-                       //With canonical text
-                       if (!empty($data['canonical'])) {
-                               //Prevent canonical to finish in alt
-                               unset($texts[$text]);
-                       }
-               }
-
-               //Draw on image
-               $image->drawImage($draw);
-
-               //Strip image exif data and properties
-               $image->stripImage();
-
-               //Set image format
-               $image->setImageFormat('jpeg');
-
-               //Set progressive jpeg
-               $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
-               //Save image
-               if (!$image->writeImage($path)) {
-                       //Throw error
-                       throw new \Exception(sprintf('Unable to write image "%s"', $path));
-               }
-
-               //Return image data
-               return [
-                       'og:image' => $this->router->generate('rapsyspack_facebook', ['mtime' => stat($path)['mtime'], 'path' => $pathInfo], UrlGeneratorInterface::ABSOLUTE_URL),
-                       'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
-                       'og:image:height' => $height,
-                       'og:image:width' => $width
-               ];
+       public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) {
+               //Retrieve config
+               $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
        }
 }
index 871b99a93cc80df68075e8fc41b705173e1cda29..44afe28bb7234221c685061b8c4c54d44d287024 100644 (file)
 
 namespace Rapsys\PackBundle\Util;
 
+use Psr\Container\ContainerInterface;
+
+use Rapsys\PackBundle\RapsysPackBundle;
+
 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
 use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Routing\RouterInterface;
 
 /**
@@ -20,79 +25,58 @@ use Symfony\Component\Routing\RouterInterface;
  */
 class ImageUtil {
        /**
-        * The captcha width
-        */
-       const width = 192;
-
-       /**
-        * The captcha height
-        */
-       const height = 52;
-
-       /**
-        * The captcha background color
-        */
-       const background = 'white';
-
-       /**
-        * The captcha fill color
-        */
-       const fill = '#cff';
-
-       /**
-        * The captcha font size
+        * Alias string
         */
-       const fontSize = 45;
+       protected string $alias;
 
        /**
-        * The captcha stroke color
+        * Config array
         */
-       const stroke = '#00c3f9';
-
-       /**
-        * The captcha stroke width
-        */
-       const strokeWidth = 2;
-
-       /**
-        * The thumb width
-        */
-       const thumbWidth = 640;
-
-       /**
-        * The thumb height
-        */
-       const thumbHeight = 640;
+       protected array $config;
 
        /**
         * Creates a new image util
         *
+        * @param ContainerInterface $container The container instance
         * @param RouterInterface $router The RouterInterface instance
         * @param SluggerUtil $slugger The SluggerUtil instance
-        * @param string $cache The cache directory
-        * @param string $path The public path
-        * @param string $prefix The prefix
         */
-       function __construct(protected RouterInterface $router, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'image', protected string $background = self::background, protected string $fill = self::fill, protected int $fontSize = self::fontSize, protected string $stroke = self::stroke, protected int $strokeWidth = self::strokeWidth) {
+       public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) {
+               //Retrieve config
+               $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
        }
 
        /**
         * Get captcha data
         *
-        * @param int $updated The updated timestamp
-        * @param int $width The width
-        * @param int $height The height
+        * @param ?int $height The height
+        * @param ?int $width The width
         * @return array The captcha data
         */
-       public function getCaptcha(int $updated, int $width = self::width, int $height = self::height): array {
+       public function getCaptcha(?int $height = null, ?int $width = null): array {
+               //Without height
+               if ($height === null) {
+                       //Set height from config
+                       $height = $this->config['captcha']['height'];
+               }
+
+               //Without width
+               if ($width === null) {
+                       //Set width from config
+                       $width = $this->config['captcha']['width'];
+               }
+
+               //Get random
+               $random = rand(0, 999);
+
                //Set a
-               $a = rand(0, 9);
+               $a = $random % 10;
 
                //Set b
-               $b = rand(0, 5);
+               $b = $random / 10 % 10;
 
                //Set c
-               $c = rand(0, 9);
+               $c = $random / 100 % 10;
 
                //Set equation
                $equation = $a.' * '.$b.' + '.$c;
@@ -101,100 +85,669 @@ class ImageUtil {
                $short = $this->slugger->short($equation);
 
                //Set hash
-               $hash = $this->slugger->serialize([$updated, $short, $width, $height]);
+               $hash = $this->slugger->serialize([$short, $height, $width]);
 
                //Return array
                return [
                        'token' => $this->slugger->hash(strval($a * $b + $c)),
                        'value' => strval($a * $b + $c),
                        'equation' => str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation),
-                       'src' => $this->router->generate('rapsyspack_captcha', ['hash' => $hash, 'updated' => $updated, 'equation' => $short, 'width' => $width, 'height' => $height]),
+                       'src' => $this->router->generate('rapsyspack_captcha', ['hash' => $hash, 'equation' => $short, 'height' => $height, 'width' => $width, '_format' => $this->config['captcha']['format']]),
                        'width' => $width,
                        'height' => $height
                ];
        }
 
        /**
-        * Get thumb data
+        * Return the facebook image
+        *
+        * Generate simple image in jpeg format or load it from cache
+        *
+        * @TODO: move to a svg merging system ?
         *
-        * @param string $caption The caption
+        * @param string $path The request path info
+        * @param array $texts The image texts
         * @param int $updated The updated timestamp
-        * @param string $path The path
-        * @param int $width The width
+        * @param ?string $source The image source
+        * @param ?int $height The height
+        * @param ?int $width The width
+        * @return array The image array
+        */
+       public function getFacebook(string $path, array $texts, int $updated, ?string $source = null, ?int $height = null, ?int $width = null): array {
+               //Without source
+               if ($source === null && $this->config['facebook']['source'] === null) {
+                       //Return empty image data
+                       return [];
+               //Without local source
+               } elseif ($source === null) {
+                       //Set local source
+                       $source = $this->config['facebook']['source'];
+               }
+
+               //Without height
+               if ($height === null) {
+                       //Set height from config
+                       $height = $this->config['facebook']['height'];
+               }
+
+               //Without width
+               if ($width === null) {
+                       //Set width from config
+                       $width = $this->config['facebook']['width'];
+               }
+
+               //Set path file
+               $facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.jpeg';
+
+               //Without existing path
+               if (!is_dir($dir = dirname($facebook))) {
+                       //Create filesystem object
+                       $filesystem = new Filesystem();
+
+                       try {
+                               //Create path
+                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                               $filesystem->mkdir($dir, 0775);
+                       } catch (IOExceptionInterface $e) {
+                               //Throw error
+                               throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
+                       }
+               }
+
+               //With path file
+               if (is_file($facebook) && ($mtime = stat($facebook)['mtime']) && $mtime >= $updated) {
+                       #XXX: we used to drop texts with $data['canonical'] === true !!!
+
+                       //Set short path
+                       $short = $this->slugger->short($path);
+
+                       //Set hash
+                       $hash = $this->slugger->serialize([$short, $height, $width]);
+
+                       //Return image data
+                       return [
+                               'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => $mtime, '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL),
+                               'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
+                               'og:image:height' => $height,
+                               'og:image:width' => $width
+                       ];
+               }
+
+               //Set cache path
+               $cache = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.png';
+
+               //Without cache path
+               if (!is_dir($dir = dirname($cache))) {
+                       //Create filesystem object
+                       $filesystem = new Filesystem();
+
+                       try {
+                               //Create path
+                               //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                               $filesystem->mkdir($dir, 0775);
+                       } catch (IOExceptionInterface $e) {
+                               //Throw error
+                               throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                       }
+               }
+
+               //Create image object
+               $image = new \Imagick();
+
+               //Without cache image
+               if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
+                       //Check target directory
+                       if (!is_dir($dir = dirname($cache))) {
+                               //Create filesystem object
+                               $filesystem = new Filesystem();
+
+                               try {
+                                       //Create dir
+                                       //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+                                       $filesystem->mkdir($dir, 0775);
+                               } catch (IOExceptionInterface $e) {
+                                       //Throw error
+                                       throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+                               }
+                       }
+
+                       //Without source
+                       if (!is_file($source)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Source file "%s" do not exists', $source));
+                       }
+
+                       //Convert to absolute path
+                       $source = realpath($source);
+
+                       //Read image
+                       //XXX: Imagick::readImage only supports absolute path
+                       $image->readImage($source);
+
+                       //Crop using aspect ratio
+                       //XXX: for better result upload image directly in aspect ratio :)
+                       $image->cropThumbnailImage($width, $height);
+
+                       //Strip image exif data and properties
+                       $image->stripImage();
+
+                       //Save cache image
+                       if (!$image->writeImage($cache)) {
+                               //Throw error
+                               throw new \Exception(sprintf('Unable to write image "%s"', $cache));
+                       }
+               //With cache
+               } else {
+                       //Read image
+                       $image->readImage($cache);
+               }
+
+               //Create draw
+               $draw = new \ImagickDraw();
+
+               //Set stroke antialias
+               $draw->setStrokeAntialias(true);
+
+               //Set text antialias
+               $draw->setTextAntialias(true);
+
+               //Set align aliases
+               $aligns = [
+                       'left' => \Imagick::ALIGN_LEFT,
+                       'center' => \Imagick::ALIGN_CENTER,
+                       'right' => \Imagick::ALIGN_RIGHT
+               ];
+
+               //Init counter
+               $i = 1;
+
+               //Set text count
+               $count = count($texts);
+
+               //Draw each text stroke
+               foreach($texts as $text => $data) {
+                       //Set font
+                       $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]);
+
+                       //Set font size
+                       $draw->setFontSize($data['size']??$this->config['facebook']['size']);
+
+                       //Set stroke width
+                       $draw->setStrokeWidth($data['thickness']??$this->config['facebook']['thickness']);
+
+                       //Set text alignment
+                       $draw->setTextAlignment($align = ($aligns[$data['align']??$this->config['facebook']['align']]));
+
+                       //Get font metrics
+                       $metrics = $image->queryFontMetrics($draw, $text);
+
+                       //Without y
+                       if (empty($data['y'])) {
+                               //Position verticaly each text evenly
+                               $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
+                       }
+
+                       //Without x
+                       if (empty($data['x'])) {
+                               if ($align == \Imagick::ALIGN_CENTER) {
+                                       $texts[$text]['x'] = $data['x'] = $width/2;
+                               } elseif ($align == \Imagick::ALIGN_LEFT) {
+                                       $texts[$text]['x'] = $data['x'] = 50;
+                               } elseif ($align == \Imagick::ALIGN_RIGHT) {
+                                       $texts[$text]['x'] = $data['x'] = $width - 50;
+                               }
+                       }
+
+                       //Center verticaly
+                       //XXX: add ascender part then center it back by half of textHeight
+                       //TODO: maybe add a boundingbox ???
+                       $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
+
+                       //Set stroke color
+                       $draw->setStrokeColor(new \ImagickPixel($data['border']??$this->config['facebook']['border']));
+
+                       //Set fill color
+                       $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill']));
+
+                       //Add annotation
+                       $draw->annotation($data['x'], $data['y'], $text);
+
+                       //Increase counter
+                       $i++;
+               }
+
+               //Create stroke object
+               $stroke = new \Imagick();
+
+               //Add new image
+               $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
+
+               //Draw on image
+               $stroke->drawImage($draw);
+
+               //Blur image
+               //XXX: blur the stroke canvas only
+               $stroke->blurImage(5,3);
+
+               //Set opacity to 0.5
+               //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
+               $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
+
+               //Compose image
+               $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
+
+               //Clear stroke
+               $stroke->clear();
+
+               //Destroy stroke
+               unset($stroke);
+
+               //Clear draw
+               $draw->clear();
+
+               //Set text antialias
+               $draw->setTextAntialias(true);
+
+               //Draw each text
+               foreach($texts as $text => $data) {
+                       //Set font
+                       $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]);
+
+                       //Set font size
+                       $draw->setFontSize($data['size']??$this->config['facebook']['size']);
+
+                       //Set text alignment
+                       $draw->setTextAlignment($aligns[$data['align']??$this->config['facebook']['align']]);
+
+                       //Set fill color
+                       $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill']));
+
+                       //Add annotation
+                       $draw->annotation($data['x'], $data['y'], $text);
+
+                       //With canonical text
+                       if (!empty($data['canonical'])) {
+                               //Prevent canonical to finish in alt
+                               unset($texts[$text]);
+                       }
+               }
+
+               //Draw on image
+               $image->drawImage($draw);
+
+               //Strip image exif data and properties
+               $image->stripImage();
+
+               //Set image format
+               $image->setImageFormat('jpeg');
+
+               //Set progressive jpeg
+               $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
+
+               //Save image
+               if (!$image->writeImage($facebook)) {
+                       //Throw error
+                       throw new \Exception(sprintf('Unable to write image "%s"', $facebook));
+               }
+
+               //Set short path
+               $short = $this->slugger->short($path);
+
+               //Set hash
+               $hash = $this->slugger->serialize([$short, $height, $width]);
+
+               //Return image data
+               return [
+                       'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => stat($facebook)['mtime'], '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL),
+                       'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
+                       'og:image:height' => $height,
+                       'og:image:width' => $width
+               ];
+       }
+
+       /**
+        * Get map data
+        *
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @param ?int $height The height
+        * @param ?int $width The width
+        * @param ?int $zoom The zoom
+        * @return array The map data
+        */
+       public function getMap(float $latitude, float $longitude, ?int $height = null, ?int $width = null, ?int $zoom = null): array {
+               //Without height
+               if ($height === null) {
+                       //Set height from config
+                       $height = $this->config['map']['height'];
+               }
+
+               //Without width
+               if ($width === null) {
+                       //Set width from config
+                       $width = $this->config['map']['width'];
+               }
+
+               //Without zoom
+               if ($zoom === null) {
+                       //Set zoom from config
+                       $zoom = $this->config['map']['zoom'];
+               }
+
+               //Set hash
+               $hash = $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude]);
+
+               //Return array
+               return [
+                       'latitude' => $latitude,
+                       'longitude' => $longitude,
+                       'height' => $height,
+                       'src' => $this->router->generate('rapsyspack_map', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'latitude' => $latitude, 'longitude' => $longitude, '_format' => $this->config['map']['format']]),
+                       'width' => $width,
+                       'zoom' => $zoom
+               ];
+       }
+
+       /**
+        * Get multi map data
+        *
+        * @param array $coordinates The coordinates array
+        * @param ?int $height The height
+        * @param ?int $width The width
+        * @param ?int $zoom The zoom
+        * @return array The multi map data
+        */
+       public function getMulti(array $coordinates, ?int $height = null, ?int $width = null, ?int $zoom = null): array {
+               //Without coordinates
+               if ($coordinates === []) {
+                       //Throw error
+                       throw new \Exception('Missing coordinates');
+               }
+
+               //Without height
+               if ($height === null) {
+                       //Set height from config
+                       $height = $this->config['multi']['height'];
+               }
+
+               //Without width
+               if ($width === null) {
+                       //Set width from config
+                       $width = $this->config['multi']['width'];
+               }
+
+               //Without zoom
+               if ($zoom === null) {
+                       //Set zoom from config
+                       $zoom = $this->config['multi']['zoom'];
+               }
+
+               //Initialize latitudes and longitudes arrays
+               $latitudes = $longitudes = [];
+
+               //Set coordinate
+               $coordinate = implode(
+                       '-',
+                       array_map(
+                               function ($v) use (&$latitudes, &$longitudes) {
+                                       //Get latitude and longitude
+                                       list($latitude, $longitude) = $v;
+
+                                       //Append latitude
+                                       $latitudes[] = $latitude;
+
+                                       //Append longitude
+                                       $longitudes[] = $longitude;
+
+                                       //Append coordinate
+                                       return $latitude.','.$longitude;
+                               },
+                               $coordinates
+                       )
+               );
+
+               //Set latitude
+               $latitude = round((min($latitudes)+max($latitudes))/2, 6);
+
+               //Set longitude
+               $longitude = round((min($longitudes)+max($longitudes))/2, 6);
+
+               //Set zoom
+               $zoom = $this->getZoom($latitude, $longitude, $coordinates, $height, $width, $zoom);
+
+               //Set hash
+               $hash = $this->slugger->hash([$height, $width, $zoom, $coordinate]);
+
+               //Return array
+               return [
+                       'coordinate' => $coordinate,
+                       'height' => $height,
+                       'src' => $this->router->generate('rapsyspack_multi', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'coordinate' => $coordinate, '_format' => $this->config['multi']['format']]),
+                       'width' => $width,
+                       'zoom' => $zoom
+               ];
+       }
+
+       /**
+        * Get multi zoom
+        *
+        * Compute a zoom to have all coordinates on multi map
+        * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2)
+        *
+        * @TODO Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / $this->config['multi']['tz'])
+        *
+        * @param float $latitude The latitude
+        * @param float $longitude The longitude
+        * @param array $coordinates The coordinates array
         * @param int $height The height
+        * @param int $width The width
+        * @param int $zoom The zoom
+        * @return int The zoom
+        */
+       public function getZoom(float $latitude, float $longitude, array $coordinates, int $height, int $width, int $zoom): int {
+               //Iterate on each zoom
+               for ($i = $zoom; $i >= 1; $i--) {
+                       //Get tile xy
+                       $centerX = $this->longitudeToX($longitude, $i);
+                       $centerY = $this->latitudeToY($latitude, $i);
+
+                       //Calculate start xy
+                       $startX = floor($centerX - $width / 2 / $this->config['multi']['tz']);
+                       $startY = floor($centerY - $height / 2 / $this->config['multi']['tz']);
+
+                       //Calculate end xy
+                       $endX = ceil($centerX + $width / 2 / $this->config['multi']['tz']);
+                       $endY = ceil($centerY + $height / 2 / $this->config['multi']['tz']);
+
+                       //Iterate on each coordinates
+                       foreach($coordinates as $k => $coordinate) {
+                               //Get coordinates
+                               list($clatitude, $clongitude) = $coordinate;
+
+                               //Set dest x
+                               $destX = $this->longitudeToX($clongitude, $i);
+
+                               //With outside point
+                               if ($startX >= $destX || $endX <= $destX) {
+                                       //Skip zoom
+                                       continue(2);
+                               }
+
+                               //Set dest y
+                               $destY = $this->latitudeToY($clatitude, $i);
+
+                               //With outside point
+                               if ($startY >= $destY || $endY <= $destY) {
+                                       //Skip zoom
+                                       continue(2);
+                               }
+                       }
+
+                       //Found zoom
+                       break;
+               }
+
+               //Return zoom
+               return $i;
+       }
+
+       /**
+        * Get thumb data
+        *
+        * @param string $path The path
+        * @param ?int $height The height
+        * @param ?int $width The width
         * @return array The thumb data
         */
-       public function getThumb(string $caption, int $updated, string $path, int $width = self::thumbWidth, int $height = self::thumbHeight): array {
-               //Get image width and height
-               list($imageWidth, $imageHeight) = getimagesize($path);
+       public function getThumb(string $path, ?int $height = null, ?int $width = null): array {
+               //Without height
+               if ($height === null) {
+                       //Set height from config
+                       $height = $this->config['thumb']['height'];
+               }
+
+               //Without width
+               if ($width === null) {
+                       //Set width from config
+                       $width = $this->config['thumb']['width'];
+               }
 
                //Short path
                $short = $this->slugger->short($path);
 
-               //Set link hash
-               $link = $this->slugger->serialize([$updated, $short, $imageWidth, $imageHeight]);
+               //Set hash
+               $hash = $this->slugger->serialize([$short, $height, $width]);
 
-               //Set src hash
-               $src = $this->slugger->serialize([$updated, $short, $width, $height]);
+               #TODO: compute thumb from file type ?
+               #TODO: see if setting format there is smart ? we do not yet know if we want a image or movie thumb ?
+               #TODO: do we add to route '_format' => $this->config['thumb']['format']
 
                //Return array
                return [
-                       'caption' => $caption,
-                       'link' => $this->router->generate('rapsyspack_thumb', ['hash' => $link, 'updated' => $updated, 'path' => $short, 'width' => $imageWidth, 'height' => $imageHeight]),
-                       'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $src, 'updated' => $updated, 'path' => $short, 'width' => $width, 'height' => $height]),
+                       'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width]),
                        'width' => $width,
                        'height' => $height
                ];
        }
 
        /**
-        * Get captcha background color
+        * Convert longitude to tile x number
+        *
+        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
+        *
+        * @param float $longitude The longitude
+        * @param int $zoom The zoom
+        *
+        * @return float The tile x
         */
-       public function getBackground() {
-               return $this->background;
+       public function longitudeToX(float $longitude, int $zoom): float {
+               return (($longitude + 180) / 360) * pow(2, $zoom);
        }
 
        /**
-        * Get captcha fill color
+        * Convert latitude to tile y number
+        *
+        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
+        *
+        * @param $latitude The latitude
+        * @param $zoom The zoom
+        *
+        * @return float The tile y
         */
-       public function getFill() {
-               return $this->fill;
+       public function latitudeToY(float $latitude, int $zoom): float {
+               return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
        }
 
        /**
-        * Get captcha font size
+        * Convert tile x to longitude
+        *
+        * @param float $x The tile x
+        * @param int $zoom The zoom
+        *
+        * @return float The longitude
         */
-       public function getFontSize() {
-               return $this->fontSize;
+       public function xToLongitude(float $x, int $zoom): float {
+               return $x / pow(2, $zoom) * 360.0 - 180.0;
        }
 
        /**
-        * Get captcha stroke color
+        * Convert tile y to latitude
+        *
+        * @param float $y The tile y
+        * @param int $zoom The zoom
+        *
+        * @return float The latitude
         */
-       public function getStroke() {
-               return $this->stroke;
+       public function yToLatitude(float $y, int $zoom): float {
+               return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
        }
 
        /**
-        * Get captcha stroke width
+        * Convert decimal latitude to sexagesimal
+        *
+        * @param float $latitude The decimal latitude
+        *
+        * @return string The sexagesimal longitude
         */
-       public function getStrokeWidth() {
-               return $this->strokeWidth;
+       public function latitudeToSexagesimal(float $latitude): string {
+               //Set degree
+               //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
+               $degree = round($latitude) % 60;
+
+               //Set minute
+               $minute = round(($latitude - $degree) * 60) % 60;
+
+               //Set second
+               $second = round(($latitude - $degree - $minute / 60) * 3600) % 3600;
+
+               //Return sexagesimal longitude
+               return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
+       }
+
+       /**
+        * Convert decimal longitude to sexagesimal
+        *
+        * @param float $longitude The decimal longitude
+        *
+        * @return string The sexagesimal longitude
+        */
+       public function longitudeToSexagesimal(float $longitude): string {
+               //Set degree
+               //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
+               $degree = round($longitude) % 60;
+
+               //Set minute
+               $minute = round(($longitude - $degree) * 60) % 60;
+
+               //Set second
+               $second = round(($longitude - $degree - $minute / 60) * 3600) % 3600;
+
+               //Return sexagesimal longitude
+               return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
        }
 
        /**
         * Remove image
         *
         * @param int $updated The updated timestamp
+        * @param string $prefix The prefix
         * @param string $path The path
         * @return array The thumb clear success
         */
-       public function remove(int $updated, string $path): bool {
+       public function remove(int $updated, string $prefix, string $path): bool {
+               die('TODO: see how to make it work');
+
+               //Without valid prefix
+               if (!isset($this->config['prefixes'][$prefix])) {
+                       //Throw error
+                       throw new \Exception(sprintf('Invalid prefix "%s"', $prefix));
+               }
+
                //Set hash tree
                $hash = array_reverse(str_split(strval($updated)));
 
                //Set dir
-               $dir = $this->path.'/'.$this->prefix.'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$updated.'/'.$this->slugger->short($path);
+               $dir = $this->config['public'].'/'.$this->config['prefixes'][$prefix].'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$this->slugger->short($path);
 
                //Set removes
                $removes = [];
index 84d468fa16e9398163606a793b53a4450c3aed55..80b95308255e4a719ea39e00414650adced75366 100644 (file)
@@ -164,41 +164,4 @@ class IntlUtil {
                //Return formatted number
                return $formatter->format($number, $types[$type]);
        }
-
-       /**
-        * Format size
-        */
-       public function size(int|float $number, $si = true, $style = 'decimal', $type = 'default', ?string $locale = null) {
-               //Set types
-               static $types = [
-                       'default' => \NumberFormatter::TYPE_DEFAULT,
-                       'int32' => \NumberFormatter::TYPE_INT32,
-                       'int64' => \NumberFormatter::TYPE_INT64,
-                       'double' => \NumberFormatter::TYPE_DOUBLE,
-                       'currency' => \NumberFormatter::TYPE_CURRENCY
-               ];
-
-               //Get formatter
-               $formatter = $this->getNumberFormatter($locale, $style);
-
-               //Without type
-               if (!isset($types[$type])) {
-                       throw new SyntaxError(sprintf('The type "%s" does not exist. Known types are: "%s"', $type, implode('", "', array_keys($types))));
-               }
-
-               //Set unit
-               $unit = $si ? 1000 : 1024;
-
-               //Set index
-               $index = [ '', $si ? 'k' : 'K', 'M', 'G', 'T', 'P', 'E' ];
-
-               //Get exp
-               $exp = intval((log($number) / log($unit)));
-
-               //Rebase number
-               $number = round($number / pow($unit, $exp), 2);
-
-               //Return formatted number
-               return $formatter->format($number, $types[$type]).' '.$index[$exp].($si ? '' : 'i').'B';
-       }
 }
index b7d2232eca9774b39e12c525ea0b9a457db9266a..b281555014ee1eeb1d3bb1ebb97d0c13afa4b1a5 100644 (file)
 
 namespace Rapsys\PackBundle\Util;
 
+use Psr\Container\ContainerInterface;
+
+use Rapsys\PackBundle\RapsysPackBundle;
+
+use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Filesystem;
 use Symfony\Component\Routing\RouterInterface;
 
 /**
@@ -18,411 +24,24 @@ use Symfony\Component\Routing\RouterInterface;
  */
 class MapUtil {
        /**
-        * The cycle tile server
-        *
-        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
-        */
-       const cycle = 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png';
-
-       /**
-        * The fill color
-        */
-       const fill = '#cff';
-
-       /**
-        * The font size
-        */
-       const fontSize = 20;
-
-       /**
-        * The high fill color
-        */
-       const highFill = '#c3c3f9';
-
-       /**
-        * The high font size
+        * Alias string
         */
-       const highFontSize = 30;
+       protected string $alias;
 
        /**
-        * The high radius size
+        * Config array
         */
-       const highRadius = 6;
+       protected array $config;
 
        /**
-        * The high stroke color
-        */
-       const highStroke = '#3333c3';
-
-       /**
-        * The high stroke width
-        */
-       const highStrokeWidth = 4;
-
-       /**
-        * The map width
-        */
-       const width = 640;
-
-       /**
-        * The map height
-        */
-       const height = 640;
-       
-       /**
-        * The osm tile server
-        *
-        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
-        */
-       const osm = 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png';
-
-       /**
-        * The radius size
-        */
-       const radius = 5;
-
-       /**
-        * The stroke color
-        */
-       const stroke = '#00c3f9';
-
-       /**
-        * The stroke width
-        */
-       const strokeWidth = 2;
-
-       /**
-        * The transport tile server
-        *
-        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
-        */
-       const transport = 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png';
-
-       /**
-        * The tile size
-        */
-       const tz = 256;
-
-       /**
-        * The map zoom
-        */
-       const zoom = 17;
-
-       /**
-        * Creates a new map util
+        * Creates a new image util
         *
+        * @param ContainerInterface $container The container instance
         * @param RouterInterface $router The RouterInterface instance
         * @param SluggerUtil $slugger The SluggerUtil instance
         */
-       function __construct(protected RouterInterface $router, protected SluggerUtil $slugger, protected string $fill = self::fill, protected int $fontSize = self::fontSize, protected string $highFill = self::highFill, protected int $highFontSize = self::highFontSize, protected int $highRadius = self::highRadius, protected string $highStroke = self::highStroke, protected int $highStrokeWidth = self::highStrokeWidth, protected int $radius = self::radius, protected string $stroke = self::stroke, protected int $strokeWidth = self::strokeWidth) {
-       }
-
-       /**
-        * Get fill color
-        */
-       function getFill() {
-               return $this->fill;
-       }
-
-       /**
-        * Get font size
-        */
-       function getFontSize() {
-               return $this->fontSize;
-       }
-
-       /**
-        * Get high fill color
-        */
-       function getHighFill() {
-               return $this->highFill;
-       }
-
-       /**
-        * Get high font size
-        */
-       function getHighFontSize() {
-               return $this->highFontSize;
-       }
-
-       /**
-        * Get high radius size
-        */
-       function getHighRadius() {
-               return $this->highRadius;
-       }
-
-       /**
-        * Get high stroke color
-        */
-       function getHighStroke() {
-               return $this->highStroke;
-       }
-
-       /**
-        * Get high stroke width
-        */
-       function getHighStrokeWidth() {
-               return $this->highStrokeWidth;
-       }
-
-       /**
-        * Get radius size
-        */
-       function getRadius() {
-               return $this->radius;
-       }
-
-       /**
-        * Get stroke color
-        */
-       function getStroke() {
-               return $this->stroke;
-       }
-
-       /**
-        * Get stroke width
-        */
-       function getStrokeWidth() {
-               return $this->strokeWidth;
-       }
-
-       /**
-        * Get map data
-        *
-        * @param string $caption The caption
-        * @param int $updated The updated timestamp
-        * @param float $latitude The latitude
-        * @param float $longitude The longitude
-        * @param int $zoom The zoom
-        * @param int $width The width
-        * @param int $height The height
-        * @return array The map data
-        */
-       public function getMap(string $caption, int $updated, float $latitude, float $longitude, int $zoom = self::zoom, int $width = self::width, int $height = self::height): array {
-               //Set link hash
-               $link = $this->slugger->hash([$updated, $latitude, $longitude, $zoom + 1, $width * 2, $height * 2]);
-
-               //Set src hash
-               $src = $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height]);
-
-               //Return array
-               return [
-                       'caption' => $caption,
-                       'link' => $this->router->generate('rapsyspack_map', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
-                       'src' => $this->router->generate('rapsyspack_map', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
-                       'width' => $width,
-                       'height' => $height
-               ];
-       }
-
-       /**
-        * Get multi map data
-        *
-        * @param string $caption The caption
-        * @param int $updated The updated timestamp
-        * @param array $coordinates The coordinates array
-        * @param int $width The width
-        * @param int $height The height
-        * @return array The multi map data
-        */
-       public function getMultiMap(string $caption, int $updated, array $coordinates, int $width = self::width, int $height = self::height): array {
-               //Without coordinates
-               if (empty($coordinates)) {
-                       //Return empty array
-                       return [];
-               }
-
-               //Set latitudes
-               $latitudes = array_map(function ($v) { return $v['latitude']; }, $coordinates);
-
-               //Set longitudes
-               $longitudes = array_map(function ($v) { return $v['longitude']; }, $coordinates);
-
-               //Set latitude
-               $latitude = round((min($latitudes)+max($latitudes))/2, 6);
-
-               //Set longitude
-               $longitude = round((min($longitudes)+max($longitudes))/2, 6);
-
-               //Set zoom
-               $zoom = $this->getMultiZoom($latitude, $longitude, $coordinates, $width, $height);
-
-               //Set coordinate
-               $coordinate = implode('-', array_map(function ($v) { return $v['latitude'].','.$v['longitude']; }, $coordinates));
-
-               //Set coordinate hash
-               $hash = $this->slugger->hash($coordinate);
-
-               //Set link hash
-               $link = $this->slugger->hash([$updated, $latitude, $longitude, $hash, $zoom + 1, $width * 2, $height * 2]);
-
-               //Set src hash
-               $src = $this->slugger->hash([$updated, $latitude, $longitude, $hash, $zoom, $width, $height]);
-
-               //Return array
-               return [
-                       'caption' => $caption,
-                       'link' => $this->router->generate('rapsyspack_multimap', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
-                       'src' => $this->router->generate('rapsyspack_multimap', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
-                       'width' => $width,
-                       'height' => $height
-               ];
-       }
-
-       /**
-        * Get multi zoom
-        *
-        * Compute a zoom to have all coordinates on multi map
-        * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2)
-        *
-        * @see Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / self::tz)
-        *
-        * @param float $latitude The latitude
-        * @param float $longitude The longitude
-        * @param array $coordinates The coordinates array
-        * @param int $width The width
-        * @param int $height The height
-        * @param int $zoom The zoom
-        * @return int The zoom
-        */
-       public function getMultiZoom(float $latitude, float $longitude, array $coordinates, int $width, int $height, int $zoom = self::zoom): int {
-               //Iterate on each zoom
-               for ($i = $zoom; $i >= 1; $i--) {
-                       //Get tile xy
-                       $centerX = self::longitudeToX($longitude, $i);
-                       $centerY = self::latitudeToY($latitude, $i);
-
-                       //Calculate start xy
-                       $startX = floor($centerX - $width / 2 / self::tz);
-                       $startY = floor($centerY - $height / 2 / self::tz);
-
-                       //Calculate end xy
-                       $endX = ceil($centerX + $width / 2 / self::tz);
-                       $endY = ceil($centerY + $height / 2 / self::tz);
-
-                       //Iterate on each coordinates
-                       foreach($coordinates as $k => $coordinate) {
-                               //Set dest x
-                               $destX = self::longitudeToX($coordinate['longitude'], $i);
-
-                               //With outside point
-                               if ($startX >= $destX || $endX <= $destX) {
-                                       //Skip zoom
-                                       continue(2);
-                               }
-
-                               //Set dest y
-                               $destY = self::latitudeToY($coordinate['latitude'], $i);
-
-                               //With outside point
-                               if ($startY >= $destY || $endY <= $destY) {
-                                       //Skip zoom
-                                       continue(2);
-                               }
-                       }
-
-                       //Found zoom
-                       break;
-               }
-
-               //Return zoom
-               return $i;
-       }
-
-       /**
-        * Convert longitude to tile x number
-        *
-        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
-        *
-        * @param float $longitude The longitude
-        * @param int $zoom The zoom
-        *
-        * @return float The tile x
-        */
-       public static function longitudeToX(float $longitude, int $zoom): float {
-               return (($longitude + 180) / 360) * pow(2, $zoom);
-       }
-
-       /**
-        * Convert latitude to tile y number
-        *
-        * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
-        *
-        * @param $latitude The latitude
-        * @param $zoom The zoom
-        *
-        * @return float The tile y
-        */
-       public static function latitudeToY(float $latitude, int $zoom): float {
-               return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
-       }
-
-       /**
-        * Convert tile x to longitude
-        *
-        * @param float $x The tile x
-        * @param int $zoom The zoom
-        *
-        * @return float The longitude
-        */
-       public static function xToLongitude(float $x, int $zoom): float {
-               return $x / pow(2, $zoom) * 360.0 - 180.0;
-       }
-
-       /**
-        * Convert tile y to latitude
-        *
-        * @param float $y The tile y
-        * @param int $zoom The zoom
-        *
-        * @return float The latitude
-        */
-       public static function yToLatitude(float $y, int $zoom): float {
-               return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
-       }
-
-       /**
-        * Convert decimal latitude to sexagesimal
-        *
-        * @param float $latitude The decimal latitude
-        *
-        * @return string The sexagesimal longitude
-        */
-       public static function latitudeToSexagesimal(float $latitude): string {
-               //Set degree
-               //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
-               $degree = round($latitude) % 60;
-
-               //Set minute
-               $minute = round(($latitude - $degree) * 60) % 60;
-
-               //Set second
-               $second = round(($latitude - $degree - $minute / 60) * 3600) % 3600;
-
-               //Return sexagesimal longitude
-               return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
-       }
-
-       /**
-        * Convert decimal longitude to sexagesimal
-        *
-        * @param float $longitude The decimal longitude
-        *
-        * @return string The sexagesimal longitude
-        */
-       public static function longitudeToSexagesimal(float $longitude): string {
-               //Set degree
-               //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
-               $degree = round($longitude) % 60;
-
-               //Set minute
-               $minute = round(($longitude - $degree) * 60) % 60;
-
-               //Set second
-               $second = round(($longitude - $degree - $minute / 60) * 3600) % 3600;
-
-               //Return sexagesimal longitude
-               return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
+       public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) {
+               //Retrieve config
+               $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
        }
 }
index 7abf5d325231d519ef28b35f765bde841d216098..0ff32c59ac1f7ac6c20945bf62083517510c355e 100644 (file)
@@ -116,7 +116,7 @@ class SluggerUtil {
                //Return hashed data
                //XXX: we use hash_hmac with md5 hash
                //XXX: crypt was dropped because it provided identical signature for string starting with same pattern
-               return str_replace(['+','/'], ['-','_'], base64_encode(hash_hmac('md5', $data, $this->secret, true)));
+               return str_replace(['+','/','='], ['-','_',''], base64_encode(hash_hmac('md5', $data, $this->secret, true)));
        }
 
        /**
similarity index 65%
rename from Resources/config/packages/rapsyspack.yaml
rename to config/packages/rapsyspack.yaml
index f57cf7445cbfdbec9510ba9fce7e3abdd2b97ca6..0efaa0bb2e7eb90e116ea82bbfae5ed75d10f928 100644 (file)
@@ -21,29 +21,24 @@ services:
     assets.packages:
         class: 'Symfony\Component\Asset\Packages'
         arguments: [ '@rapsyspack.path_package' ]
-    # Register facebook util service
-    rapsyspack.facebook_util:
-        class: 'Rapsys\PackBundle\Util\FacebookUtil'
-        arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
+    # Register file util service
+    rapsyspack.file_util:
+        class: 'Rapsys\PackBundle\Util\FileUtil'
+        arguments: [ '@rapsyspack.image_util', '@rapsyspack.intl_util', '@translator' ]
         public: true
     # Register image util service
     rapsyspack.image_util:
         class: 'Rapsys\PackBundle\Util\ImageUtil'
-        arguments: [ '@router', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
+        arguments: [ '@service_container', '@router', '@rapsyspack.slugger_util' ]
         public: true
     # Register intl util service
     rapsyspack.intl_util:
         class: 'Rapsys\PackBundle\Util\IntlUtil'
         public: true
-    # Register map util service
-    rapsyspack.map_util:
-        class: 'Rapsys\PackBundle\Util\MapUtil'
-        arguments: [ '@router', '@rapsyspack.slugger_util' ]
-        public: true
     # Register twig pack extension
     rapsyspack.pack_extension:
         class: 'Rapsys\PackBundle\Extension\PackExtension'
-        arguments: [ '@rapsyspack.intl_util', '@file_locator', '@rapsyspack.path_package', '@rapsyspack.slugger_util', '%rapsyspack%' ]
+        arguments: [ '@service_container', '@rapsyspack.intl_util', '@file_locator', '@router', '@rapsyspack.slugger_util' ]
         tags: [ 'twig.extension' ]
     # Register assets pack package
     rapsyspack.path_package:
@@ -59,34 +54,31 @@ services:
     Rapsys\PackBundle\Command\RangeCommand:
         arguments: [ '%kernel.project_dir%/.env.local' ]
         tags: [ 'console.command' ]
-    # Register image controller
-    Rapsys\PackBundle\Controller\ImageController:
-        arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
-        tags: [ 'controller.service_arguments' ]
-    # Register map controller
-    Rapsys\PackBundle\Controller\MapController:
-        arguments: [ '@service_container', '@rapsyspack.map_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ]
+    # Register controller
+    Rapsys\PackBundle\Controller:
+        arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util' ]
         tags: [ 'controller.service_arguments' ]
-    # Register captcha form type
+    # Register captcha form
     Rapsys\PackBundle\Form\CaptchaType:
         arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ]
         tags: [ 'form.type' ]
+    # Register contact form
+    Rapsys\PackBundle\Form\ContactType:
+        arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ]
+        tags: [ 'form.type' ]
     # Register facebook event subscriber
     Rapsys\PackBundle\Subscriber\FacebookSubscriber:
         arguments: [ '@router', [] ]
         tags: [ 'kernel.event_subscriber' ]
-    # Register facebook util class alias
-    Rapsys\PackBundle\Util\FacebookUtil:
-        alias: 'rapsyspack.facebook_util'
+    # Register file util class alias
+    Rapsys\PackBundle\Util\FileUtil:
+        alias: 'rapsyspack.file_util'
     # Register image util class alias
     Rapsys\PackBundle\Util\ImageUtil:
         alias: 'rapsyspack.image_util'
     # Register intl util class alias
     Rapsys\PackBundle\Util\IntlUtil:
         alias: 'rapsyspack.intl_util'
-    # Register map util class alias
-    Rapsys\PackBundle\Util\MapUtil:
-        alias: 'rapsyspack.map_util'
     # Register slugger util class alias
     Rapsys\PackBundle\Util\SluggerUtil:
         alias: 'rapsyspack.slugger_util'
diff --git a/config/routes/rapsyspack.yaml b/config/routes/rapsyspack.yaml
new file mode 100644 (file)
index 0000000..f23660b
--- /dev/null
@@ -0,0 +1,38 @@
+#Routes configuration
+rapsyspack_captcha:
+    path: '/captcha/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{equation<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
+    controller: Rapsys\PackBundle\Controller::captcha
+    methods: GET
+
+rapsyspack_css:
+    path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format<css>?css}'
+    methods: GET
+
+rapsyspack_facebook:
+    path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
+    controller: Rapsys\PackBundle\Controller::facebook
+    methods: GET
+
+rapsyspack_img:
+    path: '/bundles/rapsyspack/pack/img/{file<[a-zA-Z0-9]+>}.{!_format<(jpeg|png|webp)>}'
+    methods: GET
+
+rapsyspack_js:
+    path: '/bundles/rapsyspack/pack/js/{file<[a-zA-Z0-9]+>}.{!_format<js>?js}'
+    methods: GET
+
+rapsyspack_map:
+    path: '/map/{hash<[a-zA-Z0-9=_-]+>}/{latitude<\d+(\.?\d+)>},{longitude<\d+(\.?\d+)>}-{zoom<\d+>}-{width<\d+>}x{height<\d+>}.{!_format<(jpeg|png|webp)>}'
+    controller: Rapsys\PackBundle\Controller::map
+    methods: GET
+
+rapsyspack_multi:
+    path: '/multi/{hash<[a-zA-Z0-9=_-]+>}/{coordinate<\d+(\.\d+)?,\d+(\.\d+)?(-\d+(\.\d+)?,\d+(\.\d+)?)+>}-{zoom<\d+>}-{width<\d+>}x{height<\d+>}.{!_format<(jpeg|png|webp)>}'
+    controller: Rapsys\PackBundle\Controller::multi
+    methods: GET
+
+rapsyspack_thumb:
+    #TODO: remove default _format when a solution is found
+    path: '/thumb/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>?jpeg}'
+    controller: Rapsys\PackBundle\Controller::thumb
+    methods: GET
similarity index 100%
rename from Resources/public/css/.keep
rename to public/css/.keep
diff --git a/public/facebook/source.php b/public/facebook/source.php
new file mode 100644 (file)
index 0000000..6cfe80e
--- /dev/null
@@ -0,0 +1,44 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+//Create image object
+$image = new Imagick();
+
+//Create draw object
+$draw = new ImagickDraw();
+
+//Create pixel object
+$pixel = new ImagickPixel('white');
+
+//Create new image
+$image->newImage(1200, 630, $pixel);
+
+//Set fill color
+$draw->setFillColor('black');
+
+//Set font properties
+$draw->setFont('../woff2/droidsans.regular.woff2');
+$draw->setFontSize(30);
+
+//Add texts
+$image->annotateImage($draw, 10, 35, 0, 'RP');
+$image->annotateImage($draw, 10, 615, 0, 'RP');
+$image->annotateImage($draw, 1155, 35, 0, 'RP');
+$image->annotateImage($draw, 1155, 615, 0, 'RP');
+
+//Set image format
+$image->setImageFormat('png');
+
+//Output image header
+header('Content-type: image/png');
+
+//Output image
+echo $image;
diff --git a/public/facebook/source.png b/public/facebook/source.png
new file mode 100644 (file)
index 0000000..edcdfeb
Binary files /dev/null and b/public/facebook/source.png differ
similarity index 100%
rename from Resources/public/js/.keep
rename to public/js/.keep
similarity index 100%
rename from Resources/public/map/.keep
rename to public/map/.keep