.*.un~
*~
-Resources/public/*
+public/*
* {@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);
}
/**
+ * 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
$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));
}
}
namespace Rapsys\PackBundle\Command;
use Rapsys\PackBundle\Command;
-use Rapsys\PackBundle\RapsysPackBundle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
*
* 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);
//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
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";
--- /dev/null
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle;
+
+use Rapsys\PackBundle\Util\ImageUtil;
+use Rapsys\PackBundle\Util\SluggerUtil;
+
+use Psr\Container\ContainerInterface;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\HeaderUtils;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Contracts\Service\ServiceSubscriberInterface;
+
+/**
+ * {@inheritdoc}
+ */
+class Controller extends AbstractController implements ServiceSubscriberInterface {
+ /**
+ * Alias string
+ */
+ protected string $alias;
+
+ /**
+ * Config array
+ */
+ protected array $config;
+
+ /**
+ * Stream context
+ */
+ protected mixed $ctx;
+
+ /**
+ * Version string
+ */
+ protected string $version;
+
+ /**
+ * Creates a new image controller
+ *
+ * @param ContainerInterface $container The ContainerInterface instance
+ * @param ImageUtil $image The MapUtil instance
+ * @param SluggerUtil $slugger The SluggerUtil instance
+ */
+ function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected SluggerUtil $slugger) {
+ //Retrieve config
+ $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
+
+ //Set ctx
+ $this->ctx = stream_context_create($this->config['context']);
+ }
+
+ /**
+ * Return captcha image
+ *
+ * @param Request $request The Request instance
+ * @param string $hash The hash
+ * @param string $equation The shorted equation
+ * @param int $height The height
+ * @param int $width The width
+ * @return Response The rendered image
+ */
+ public function captcha(Request $request, string $hash, string $equation, int $height, int $width, string $_format): Response {
+ //Without matching hash
+ if ($hash !== $this->slugger->serialize([$equation, $height, $width])) {
+ //Throw new exception
+ throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
+ //Without valid format
+ } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+ //Throw new exception
+ throw new NotFoundHttpException('Invalid thumb format');
+ }
+
+ //Unshort equation
+ $equation = $this->slugger->unshort($short = $equation);
+
+ //Set hashed tree
+ $hashed = str_split(strval($equation));
+
+ //Set captcha
+ $captcha = $this->config['cache'].'/'.$this->config['prefixes']['captcha'].'/'.$hashed[0].'/'.$hashed[4].'/'.$hashed[8].'/'.$short.'.'.$_format;
+
+ //Without captcha up to date file
+ if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < (new \DateTime('-1 hour'))->getTimestamp()) {
+ //Without existing captcha path
+ if (!is_dir($dir = dirname($captcha))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ //XXX: on CoW filesystems execute a chattr +C before filling
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Create image instance
+ $image = new \Imagick();
+
+ //Add imagick draw instance
+ //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+ $draw = new \ImagickDraw();
+
+ //Set text antialias
+ $draw->setTextAntialias(true);
+
+ //Set stroke antialias
+ $draw->setStrokeAntialias(true);
+
+ //Set text alignment
+ $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+ //Set gravity
+ $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+ //Set fill color
+ $draw->setFillColor($this->config['captcha']['fill']);
+
+ //Set stroke color
+ $draw->setStrokeColor($this->config['captcha']['border']);
+
+ //Set font size
+ $draw->setFontSize($this->config['captcha']['size'] / 1.5);
+
+ //Set stroke width
+ $draw->setStrokeWidth($this->config['captcha']['thickness'] / 3);
+
+ //Set rotation
+ $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
+
+ //Get font metrics
+ $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
+
+ //Add annotation
+ $draw->annotation($width / 2 - ceil(rand(intval(-$metrics2['textWidth']), intval($metrics2['textWidth'])) / 2) - abs($rotate), ceil($metrics2['textHeight'] + $metrics2['descender'] + $metrics2['ascender']) / 2 - $this->config['captcha']['thickness'] - $rotate, strval('stop spam'));
+
+ //Set rotation
+ $draw->rotate(-$rotate);
+
+ //Set font size
+ $draw->setFontSize($this->config['captcha']['size']);
+
+ //Set stroke width
+ $draw->setStrokeWidth($this->config['captcha']['thickness']);
+
+ //Set rotation
+ $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
+
+ //Get font metrics
+ $metrics = $image->queryFontMetrics($draw, strval($equation));
+
+ //Add annotation
+ $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->config['captcha']['thickness'], strval($equation));
+
+ //Set rotation
+ $draw->rotate(-$rotate);
+
+ //Add new image
+ #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->config['captcha']['background']), 'jpeg');
+ $image->newImage($width, $height, new \ImagickPixel($this->config['captcha']['background']), $_format);
+
+ //Draw on image
+ $image->drawImage($draw);
+
+ //Strip image exif data and properties
+ $image->stripImage();
+
+ //Set compression quality
+ $image->setImageCompressionQuality(70);
+
+ //Save captcha
+ if (!$image->writeImage($captcha)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
+ }
+
+ //Set mtime
+ $mtime = stat($captcha)['mtime'];
+ }
+
+ //Read captcha from cache
+ $response = new BinaryFileResponse($captcha);
+
+ //Set file name
+ $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'.'.$_format);
+
+ //Set etag
+ $response->setEtag(md5($hash));
+
+ //Set last modified
+ $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+
+ //Set as public
+ $response->setPublic();
+
+ //Return 304 response if not modified
+ $response->isNotModified($request);
+
+ //Return response
+ return $response;
+ }
+
+ /**
+ * Return facebook image
+ *
+ * @param Request $request The Request instance
+ * @param string $hash The hash
+ * @param string $path The image path
+ * @param int $height The height
+ * @param int $width The width
+ * @return Response The rendered image
+ */
+ public function facebook(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
+ //Without matching hash
+ if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
+ //Throw new exception
+ throw new NotFoundHttpException(sprintf('Unable to match facebook hash: %s', $hash));
+ //Without matching format
+ } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+ //Throw new exception
+ throw new NotFoundHttpException(sprintf('Invalid facebook format: %s', $_format));
+ }
+
+ //Unshort path
+ $path = $this->slugger->unshort($short = $path);
+
+ //Without facebook file
+ if (!is_file($facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.'.$_format)) {
+ //Throw new exception
+ throw new NotFoundHttpException('Unable to get facebook file');
+ }
+
+ //Read facebook from cache
+ $response = new BinaryFileResponse($facebook);
+
+ //Set file name
+ $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format);
+
+ //Set etag
+ $response->setEtag(md5($hash));
+
+ //Set last modified
+ $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($facebook)['mtime'])));
+
+ //Set as public
+ $response->setPublic();
+
+ //Return 304 response if not modified
+ $response->isNotModified($request);
+
+ //Return response
+ return $response;
+ }
+
+ /**
+ * Return map image
+ *
+ * @param Request $request The Request instance
+ * @param string $hash The hash
+ * @param int $updated The updated timestamp
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param int $zoom The zoom
+ * @param int $width The width
+ * @param int $height The height
+ * @return Response The rendered image
+ */
+ public function map(Request $request, string $hash, float $latitude, float $longitude, int $height, int $width, int $zoom, string $_format): Response {
+ //Without matching hash
+ if ($hash !== $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude])) {
+ //Throw new exception
+ throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
+ }
+
+ //Set map
+ $map = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
+
+ //Without map file
+ //TODO: refresh after config modification ?
+ if (!is_file($map)) {
+ //Without existing map path
+ if (!is_dir($dir = dirname($map))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ //XXX: on CoW filesystems execute a chattr +C before filling
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Create image instance
+ $image = new \Imagick();
+
+ //Add new image
+ $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
+
+ //Create tile instance
+ $tile = new \Imagick();
+
+ //Get tile xy
+ $centerX = $this->image->longitudeToX($longitude, $zoom);
+ $centerY = $this->image->latitudeToY($latitude, $zoom);
+
+ //Calculate start xy
+ $startX = floor(floor($centerX) - $width / $this->config['map']['tz']);
+ $startY = floor(floor($centerY) - $height / $this->config['map']['tz']);
+
+ //Calculate end xy
+ $endX = ceil(ceil($centerX) + $width / $this->config['map']['tz']);
+ $endY = ceil(ceil($centerY) + $height / $this->config['map']['tz']);
+
+ for($x = $startX; $x <= $endX; $x++) {
+ for($y = $startY; $y <= $endY; $y++) {
+ //Set cache path
+ $cache = $this->config['cache'].'/'.$this->config['prefixes']['map'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
+
+ //Without cache image
+ if (!is_file($cache)) {
+ //Set tile url
+ $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['map']['server']]);
+
+ //Without cache path
+ if (!is_dir($dir = dirname($cache))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Store tile in cache
+ file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
+ }
+
+ //Set dest x
+ $destX = intval(floor($width / 2 - $this->config['map']['tz'] * ($centerX - $x)));
+
+ //Set dest y
+ $destY = intval(floor($height / 2 - $this->config['map']['tz'] * ($centerY - $y)));
+
+ //Read tile from cache
+ $tile->readImage($cache);
+
+ //Compose image
+ $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
+
+ //Clear tile
+ $tile->clear();
+ }
+ }
+
+ //Add imagick draw instance
+ //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+ $draw = new \ImagickDraw();
+
+ //Set text antialias
+ $draw->setTextAntialias(true);
+
+ //Set stroke antialias
+ $draw->setStrokeAntialias(true);
+
+ //Set text alignment
+ $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+ //Set gravity
+ $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+ //Set fill color
+ $draw->setFillColor($this->config['map']['fill']);
+
+ //Set stroke color
+ $draw->setStrokeColor($this->config['map']['border']);
+
+ //Set stroke width
+ $draw->setStrokeWidth($this->config['map']['thickness']);
+
+ //Draw circle
+ $draw->circle($width/2 - $this->config['map']['radius'], $height/2 - $this->config['map']['radius'], $width/2 + $this->config['map']['radius'], $height/2 + $this->config['map']['radius']);
+
+ //Draw on image
+ $image->drawImage($draw);
+
+ //Strip image exif data and properties
+ $image->stripImage();
+
+ //Add latitude
+ //XXX: not supported by imagick :'(
+ $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
+
+ //Add longitude
+ //XXX: not supported by imagick :'(
+ $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
+
+ //Set progressive jpeg
+ $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
+
+ //Set compression quality
+ $image->setImageCompressionQuality($this->config['map']['quality']);
+
+ //Save image
+ if (!$image->writeImage($map)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $map));
+ }
+ }
+
+ //Read map from cache
+ $response = new BinaryFileResponse($map);
+
+ //Set file name
+ $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
+
+ //Set etag
+ $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude])));
+
+ //Set last modified
+ $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
+
+ //Disable robot index
+ $response->headers->set('X-Robots-Tag', 'noindex');
+
+ //Set as public
+ $response->setPublic();
+
+ //Return 304 response if not modified
+ $response->isNotModified($request);
+
+ //Return response
+ return $response;
+ }
+
+ /**
+ * Return multi map image
+ *
+ * @param Request $request The Request instance
+ * @param string $hash The hash
+ * @param int $updated The updated timestamp
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param string $coordinates The coordinates
+ * @param int $zoom The zoom
+ * @param int $width The width
+ * @param int $height The height
+ * @return Response The rendered image
+ */
+ public function multi(Request $request, string $hash, string $coordinate, int $height, int $width, int $zoom, string $_format): Response {
+ //Without matching hash
+ if ($hash !== $this->slugger->hash([$height, $width, $zoom, $coordinate])) {
+ //Throw new exception
+ throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
+ }
+
+ //Set latitudes and longitudes array
+ $latitudes = $longitudes = [];
+
+ //Set coordinates
+ $coordinates = array_map(
+ function ($v) use (&$latitudes, &$longitudes) {
+ list($latitude, $longitude) = explode(',', $v);
+ $latitudes[] = $latitude;
+ $longitudes[] = $longitude;
+ return [ $latitude, $longitude ];
+ },
+ explode('-', $coordinate)
+ );
+
+ //Set latitude
+ $latitude = round((min($latitudes)+max($latitudes))/2, 6);
+
+ //Set longitude
+ $longitude = round((min($longitudes)+max($longitudes))/2, 6);
+
+ //Set map
+ $map = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($latitude*1000000%10).'/'.($longitude*1000000%10).'/'.$coordinate.'-'.$zoom.'-'.$width.'x'.$height.'.'.$_format;
+
+ //Without map file
+ if (!is_file($map)) {
+ //Without existing multi path
+ if (!is_dir($dir = dirname($map))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ //XXX: on CoW filesystems execute a chattr +C before filling
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Create image instance
+ $image = new \Imagick();
+
+ //Add new image
+ $image->newImage($width, $height, new \ImagickPixel('transparent'), $_format);
+
+ //Create tile instance
+ $tile = new \Imagick();
+
+ //Get tile xy
+ $centerX = $this->image->longitudeToX($longitude, $zoom);
+ $centerY = $this->image->latitudeToY($latitude, $zoom);
+
+ //Calculate start xy
+ $startX = floor(floor($centerX) - $width / $this->config['multi']['tz']);
+ $startY = floor(floor($centerY) - $height / $this->config['multi']['tz']);
+
+ //Calculate end xy
+ $endX = ceil(ceil($centerX) + $width / $this->config['multi']['tz']);
+ $endY = ceil(ceil($centerY) + $height / $this->config['multi']['tz']);
+
+ for($x = $startX; $x <= $endX; $x++) {
+ for($y = $startY; $y <= $endY; $y++) {
+ //Set cache path
+ $cache = $this->config['cache'].'/'.$this->config['prefixes']['multi'].'/'.$zoom.'/'.($x%10).'/'.($y%10).'/'.$x.','.$y.'.png';
+
+ //Without cache image
+ if (!is_file($cache)) {
+ //Set tile url
+ $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->config['servers'][$this->config['multi']['server']]);
+
+ //Without cache path
+ if (!is_dir($dir = dirname($cache))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Store tile in cache
+ file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
+ }
+
+ //Set dest x
+ $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $x)));
+
+ //Set dest y
+ $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $y)));
+
+ //Read tile from cache
+ $tile->readImage($cache);
+
+ //Compose image
+ $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
+
+ //Clear tile
+ $tile->clear();
+ }
+ }
+
+ //Add imagick draw instance
+ //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
+ $draw = new \ImagickDraw();
+
+ //Set text antialias
+ $draw->setTextAntialias(true);
+
+ //Set stroke antialias
+ $draw->setStrokeAntialias(true);
+
+ //Set text alignment
+ $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
+
+ //Set gravity
+ $draw->setGravity(\Imagick::GRAVITY_CENTER);
+
+ //Iterate on locations
+ foreach($coordinates as $id => $coordinate) {
+ //Get coordinates
+ list($clatitude, $clongitude) = $coordinate;
+
+ //Set dest x
+ $destX = intval(floor($width / 2 - $this->config['multi']['tz'] * ($centerX - $this->image->longitudeToX(floatval($clongitude), $zoom))));
+
+ //Set dest y
+ $destY = intval(floor($height / 2 - $this->config['multi']['tz'] * ($centerY - $this->image->latitudeToY(floatval($clatitude), $zoom))));
+
+ //Set fill color
+ $draw->setFillColor($this->config['multi']['fill']);
+
+ //Set font size
+ $draw->setFontSize($this->config['multi']['size']);
+
+ //Set stroke color
+ $draw->setStrokeColor($this->config['multi']['border']);
+
+ //Set circle radius
+ $radius = $this->config['multi']['radius'];
+
+ //Set stroke width
+ $stroke = $this->config['multi']['thickness'];
+
+ //With matching position
+ if ($clatitude === $latitude && $clongitude == $longitude) {
+ //Set fill color
+ $draw->setFillColor($this->config['multi']['highfill']);
+
+ //Set font size
+ $draw->setFontSize($this->config['multi']['highsize']);
+
+ //Set stroke color
+ $draw->setStrokeColor($this->config['multi']['highborder']);
+
+ //Set circle radius
+ $radius = $this->config['multi']['highradius'];
+
+ //Set stroke width
+ $stroke = $this->config['multi']['highthickness'];
+ }
+
+ //Set stroke width
+ $draw->setStrokeWidth($stroke);
+
+ //Draw circle
+ $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
+
+ //Set fill color
+ $draw->setFillColor($draw->getStrokeColor());
+
+ //Set stroke width
+ $draw->setStrokeWidth($stroke / 4);
+
+ //Get font metrics
+ #$metrics = $image->queryFontMetrics($draw, strval($id));
+
+ //Add annotation
+ $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
+ }
+
+ //Draw on image
+ $image->drawImage($draw);
+
+ //Strip image exif data and properties
+ $image->stripImage();
+
+ //Add latitude
+ //XXX: not supported by imagick :'(
+ $image->setImageProperty('exif:GPSLatitude', $this->image->latitudeToSexagesimal($latitude));
+
+ //Add longitude
+ //XXX: not supported by imagick :'(
+ $image->setImageProperty('exif:GPSLongitude', $this->image->longitudeToSexagesimal($longitude));
+
+ //Add description
+ //XXX: not supported by imagick :'(
+ #$image->setImageProperty('exif:Description', $caption);
+
+ //Set progressive jpeg
+ $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
+
+ //Set compression quality
+ $image->setImageCompressionQuality($this->config['multi']['quality']);
+
+ //Save image
+ if (!$image->writeImage($map)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $path));
+ }
+ }
+
+ //Read map from cache
+ $response = new BinaryFileResponse($map);
+
+ //Set file name
+ #$response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
+ $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map));
+
+ //Set etag
+ $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate])));
+
+ //Set last modified
+ $response->setLastModified(\DateTime::createFromFormat('U', strval(stat($map)['mtime'])));
+
+ //Disable robot index
+ $response->headers->set('X-Robots-Tag', 'noindex');
+
+ //Set as public
+ $response->setPublic();
+
+ //Return 304 response if not modified
+ $response->isNotModified($request);
+
+ //Return response
+ return $response;
+ }
+
+ /**
+ * Return thumb image
+ *
+ * @param Request $request The Request instance
+ * @param string $hash The hash
+ * @param string $path The image path
+ * @param int $height The height
+ * @param int $width The width
+ * @return Response The rendered image
+ */
+ public function thumb(Request $request, string $hash, string $path, int $height, int $width, string $_format): Response {
+ //Without matching hash
+ if ($hash !== $this->slugger->serialize([$path, $height, $width])) {
+ //Throw new exception
+ throw new NotFoundHttpException('Invalid thumb hash');
+ //Without valid format
+ } elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') {
+ //Throw new exception
+ throw new NotFoundHttpException('Invalid thumb format');
+ }
+
+ //Unshort path
+ $path = $this->slugger->unshort($short = $path);
+
+ //Set thumb
+ $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$_format;
+
+ //Without file
+ if (!is_file($path) || !($updated = stat($path)['mtime'])) {
+ //Throw new exception
+ throw new NotFoundHttpException('Unable to get thumb file');
+ }
+
+ //Without thumb up to date file
+ if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
+ //Without existing thumb path
+ if (!is_dir($dir = dirname($thumb))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create path
+ //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
+ //XXX: on CoW filesystems execute a chattr +C before filling
+ $filesystem->mkdir($dir, 0775);
+ } catch (IOExceptionInterface $e) {
+ //Throw error
+ throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
+ }
+ }
+
+ //Create image instance
+ $image = new \Imagick();
+
+ //Read image
+ $image->readImage(realpath($path));
+
+ //Crop using aspect ratio
+ //XXX: for better result upload image directly in aspect ratio :)
+ $image->cropThumbnailImage($width, $height);
+
+ //Strip image exif data and properties
+ $image->stripImage();
+
+ //Set compression quality
+ //TODO: ajust that
+ $image->setImageCompressionQuality(70);
+
+ //Set image format
+ #$image->setImageFormat($_format);
+
+ //Save thumb
+ if (!$image->writeImage($thumb)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
+ }
+
+ //Set mtime
+ $mtime = stat($thumb)['mtime'];
+ }
+
+ //Read thumb from cache
+ $response = new BinaryFileResponse($thumb);
+
+ //Set file name
+ $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format);
+
+ //Set etag
+ $response->setEtag(md5($hash));
+
+ //Set last modified
+ $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
+
+ //Set as public
+ $response->setPublic();
+
+ //Return 304 response if not modified
+ $response->isNotModified($request);
+
+ //Return response
+ return $response;
+ }
+}
+++ /dev/null
-<?php declare(strict_types=1);
-
-/*
- * This file is part of the Rapsys PackBundle package.
- *
- * (c) Raphaël Gertz <symfony@rapsys.eu>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Rapsys\PackBundle\Controller;
-
-use Rapsys\PackBundle\Util\ImageUtil;
-use Rapsys\PackBundle\Util\SluggerUtil;
-
-use Psr\Container\ContainerInterface;
-
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
-use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
-use Symfony\Component\HttpFoundation\HeaderUtils;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\Routing\RequestContext;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
-
-/**
- * {@inheritdoc}
- */
-class ImageController extends AbstractController implements ServiceSubscriberInterface {
- /**
- * Creates a new image controller
- *
- * @param ContainerInterface $container The ContainerInterface instance
- * @param ImageUtil $image The MapUtil instance
- * @param SluggerUtil $slugger The SluggerUtil instance
- * @param string $cache The cache path
- * @param string $path The public path
- * @param string $prefix The prefix
- */
- function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'image') {
- }
-
- /**
- * Return captcha image
- *
- * @param Request $request The Request instance
- * @param string $hash The hash
- * @param int $updated The updated timestamp
- * @param string $equation The shorted equation
- * @param int $width The width
- * @param int $height The height
- * @return Response The rendered image
- */
- public function captcha(Request $request, string $hash, int $updated, string $equation, int $width, int $height): Response {
- //Without matching hash
- if ($hash !== $this->slugger->serialize([$updated, $equation, $width, $height])) {
- //Throw new exception
- throw new NotFoundHttpException(sprintf('Unable to match captcha hash: %s', $hash));
- }
-
- //Set hashed tree
- $hashed = array_reverse(str_split(strval($updated)));
-
- //Set captcha
- $captcha = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$equation.'/'.$width.'x'.$height.'.jpeg';
-
- //Unshort equation
- $equation = $this->slugger->unshort($equation);
-
- //Without captcha up to date file
- if (!is_file($captcha) || !($mtime = stat($captcha)['mtime']) || $mtime < $updated) {
- //Without existing captcha path
- if (!is_dir($dir = dirname($captcha))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- //XXX: on CoW filesystems execute a chattr +C before filling
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Add imagick draw instance
- //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
- $draw = new \ImagickDraw();
-
- //Set text antialias
- $draw->setTextAntialias(true);
-
- //Set stroke antialias
- $draw->setStrokeAntialias(true);
-
- //Set text alignment
- $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
- //Set gravity
- $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
- //Set fill color
- $draw->setFillColor($this->image->getFill());
-
- //Set stroke color
- $draw->setStrokeColor($this->image->getStroke());
-
- //Set font size
- $draw->setFontSize($this->image->getFontSize() / 1.5);
-
- //Set stroke width
- $draw->setStrokeWidth($this->image->getStrokeWidth() / 3);
-
- //Set rotation
- $draw->rotate($rotate = (rand(25, 75)*(rand(0,1)?-.1:.1)));
-
- //Get font metrics
- $metrics2 = $image->queryFontMetrics($draw, strval('stop spam'));
-
- //Add annotation
- $draw->annotation($width / 2 - ceil(rand(intval(-$metrics2['textWidth']), intval($metrics2['textWidth'])) / 2) - abs($rotate), ceil($metrics2['textHeight'] + $metrics2['descender'] + $metrics2['ascender']) / 2 - $this->image->getStrokeWidth() - $rotate, strval('stop spam'));
-
- //Set rotation
- $draw->rotate(-$rotate);
-
- //Set font size
- $draw->setFontSize($this->image->getFontSize());
-
- //Set stroke width
- $draw->setStrokeWidth($this->image->getStrokeWidth());
-
- //Set rotation
- $draw->rotate($rotate = (rand(25, 50)*(rand(0,1)?-.1:.1)));
-
- //Get font metrics
- $metrics = $image->queryFontMetrics($draw, strval($equation));
-
- //Add annotation
- $draw->annotation($width / 2, ceil($metrics['textHeight'] + $metrics['descender'] + $metrics['ascender']) / 2 - $this->image->getStrokeWidth(), strval($equation));
-
- //Set rotation
- $draw->rotate(-$rotate);
-
- //Add new image
- #$image->newImage(intval(ceil($metrics['textWidth'])), intval(ceil($metrics['textHeight'] + $metrics['descender'])), new \ImagickPixel($this->image->getBackground()), 'jpeg');
- $image->newImage($width, $height, new \ImagickPixel($this->image->getBackground()), 'jpeg');
-
- //Draw on image
- $image->drawImage($draw);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Set compression quality
- $image->setImageCompressionQuality(70);
-
- //Save captcha
- if (!$image->writeImage($captcha)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $captcha));
- }
-
- //Set mtime
- $mtime = stat($captcha)['mtime'];
- }
-
- //Read captcha from cache
- $response = new BinaryFileResponse($captcha);
-
- //Set file name
- $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'captcha-stop-spam-'.str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation).'-'.$width.'x'.$height.'.jpeg');
-
- //Set etag
- $response->setEtag(md5($hash));
-
- //Set last modified
- $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
- //Set as public
- $response->setPublic();
-
- //Return 304 response if not modified
- $response->isNotModified($request);
-
- //Return response
- return $response;
- }
-
- /**
- * Return thumb image
- *
- * @param Request $request The Request instance
- * @param string $hash The hash
- * @param int $updated The updated timestamp
- * @param string $path The image path
- * @param int $width The width
- * @param int $height The height
- * @return Response The rendered image
- */
- public function thumb(Request $request, string $hash, int $updated, string $path, int $width, int $height): Response {
- //Without matching hash
- if ($hash !== $this->slugger->serialize([$updated, $path, $width, $height])) {
- //Throw new exception
- throw new NotFoundHttpException(sprintf('Unable to match thumb hash: %s', $hash));
- }
-
- //Set hashed tree
- $hashed = array_reverse(str_split(strval($updated)));
-
- //Set thumb
- $thumb = $this->path.'/'.$this->prefix.'/'.$hashed[0].'/'.$hashed[1].'/'.$hashed[2].'/'.$updated.'/'.$path.'/'.$width.'x'.$height.'.jpeg';
-
- //Unshort path
- $path = $this->slugger->unshort($path);
-
- //Without thumb up to date file
- if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) {
- //Without existing thumb path
- if (!is_dir($dir = dirname($thumb))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- //XXX: on CoW filesystems execute a chattr +C before filling
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Read image
- $image->readImage(realpath($path));
-
- //Crop using aspect ratio
- //XXX: for better result upload image directly in aspect ratio :)
- $image->cropThumbnailImage($width, $height);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Set compression quality
- //TODO: ajust that
- $image->setImageCompressionQuality(70);
-
- //Save thumb
- if (!$image->writeImage($thumb)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $thumb));
- }
-
- //Set mtime
- $mtime = stat($thumb)['mtime'];
- }
-
- //Read thumb from cache
- $response = new BinaryFileResponse($thumb);
-
- //Set file name
- $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.str_replace('/', '_', $path).'-'.$width.'x'.$height.'.jpeg');
-
- //Set etag
- $response->setEtag(md5($hash));
-
- //Set last modified
- $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
- //Set as public
- $response->setPublic();
-
- //Return 304 response if not modified
- $response->isNotModified($request);
-
- //Return response
- return $response;
- }
-}
+++ /dev/null
-<?php declare(strict_types=1);
-
-/*
- * This file is part of the Rapsys PackBundle package.
- *
- * (c) Raphaël Gertz <symfony@rapsys.eu>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Rapsys\PackBundle\Controller;
-
-use Rapsys\PackBundle\Util\MapUtil;
-use Rapsys\PackBundle\Util\SluggerUtil;
-
-use Psr\Container\ContainerInterface;
-
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
-use Symfony\Component\Filesystem\Filesystem;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
-use Symfony\Component\HttpFoundation\HeaderUtils;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\Routing\RequestContext;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
-
-/**
- * {@inheritdoc}
- */
-class MapController extends AbstractController implements ServiceSubscriberInterface {
- /**
- * The stream context instance
- */
- protected mixed $ctx;
-
- /**
- * Creates a new osm controller
- *
- * @param ContainerInterface $container The ContainerInterface instance
- * @param MapUtil $map The MapUtil instance
- * @param SluggerUtil $slugger The SluggerUtil instance
- * @param string $cache The cache path
- * @param string $path The public path
- * @param string $prefix The prefix
- * @param string $url The tile server url
- */
- function __construct(protected ContainerInterface $container, protected MapUtil $map, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'map', protected string $url = MapUtil::osm) {
- //Set ctx
- $this->ctx = stream_context_create(
- [
- 'http' => [
- #'header' => ['Referer: https://www.openstreetmap.org/'],
- 'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20,
- 'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== "" ? (float)$timeout : 60),
- 'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== "" ? (string)$agent : RapsysPackBundle::getAlias().'/'.RapsysPackBundle::getVersion())
- ]
- ]
- );
- }
-
- /**
- * Return map image
- *
- * @param Request $request The Request instance
- * @param string $hash The hash
- * @param int $updated The updated timestamp
- * @param float $latitude The latitude
- * @param float $longitude The longitude
- * @param int $zoom The zoom
- * @param int $width The width
- * @param int $height The height
- * @return Response The rendered image
- */
- public function map(Request $request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response {
- //Without matching hash
- if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
- //Throw new exception
- throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
- }
-
- //Set map
- $map = $this->path.'/'.$this->prefix.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
-
- //Without multi up to date file
- if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
- //Without existing map path
- if (!is_dir($dir = dirname($map))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- //XXX: on CoW filesystems execute a chattr +C before filling
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Add new image
- $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
- //Create tile instance
- $tile = new \Imagick();
-
- //Get tile xy
- $centerX = $this->map->longitudeToX($longitude, $zoom);
- $centerY = $this->map->latitudeToY($latitude, $zoom);
-
- //Calculate start xy
- $startX = floor(floor($centerX) - $width / MapUtil::tz);
- $startY = floor(floor($centerY) - $height / MapUtil::tz);
-
- //Calculate end xy
- $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
- $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
-
- for($x = $startX; $x <= $endX; $x++) {
- for($y = $startY; $y <= $endY; $y++) {
- //Set cache path
- $cache = $this->cache.'/'.$this->prefix.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
- //Without cache image
- if (!is_file($cache)) {
- //Set tile url
- $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
-
- //Without cache path
- if (!is_dir($dir = dirname($cache))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Store tile in cache
- file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
- }
-
- //Set dest x
- $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
-
- //Set dest y
- $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
-
- //Read tile from cache
- $tile->readImage($cache);
-
- //Compose image
- $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
- //Clear tile
- $tile->clear();
- }
- }
-
- //Add imagick draw instance
- //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
- $draw = new \ImagickDraw();
-
- //Set text antialias
- $draw->setTextAntialias(true);
-
- //Set stroke antialias
- $draw->setStrokeAntialias(true);
-
- //Set text alignment
- $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
- //Set gravity
- $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
- //Set fill color
- $draw->setFillColor('#cff');
-
- //Set stroke color
- $draw->setStrokeColor('#00c3f9');
-
- //Set stroke width
- $draw->setStrokeWidth(2);
-
- //Draw circle
- $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 5, $height/2 + 5);
-
- //Draw on image
- $image->drawImage($draw);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Add latitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
-
- //Add longitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
-
- //Add description
- //XXX: not supported by imagick :'(
- #$image->setImageProperty('exif:Description', $caption);
-
- //Set progressive jpeg
- $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
- //Set compression quality
- //TODO: ajust that
- $image->setImageCompressionQuality(70);
-
- //Save image
- if (!$image->writeImage($map)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $path));
- }
-
- //Set mtime
- $mtime = stat($map)['mtime'];
- }
-
- //Read map from cache
- $response = new BinaryFileResponse($map);
-
- //Set file name
- $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
-
- //Set etag
- $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
-
- //Set last modified
- $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
- //Disable robot index
- $response->headers->set('X-Robots-Tag', 'noindex');
-
- //Set as public
- $response->setPublic();
-
- //Return 304 response if not modified
- $response->isNotModified($request);
-
- //Return response
- return $response;
- }
-
- /**
- * Return multi map image
- *
- * @param Request $request The Request instance
- * @param string $hash The hash
- * @param int $updated The updated timestamp
- * @param float $latitude The latitude
- * @param float $longitude The longitude
- * @param string $coordinates The coordinates
- * @param int $zoom The zoom
- * @param int $width The width
- * @param int $height The height
- * @return Response The rendered image
- */
- public function multiMap(Request $request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response {
- //Without matching hash
- if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger->hash($coordinates), $zoom, $width, $height])) {
- //Throw new exception
- throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
- }
-
- //Set multi
- $map = $this->path.'/'.$this->prefix.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
-
- //Without multi up to date file
- if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
- //Without existing multi path
- if (!is_dir($dir = dirname($map))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- //XXX: on CoW filesystems execute a chattr +C before filling
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Add new image
- $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
- //Create tile instance
- $tile = new \Imagick();
-
- //Get tile xy
- $centerX = $this->map->longitudeToX($longitude, $zoom);
- $centerY = $this->map->latitudeToY($latitude, $zoom);
-
- //Calculate start xy
- $startX = floor(floor($centerX) - $width / MapUtil::tz);
- $startY = floor(floor($centerY) - $height / MapUtil::tz);
-
- //Calculate end xy
- $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
- $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
-
- for($x = $startX; $x <= $endX; $x++) {
- for($y = $startY; $y <= $endY; $y++) {
- //Set cache path
- $cache = $this->cache.'/'.$this->prefix.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
- //Without cache image
- if (!is_file($cache)) {
- //Set tile url
- $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
-
- //Without cache path
- if (!is_dir($dir = dirname($cache))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Store tile in cache
- file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
- }
-
- //Set dest x
- $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
-
- //Set dest y
- $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
-
- //Read tile from cache
- $tile->readImage($cache);
-
- //Compose image
- $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
- //Clear tile
- $tile->clear();
- }
- }
-
- //Add imagick draw instance
- //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
- $draw = new \ImagickDraw();
-
- //Set text antialias
- $draw->setTextAntialias(true);
-
- //Set stroke antialias
- $draw->setStrokeAntialias(true);
-
- //Set text alignment
- $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
- //Set gravity
- $draw->setGravity(\Imagick::GRAVITY_CENTER);
-
- //Convert to array
- $coordinates = array_reverse(array_map(function ($v) { $p = strpos($v, ','); return ['latitude' => floatval(substr($v, 0, $p)), 'longitude' => floatval(substr($v, $p + 1))]; }, explode('-', $coordinates)), true);
-
- //Iterate on locations
- foreach($coordinates as $id => $coordinate) {
- //Set dest x
- $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $this->map->longitudeToX(floatval($coordinate['longitude']), $zoom))));
-
- //Set dest y
- $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $this->map->latitudeToY(floatval($coordinate['latitude']), $zoom))));
-
- //Set fill color
- $draw->setFillColor($this->map->getFill());
-
- //Set font size
- $draw->setFontSize($this->map->getFontSize());
-
- //Set stroke color
- $draw->setStrokeColor($this->map->getStroke());
-
- //Set circle radius
- $radius = $this->map->getRadius();
-
- //Set stroke width
- $stroke = $this->map->getStrokeWidth();
-
- //With matching position
- if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
- //Set fill color
- $draw->setFillColor($this->map->getHighFill());
-
- //Set font size
- $draw->setFontSize($this->map->getHighFontSize());
-
- //Set stroke color
- $draw->setStrokeColor($this->map->getHighStroke());
-
- //Set circle radius
- $radius = $this->map->getHighRadius();
-
- //Set stroke width
- $stroke = $this->map->getHighStrokeWidth();
- }
-
- //Set stroke width
- $draw->setStrokeWidth($stroke);
-
- //Draw circle
- $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
-
- //Set fill color
- $draw->setFillColor($draw->getStrokeColor());
-
- //Set stroke width
- $draw->setStrokeWidth($stroke / 4);
-
- //Get font metrics
- #$metrics = $image->queryFontMetrics($draw, strval($id));
-
- //Add annotation
- $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
- }
-
- //Draw on image
- $image->drawImage($draw);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Add latitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
-
- //Add longitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
-
- //Add description
- //XXX: not supported by imagick :'(
- #$image->setImageProperty('exif:Description', $caption);
-
- //Set progressive jpeg
- $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
- //Set compression quality
- //TODO: ajust that
- $image->setImageCompressionQuality(70);
-
- //Save image
- if (!$image->writeImage($map)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $path));
- }
-
- //Set mtime
- $mtime = stat($map)['mtime'];
- }
-
- //Read map from cache
- $response = new BinaryFileResponse($map);
-
- //Set file name
- $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
-
- //Set etag
- $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
-
- //Set last modified
- $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
-
- //Disable robot index
- $response->headers->set('X-Robots-Tag', 'noindex');
-
- //Set as public
- $response->setPublic();
-
- //Return 304 response if not modified
- $response->isNotModified($request);
-
- //Return response
- return $response;
- }
-}
//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 => [
]
]
],
- #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'
+ ]
];
/**
->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()
->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();
//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());
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;
*/
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']);
}
/**
*/
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')
];
}
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']),
namespace Rapsys\PackBundle\Form;
+use Rapsys\PackBundle\RapsysPackBundle;
use Rapsys\PackBundle\Util\ImageUtil;
use Rapsys\PackBundle\Util\SluggerUtil;
//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]);
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');
--- /dev/null
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle\Form;
+
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Validator\Constraints\Email;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+/**
+ * {@inheritdoc}
+ */
+class ContactType extends CaptchaType {
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void {
+ //Add fields
+ $builder
+ ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'constraints' => [new NotBlank(['message' => 'Please provide your name'])]])
+ ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'constraints' => [new NotBlank(['message' => 'Please provide your subject'])]])
+ ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'constraints' => [new NotBlank(['message' => 'Please provide a valid mail']), new Email(['message' => 'Your mail doesn\'t seems to be valid'])]])
+ ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'constraints' => [new NotBlank(['message' => 'Please provide your message'])]])
+ ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]);
+
+ //Call parent
+ parent::buildForm($builder, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver): void {
+ //Call parent configure options
+ parent::configureOptions($resolver);
+
+ //Set defaults
+ $resolver->setDefaults(['captcha' => true]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName() {
+ return 'contact_form';
+ }
+}
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;
*/
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];
}
/**
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')) {
$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'
//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());
}
}
//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
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());
}
}
$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());
}
#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
$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([
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);
}
}
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
*
*/
public static function getVersion(): string {
//Return version
- return '0.5.2';
+ return '0.5.4';
}
}
+++ /dev/null
-#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
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;
*/
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());
}
}
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;
/**
*/
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;
$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 = [];
//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';
- }
}
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;
/**
*/
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());
}
}
//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)));
}
/**
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:
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'
--- /dev/null
+#Routes configuration
+rapsyspack_captcha:
+ path: '/captcha/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{equation<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
+ controller: Rapsys\PackBundle\Controller::captcha
+ methods: GET
+
+rapsyspack_css:
+ path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format<css>?css}'
+ methods: GET
+
+rapsyspack_facebook:
+ path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}'
+ controller: Rapsys\PackBundle\Controller::facebook
+ methods: GET
+
+rapsyspack_img:
+ path: '/bundles/rapsyspack/pack/img/{file<[a-zA-Z0-9]+>}.{!_format<(jpeg|png|webp)>}'
+ methods: GET
+
+rapsyspack_js:
+ path: '/bundles/rapsyspack/pack/js/{file<[a-zA-Z0-9]+>}.{!_format<js>?js}'
+ methods: GET
+
+rapsyspack_map:
+ path: '/map/{hash<[a-zA-Z0-9=_-]+>}/{latitude<\d+(\.?\d+)>},{longitude<\d+(\.?\d+)>}-{zoom<\d+>}-{width<\d+>}x{height<\d+>}.{!_format<(jpeg|png|webp)>}'
+ controller: Rapsys\PackBundle\Controller::map
+ methods: GET
+
+rapsyspack_multi:
+ path: '/multi/{hash<[a-zA-Z0-9=_-]+>}/{coordinate<\d+(\.\d+)?,\d+(\.\d+)?(-\d+(\.\d+)?,\d+(\.\d+)?)+>}-{zoom<\d+>}-{width<\d+>}x{height<\d+>}.{!_format<(jpeg|png|webp)>}'
+ controller: Rapsys\PackBundle\Controller::multi
+ methods: GET
+
+rapsyspack_thumb:
+ #TODO: remove default _format when a solution is found
+ path: '/thumb/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>?jpeg}'
+ controller: Rapsys\PackBundle\Controller::thumb
+ methods: GET
--- /dev/null
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+//Create image object
+$image = new Imagick();
+
+//Create draw object
+$draw = new ImagickDraw();
+
+//Create pixel object
+$pixel = new ImagickPixel('white');
+
+//Create new image
+$image->newImage(1200, 630, $pixel);
+
+//Set fill color
+$draw->setFillColor('black');
+
+//Set font properties
+$draw->setFont('../woff2/droidsans.regular.woff2');
+$draw->setFontSize(30);
+
+//Add texts
+$image->annotateImage($draw, 10, 35, 0, 'RP');
+$image->annotateImage($draw, 10, 615, 0, 'RP');
+$image->annotateImage($draw, 1155, 35, 0, 'RP');
+$image->annotateImage($draw, 1155, 615, 0, 'RP');
+
+//Set image format
+$image->setImageFormat('png');
+
+//Output image header
+header('Content-type: image/png');
+
+//Output image
+echo $image;