X-Git-Url: https://git.rapsys.eu/.gitweb.cgi/airbundle/blobdiff_plain/6ccedcf49c3abe5e576c841e0a0e6fca60a657cb..57198b462928673e15458d19756577f5f83e56bf:/Controller/DefaultController.php diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php index 034448a..4f1fbf4 100644 --- a/Controller/DefaultController.php +++ b/Controller/DefaultController.php @@ -2,15 +2,13 @@ namespace Rapsys\AirBundle\Controller; -use Rapsys\AirBundle\Entity\Application; -use Rapsys\AirBundle\Entity\Location; -use Rapsys\AirBundle\Entity\Session; -use Rapsys\AirBundle\Entity\Slot; -use Rapsys\AirBundle\Entity\User; -use Rapsys\AirBundle\Pdf\DisputePdf; -use Rapsys\UserBundle\Utils\Slugger; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Filesystem\Exception\IOExceptionInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; @@ -22,8 +20,14 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; + +use Rapsys\AirBundle\Entity\Application; +use Rapsys\AirBundle\Entity\Location; +use Rapsys\AirBundle\Entity\Session; +use Rapsys\AirBundle\Entity\Slot; +use Rapsys\AirBundle\Entity\User; +use Rapsys\AirBundle\Pdf\DisputePdf; +use Rapsys\UserBundle\Utils\Slugger; class DefaultController { @@ -44,19 +48,29 @@ class DefaultController { ///Translator instance protected $translator; + ///Packages instance + protected $asset; + + ///RequestStack instance + protected $stack; + /** * @var ContainerInterface */ protected $container; + ///Facebook image array + protected $facebookImage = []; + /** * Inject container and translator interface * * @param ContainerInterface $container The container instance * @param RouterInterface $router The router instance + * @param RequestStack $stack The request stack * @param TranslatorInterface $translator The translator instance */ - public function __construct(ContainerInterface $container, RouterInterface $router, RequestStack $requestStack, TranslatorInterface $translator) { + public function __construct(ContainerInterface $container, RouterInterface $router, RequestStack $stack, TranslatorInterface $translator, Packages $asset) { //Retrieve config $this->config = $container->getParameter($this->getAlias()); @@ -69,6 +83,12 @@ class DefaultController { //Set the translator $this->translator = $translator; + //Set the asset + $this->asset = $asset; + + //Set the request stack + $this->stack = $stack; + //Set the context $this->context = [ 'copy' => [ @@ -78,6 +98,11 @@ class DefaultController { 'short' => $translator->trans($this->config['copy']['short']), 'title' => $this->config['copy']['title'] ], + 'page' => [ + 'description' => null, + 'section' => null, + 'title' => null + ], 'site' => [ 'ico' => $this->config['site']['ico'], 'logo' => $this->config['site']['logo'], @@ -88,12 +113,23 @@ class DefaultController { ], 'canonical' => null, 'alternates' => [], + 'ogps' => [ + 'type' => 'article', + 'site_name' => $this->translator->trans($this->config['site']['title']) + ], + 'facebooks' => [ + #'admins' => $this->config['facebook']['admins'], + 'app_id' => $this->config['facebook']['apps'] + ], 'forms' => [] ]; + //Get current request + $currentRequest = $stack->getCurrentRequest(); + //Get current locale #$currentLocale = $router->getContext()->getParameters()['_locale']; - $currentLocale = $requestStack->getCurrentRequest()->getLocale(); + $currentLocale = $currentRequest->getLocale(); //Set translator locale //XXX: allow LocaleSubscriber on the fly locale change for first page @@ -149,11 +185,11 @@ class DefaultController { * @return Response The rendered view or redirection */ public function contact(Request $request, MailerInterface $mailer): Response { - //Set section - $section = $this->translator->trans('Contact'); + //Set page + $this->context['page']['title'] = $this->translator->trans('Contact'); //Set description - $this->context['description'] = $this->translator->trans('Contact Libre Air'); + $this->context['page']['description'] = $this->translator->trans('Contact Libre Air'); //Set keywords $this->context['keywords'] = [ @@ -164,9 +200,6 @@ class DefaultController { $this->translator->trans('calendar') ]; - //Set title - $title = $this->translator->trans($this->config['site']['title']).' - '.$section; - //Create the form according to the FormType created previously. //And give the proper parameters $form = $this->createForm('Rapsys\AirBundle\Form\ContactType', null, [ @@ -226,10 +259,9 @@ class DefaultController { } //Render template - return $this->render('@RapsysAir/form/contact.html.twig', ['title' => $title, 'section' => $section, 'form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context); + return $this->render('@RapsysAir/form/contact.html.twig', ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context); } - /** * The dispute page * @@ -367,11 +399,11 @@ class DefaultController { //Fetch doctrine $doctrine = $this->getDoctrine(); - //Set section - $section = $this->translator->trans('Argentine Tango in Paris'); + //Set page + $this->context['page']['title'] = $this->translator->trans('Argentine Tango in Paris'); //Set description - $this->context['description'] = $this->translator->trans('Outdoor Argentine Tango session calendar in Paris'); + $this->context['page']['description'] = $this->translator->trans('Outdoor Argentine Tango session calendar in Paris'); //Set keywords $this->context['keywords'] = [ @@ -382,8 +414,29 @@ class DefaultController { $this->translator->trans('Libre Air') ]; - //Set title - $title = $this->translator->trans($this->config['site']['title']).' - '.$section; + //Set type + //XXX: only valid for home page + $this->context['ogps']['type'] = 'website'; + + //Set facebook image + $this->facebookImage = [ + //XXX: format facebook//..jpeg + 'destination' => 'facebook/default/index.'.$request->getLocale().'.jpeg', + 'texts' => [ + $this->context['site']['title'] => [ + 'font' => 'irishgrover', + 'size' => 110 + ], + $this->context['page']['title'] => [ + 'align' => 'left' + ]/*, + $this->context['canonical'] => [ + 'align' => 'right', + 'font' => 'labelleaurore', + 'size' => 75 + ]*/ + ] + ]; //Compute period $period = new \DatePeriod( @@ -405,7 +458,7 @@ class DefaultController { $locations = $doctrine->getRepository(Location::class)->findTranslatedSortedByPeriod($this->translator, $period); //Render the view - return $this->render('@RapsysAir/default/index.html.twig', ['title' => $title, 'section' => $section, 'calendar' => $calendar, 'locations' => $locations]+$this->context); + return $this->render('@RapsysAir/default/index.html.twig', ['calendar' => $calendar, 'locations' => $locations]+$this->context); //Set Cache-Control must-revalidate directive //TODO: add a javascript forced refresh after 1h ? or header refresh ? @@ -509,6 +562,298 @@ class DefaultController { return 'rapsys_air'; } + /** + * Return the facebook image + * + * @desc Generate image in jpeg format or load it from cache + * + * @return array The image array + */ + protected function getFacebookImage(): array { + //Set texts + $texts = $this->facebookImage['texts'] ?? []; + + //Set default source + $source = $this->facebookImage['source'] ?? 'png/facebook.png'; + + //Set default source + $updated = $this->facebookImage['updated'] ?? strtotime('last week'); + + //Set source path + $src = $this->config['path']['public'].'/'.$source; + + //Set cache path + //XXX: remove extension and store as png anyway + $cache = $this->config['path']['cache'].'/facebook/'.substr($source, 0, strrpos($source, '.')).'.'.$this->config['facebook']['width'].'x'.$this->config['facebook']['height'].'.png'; + + //Set destination path + $dest = $this->config['path']['public'].'/'.$this->facebookImage['destination']; + + //Set asset + $asset = '@RapsysAir/'.$this->facebookImage['destination']; + + //With up to date generated image + if ( + is_file($dest) && + ($stat = stat($dest)) && + $stat['mtime'] >= $updated + ) { + //Get image size + //TODO: see if it works every time + list ($width, $height) = getimagesize($dest); + + //Return image data + return [ + //TODO: see if it works every time + 'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'], + 'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), + 'image:height' => $height, + 'image:width' => $width + ]; + //With image candidate + } elseif (is_file($src)) { + //Create image object + $image = new \Imagick(); + + //With cache image + if (is_file($cache)) { + //Read image + $image->readImage($cache); + //Without we generate it + } else { + //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); + } + } + + //Read image + $image->readImage($src); + + //Crop using aspect ratio + //XXX: for better result upload image directly in aspect ratio :) + $image->cropThumbnailImage($this->config['facebook']['width'], $this->config['facebook']['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)); + } + } + //Check target directory + if (!is_dir($dir = dirname($dest))) { + //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); + } + } + + //Get image width + $width = $image->getImageWidth(); + + //Get image height + $height = $image->getImageHeight(); + + //Create draw + $draw = new \ImagickDraw(); + + //Set stroke antialias + $draw->setStrokeAntialias(true); + + //Set text antialias + $draw->setTextAntialias(true); + + //Set stroke width + //TODO: configure that ? + $draw->setStrokeWidth(15); + + //Set font aliases + $fonts = [ + 'irishgrover' => $this->config['path']['public'].'/ttf/irishgrover.v10.ttf', + 'droidsans' => $this->config['path']['public'].'/ttf/droidsans.regular.ttf', + 'dejavusans' => $this->config['path']['public'].'/ttf/dejavusans.2.37.ttf', + 'labelleaurore' => $this->config['path']['public'].'/ttf/labelleaurore.v10.ttf' + ]; + + //Set align aliases + $aligns = [ + 'left' => \Imagick::ALIGN_LEFT, + 'center' => \Imagick::ALIGN_CENTER, + 'right' => \Imagick::ALIGN_RIGHT + ]; + + //Set default font + $defaultFont = 'dejavusans'; + + //Set default align + $defaultAlign = 'center'; + + //Set default size + $defaultSize = 60; + + //Set default stroke + $defaultStroke = '#00c3f9'; + + //Set default fill + $defaultFill = 'white'; + + //Init counter + $i = 1; + + //Set text count + $count = count($texts); + + //Draw each text stroke + foreach($texts as $text => $data) { + //Set font + $draw->setFont($fonts[$data['font']??$defaultFont]); + + //Set font size + $draw->setFontSize($data['size']??$defaultSize); + + //Set text alignment + $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign])); + + //Get font metrics + $metrics = $image->queryFontMetrics($draw, $text); + + //Without y + if (empty($data['y'])) { + //Position verticaly each text evenly + $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50; + } + + //Without x + if (empty($data['x'])) { + if ($align == \Imagick::ALIGN_CENTER) { + $texts[$text]['x'] = $data['x'] = $width/2; + } elseif ($align == \Imagick::ALIGN_LEFT) { + $texts[$text]['x'] = $data['x'] = 50; + } elseif ($align == \Imagick::ALIGN_RIGHT) { + $texts[$text]['x'] = $data['x'] = $width - 50; + } + } + + //Center verticaly + //XXX: add ascender part then center it back by half of textHeight + //TODO: maybe add a boundingbox ??? + $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2; + + //Set stroke color + $draw->setStrokeColor(new \ImagickPixel($data['stroke']??$defaultStroke)); + + //Set fill color + $draw->setFillColor(new \ImagickPixel($data['stroke']??$defaultStroke)); + + //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($fonts[$data['font']??$defaultFont]); + + //Set font size + $draw->setFontSize($data['size']??$defaultSize); + + //Set text alignment + $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]); + + //Set fill color + $draw->setFillColor(new \ImagickPixel($data['fill']??$defaultFill)); + + //Add annotation + $draw->annotation($data['x'], $data['y'], $text); + } + + //Draw on image + $image->drawImage($draw); + + //Strip image exif data and properties + $image->stripImage(); + + //Set image format + $image->setImageFormat('jpeg'); + + //Save image + if (!$image->writeImage($dest)) { + //Throw error + throw new \Exception(sprintf('Unable to write image "%s"', $dest)); + } + + //Get dest stat + //TODO: see if it works every time + $stat = stat($dest); + + //Return image data + return [ + //TODO: see if it works every time + 'image' => $this->stack->getCurrentRequest()->getUriForPath($this->asset->getUrl($asset), true),#.'?fbrefresh='.$stat['mtime'], + 'image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), + 'image:height' => $height, + 'image:width' => $width + ]; + } + + //Return empty array without image + return []; + } + /** * Renders a view * @@ -554,6 +899,47 @@ class DefaultController { $parameters['forms']['login'] = $login->createView(); } + /* + //TODO: set here or in constructor the controller and action name + //XXX: used to autogenerate the facebookimage dest + //XXX: with just page title and canonical we may generate miniatures automaticaly + if ($_SERVER['REMOTE_ADDR'] == '89.3.147.209') { + header('Content-Type: text/plain'); + #var_dump($this->getModuleName()); + #var_dump($this->getController()); + exit; + #var_dump(__CLASS__); + #var_dump($router); + exit; + var_dump($currentRequest->attributes->get('_controller')); + exit; + }*/ + + + //With canonical + if (!empty($parameters['canonical'])) { + //Set facebook url + $parameters['ogps']['url'] = $parameters['canonical']; + } + + //With page title + if (!empty($parameters['page']['title'])) { + //Set facebook title + $parameters['ogps']['title'] = $parameters['page']['title']; + } + + //With page description + if (!empty($parameters['page']['description'])) { + //Set facebook description + $parameters['ogps']['description'] = $parameters['page']['description']; + } + + //With facebook image defined + if (!empty($this->facebookImage)) { + //Get facebook image + $parameters['ogps'] += $this->getFacebookImage(); + } + //Call parent method return $this->_render($view, $parameters, $response); }