1 <?php 
declare(strict_types
=1); 
   4  * This file is part of the Rapsys PackBundle package. 
   6  * (c) Raphaël Gertz <symfony@rapsys.eu> 
   8  * For the full copyright and license information, please view the LICENSE 
   9  * file that was distributed with this source code. 
  12 namespace Rapsys\PackBundle\Util
; 
  14 use Symfony\Component\Asset\PackageInterface
; 
  15 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
; 
  16 use Symfony\Component\Filesystem\Filesystem
; 
  19  * Helps manage facebook images 
  51          * The package instance 
  53          * @var PackageInterface 
  72          * Creates a new osm util 
  74          * @param PackageInterface $package The package instance 
  75          * @param string $cache The cache directory 
  76          * @param string $path The public path 
  77          * @param string $url The public url 
  78          * @param string $prefix The prefix 
  80         function __construct(PackageInterface 
$package, string $cache, string $path, string $url, string $prefix = 'facebook', string $source = 'png/facebook.png', array $fonts = [ 'default' => 'ttf/default.ttf' ]) { 
  82                 $this->cache 
= $cache.'/'.$prefix; 
  85                 $this->fonts 
= $fonts; 
  88                 $this->path 
= $path.'/'.$prefix; 
  91                 $this->url 
= $url.'/'.$prefix; 
  93                 //Set package instance 
  94                 $this->package 
= $package; 
  97                 $this->prefix 
= $prefix; 
 100                 $this->source 
= $source; 
 104          * Return the facebook image 
 106          * Generate simple image in jpeg format or load it from cache 
 108          * @param string $pathInfo The request path info 
 109          * @param array $texts The image texts 
 110          * @param int $updated The updated timestamp 
 111          * @param ?string $source The image source 
 112          * @param int $width The width 
 113          * @param int $height The height 
 114          * @return array The image array 
 116         public function getImage(string $pathInfo, array $texts, int $updated, ?string $source = null, int $width = 1200, int $height = 630): array { 
 118                 $path = $this->path
.$pathInfo.'.jpeg'; 
 120                 //Without existing path 
 121                 if (!is_dir($dir = dirname($path))) { 
 122                         //Create filesystem object 
 123                         $filesystem = new Filesystem(); 
 127                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 128                                 $filesystem->mkdir($dir, 0775); 
 129                         } catch (IOExceptionInterface 
$e) { 
 131                                 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e); 
 136                 if (is_file($path) && ($mtime = stat($path)['mtime']) && $mtime >= $updated) { 
 137                         #XXX: we used to drop texts with $data['canonical'] === true !!! 
 141                                 #'og:image' => $this->package->getAbsoluteUrl('@RapsysAir/facebook/'.$mtime.$pathInfo.'.jpeg'), 
 142                                 'og:image' => $this->package
->getAbsoluteUrl($this->url
.'/'.$mtime.$pathInfo.'.jpeg'), 
 143                                 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
 144                                 'og:image:height' => $height, 
 145                                 'og:image:width' => $width 
 150                 $cache = $this->cache
.$pathInfo.'.png'; 
 153                 if (!is_dir($dir = dirname($cache))) { 
 154                         //Create filesystem object 
 155                         $filesystem = new Filesystem(); 
 159                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 160                                 $filesystem->mkdir($dir, 0775); 
 161                         } catch (IOExceptionInterface 
$e) { 
 163                                 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 168                 if ($source === null) { 
 170                         $source = realpath($this->source
); 
 173                 //Create image object 
 174                 $image = new \
Imagick(); 
 176                 //Without cache image 
 177                 if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) { 
 178                         //Check target directory 
 179                         if (!is_dir($dir = dirname($cache))) { 
 180                                 //Create filesystem object 
 181                                 $filesystem = new Filesystem(); 
 185                                         //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 186                                         $filesystem->mkdir($dir, 0775); 
 187                                 } catch (IOExceptionInterface 
$e) { 
 189                                         throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 194                         $image->readImage($source); 
 196                         //Crop using aspect ratio 
 197                         //XXX: for better result upload image directly in aspect ratio :) 
 198                         $image->cropThumbnailImage($width, $height); 
 200                         //Strip image exif data and properties 
 201                         $image->stripImage(); 
 204                         if (!$image->writeImage($cache)) { 
 206                                 throw new \
Exception(sprintf('Unable to write image "%s"', $cache)); 
 211                         $image->readImage($cache); 
 215                 $draw = new \
ImagickDraw(); 
 217                 //Set stroke antialias 
 218                 $draw->setStrokeAntialias(true); 
 221                 $draw->setTextAntialias(true); 
 225                         'left' => \Imagick
::ALIGN_LEFT
, 
 226                         'center' => \Imagick
::ALIGN_CENTER
, 
 227                         'right' => \Imagick
::ALIGN_RIGHT
 
 231                 $defaultFont = 'dejavusans'; 
 234                 $defaultAlign = 'center'; 
 240                 $defaultStroke = '#00c3f9'; 
 246                 $defaultFill = 'white'; 
 252                 $count = count($texts); 
 254                 //Draw each text stroke 
 255                 foreach($texts as $text => $data) { 
 257                         $draw->setFont($this->fonts
[$data['font']??$defaultFont]); 
 260                         $draw->setFontSize($data['size']??$defaultSize); 
 263                         $draw->setStrokeWidth($data['width']??$defaultWidth); 
 266                         $draw->setTextAlignment($align = ($aligns[$data['align']??$defaultAlign])); 
 269                         $metrics = $image->queryFontMetrics($draw, $text); 
 272                         if (empty($data['y'])) { 
 273                                 //Position verticaly each text evenly 
 274                                 $texts[$text]['y'] = $data['y'] = (($height + 
100) / (count($texts) + 
1) * $i) - 50; 
 278                         if (empty($data['x'])) { 
 279                                 if ($align == \Imagick
::ALIGN_CENTER
) { 
 280                                         $texts[$text]['x'] = $data['x'] = $width/2; 
 281                                 } elseif ($align == \Imagick
::ALIGN_LEFT
) { 
 282                                         $texts[$text]['x'] = $data['x'] = 50; 
 283                                 } elseif ($align == \Imagick
::ALIGN_RIGHT
) { 
 284                                         $texts[$text]['x'] = $data['x'] = $width - 50; 
 289                         //XXX: add ascender part then center it back by half of textHeight 
 290                         //TODO: maybe add a boundingbox ??? 
 291                         $texts[$text]['y'] = $data['y'] +
= $metrics['ascender'] - $metrics['textHeight']/2; 
 294                         $draw->setStrokeColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 297                         $draw->setFillColor(new \
ImagickPixel($data['stroke']??$defaultStroke)); 
 300                         $draw->annotation($data['x'], $data['y'], $text); 
 306                 //Create stroke object 
 307                 $stroke = new \
Imagick(); 
 310                 $stroke->newImage($width, $height, new \
ImagickPixel('transparent')); 
 313                 $stroke->drawImage($draw); 
 316                 //XXX: blur the stroke canvas only 
 317                 $stroke->blurImage(5,3); 
 320                 //XXX: see https://www.php.net/manual/en/image.evaluateimage.php 
 321                 $stroke->evaluateImage(\Imagick
::EVALUATE_DIVIDE
, 1.5, \Imagick
::CHANNEL_ALPHA
); 
 324                 $image->compositeImage($stroke, \Imagick
::COMPOSITE_OVER
, 0, 0); 
 336                 $draw->setTextAntialias(true); 
 339                 foreach($texts as $text => $data) { 
 341                         $draw->setFont($this->fonts
[$data['font']??$defaultFont]); 
 344                         $draw->setFontSize($data['size']??$defaultSize); 
 347                         $draw->setTextAlignment($aligns[$data['align']??$defaultAlign]); 
 350                         $draw->setFillColor(new \
ImagickPixel($data['fill']??$defaultFill)); 
 353                         $draw->annotation($data['x'], $data['y'], $text); 
 355                         //With canonical text 
 356                         if (!empty($data['canonical'])) { 
 357                                 //Prevent canonical to finish in alt 
 358                                 unset($texts[$text]); 
 363                 $image->drawImage($draw); 
 365                 //Strip image exif data and properties 
 366                 $image->stripImage(); 
 369                 $image->setImageFormat('jpeg'); 
 371                 //Set progressive jpeg 
 372                 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
); 
 375                 if (!$image->writeImage($path)) { 
 377                         throw new \
Exception(sprintf('Unable to write image "%s"', $path)); 
 382                         'og:image' => $this->package
->getAbsoluteUrl($this->url
.'/'.stat($path)['mtime'].$pathInfo.'.jpeg'), 
 383                         'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))), 
 384                         'og:image:height' => $height, 
 385                         'og:image:width' => $width