From: Raphaƫl Gertz Date: Fri, 23 Jul 2021 16:58:30 +0000 (+0200) Subject: Add stack and asset controller arguments X-Git-Url: https://git.rapsys.eu/airbundle/commitdiff_plain/53fab758ec6e94b04f8043942b44415327f46c27 Add stack and asset controller arguments Add facebook specific tags feature Add facebook image feature Move page related data in page context Add getFacebookImage function Generate facebook specific tags in render post hook --- 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); }