"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/Page/Medium/ImageMedium.php" (1 Sep 2020, 17463 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "ImageMedium.php" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: v1.6.25_vs_v1.6.26.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Common\Page
    5  *
    6  * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
    7  * @license    MIT License; see LICENSE file for details.
    8  */
    9 
   10 namespace Grav\Common\Page\Medium;
   11 
   12 use Grav\Common\Data\Blueprint;
   13 use Grav\Common\Grav;
   14 use Grav\Common\Utils;
   15 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
   16 
   17 class ImageMedium extends Medium
   18 {
   19     /**
   20      * @var array
   21      */
   22     protected $thumbnailTypes = ['page', 'media', 'default'];
   23 
   24     /**
   25      * @var ImageFile
   26      */
   27     protected $image;
   28 
   29     /**
   30      * @var string
   31      */
   32     protected $format = 'guess';
   33 
   34     /**
   35      * @var int
   36      */
   37     protected $quality;
   38 
   39     /**
   40      * @var int
   41      */
   42     protected $default_quality;
   43 
   44     /**
   45      * @var bool
   46      */
   47     protected $debug_watermarked = false;
   48 
   49     /**
   50      * @var array
   51      */
   52     public static $magic_actions = [
   53         'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
   54         'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
   55         'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
   56         'rotate', 'flip', 'fixOrientation', 'gaussianBlur'
   57     ];
   58 
   59     /**
   60      * @var array
   61      */
   62     public static $magic_resize_actions = [
   63         'resize' => [0, 1],
   64         'forceResize' => [0, 1],
   65         'cropResize' => [0, 1],
   66         'crop' => [0, 1, 2, 3],
   67         'zoomCrop' => [0, 1]
   68     ];
   69 
   70     /**
   71      * @var string
   72      */
   73     protected $sizes = '100vw';
   74 
   75     /**
   76      * Construct.
   77      *
   78      * @param array $items
   79      * @param Blueprint $blueprint
   80      */
   81     public function __construct($items = [], Blueprint $blueprint = null)
   82     {
   83         parent::__construct($items, $blueprint);
   84 
   85         $config = Grav::instance()['config'];
   86 
   87         $path = $this->get('filepath');
   88         if (!$path || !file_exists($path) || !filesize($path)) {
   89             return;
   90         }
   91 
   92         $image_info = getimagesize($path);
   93 
   94         $this->def('width', $image_info[0]);
   95         $this->def('height', $image_info[1]);
   96         $this->def('mime', $image_info['mime']);
   97         $this->def('debug', $config->get('system.images.debug'));
   98 
   99         $this->set('thumbnails.media', $this->get('filepath'));
  100 
  101         $this->default_quality = $config->get('system.images.default_image_quality', 85);
  102 
  103         $this->reset();
  104 
  105         if ($config->get('system.images.cache_all', false)) {
  106             $this->cache();
  107         }
  108     }
  109 
  110     public function __destruct()
  111     {
  112         unset($this->image);
  113     }
  114 
  115     public function __clone()
  116     {
  117         $this->image = $this->image ? clone $this->image : null;
  118 
  119         parent::__clone();
  120     }
  121 
  122     /**
  123      * Add meta file for the medium.
  124      *
  125      * @param string $filepath
  126      * @return $this
  127      */
  128     public function addMetaFile($filepath)
  129     {
  130         parent::addMetaFile($filepath);
  131 
  132         // Apply filters in meta file
  133         $this->reset();
  134 
  135         return $this;
  136     }
  137 
  138     /**
  139      * Clear out the alternatives
  140      */
  141     public function clearAlternatives()
  142     {
  143         $this->alternatives = [];
  144     }
  145 
  146     /**
  147      * Return PATH to image.
  148      *
  149      * @param bool $reset
  150      * @return string path to image
  151      */
  152     public function path($reset = true)
  153     {
  154         $output = $this->saveImage();
  155 
  156         if ($reset) {
  157             $this->reset();
  158         }
  159 
  160         return $output;
  161     }
  162 
  163     /**
  164      * Return URL to image.
  165      *
  166      * @param bool $reset
  167      * @return string
  168      */
  169     public function url($reset = true)
  170     {
  171         /** @var UniformResourceLocator $locator */
  172         $locator = Grav::instance()['locator'];
  173         $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
  174         $saved_image_path = $this->saveImage();
  175 
  176         $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
  177 
  178         if ($locator->isStream($output)) {
  179             $output = $locator->findResource($output, false);
  180         }
  181 
  182         if (Utils::startsWith($output, $image_path)) {
  183             $image_dir = $locator->findResource('cache://images', false);
  184             $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
  185         }
  186 
  187         if ($reset) {
  188             $this->reset();
  189         }
  190 
  191         return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
  192     }
  193 
  194     /**
  195      * Simply processes with no extra methods.  Useful for triggering events.
  196      *
  197      * @return $this
  198      */
  199     public function cache()
  200     {
  201         if (!$this->image) {
  202             $this->image();
  203         }
  204 
  205         return $this;
  206     }
  207 
  208 
  209     /**
  210      * Return srcset string for this Medium and its alternatives.
  211      *
  212      * @param bool $reset
  213      * @return string
  214      */
  215     public function srcset($reset = true)
  216     {
  217         if (empty($this->alternatives)) {
  218             if ($reset) {
  219                 $this->reset();
  220             }
  221 
  222             return '';
  223         }
  224 
  225         $srcset = [];
  226         foreach ($this->alternatives as $ratio => $medium) {
  227             $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
  228         }
  229         $srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
  230 
  231         return implode(', ', $srcset);
  232     }
  233 
  234     /**
  235      * Allows the ability to override the image's pretty name stored in cache
  236      *
  237      * @param string $name
  238      */
  239     public function setImagePrettyName($name)
  240     {
  241         $this->set('prettyname', $name);
  242         if ($this->image) {
  243             $this->image->setPrettyName($name);
  244         }
  245     }
  246 
  247     public function getImagePrettyName()
  248     {
  249         if ($this->get('prettyname')) {
  250             return $this->get('prettyname');
  251         }
  252 
  253         $basename = $this->get('basename');
  254         if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
  255             $basename = $matches[1];
  256         }
  257         return $basename;
  258     }
  259 
  260     /**
  261      * Generate alternative image widths, using either an array of integers, or
  262      * a min width, a max width, and a step parameter to fill out the necessary
  263      * widths. Existing image alternatives won't be overwritten.
  264      *
  265      * @param  int|int[] $min_width
  266      * @param  int       $max_width
  267      * @param  int       $step
  268      * @return $this
  269      */
  270     public function derivatives($min_width, $max_width = 2500, $step = 200)
  271     {
  272         if (!empty($this->alternatives)) {
  273             $max = max(array_keys($this->alternatives));
  274             $base = $this->alternatives[$max];
  275         } else {
  276             $base = $this;
  277         }
  278 
  279         $widths = [];
  280 
  281         if (func_num_args() === 1) {
  282             foreach ((array) func_get_arg(0) as $width) {
  283                 if ($width < $base->get('width')) {
  284                     $widths[] = $width;
  285                 }
  286             }
  287         } else {
  288             $max_width = min($max_width, $base->get('width'));
  289 
  290             for ($width = $min_width; $width < $max_width; $width = $width + $step) {
  291                 $widths[] = $width;
  292             }
  293         }
  294 
  295         foreach ($widths as $width) {
  296             // Only generate image alternatives that don't already exist
  297             if (array_key_exists((int) $width, $this->alternatives)) {
  298                 continue;
  299             }
  300 
  301             $derivative = MediumFactory::fromFile($base->get('filepath'));
  302 
  303             // It's possible that MediumFactory::fromFile returns null if the
  304             // original image file no longer exists and this class instance was
  305             // retrieved from the page cache
  306             if (null !== $derivative) {
  307                 $index = 2;
  308                 $alt_widths = array_keys($this->alternatives);
  309                 sort($alt_widths);
  310 
  311                 foreach ($alt_widths as $i => $key) {
  312                     if ($width > $key) {
  313                         $index += max($i, 1);
  314                     }
  315                 }
  316 
  317                 $basename = preg_replace('/(@\d+x){0,1}$/', "@{$width}w", $base->get('basename'), 1);
  318                 $derivative->setImagePrettyName($basename);
  319 
  320                 $ratio = $base->get('width') / $width;
  321                 $height = $derivative->get('height') / $ratio;
  322 
  323                 $derivative->resize($width, $height);
  324                 $derivative->set('width', $width);
  325                 $derivative->set('height', $height);
  326 
  327                 $this->addAlternative($ratio, $derivative);
  328             }
  329         }
  330 
  331         return $this;
  332     }
  333 
  334     /**
  335      * Parsedown element for source display mode
  336      *
  337      * @param  array $attributes
  338      * @param  bool $reset
  339      * @return array
  340      */
  341     public function sourceParsedownElement(array $attributes, $reset = true)
  342     {
  343         empty($attributes['src']) && $attributes['src'] = $this->url(false);
  344 
  345         $srcset = $this->srcset($reset);
  346         if ($srcset) {
  347             empty($attributes['srcset']) && $attributes['srcset'] = $srcset;
  348             $attributes['sizes'] = $this->sizes();
  349         }
  350 
  351         return ['name' => 'img', 'attributes' => $attributes];
  352     }
  353 
  354     /**
  355      * Reset image.
  356      *
  357      * @return $this
  358      */
  359     public function reset()
  360     {
  361         parent::reset();
  362 
  363         if ($this->image) {
  364             $this->image();
  365             $this->medium_querystring = [];
  366             $this->filter();
  367             $this->clearAlternatives();
  368         }
  369 
  370         $this->format = 'guess';
  371         $this->quality = $this->default_quality;
  372 
  373         $this->debug_watermarked = false;
  374 
  375         return $this;
  376     }
  377 
  378     /**
  379      * Turn the current Medium into a Link
  380      *
  381      * @param  bool $reset
  382      * @param  array  $attributes
  383      * @return Link
  384      */
  385     public function link($reset = true, array $attributes = [])
  386     {
  387         $attributes['href'] = $this->url(false);
  388         $srcset = $this->srcset(false);
  389         if ($srcset) {
  390             $attributes['data-srcset'] = $srcset;
  391         }
  392 
  393         return parent::link($reset, $attributes);
  394     }
  395 
  396     /**
  397      * Turn the current Medium into a Link with lightbox enabled
  398      *
  399      * @param  int  $width
  400      * @param  int  $height
  401      * @param  bool $reset
  402      * @return Link
  403      */
  404     public function lightbox($width = null, $height = null, $reset = true)
  405     {
  406         if ($this->mode !== 'source') {
  407             $this->display('source');
  408         }
  409 
  410         if ($width && $height) {
  411             $this->__call('cropResize', [$width, $height]);
  412         }
  413 
  414         return parent::lightbox($width, $height, $reset);
  415     }
  416 
  417     /**
  418      * Sets or gets the quality of the image
  419      *
  420      * @param  int $quality 0-100 quality
  421      * @return int|$this
  422      */
  423     public function quality($quality = null)
  424     {
  425         if ($quality) {
  426             if (!$this->image) {
  427                 $this->image();
  428             }
  429 
  430             $this->quality = $quality;
  431 
  432             return $this;
  433         }
  434 
  435         return $this->quality;
  436     }
  437 
  438     /**
  439      * Sets image output format.
  440      *
  441      * @param string $format
  442      * @return $this
  443      */
  444     public function format($format)
  445     {
  446         if (!$this->image) {
  447             $this->image();
  448         }
  449 
  450         $this->format = $format;
  451 
  452         return $this;
  453     }
  454 
  455     /**
  456      * Set or get sizes parameter for srcset media action
  457      *
  458      * @param  string $sizes
  459      * @return string
  460      */
  461     public function sizes($sizes = null)
  462     {
  463 
  464         if ($sizes) {
  465             $this->sizes = $sizes;
  466 
  467             return $this;
  468         }
  469 
  470         return empty($this->sizes) ? '100vw' : $this->sizes;
  471     }
  472 
  473     /**
  474      * Allows to set the width attribute from Markdown or Twig
  475      * Examples: ![Example](myimg.png?width=200&height=400)
  476      *           ![Example](myimg.png?resize=100,200&width=100&height=200)
  477      *           ![Example](myimg.png?width=auto&height=auto)
  478      *           ![Example](myimg.png?width&height)
  479      *           {{ page.media['myimg.png'].width().height().html }}
  480      *           {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
  481      *
  482      * @param mixed $value A value or 'auto' or empty to use the width of the image
  483      * @return $this
  484      */
  485     public function width($value = 'auto')
  486     {
  487         if (!$value || $value === 'auto') {
  488             $this->attributes['width'] = $this->get('width');
  489         } else {
  490             $this->attributes['width'] = $value;
  491         }
  492 
  493         return $this;
  494     }
  495 
  496     /**
  497      * Allows to set the height attribute from Markdown or Twig
  498      * Examples: ![Example](myimg.png?width=200&height=400)
  499      *           ![Example](myimg.png?resize=100,200&width=100&height=200)
  500      *           ![Example](myimg.png?width=auto&height=auto)
  501      *           ![Example](myimg.png?width&height)
  502      *           {{ page.media['myimg.png'].width().height().html }}
  503      *           {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
  504      *
  505      * @param mixed $value A value or 'auto' or empty to use the height of the image
  506      * @return $this
  507      */
  508     public function height($value = 'auto')
  509     {
  510         if (!$value || $value === 'auto') {
  511             $this->attributes['height'] = $this->get('height');
  512         } else {
  513             $this->attributes['height'] = $value;
  514         }
  515 
  516         return $this;
  517     }
  518 
  519     /**
  520      * Handle this commonly used variant
  521      */
  522     public function cropZoom()
  523     {
  524         $this->__call('zoomCrop', func_get_args());
  525         return $this;
  526     }
  527 
  528     /**
  529      * Forward the call to the image processing method.
  530      *
  531      * @param string $method
  532      * @param mixed $args
  533      * @return $this|mixed
  534      */
  535     public function __call($method, $args)
  536     {
  537         if (!\in_array($method, self::$magic_actions, true)) {
  538             return parent::__call($method, $args);
  539         }
  540 
  541         // Always initialize image.
  542         if (!$this->image) {
  543             $this->image();
  544         }
  545 
  546         try {
  547             call_user_func_array([$this->image, $method], $args);
  548 
  549             foreach ($this->alternatives as $medium) {
  550                 if (!$medium->image) {
  551                     $medium->image();
  552                 }
  553 
  554                 $args_copy = $args;
  555 
  556                 // regular image: resize 400x400 -> 200x200
  557                 // --> @2x: resize 800x800->400x400
  558                 if (isset(self::$magic_resize_actions[$method])) {
  559                     foreach (self::$magic_resize_actions[$method] as $param) {
  560                         if (isset($args_copy[$param])) {
  561                             $args_copy[$param] *= $medium->get('ratio');
  562                         }
  563                     }
  564                 }
  565 
  566                 call_user_func_array([$medium, $method], $args_copy);
  567             }
  568         } catch (\BadFunctionCallException $e) {
  569         }
  570 
  571         return $this;
  572     }
  573 
  574     /**
  575      * Gets medium image, resets image manipulation operations.
  576      *
  577      * @return $this
  578      */
  579     protected function image()
  580     {
  581         $locator = Grav::instance()['locator'];
  582 
  583         $file = $this->get('filepath');
  584 
  585         // Use existing cache folder or if it doesn't exist, create it.
  586         $cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
  587 
  588         // Make sure we free previous image.
  589         unset($this->image);
  590 
  591         $this->image = ImageFile::open($file)
  592             ->setCacheDir($cacheDir)
  593             ->setActualCacheDir($cacheDir)
  594             ->setPrettyName($this->getImagePrettyName());
  595 
  596         return $this;
  597     }
  598 
  599     /**
  600      * Save the image with cache.
  601      *
  602      * @return string
  603      */
  604     protected function saveImage()
  605     {
  606         if (!$this->image) {
  607             return parent::path(false);
  608         }
  609 
  610         $this->filter();
  611 
  612         if (isset($this->result)) {
  613             return $this->result;
  614         }
  615 
  616         if (!$this->debug_watermarked && $this->get('debug')) {
  617             $ratio = $this->get('ratio');
  618             if (!$ratio) {
  619                 $ratio = 1;
  620             }
  621 
  622             $locator = Grav::instance()['locator'];
  623             $overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
  624             $this->image->merge(ImageFile::open($overlay));
  625         }
  626 
  627         return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
  628     }
  629 
  630     /**
  631      * Filter image by using user defined filter parameters.
  632      *
  633      * @param string $filter Filter to be used.
  634      */
  635     public function filter($filter = 'image.filters.default')
  636     {
  637         $filters = (array) $this->get($filter, []);
  638         foreach ($filters as $params) {
  639             $params = (array) $params;
  640             $method = array_shift($params);
  641             $this->__call($method, $params);
  642         }
  643     }
  644 
  645     /**
  646      * Return the image higher quality version
  647      *
  648      * @return ImageMedium the alternative version with higher quality
  649      */
  650     public function higherQualityAlternative()
  651     {
  652         if ($this->alternatives) {
  653             $max = reset($this->alternatives);
  654             foreach($this->alternatives as $alternative)
  655             {
  656                 if($alternative->quality() > $max->quality())
  657                 {
  658                     $max = $alternative;
  659                 }
  660             }
  661 
  662             return $max;
  663         }
  664 
  665         return $this;
  666     }
  667 
  668 }