From 2094432d9c882d53f234e627d6212a2d08f71ae6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 05:53:05 +0100 Subject: [PATCH 01/16] Use bundle alias Remove file default value --- Command/RangeCommand.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Command/RangeCommand.php b/Command/RangeCommand.php index e4f15da..0597138 100644 --- a/Command/RangeCommand.php +++ b/Command/RangeCommand.php @@ -12,7 +12,6 @@ namespace Rapsys\PackBundle\Command; use Rapsys\PackBundle\Command; -use Rapsys\PackBundle\RapsysPackBundle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -29,19 +28,19 @@ class RangeCommand extends Command { * * Shown with bin/console list */ - protected string $description = 'Outputs a shuffled printable characters range'; + protected string $description = 'Generates a shuffled printable characters range'; /** * Set help * * Shown with bin/console --help rapsyspack:range */ - protected string $help = 'This command outputs a shuffled printable characters range'; + protected string $help = 'This command generates a shuffled printable characters range'; /** * {@inheritdoc} */ - public function __construct(protected string $file = '.env.local', protected ?string $name = null) { + public function __construct(protected string $file, protected ?string $name = null) { //Call parent constructor parent::__construct($this->name); @@ -99,7 +98,7 @@ class RangeCommand extends Command { //Without match } else { //Append string - $content .= (strlen($content)?"\n\n":'').'###> '.RapsysPackBundle::getBundleAlias().' ###'."\n".$string."\n".'###< '.RapsysPackBundle::getBundleAlias().' ###'; + $content .= (strlen($content)?"\n\n":'').'###> '.$this->bundle.' ###'."\n".$string."\n".'###< '.$this->bundle.' ###'; } //Write file content @@ -119,7 +118,7 @@ class RangeCommand extends Command { echo '# Add to '.$file."\n"; //Print rapsys pack range variable - echo '###> '.RapsysPackBundle::getBundleAlias().' ###'."\n".$string."\n".'###< '.RapsysPackBundle::getBundleAlias().' ###'; + echo '###> '.$this->bundle.' ###'."\n".$string."\n".'###< '.$this->bundle.' ###'; //Add trailing line echo "\n"; -- 2.41.3 From 67d50a43b1bc369544315f6e407db813ad2edf5c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 05:53:30 +0100 Subject: [PATCH 02/16] Remove path bundle parameter Add cache and public bundle parameters --- DependencyInjection/RapsysPackExtension.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/RapsysPackExtension.php b/DependencyInjection/RapsysPackExtension.php index fc9efce..3a6ed53 100644 --- a/DependencyInjection/RapsysPackExtension.php +++ b/DependencyInjection/RapsysPackExtension.php @@ -49,8 +49,11 @@ class RapsysPackExtension extends Extension { //Set rapsyspack.alias key $container->setParameter($alias.'.alias', $alias); - //Set rapsyspack.path key - $container->setParameter($alias.'.path', $config['path']); + //Set rapsyspack.cache key + $container->setParameter($alias.'.cache', $config['cache']); + + //Set rapsyspack.public key + $container->setParameter($alias.'.public', $config['public']); //Set rapsyspack.version key $container->setParameter($alias.'.version', RapsysPackBundle::getVersion()); -- 2.41.3 From 668dea7acb24b4713e7ec90732af97aed06f70e1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 05:56:58 +0100 Subject: [PATCH 03/16] Drop output, path and token parameters Add captcha, facebook, fonts, map, multi, prefixes, routes, servers, thumb and tokens parameters Add cache and public parameter paths Cleanup --- DependencyInjection/Configuration.php | 210 ++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 14 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 60b68b6..8f3beb2 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -38,6 +38,29 @@ class Configuration implements ConfigurationInterface { //The bundle default values $defaults = [ + //XXX: use a path relative to __DIR__ as console and index do not have the same execution directory + //XXX: use realpath on var/cache only as alias subdirectory may not yet exists + 'cache' => realpath(dirname(__DIR__).'/../../../var/cache').'/'.$alias, + 'captcha' => [ + 'background' => 'white', + 'fill' => '#cff', + 'height' => 52, + 'size' => 45, + 'border' => '#00c3f9', + 'thickness' => 2, + 'width' => 192 + ], + 'facebook' => [ + 'align' => 'center', + 'fill' => 'white', + 'font' => 'default', + 'height' => 630, + 'size' => 60, + 'source' => dirname(__DIR__).'/public/facebook/source.png', + 'border' => '#00c3f9', + 'thickness' => 15, + 'width' => 1200 + ], 'filters' => [ 'css' => [ 0 => [ @@ -64,15 +87,82 @@ class Configuration implements ConfigurationInterface { ] ] ], - #TODO: migrate to public.path, public.url and router->generateUrl ? - #XXX: that would means dropping the PathPackage stuff and use static route like rapsyspack_facebook - 'output' => [ - 'css' => '@RapsysPack/css/*.pack.css', - 'img' => '@RapsysPack/img/*.pack.jpg', - 'js' => '@RapsysPack/js/*.pack.js' + 'fonts' => [ + 'default' => '/usr/share/fonts/TTF/dejavu/DejaVuSans.ttf', + #TODO: move these in veranda config ? with *: %rapsyspack.public%/woff2/*.woff2 ? + 'droidsans' => dirname(__DIR__).'/public/woff2/droidsans.regular.woff2', + 'droidsansb' => dirname(__DIR__).'/public/woff2/droidsans.bold.woff2', + 'droidsansi' => dirname(__DIR__).'/public/woff2/droidserif.italic.woff2', + 'droidsansm' => dirname(__DIR__).'/public/woff2/droidsansmono.regular.woff2', + 'droidserif' => dirname(__DIR__).'/public/woff2/droidserif.regular.woff2', + 'droidserifb' => dirname(__DIR__).'/public/woff2/droidserif.bold.woff2', + 'droidserifbi' => dirname(__DIR__).'/public/woff2/droidserif.bolditalic.woff2', + 'irishgrover' => dirname(__DIR__).'/public/woff2/irishgrover.v10.woff2', + 'lemon' => dirname(__DIR__).'/public/woff2/lemon.woff2', + 'notoemoji' => dirname(__DIR__).'/public/woff2/notoemoji.woff2' + ], + 'map' => [ + 'border' => '#00c3f9', + 'fill' => '#cff', + 'height' => 640, + 'quality' => 70, + 'radius' => 5, + 'server' => 'osm', + 'thickness' => 2, + 'tz' => 256, + 'width' => 640, + 'zoom' => 17 + ], + 'multi' => [ + 'border' => '#00c3f9', + 'fill' => '#cff', + '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 + ], + 'prefixes' => [ + 'captcha' => 'captcha', + 'css' => 'css', + 'facebook' => 'facebook', + 'img' => 'img', + 'map' => 'map', + 'multi' => 'multi', + 'pack' => 'pack', + 'thumb' => 'thumb', + 'js' => 'js' ], - 'path' => dirname(__DIR__).'/Resources/public', - 'token' => 'asset_url' + //XXX: use a path relative to __DIR__ as console and index do not have the same execution directory + 'public' => dirname(__DIR__).'/public', + 'routes' => [ + 'css' => 'rapsyspack_css', + 'img' => 'rapsyspack_img', + 'js' => 'rapsyspack_js' + ], + 'servers' => [ + 'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png', + 'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png', + 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png' + ], + 'thumb' => [ + 'height' => 128, + 'width' => 128 + ], + 'tokens' => [ + 'css' => 'asset', + 'img' => 'asset', + 'js' => 'asset' + ] ]; /** @@ -90,6 +180,33 @@ class Configuration implements ConfigurationInterface { ->getRootNode() ->addDefaultsIfNotSet() ->children() + ->scalarNode('cache')->cannotBeEmpty()->defaultValue($defaults['cache'])->end() + ->arrayNode('captcha') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('background')->cannotBeEmpty()->defaultValue($defaults['captcha']['background'])->end() + ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['captcha']['fill'])->end() + ->scalarNode('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('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('height')->cannotBeEmpty()->defaultValue($defaults['facebook']['height'])->end() + ->scalarNode('size')->cannotBeEmpty()->defaultValue($defaults['facebook']['size'])->end() + ->scalarNode('source')->cannotBeEmpty()->defaultValue($defaults['facebook']['source'])->end() + ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['facebook']['border'])->end() + ->scalarNode('thickness')->cannotBeEmpty()->defaultValue($defaults['facebook']['thickness'])->end() + ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['facebook']['width'])->end() + ->end() + ->end() ->arrayNode('filters') ->addDefaultsIfNotSet() ->children() @@ -162,16 +279,81 @@ class Configuration implements ConfigurationInterface { ->end() ->end() ->end() - ->arrayNode('output') + ->arrayNode('fonts') + ->treatNullLike([]) + ->defaultValue($defaults['fonts']) + ->scalarPrototype()->end() + ->end() + ->arrayNode('map') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('border')->cannotBeEmpty()->defaultValue($defaults['map']['border'])->end() + ->scalarNode('fill')->cannotBeEmpty()->defaultValue($defaults['map']['fill'])->end() + ->scalarNode('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('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(); -- 2.41.3 From a6ed75729b0e17fb79e3a1afcba518f6856fd5b3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:00:22 +0100 Subject: [PATCH 04/16] Add alias and config members Replace member variables by config parameters Cleanup --- Util/FacebookUtil.php | 130 +++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/Util/FacebookUtil.php b/Util/FacebookUtil.php index 91664d1..4def01b 100644 --- a/Util/FacebookUtil.php +++ b/Util/FacebookUtil.php @@ -11,6 +11,10 @@ namespace Rapsys\PackBundle\Util; +use Psr\Container\ContainerInterface; + +use Rapsys\PackBundle\RapsysPackBundle; + use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -21,57 +25,25 @@ use Symfony\Component\Routing\RouterInterface; */ class FacebookUtil { /** - * The default fonts - */ - const fonts = [ 'default' => 'ttf/default.ttf' ]; - - /** - * The default font - */ - const font = 'default'; - - /** - * The default font size - */ - const size = 60; - - /** - * The default width - */ - const width = 15; - - /** - * The default fill - */ - const fill = 'white'; - - /** - * The default stroke + * Alias string */ - const stroke = '#00c3f9'; + protected string $alias; /** - * The default align + * Config array */ - const align = 'center'; + protected array $config; /** * Creates a new facebook util * + * @param ContainerInterface $container The container instance * @param RouterInterface $router The RouterInterface instance - * @param string $cache The cache directory - * @param string $path The public path - * @param string $prefix The prefix - * @param ?string $source The source - * @param array $fonts The fonts - * @param string $font The font - * @param int $size The size - * @param int $width The width - * @param string $fill The fill - * @param string $stroke The stroke - * @param string $align The align + * @param SluggerUtil $slugger The SluggerUtil instance */ - 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) { + public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) { + //Retrieve config + $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias()); } /** @@ -79,30 +51,44 @@ class FacebookUtil { * * Generate simple image in jpeg format or load it from cache * - * @param string $pathInfo The request path info + * @TODO: move to a svg merging system ? + * + * @param string $path 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 + * @param ?int $height The height + * @param ?int $width The width * @return array The image array */ - public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array { + public function getImage(string $path, array $texts, int $updated, ?string $source = null, ?int $height = null, ?int $width = null): array { //Without source - if ($source === null && $this->source === null) { + if ($source === null && $this->config['facebook']['source'] === null) { //Return empty image data return []; //Without local source } elseif ($source === null) { //Set local source - $source = $this->source; + $source = $this->config['facebook']['source']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['facebook']['width']; + } + + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['facebook']['height']; } //Set path file - $path = $this->path.'/'.$this->prefix.$pathInfo.'.jpeg'; + $facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.jpeg'; //Without existing path - if (!is_dir($dir = dirname($path))) { + if (!is_dir($dir = dirname($facebook))) { //Create filesystem object $filesystem = new Filesystem(); @@ -117,12 +103,18 @@ class FacebookUtil { } //With path file - if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) { + 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', ['mtime' => $mtime, 'path' => $pathInfo], UrlGeneratorInterface::ABSOLUTE_URL), + 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => $mtime], UrlGeneratorInterface::ABSOLUTE_URL), 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 'og:image:height' => $height, 'og:image:width' => $width @@ -130,7 +122,7 @@ class FacebookUtil { } //Set cache path - $cache = $this->cache.'/'.$this->prefix.$pathInfo.'.png'; + $cache = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.png'; //Without cache path if (!is_dir($dir = dirname($cache))) { @@ -170,7 +162,7 @@ class FacebookUtil { //Without source if (!is_file($source)) { //Throw error - throw new \Exception(sprintf('Source file "%s" do not exists', $this->source)); + throw new \Exception(sprintf('Source file "%s" do not exists', $source)); } //Convert to absolute path @@ -223,16 +215,16 @@ class FacebookUtil { //Draw each text stroke foreach($texts as $text => $data) { //Set font - $draw->setFont($this->fonts[$data['font']??$this->font]); + $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]); //Set font size - $draw->setFontSize($data['size']??$this->size); + $draw->setFontSize($data['size']??$this->config['facebook']['size']); //Set stroke width - $draw->setStrokeWidth($data['width']??$this->width); + $draw->setStrokeWidth($data['thickness']??$this->config['facebook']['thickness']); //Set text alignment - $draw->setTextAlignment($align = ($aligns[$data['align']??$this->align])); + $draw->setTextAlignment($align = ($aligns[$data['align']??$this->config['facebook']['align']])); //Get font metrics $metrics = $image->queryFontMetrics($draw, $text); @@ -260,10 +252,10 @@ class FacebookUtil { $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2; //Set stroke color - $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$this->stroke)); + $draw->setStrokeColor(new \ImagickPixel($data['border']??$this->config['facebook']['border'])); //Set fill color - $draw->setFillColor(new \ImagickPixel($data['stroke']??$this->stroke)); + $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill'])); //Add annotation $draw->annotation($data['x'], $data['y'], $text); @@ -307,16 +299,16 @@ class FacebookUtil { //Draw each text foreach($texts as $text => $data) { //Set font - $draw->setFont($this->fonts[$data['font']??$this->font]); + $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]); //Set font size - $draw->setFontSize($data['size']??$this->size); + $draw->setFontSize($data['size']??$this->config['facebook']['size']); //Set text alignment - $draw->setTextAlignment($aligns[$data['align']??$this->align]); + $draw->setTextAlignment($aligns[$data['align']??$this->config['facebook']['align']]); //Set fill color - $draw->setFillColor(new \ImagickPixel($data['fill']??$this->fill)); + $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill'])); //Add annotation $draw->annotation($data['x'], $data['y'], $text); @@ -341,14 +333,20 @@ class FacebookUtil { $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE); //Save image - if (!$image->writeImage($path)) { + if (!$image->writeImage($facebook)) { //Throw error - throw new \Exception(sprintf('Unable to write image "%s"', $path)); + 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', ['mtime' => stat($path)['mtime'], 'path' => $pathInfo], UrlGeneratorInterface::ABSOLUTE_URL), + 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => stat($facebook)['mtime']], UrlGeneratorInterface::ABSOLUTE_URL), 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 'og:image:height' => $height, 'og:image:width' => $width -- 2.41.3 From 5b4dd4270841cc519adb7c20688d8ebd3c467102 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:10:59 +0100 Subject: [PATCH 05/16] Rename getImage from facebook util to getFacebook in image util --- Util/FacebookUtil.php | 307 ------------------------------------------ 1 file changed, 307 deletions(-) diff --git a/Util/FacebookUtil.php b/Util/FacebookUtil.php index 4def01b..633702b 100644 --- a/Util/FacebookUtil.php +++ b/Util/FacebookUtil.php @@ -45,311 +45,4 @@ class FacebookUtil { //Retrieve config $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias()); } - - /** - * Return the facebook image - * - * Generate simple image in jpeg format or load it from cache - * - * @TODO: move to a svg merging system ? - * - * @param string $path The request path info - * @param array $texts The image texts - * @param int $updated The updated timestamp - * @param ?string $source The image source - * @param ?int $height The height - * @param ?int $width The width - * @return array The image array - */ - public function getImage(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 width - if ($width === null) { - //Set width from config - $width = $this->config['facebook']['width']; - } - - //Without height - if ($height === null) { - //Set height from config - $height = $this->config['facebook']['height']; - } - - //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], 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']], UrlGeneratorInterface::ABSOLUTE_URL), - 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), - 'og:image:height' => $height, - 'og:image:width' => $width - ]; - } } -- 2.41.3 From 20503eaf77166c0b638beb84ee6d0a0be8e95113 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:12:54 +0100 Subject: [PATCH 06/16] Add css, img and js bundle routes Cleaner routes Cleanup --- config/routes/rapsyspack.yaml | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/config/routes/rapsyspack.yaml b/config/routes/rapsyspack.yaml index aa0a953..b238403 100644 --- a/config/routes/rapsyspack.yaml +++ b/config/routes/rapsyspack.yaml @@ -1,26 +1,37 @@ #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 + path: '/captcha/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{equation<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>?jpeg}' + controller: Rapsys\PackBundle\Controller::captcha + methods: GET + +rapsyspack_css: + path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format?css}' 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}' + path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>?jpeg}' + controller: Rapsys\PackBundle\Controller::facebook + methods: GET + +rapsyspack_img: + path: '/bundles/rapsyspack/pack/img/{file<[a-zA-Z0-9]+>}.{!_format<(jpeg|png|webp)>?jpeg}' + methods: GET + +rapsyspack_js: + path: '/bundles/rapsyspack/pack/js/{file<[a-zA-Z0-9]+>}.{!_format?js}' methods: GET rapsyspack_map: - path: '/map/{hash<[a-zA-Z0-9=_-]+>}/{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 + path: '/map/{hash<[a-zA-Z0-9=_-]+>}/{latitude<\d+(\.?\d+)>},{longitude<\d+(\.?\d+)>}-{zoom<\d+>}-{width<\d+>}x{height<\d+>}.{!_format<(jpeg|png|webp)>?jpeg}' + controller: Rapsys\PackBundle\Controller::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 +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)>?jpeg}' + controller: Rapsys\PackBundle\Controller::multi 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 + 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 -- 2.41.3 From 6b88d91497383051d0d4994436aae1130a397210 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:14:09 +0100 Subject: [PATCH 07/16] Remove facebook and map util services Update image util parameters Merge image and map controllers into base controller Add contact form service definition Cleanup --- config/packages/rapsyspack.yaml | 36 +++++++++------------------------ 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/config/packages/rapsyspack.yaml b/config/packages/rapsyspack.yaml index fb84758..0efaa0b 100644 --- a/config/packages/rapsyspack.yaml +++ b/config/packages/rapsyspack.yaml @@ -21,11 +21,6 @@ services: assets.packages: class: 'Symfony\Component\Asset\Packages' arguments: [ '@rapsyspack.path_package' ] - # Register facebook util service - rapsyspack.facebook_util: - class: 'Rapsys\PackBundle\Util\FacebookUtil' - arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] - public: true # Register file util service rapsyspack.file_util: class: 'Rapsys\PackBundle\Util\FileUtil' @@ -34,21 +29,16 @@ services: # 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: @@ -64,25 +54,22 @@ services: Rapsys\PackBundle\Command\RangeCommand: arguments: [ '%kernel.project_dir%/.env.local' ] tags: [ 'console.command' ] - # Register image controller - Rapsys\PackBundle\Controller\ImageController: - arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] - tags: [ 'controller.service_arguments' ] - # Register map controller - Rapsys\PackBundle\Controller\MapController: - arguments: [ '@service_container', '@rapsyspack.map_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] + # Register controller + Rapsys\PackBundle\Controller: + arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util' ] tags: [ 'controller.service_arguments' ] - # Register captcha form type + # Register captcha form Rapsys\PackBundle\Form\CaptchaType: arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ] tags: [ 'form.type' ] + # Register contact form + Rapsys\PackBundle\Form\ContactType: + arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ] + tags: [ 'form.type' ] # Register facebook event subscriber Rapsys\PackBundle\Subscriber\FacebookSubscriber: arguments: [ '@router', [] ] tags: [ 'kernel.event_subscriber' ] - # Register facebook util class alias - Rapsys\PackBundle\Util\FacebookUtil: - alias: 'rapsyspack.facebook_util' # Register file util class alias Rapsys\PackBundle\Util\FileUtil: alias: 'rapsyspack.file_util' @@ -92,9 +79,6 @@ services: # 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' -- 2.41.3 From 569db7a24c07ddc92ca69be46ae6a90d3c0d04a8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:27:25 +0100 Subject: [PATCH 08/16] Move stream context array in context parameter --- Controller.php | 10 +--------- DependencyInjection/Configuration.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Controller.php b/Controller.php index 148d37c..77a3e14 100644 --- a/Controller.php +++ b/Controller.php @@ -63,15 +63,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias()); //Set ctx - $this->ctx = stream_context_create( - [ - '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 : $this->alias.'/'.($this->version = RapsysPackBundle::getVersion())) - ] - ] - ); + $this->ctx = stream_context_create($this->config['context']); } /** diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 8f3beb2..af0c83e 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -50,6 +50,13 @@ class Configuration implements ConfigurationInterface { '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', @@ -193,6 +200,18 @@ class Configuration implements ConfigurationInterface { ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['captcha']['width'])->end() ->end() ->end() + ->arrayNode('context') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('http') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('max_redirects')->defaultValue($defaults['captcha']['max_redirects'])->end() + ->scalarNode('timeout')->defaultValue($defaults['captcha']['timeout'])->end() + ->scalarNode('user_agent')->cannotBeEmpty()->defaultValue($defaults['captcha']['user_agent'])->end() + ->end() + ->end() + ->end() ->arrayNode('facebook') ->addDefaultsIfNotSet() ->children() -- 2.41.3 From 7a884a8cd503031c762e830044dbfce09aeddf5e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 06:35:12 +0100 Subject: [PATCH 09/16] Add config and ctx member variables Drop intl size filter Drop package parameter Add container parameter Clean token parser arguments Cleanup --- Extension/PackExtension.php | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/Extension/PackExtension.php b/Extension/PackExtension.php index aa11688..5c89099 100644 --- a/Extension/PackExtension.php +++ b/Extension/PackExtension.php @@ -11,12 +11,14 @@ namespace Rapsys\PackBundle\Extension; +use Psr\Container\ContainerInterface; + use Rapsys\PackBundle\Parser\TokenParser; use Rapsys\PackBundle\RapsysPackBundle; use Rapsys\PackBundle\Util\IntlUtil; use Rapsys\PackBundle\Util\SluggerUtil; -use Symfony\Component\Asset\PackageInterface; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\HttpKernel\Config\FileLocator; use Twig\Extension\AbstractExtension; @@ -26,11 +28,34 @@ use Twig\Extension\AbstractExtension; */ class PackExtension extends AbstractExtension { /** + * Config array + */ + protected array $config; + + /** + * The stream context instance + */ + protected mixed $ctx; + + /** + * Creates pack extension + * * {@inheritdoc} * * @link https://twig.symfony.com/doc/2.x/advanced.html + * + * @param ContainerInterface $container The ContainerInterface instance + * @param IntlUtil $intl The IntlUtil instance + * @param FileLocator $locator The FileLocator instance + * @param RouterInterface $router The RouterInterface instance + * @param SluggerUtil $slugger The SluggerUtil instance */ - public function __construct(protected IntlUtil $intl, protected FileLocator $locator, protected PackageInterface $package, protected SluggerUtil $slugger, protected array $parameters) { + public function __construct(protected ContainerInterface $container, protected IntlUtil $intl, protected FileLocator $locator, protected RouterInterface $router, protected SluggerUtil $slugger) { + //Retrieve config + $this->config = $container->getParameter(RapsysPackBundle::getAlias()); + + //Set ctx + $this->ctx = stream_context_create($this->config['context']); } /** @@ -40,9 +65,9 @@ class PackExtension extends AbstractExtension { */ public function getTokenParsers(): array { return [ - new TokenParser($this->locator, $this->package, $this->parameters['token'], 'stylesheet', $this->parameters['output']['css'], $this->parameters['filters']['css']), - new TokenParser($this->locator, $this->package, $this->parameters['token'], 'javascript', $this->parameters['output']['js'], $this->parameters['filters']['js']), - new TokenParser($this->locator, $this->package, $this->parameters['token'], 'image', $this->parameters['output']['img'], $this->parameters['filters']['img']) + new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'css', 'stylesheet'), + new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'js', 'javascript'), + new TokenParser($this->container, $this->locator, $this->router, $this->slugger, $this->config, $this->ctx, 'img', 'image') ]; } @@ -60,7 +85,6 @@ class PackExtension extends AbstractExtension { new \Twig\TwigFilter('intlcurrency', [$this->intl, 'currency']), new \Twig\TwigFilter('intldate', [$this->intl, 'date'], ['needs_environment' => true]), new \Twig\TwigFilter('intlnumber', [$this->intl, 'number']), - new \Twig\TwigFilter('intlsize', [$this->intl, 'size']), new \Twig\TwigFilter('lcfirst', 'lcfirst'), new \Twig\TwigFilter('short', [$this->slugger, 'short']), new \Twig\TwigFilter('slug', [$this->slugger, 'slug']), -- 2.41.3 From 9dc9c9ac835f99cb2bc1cdf9940cb347a14771a8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 07:11:58 +0100 Subject: [PATCH 10/16] Use bundle config instead of member variable Clean constructor arguments Add alias and config member variables Improve captcha random generation Add get facebook, map, multi, zoom functions Add map functions Disable remove function until rewrite Cleanup --- Util/ImageUtil.php | 719 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 636 insertions(+), 83 deletions(-) diff --git a/Util/ImageUtil.php b/Util/ImageUtil.php index 871b99a..44afe28 100644 --- a/Util/ImageUtil.php +++ b/Util/ImageUtil.php @@ -11,8 +11,13 @@ namespace Rapsys\PackBundle\Util; +use Psr\Container\ContainerInterface; + +use Rapsys\PackBundle\RapsysPackBundle; + use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; /** @@ -20,79 +25,58 @@ use Symfony\Component\Routing\RouterInterface; */ class ImageUtil { /** - * The captcha width - */ - const width = 192; - - /** - * The captcha height - */ - const height = 52; - - /** - * The captcha background color - */ - const background = 'white'; - - /** - * The captcha fill color - */ - const fill = '#cff'; - - /** - * The captcha font size + * Alias string */ - const fontSize = 45; + protected string $alias; /** - * The captcha stroke color + * Config array */ - const stroke = '#00c3f9'; - - /** - * The captcha stroke width - */ - const strokeWidth = 2; - - /** - * The thumb width - */ - const thumbWidth = 640; - - /** - * The thumb height - */ - const thumbHeight = 640; + protected array $config; /** * Creates a new image util * + * @param ContainerInterface $container The container instance * @param RouterInterface $router The RouterInterface instance * @param SluggerUtil $slugger The SluggerUtil instance - * @param string $cache The cache directory - * @param string $path The public path - * @param string $prefix The prefix */ - function __construct(protected RouterInterface $router, protected SluggerUtil $slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'image', protected string $background = self::background, protected string $fill = self::fill, protected int $fontSize = self::fontSize, protected string $stroke = self::stroke, protected int $strokeWidth = self::strokeWidth) { + public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) { + //Retrieve config + $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias()); } /** * Get captcha data * - * @param int $updated The updated timestamp - * @param int $width The width - * @param int $height The height + * @param ?int $height The height + * @param ?int $width The width * @return array The captcha data */ - public function getCaptcha(int $updated, int $width = self::width, int $height = self::height): array { + public function getCaptcha(?int $height = null, ?int $width = null): array { + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['captcha']['height']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['captcha']['width']; + } + + //Get random + $random = rand(0, 999); + //Set a - $a = rand(0, 9); + $a = $random % 10; //Set b - $b = rand(0, 5); + $b = $random / 10 % 10; //Set c - $c = rand(0, 9); + $c = $random / 100 % 10; //Set equation $equation = $a.' * '.$b.' + '.$c; @@ -101,100 +85,669 @@ class ImageUtil { $short = $this->slugger->short($equation); //Set hash - $hash = $this->slugger->serialize([$updated, $short, $width, $height]); + $hash = $this->slugger->serialize([$short, $height, $width]); //Return array return [ 'token' => $this->slugger->hash(strval($a * $b + $c)), 'value' => strval($a * $b + $c), 'equation' => str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation), - 'src' => $this->router->generate('rapsyspack_captcha', ['hash' => $hash, 'updated' => $updated, 'equation' => $short, 'width' => $width, 'height' => $height]), + 'src' => $this->router->generate('rapsyspack_captcha', ['hash' => $hash, 'equation' => $short, 'height' => $height, 'width' => $width, '_format' => $this->config['captcha']['format']]), 'width' => $width, 'height' => $height ]; } /** - * Get thumb data + * Return the facebook image + * + * Generate simple image in jpeg format or load it from cache + * + * @TODO: move to a svg merging system ? * - * @param string $caption The caption + * @param string $path The request path info + * @param array $texts The image texts * @param int $updated The updated timestamp - * @param string $path The path - * @param int $width The width + * @param ?string $source The image source + * @param ?int $height The height + * @param ?int $width The width + * @return array The image array + */ + public function getFacebook(string $path, array $texts, int $updated, ?string $source = null, ?int $height = null, ?int $width = null): array { + //Without source + if ($source === null && $this->config['facebook']['source'] === null) { + //Return empty image data + return []; + //Without local source + } elseif ($source === null) { + //Set local source + $source = $this->config['facebook']['source']; + } + + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['facebook']['height']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['facebook']['width']; + } + + //Set path file + $facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.jpeg'; + + //Without existing path + if (!is_dir($dir = dirname($facebook))) { + //Create filesystem object + $filesystem = new Filesystem(); + + try { + //Create path + //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) + $filesystem->mkdir($dir, 0775); + } catch (IOExceptionInterface $e) { + //Throw error + throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e); + } + } + + //With path file + if (is_file($facebook) && ($mtime = stat($facebook)['mtime']) && $mtime >= $updated) { + #XXX: we used to drop texts with $data['canonical'] === true !!! + + //Set short path + $short = $this->slugger->short($path); + + //Set hash + $hash = $this->slugger->serialize([$short, $height, $width]); + + //Return image data + return [ + 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => $mtime, '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL), + 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), + 'og:image:height' => $height, + 'og:image:width' => $width + ]; + } + + //Set cache path + $cache = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.png'; + + //Without cache path + if (!is_dir($dir = dirname($cache))) { + //Create filesystem object + $filesystem = new Filesystem(); + + try { + //Create path + //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) + $filesystem->mkdir($dir, 0775); + } catch (IOExceptionInterface $e) { + //Throw error + throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); + } + } + + //Create image object + $image = new \Imagick(); + + //Without cache image + if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) { + //Check target directory + if (!is_dir($dir = dirname($cache))) { + //Create filesystem object + $filesystem = new Filesystem(); + + try { + //Create dir + //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) + $filesystem->mkdir($dir, 0775); + } catch (IOExceptionInterface $e) { + //Throw error + throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); + } + } + + //Without source + if (!is_file($source)) { + //Throw error + throw new \Exception(sprintf('Source file "%s" do not exists', $source)); + } + + //Convert to absolute path + $source = realpath($source); + + //Read image + //XXX: Imagick::readImage only supports absolute path + $image->readImage($source); + + //Crop using aspect ratio + //XXX: for better result upload image directly in aspect ratio :) + $image->cropThumbnailImage($width, $height); + + //Strip image exif data and properties + $image->stripImage(); + + //Save cache image + if (!$image->writeImage($cache)) { + //Throw error + throw new \Exception(sprintf('Unable to write image "%s"', $cache)); + } + //With cache + } else { + //Read image + $image->readImage($cache); + } + + //Create draw + $draw = new \ImagickDraw(); + + //Set stroke antialias + $draw->setStrokeAntialias(true); + + //Set text antialias + $draw->setTextAntialias(true); + + //Set align aliases + $aligns = [ + 'left' => \Imagick::ALIGN_LEFT, + 'center' => \Imagick::ALIGN_CENTER, + 'right' => \Imagick::ALIGN_RIGHT + ]; + + //Init counter + $i = 1; + + //Set text count + $count = count($texts); + + //Draw each text stroke + foreach($texts as $text => $data) { + //Set font + $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]); + + //Set font size + $draw->setFontSize($data['size']??$this->config['facebook']['size']); + + //Set stroke width + $draw->setStrokeWidth($data['thickness']??$this->config['facebook']['thickness']); + + //Set text alignment + $draw->setTextAlignment($align = ($aligns[$data['align']??$this->config['facebook']['align']])); + + //Get font metrics + $metrics = $image->queryFontMetrics($draw, $text); + + //Without y + if (empty($data['y'])) { + //Position verticaly each text evenly + $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50; + } + + //Without x + if (empty($data['x'])) { + if ($align == \Imagick::ALIGN_CENTER) { + $texts[$text]['x'] = $data['x'] = $width/2; + } elseif ($align == \Imagick::ALIGN_LEFT) { + $texts[$text]['x'] = $data['x'] = 50; + } elseif ($align == \Imagick::ALIGN_RIGHT) { + $texts[$text]['x'] = $data['x'] = $width - 50; + } + } + + //Center verticaly + //XXX: add ascender part then center it back by half of textHeight + //TODO: maybe add a boundingbox ??? + $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2; + + //Set stroke color + $draw->setStrokeColor(new \ImagickPixel($data['border']??$this->config['facebook']['border'])); + + //Set fill color + $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill'])); + + //Add annotation + $draw->annotation($data['x'], $data['y'], $text); + + //Increase counter + $i++; + } + + //Create stroke object + $stroke = new \Imagick(); + + //Add new image + $stroke->newImage($width, $height, new \ImagickPixel('transparent')); + + //Draw on image + $stroke->drawImage($draw); + + //Blur image + //XXX: blur the stroke canvas only + $stroke->blurImage(5,3); + + //Set opacity to 0.5 + //XXX: see https://www.php.net/manual/en/image.evaluateimage.php + $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA); + + //Compose image + $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0); + + //Clear stroke + $stroke->clear(); + + //Destroy stroke + unset($stroke); + + //Clear draw + $draw->clear(); + + //Set text antialias + $draw->setTextAntialias(true); + + //Draw each text + foreach($texts as $text => $data) { + //Set font + $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]); + + //Set font size + $draw->setFontSize($data['size']??$this->config['facebook']['size']); + + //Set text alignment + $draw->setTextAlignment($aligns[$data['align']??$this->config['facebook']['align']]); + + //Set fill color + $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill'])); + + //Add annotation + $draw->annotation($data['x'], $data['y'], $text); + + //With canonical text + if (!empty($data['canonical'])) { + //Prevent canonical to finish in alt + unset($texts[$text]); + } + } + + //Draw on image + $image->drawImage($draw); + + //Strip image exif data and properties + $image->stripImage(); + + //Set image format + $image->setImageFormat('jpeg'); + + //Set progressive jpeg + $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE); + + //Save image + if (!$image->writeImage($facebook)) { + //Throw error + throw new \Exception(sprintf('Unable to write image "%s"', $facebook)); + } + + //Set short path + $short = $this->slugger->short($path); + + //Set hash + $hash = $this->slugger->serialize([$short, $height, $width]); + + //Return image data + return [ + 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => stat($facebook)['mtime'], '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL), + 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), + 'og:image:height' => $height, + 'og:image:width' => $width + ]; + } + + /** + * Get map data + * + * @param float $latitude The latitude + * @param float $longitude The longitude + * @param ?int $height The height + * @param ?int $width The width + * @param ?int $zoom The zoom + * @return array The map data + */ + public function getMap(float $latitude, float $longitude, ?int $height = null, ?int $width = null, ?int $zoom = null): array { + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['map']['height']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['map']['width']; + } + + //Without zoom + if ($zoom === null) { + //Set zoom from config + $zoom = $this->config['map']['zoom']; + } + + //Set hash + $hash = $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude]); + + //Return array + return [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'height' => $height, + 'src' => $this->router->generate('rapsyspack_map', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'latitude' => $latitude, 'longitude' => $longitude, '_format' => $this->config['map']['format']]), + 'width' => $width, + 'zoom' => $zoom + ]; + } + + /** + * Get multi map data + * + * @param array $coordinates The coordinates array + * @param ?int $height The height + * @param ?int $width The width + * @param ?int $zoom The zoom + * @return array The multi map data + */ + public function getMulti(array $coordinates, ?int $height = null, ?int $width = null, ?int $zoom = null): array { + //Without coordinates + if ($coordinates === []) { + //Throw error + throw new \Exception('Missing coordinates'); + } + + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['multi']['height']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['multi']['width']; + } + + //Without zoom + if ($zoom === null) { + //Set zoom from config + $zoom = $this->config['multi']['zoom']; + } + + //Initialize latitudes and longitudes arrays + $latitudes = $longitudes = []; + + //Set coordinate + $coordinate = implode( + '-', + array_map( + function ($v) use (&$latitudes, &$longitudes) { + //Get latitude and longitude + list($latitude, $longitude) = $v; + + //Append latitude + $latitudes[] = $latitude; + + //Append longitude + $longitudes[] = $longitude; + + //Append coordinate + return $latitude.','.$longitude; + }, + $coordinates + ) + ); + + //Set latitude + $latitude = round((min($latitudes)+max($latitudes))/2, 6); + + //Set longitude + $longitude = round((min($longitudes)+max($longitudes))/2, 6); + + //Set zoom + $zoom = $this->getZoom($latitude, $longitude, $coordinates, $height, $width, $zoom); + + //Set hash + $hash = $this->slugger->hash([$height, $width, $zoom, $coordinate]); + + //Return array + return [ + 'coordinate' => $coordinate, + 'height' => $height, + 'src' => $this->router->generate('rapsyspack_multi', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'coordinate' => $coordinate, '_format' => $this->config['multi']['format']]), + 'width' => $width, + 'zoom' => $zoom + ]; + } + + /** + * Get multi zoom + * + * Compute a zoom to have all coordinates on multi map + * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2) + * + * @TODO Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / $this->config['multi']['tz']) + * + * @param float $latitude The latitude + * @param float $longitude The longitude + * @param array $coordinates The coordinates array * @param int $height The height + * @param int $width The width + * @param int $zoom The zoom + * @return int The zoom + */ + public function getZoom(float $latitude, float $longitude, array $coordinates, int $height, int $width, int $zoom): int { + //Iterate on each zoom + for ($i = $zoom; $i >= 1; $i--) { + //Get tile xy + $centerX = $this->longitudeToX($longitude, $i); + $centerY = $this->latitudeToY($latitude, $i); + + //Calculate start xy + $startX = floor($centerX - $width / 2 / $this->config['multi']['tz']); + $startY = floor($centerY - $height / 2 / $this->config['multi']['tz']); + + //Calculate end xy + $endX = ceil($centerX + $width / 2 / $this->config['multi']['tz']); + $endY = ceil($centerY + $height / 2 / $this->config['multi']['tz']); + + //Iterate on each coordinates + foreach($coordinates as $k => $coordinate) { + //Get coordinates + list($clatitude, $clongitude) = $coordinate; + + //Set dest x + $destX = $this->longitudeToX($clongitude, $i); + + //With outside point + if ($startX >= $destX || $endX <= $destX) { + //Skip zoom + continue(2); + } + + //Set dest y + $destY = $this->latitudeToY($clatitude, $i); + + //With outside point + if ($startY >= $destY || $endY <= $destY) { + //Skip zoom + continue(2); + } + } + + //Found zoom + break; + } + + //Return zoom + return $i; + } + + /** + * Get thumb data + * + * @param string $path The path + * @param ?int $height The height + * @param ?int $width The width * @return array The thumb data */ - public function getThumb(string $caption, int $updated, string $path, int $width = self::thumbWidth, int $height = self::thumbHeight): array { - //Get image width and height - list($imageWidth, $imageHeight) = getimagesize($path); + public function getThumb(string $path, ?int $height = null, ?int $width = null): array { + //Without height + if ($height === null) { + //Set height from config + $height = $this->config['thumb']['height']; + } + + //Without width + if ($width === null) { + //Set width from config + $width = $this->config['thumb']['width']; + } //Short path $short = $this->slugger->short($path); - //Set link hash - $link = $this->slugger->serialize([$updated, $short, $imageWidth, $imageHeight]); + //Set hash + $hash = $this->slugger->serialize([$short, $height, $width]); - //Set src hash - $src = $this->slugger->serialize([$updated, $short, $width, $height]); + #TODO: compute thumb from file type ? + #TODO: see if setting format there is smart ? we do not yet know if we want a image or movie thumb ? + #TODO: do we add to route '_format' => $this->config['thumb']['format'] //Return array return [ - 'caption' => $caption, - 'link' => $this->router->generate('rapsyspack_thumb', ['hash' => $link, 'updated' => $updated, 'path' => $short, 'width' => $imageWidth, 'height' => $imageHeight]), - 'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $src, 'updated' => $updated, 'path' => $short, 'width' => $width, 'height' => $height]), + 'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width]), 'width' => $width, 'height' => $height ]; } /** - * Get captcha background color + * Convert longitude to tile x number + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 + * + * @param float $longitude The longitude + * @param int $zoom The zoom + * + * @return float The tile x */ - public function getBackground() { - return $this->background; + public function longitudeToX(float $longitude, int $zoom): float { + return (($longitude + 180) / 360) * pow(2, $zoom); } /** - * Get captcha fill color + * Convert latitude to tile y number + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 + * + * @param $latitude The latitude + * @param $zoom The zoom + * + * @return float The tile y */ - public function getFill() { - return $this->fill; + public function latitudeToY(float $latitude, int $zoom): float { + return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom); } /** - * Get captcha font size + * Convert tile x to longitude + * + * @param float $x The tile x + * @param int $zoom The zoom + * + * @return float The longitude */ - public function getFontSize() { - return $this->fontSize; + public function xToLongitude(float $x, int $zoom): float { + return $x / pow(2, $zoom) * 360.0 - 180.0; } /** - * Get captcha stroke color + * Convert tile y to latitude + * + * @param float $y The tile y + * @param int $zoom The zoom + * + * @return float The latitude */ - public function getStroke() { - return $this->stroke; + public function yToLatitude(float $y, int $zoom): float { + return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom))))); } /** - * Get captcha stroke width + * Convert decimal latitude to sexagesimal + * + * @param float $latitude The decimal latitude + * + * @return string The sexagesimal longitude */ - public function getStrokeWidth() { - return $this->strokeWidth; + public function latitudeToSexagesimal(float $latitude): string { + //Set degree + //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision + $degree = round($latitude) % 60; + + //Set minute + $minute = round(($latitude - $degree) * 60) % 60; + + //Set second + $second = round(($latitude - $degree - $minute / 60) * 3600) % 3600; + + //Return sexagesimal longitude + return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S'); + } + + /** + * Convert decimal longitude to sexagesimal + * + * @param float $longitude The decimal longitude + * + * @return string The sexagesimal longitude + */ + public function longitudeToSexagesimal(float $longitude): string { + //Set degree + //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision + $degree = round($longitude) % 60; + + //Set minute + $minute = round(($longitude - $degree) * 60) % 60; + + //Set second + $second = round(($longitude - $degree - $minute / 60) * 3600) % 3600; + + //Return sexagesimal longitude + return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W'); } /** * Remove image * * @param int $updated The updated timestamp + * @param string $prefix The prefix * @param string $path The path * @return array The thumb clear success */ - public function remove(int $updated, string $path): bool { + public function remove(int $updated, string $prefix, string $path): bool { + die('TODO: see how to make it work'); + + //Without valid prefix + if (!isset($this->config['prefixes'][$prefix])) { + //Throw error + throw new \Exception(sprintf('Invalid prefix "%s"', $prefix)); + } + //Set hash tree $hash = array_reverse(str_split(strval($updated))); //Set dir - $dir = $this->path.'/'.$this->prefix.'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$updated.'/'.$this->slugger->short($path); + $dir = $this->config['public'].'/'.$this->config['prefixes'][$prefix].'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$this->slugger->short($path); //Set removes $removes = []; -- 2.41.3 From 79be0fb28e90272e87fe910d09a808211ab7804a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 07:15:44 +0100 Subject: [PATCH 11/16] Add captcha, facebook, map and multi format Fix context tree Cleanup --- DependencyInjection/Configuration.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index af0c83e..2f29209 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -44,6 +44,7 @@ class Configuration implements ConfigurationInterface { 'captcha' => [ 'background' => 'white', 'fill' => '#cff', + 'format' => 'jpeg', 'height' => 52, 'size' => 45, 'border' => '#00c3f9', @@ -61,6 +62,7 @@ class Configuration implements ConfigurationInterface { 'align' => 'center', 'fill' => 'white', 'font' => 'default', + 'format' => 'jpeg', 'height' => 630, 'size' => 60, 'source' => dirname(__DIR__).'/public/facebook/source.png', @@ -111,6 +113,7 @@ class Configuration implements ConfigurationInterface { 'map' => [ 'border' => '#00c3f9', 'fill' => '#cff', + 'format' => 'jpeg', 'height' => 640, 'quality' => 70, 'radius' => 5, @@ -123,6 +126,7 @@ class Configuration implements ConfigurationInterface { 'multi' => [ 'border' => '#00c3f9', 'fill' => '#cff', + 'format' => 'jpeg', 'height' => 640, 'highborder' => '#3333c3', 'highfill' => '#c3c3f9', @@ -193,6 +197,7 @@ class Configuration implements ConfigurationInterface { ->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() @@ -204,11 +209,12 @@ class Configuration implements ConfigurationInterface { ->addDefaultsIfNotSet() ->children() ->arrayNode('http') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('max_redirects')->defaultValue($defaults['captcha']['max_redirects'])->end() - ->scalarNode('timeout')->defaultValue($defaults['captcha']['timeout'])->end() - ->scalarNode('user_agent')->cannotBeEmpty()->defaultValue($defaults['captcha']['user_agent'])->end() + ->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() @@ -218,6 +224,7 @@ class Configuration implements ConfigurationInterface { ->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() @@ -308,6 +315,7 @@ class Configuration implements ConfigurationInterface { ->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() @@ -323,6 +331,7 @@ class Configuration implements ConfigurationInterface { ->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() -- 2.41.3 From 5cf38278a8a3b92371d4a555ef1a95d7f55a2392 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 07:49:17 +0100 Subject: [PATCH 12/16] Remove default format value --- config/routes/rapsyspack.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/routes/rapsyspack.yaml b/config/routes/rapsyspack.yaml index b238403..f23660b 100644 --- a/config/routes/rapsyspack.yaml +++ b/config/routes/rapsyspack.yaml @@ -1,6 +1,6 @@ #Routes configuration rapsyspack_captcha: - path: '/captcha/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{equation<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>?jpeg}' + 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 @@ -9,12 +9,12 @@ rapsyspack_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)>?jpeg}' + 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)>?jpeg}' + path: '/bundles/rapsyspack/pack/img/{file<[a-zA-Z0-9]+>}.{!_format<(jpeg|png|webp)>}' methods: GET rapsyspack_js: @@ -22,16 +22,17 @@ rapsyspack_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)>?jpeg}' + 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)>?jpeg}' + 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 -- 2.41.3 From 3bc9a721a4d6aaa777029a064fb48eb6f568e4e6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 07:49:54 +0100 Subject: [PATCH 13/16] Define member variables Drop ctx member variable Improve bundle and file extraction Improve bundle public directory detection Use slugger hash instead of assetic like hashing Use route instead of outputUrl Cleanup --- Parser/TokenParser.php | 203 +++++++++++++++++++++++++---------------- 1 file changed, 126 insertions(+), 77 deletions(-) diff --git a/Parser/TokenParser.php b/Parser/TokenParser.php index 67e87eb..7d73117 100644 --- a/Parser/TokenParser.php +++ b/Parser/TokenParser.php @@ -11,12 +11,19 @@ namespace Rapsys\PackBundle\Parser; +use Psr\Container\ContainerInterface; + use Rapsys\PackBundle\RapsysPackBundle; +use Rapsys\PackBundle\Util\SluggerUtil; use Symfony\Component\Asset\PackageInterface; use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Config\FileLocator; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RouterInterface; use Twig\Error\Error; use Twig\Node\Expression\AssignNameExpression; @@ -32,32 +39,49 @@ use Twig\TokenParser\AbstractTokenParser; */ class TokenParser extends AbstractTokenParser { /** - * The stream context instance + * Filters array + */ + protected array $filters; + + /** + * Output string + */ + protected string $output; + + /** + * Route string + */ + protected string $route; + + /** + * Token string */ - protected mixed $ctx; + protected string $token; /** * Constructor * + * @param ContainerInterface $container The ContainerInterface instance * @param FileLocator $locator The FileLocator instance - * @param PackageInterface $package The Assets Package instance - * @param string $token The token name + * @param RouterInterface $router The RouterInterface instance + * @param SluggerUtil $slugger The SluggerUtil instance + * @param array $config The config + * @param mixed $ctx The context stream instance + * @param string $prefix The output prefix * @param string $tag The tag name - * @param string $output The default output string - * @param array $filters The default filter array */ - public function __construct(protected FileLocator $locator, protected PackageInterface $package, protected string $token, protected string $tag, protected string $output, protected array $filters) { - //Set ctx - $this->ctx = stream_context_create( - [ - 'http' => [ - #'header' => ['Referer: https://www.openstreetmap.org/'], - 'max_redirects' => $_ENV['RAPSYSPACK_REDIRECT'] ?? 20, - 'timeout' => $_ENV['RAPSYSPACK_TIMEOUT'] ?? (($timeout = ini_get('default_socket_timeout')) !== false && $timeout !== "" ? (float)$timeout : 60), - 'user_agent' => $_ENV['RAPSYSPACK_AGENT'] ?? (($agent = ini_get('user_agent')) !== false && $agent !== "" ? (string)$agent : RapsysPackBundle::getAlias().'/'.RapsysPackBundle::getVersion()) - ] - ] - ); + public function __construct(protected ContainerInterface $container, protected FileLocator $locator, protected RouterInterface $router, protected SluggerUtil $slugger, protected array $config, protected mixed $ctx, protected string $prefix, protected string $tag) { + //Set filters + $this->filters = $config['filters'][$prefix]; + + //Set output + $this->output = $config['public'].'/'.$config['prefixes']['pack'].'/'.$config['prefixes'][$prefix].'/*.'.$prefix; + + //Set route + $this->route = $config['routes'][$prefix]; + + //Set token + $this->token = $config['tokens'][$prefix]; } /** @@ -96,7 +120,7 @@ class TokenParser extends AbstractTokenParser { while (!$stream->test(Token::BLOCK_END_TYPE)) { //The files to process if ($stream->test(Token::STRING_TYPE)) { - //'somewhere/somefile.(css,img,js)' 'somewhere/*' '@jquery' + //'somewhere/somefile.(css|img|js)' 'somewhere/*' '@jquery' $inputs[] = $stream->next()->getValue(); //The filters token } elseif ($stream->test(Token::NAME_TYPE, 'filters')) { @@ -104,12 +128,19 @@ class TokenParser extends AbstractTokenParser { $stream->next(); $stream->expect(Token::OPERATOR_TYPE, '='); $this->filters = array_merge($this->filters, array_filter(array_map('trim', explode(',', $stream->expect(Token::STRING_TYPE)->getValue())))); + //The route token + } elseif ($stream->test(Token::NAME_TYPE, 'route')) { + //output='rapsyspack_css' OR output='rapsyspack_js' OR output='rapsyspack_img' + $stream->next(); + $stream->expect(Token::OPERATOR_TYPE, '='); + $this->route = $stream->expect(Token::STRING_TYPE)->getValue(); //The output token } elseif ($stream->test(Token::NAME_TYPE, 'output')) { //output='js/packed/*.js' OR output='js/core.js' $stream->next(); $stream->expect(Token::OPERATOR_TYPE, '='); $this->output = $stream->expect(Token::STRING_TYPE)->getValue(); + //TODO: add format ? jpeg|png|gif|webp|webm ??? //The token name } elseif ($stream->test(Token::NAME_TYPE, 'token')) { //name='core_js' @@ -119,6 +150,7 @@ class TokenParser extends AbstractTokenParser { //Unexpected token } else { $token = $stream->getCurrent(); + //Throw error throw new Error(sprintf('Unexpected token "%s" of value "%s"', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getSourceContext()); } } @@ -132,12 +164,25 @@ class TokenParser extends AbstractTokenParser { //Process end block $stream->expect(Token::BLOCK_END_TYPE); - //Replace star with sha1 - if (($pos = strpos($this->output, '*')) !== false) { - //XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7) - $this->output = substr($this->output, 0, $pos).sha1(serialize($inputs).serialize($this->filters)).substr($this->output, $pos + 1); + //Without valid output + if (($pos = strpos($this->output, '*')) === false || $pos !== strrpos($this->output, '*')) { + //Throw error + throw new Error(sprintf('Invalid output "%s"', $this->output), $token->getLine(), $stream->getSourceContext()); + } + + //Without existing route + if ($this->router->getRouteCollection()->get($this->route) === null) { + //Throw error + throw new Error(sprintf('Invalid route "%s"', $this->route), $token->getLine(), $stream->getSourceContext()); } + //Set file + //XXX: assetic use substr(sha1(serialize($inputs).serialize($this->filters).serialize($this->output)), 0, 7) + $file = $this->slugger->hash([$inputs, $this->filters, $this->output, $this->route, $this->token]); + + //Replace star by file + $this->output = substr($this->output, 0, $pos).$file.substr($this->output, $pos + 1); + //Process inputs for($k = 0; $k < count($inputs); $k++) { //Deal with generic url @@ -161,6 +206,7 @@ class TokenParser extends AbstractTokenParser { foreach($replacement as $input) { //Check that it's a file if (!is_file($input)) { + //Throw error throw new Error(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getSourceContext()); } } @@ -172,17 +218,21 @@ class TokenParser extends AbstractTokenParser { $k += count($replacement) - 1; //Check that it's a file } elseif (!is_file($inputs[$k])) { + //Throw error throw new Error(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getSourceContext()); } } } + #TODO: move the inputs reading from here to inside the filters ? + //Check inputs if (!empty($inputs)) { //Retrieve files content foreach($inputs as $input) { //Try to retrieve content if (($data = file_get_contents($input, false, $this->ctx)) === false) { + //Throw error throw new Error(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getSourceContext()); } @@ -229,12 +279,6 @@ class TokenParser extends AbstractTokenParser { #throw new Error('Empty filters token', $token->getLine(), $stream->getSourceContext()); } - //Retrieve asset uri - //XXX: this path is the merge of services.assets.path_package.arguments[0] and rapsyspack.output.(css,img,js) - if (($outputUrl = $this->package->getUrl($this->output)) === false) { - throw new Error(sprintf('Unable to get url for asset: %s', $this->output), $token->getLine(), $stream->getSourceContext()); - } - //Check if we have a bundle path if ($this->output[0] == '@') { //Resolve it @@ -264,14 +308,31 @@ class TokenParser extends AbstractTokenParser { $filesystem->dumpFile($this->output, $content); } catch (IOExceptionInterface $e) { //Throw error - throw new Error(sprintf('Unable to write to: %s', $this->output), $token->getLine(), $stream->getSourceContext(), $e); + throw new Error(sprintf('Unable to write "%s"', $this->output), $token->getLine(), $stream->getSourceContext(), $e); + } + + //Without output file mtime + if (($mtime = filemtime($this->output)) === false) { + //Throw error + throw new Error(sprintf('Unable to get "%s" mtime', $this->output), $token->getLine(), $stream->getSourceContext(), $e); + } + + //TODO: get mimetype for images ? and set _format ? + + try { + //Generate asset url + $asset = $this->router->generate($this->route, [ 'file' => $file, 'u' => $mtime ]); + //Catch router exceptions + } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) { + //Throw error + throw new Error(sprintf('Unable to generate asset route "%s"', $this->route), $token->getLine(), $stream->getSourceContext(), $e); } //Set name in context key $ref = new AssignNameExpression($this->token, $token->getLine()); //Set output in context value - $value = new TextNode($outputUrl, $token->getLine()); + $value = new TextNode($asset, $token->getLine()); //Send body with context set return new Node([ @@ -312,62 +373,50 @@ class TokenParser extends AbstractTokenParser { return $this->config['jquery']; }*/ - //Check that we have a / separator between bundle name and path - if (($pos = strpos($file, '/')) === false) { - throw new Error(sprintf('Invalid path "%s"', $file), $lineno, $source); + //Extract bundle + if (($bundle = strstr($file, '/', true)) === false) { + throw new Error(sprintf('Invalid bundle "%s"', $file), $lineno, $source); } - //Set bundle - $bundle = substr($file, 0, $pos); - - //Set path - $path = substr($file, $pos + 1); - - //Check for bundle suffix presence - //XXX: use "bundle templates automatic namespace" mimicked behaviour to find intended bundle and/or path - //XXX: see https://symfony.com/doc/4.3/templates.html#bundle-templates - if (strlen($bundle) < strlen('Bundle') || substr($bundle, -strlen('Bundle')) !== 'Bundle') { - //Append Bundle in an attempt to fix it's naming for locator - $bundle .= 'Bundle'; - - //Check for public resource prefix presence - if (strlen($path) < strlen('Resources/public') || substr($path, 0, strlen('Resources/public')) != 'Resources/public') { - //Prepend standard public path - $path = 'Resources/public/'.$path; - } + //Extract path + if (($path = strstr($file, '/')) === false) { + throw new Error(sprintf('Invalid path "%s"', $file), $lineno, $source); } - //Resolve bundle prefix - try { - $prefix = $this->locator->locate($bundle); - //Catch bundle does not exist or is not enabled exception - } catch(\InvalidArgumentException $e) { - //Fix lowercase first bundle character - if ($bundle[1] > 'Z' || $bundle[1] < 'A') { - $bundle[1] = strtoupper($bundle[1]); - } - - //Detect double bundle suffix - if (strlen($bundle) > strlen('_bundleBundle') && substr($bundle, -strlen('_bundleBundle')) == '_bundleBundle') { - //Strip extra bundle - $bundle = substr($bundle, 0, -strlen('Bundle')); - } + //Extract alias + $alias = strtolower(substr($bundle, 1)); - //Convert snake case in camel case - if (strpos($bundle, '_') !== false) { - //Fix every first character following a _ - while(($cur = strpos($bundle, '_')) !== false) { - $bundle = substr($bundle, 0, $cur).ucfirst(substr($bundle, $cur + 1)); - } + //With public parameter + if ($this->container->hasParameter($alias.'.public')) { + //Set prefix + $prefix = $this->container->getParameter($alias.'.public'); + //Without public parameter + } else { + //Without bundle suffix presence + //XXX: use "bundle templates automatic namespace" mimicked behaviour to find intended bundle and/or path + //XXX: see https://symfony.com/doc/4.3/templates.html#bundle-templates + if (strlen($bundle) < strlen('@Bundle') || substr($bundle, -strlen('Bundle')) !== 'Bundle') { + //Append Bundle + $bundle .= 'Bundle'; } - //Resolve fixed bundle prefix + //Try to resolve bundle prefix try { $prefix = $this->locator->locate($bundle); - //Catch bundle does not exist or is not enabled exception again + //Catch bundle does not exist or is not enabled exception } catch(\InvalidArgumentException $e) { - //Bail out as bundle or path is invalid and we have no way to know what was meant - throw new Error(sprintf('Invalid bundle name "%s" in path "%s". Maybe you meant "%s"', substr($file, 1, $pos - 1), $file, $bundle.'/'.$path), $lineno, $source, $e); + throw new Error(sprintf('Unlocatable bundle "%s"', $bundle), $lineno, $source, $e); + } + + //With Resources/public subdirectory + if (is_dir($prefix.'Resources/public')) { + $prefix .= 'Resources/public'; + //With public subdirectory + } elseif (is_dir($prefix.'public')) { + $prefix .= 'public'; + //Without any public subdirectory + } else { + throw new Error(sprintf('Bundle "%s" lacks a public subdirectory', $bundle), $lineno, $source, $e); } } -- 2.41.3 From c839063e9ccd231abe2b7e3e314285959f7dee2a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 07:59:37 +0100 Subject: [PATCH 14/16] Version 0.5.4 --- RapsysPackBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RapsysPackBundle.php b/RapsysPackBundle.php index c9a6d48..08360b0 100644 --- a/RapsysPackBundle.php +++ b/RapsysPackBundle.php @@ -64,6 +64,6 @@ class RapsysPackBundle extends Bundle { */ public static function getVersion(): string { //Return version - return '0.5.3'; + return '0.5.4'; } } -- 2.41.3 From 8b3e5065464a5734fb7259b71c2d64b7cc463699 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 08:04:19 +0100 Subject: [PATCH 15/16] Enable error bubbling --- Form/CaptchaType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Form/CaptchaType.php b/Form/CaptchaType.php index 015a91b..f8cf3d0 100644 --- a/Form/CaptchaType.php +++ b/Form/CaptchaType.php @@ -71,7 +71,7 @@ class CaptchaType extends AbstractType { parent::configureOptions($resolver); //Set defaults - $resolver->setDefaults(['captcha' => false, 'translation_domain' => RapsysPackBundle::getAlias()]); + $resolver->setDefaults(['captcha' => false, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]); //Add extra captcha option $resolver->setAllowedTypes('captcha', 'boolean'); -- 2.41.3 From 7958c95fb124a29b4970ef8ec72f78ee4f5a71ea Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Sun, 8 Dec 2024 08:04:40 +0100 Subject: [PATCH 16/16] Import contact form --- Form/ContactType.php | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Form/ContactType.php diff --git a/Form/ContactType.php b/Form/ContactType.php new file mode 100644 index 0000000..3d70bfb --- /dev/null +++ b/Form/ContactType.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\PackBundle\Form; + +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\NotBlank; + +/** + * {@inheritdoc} + */ +class ContactType extends CaptchaType { + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options): void { + //Add fields + $builder + ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'constraints' => [new NotBlank(['message' => 'Please provide your name'])]]) + ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'constraints' => [new NotBlank(['message' => 'Please provide your subject'])]]) + ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'constraints' => [new NotBlank(['message' => 'Please provide a valid mail']), new Email(['message' => 'Your mail doesn\'t seems to be valid'])]]) + ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'constraints' => [new NotBlank(['message' => 'Please provide your message'])]]) + ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]); + + //Call parent + parent::buildForm($builder, $options); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver): void { + //Call parent configure options + parent::configureOptions($resolver); + + //Set defaults + $resolver->setDefaults(['captcha' => true]); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return 'contact_form'; + } +} -- 2.41.3