From: Raphaël Gertz Date: Sun, 8 Dec 2024 07:04:40 +0000 (+0100) Subject: Import contact form X-Git-Tag: 0.5.4 X-Git-Url: https://git.rapsys.eu/packbundle/commitdiff_plain/HEAD?ds=sidebyside;hp=fedbe97d37d0056de407abcc44ee180f84e1ca7c Import contact form --- diff --git a/.gitignore b/.gitignore index 0c7e3e9..ad3242e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .*.un~ *~ -Resources/public/* +public/* diff --git a/Command.php b/Command.php index 0097d12..ca0e3cc 100644 --- a/Command.php +++ b/Command.php @@ -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)); } } diff --git a/Command/RangeCommand.php b/Command/RangeCommand.php index e4f15da..0597138 100644 --- a/Command/RangeCommand.php +++ b/Command/RangeCommand.php @@ -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 index 0000000..77a3e14 --- /dev/null +++ b/Controller.php @@ -0,0 +1,831 @@ + + * + * 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 index 13865a7..0000000 --- a/Controller/ImageController.php +++ /dev/null @@ -1,291 +0,0 @@ - - * - * 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 index 8d38aa6..0000000 --- a/Controller/MapController.php +++ /dev/null @@ -1,507 +0,0 @@ - - * - * 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; - } -} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 60b68b6..2f29209 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -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(); diff --git a/DependencyInjection/RapsysPackExtension.php b/DependencyInjection/RapsysPackExtension.php index fc9efce..3a6ed53 100644 --- a/DependencyInjection/RapsysPackExtension.php +++ b/DependencyInjection/RapsysPackExtension.php @@ -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()); diff --git a/Extension/PackExtension.php b/Extension/PackExtension.php index aa11688..5c89099 100644 --- a/Extension/PackExtension.php +++ b/Extension/PackExtension.php @@ -11,12 +11,14 @@ 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']), diff --git a/Form/CaptchaType.php b/Form/CaptchaType.php index c3a5f69..f8cf3d0 100644 --- a/Form/CaptchaType.php +++ b/Form/CaptchaType.php @@ -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 index 0000000..3d70bfb --- /dev/null +++ b/Form/ContactType.php @@ -0,0 +1,60 @@ + + * + * 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'; + } +} diff --git a/Parser/TokenParser.php b/Parser/TokenParser.php index 67e87eb..7d73117 100644 --- a/Parser/TokenParser.php +++ b/Parser/TokenParser.php @@ -11,12 +11,19 @@ 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); } } diff --git a/RapsysPackBundle.php b/RapsysPackBundle.php index 8de9fa3..08360b0 100644 --- a/RapsysPackBundle.php +++ b/RapsysPackBundle.php @@ -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 index aa0a953..0000000 --- a/Resources/config/routes/rapsyspack.yaml +++ /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 diff --git a/Util/FacebookUtil.php b/Util/FacebookUtil.php index 91664d1..633702b 100644 --- a/Util/FacebookUtil.php +++ b/Util/FacebookUtil.php @@ -11,6 +11,10 @@ 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()); } } diff --git a/Util/ImageUtil.php b/Util/ImageUtil.php index 871b99a..44afe28 100644 --- a/Util/ImageUtil.php +++ b/Util/ImageUtil.php @@ -11,8 +11,13 @@ 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 = []; diff --git a/Util/IntlUtil.php b/Util/IntlUtil.php index 84d468f..80b9530 100644 --- a/Util/IntlUtil.php +++ b/Util/IntlUtil.php @@ -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'; - } } diff --git a/Util/MapUtil.php b/Util/MapUtil.php index b7d2232..b281555 100644 --- a/Util/MapUtil.php +++ b/Util/MapUtil.php @@ -11,6 +11,12 @@ 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()); } } diff --git a/Util/SluggerUtil.php b/Util/SluggerUtil.php index 7abf5d3..0ff32c5 100644 --- a/Util/SluggerUtil.php +++ b/Util/SluggerUtil.php @@ -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))); } /** diff --git a/Resources/config/packages/rapsyspack.yaml b/config/packages/rapsyspack.yaml similarity index 65% rename from Resources/config/packages/rapsyspack.yaml rename to config/packages/rapsyspack.yaml index f57cf74..0efaa0b 100644 --- a/Resources/config/packages/rapsyspack.yaml +++ b/config/packages/rapsyspack.yaml @@ -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 index 0000000..f23660b --- /dev/null +++ b/config/routes/rapsyspack.yaml @@ -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}' + 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}' + 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 diff --git a/Resources/public/css/.keep b/public/css/.keep similarity index 100% rename from Resources/public/css/.keep rename to public/css/.keep diff --git a/Resources/public/facebook/.keep b/public/facebook/.keep similarity index 100% rename from Resources/public/facebook/.keep rename to public/facebook/.keep diff --git a/public/facebook/source.php b/public/facebook/source.php new file mode 100644 index 0000000..6cfe80e --- /dev/null +++ b/public/facebook/source.php @@ -0,0 +1,44 @@ + + * + * 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 index 0000000..edcdfeb Binary files /dev/null and b/public/facebook/source.png differ diff --git a/Resources/public/image/.keep b/public/image/.keep similarity index 100% rename from Resources/public/image/.keep rename to public/image/.keep diff --git a/Resources/public/js/.keep b/public/js/.keep similarity index 100% rename from Resources/public/js/.keep rename to public/js/.keep diff --git a/Resources/public/map/.keep b/public/map/.keep similarity index 100% rename from Resources/public/map/.keep rename to public/map/.keep