123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668 |
- <?php
- /**
- * @package Grav\Common\Page
- *
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\Page\Medium;
- use Grav\Common\Data\Blueprint;
- use Grav\Common\Grav;
- use Grav\Common\Utils;
- use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
- class ImageMedium extends Medium
- {
- /**
- * @var array
- */
- protected $thumbnailTypes = ['page', 'media', 'default'];
- /**
- * @var ImageFile
- */
- protected $image;
- /**
- * @var string
- */
- protected $format = 'guess';
- /**
- * @var int
- */
- protected $quality;
- /**
- * @var int
- */
- protected $default_quality;
- /**
- * @var bool
- */
- protected $debug_watermarked = false;
- /**
- * @var array
- */
- public static $magic_actions = [
- 'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
- 'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
- 'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
- 'rotate', 'flip', 'fixOrientation', 'gaussianBlur'
- ];
- /**
- * @var array
- */
- public static $magic_resize_actions = [
- 'resize' => [0, 1],
- 'forceResize' => [0, 1],
- 'cropResize' => [0, 1],
- 'crop' => [0, 1, 2, 3],
- 'zoomCrop' => [0, 1]
- ];
- /**
- * @var string
- */
- protected $sizes = '100vw';
- /**
- * Construct.
- *
- * @param array $items
- * @param Blueprint $blueprint
- */
- public function __construct($items = [], Blueprint $blueprint = null)
- {
- parent::__construct($items, $blueprint);
- $config = Grav::instance()['config'];
- $path = $this->get('filepath');
- if (!$path || !file_exists($path) || !filesize($path)) {
- return;
- }
- $image_info = getimagesize($path);
- $this->def('width', $image_info[0]);
- $this->def('height', $image_info[1]);
- $this->def('mime', $image_info['mime']);
- $this->def('debug', $config->get('system.images.debug'));
- $this->set('thumbnails.media', $this->get('filepath'));
- $this->default_quality = $config->get('system.images.default_image_quality', 85);
- $this->reset();
- if ($config->get('system.images.cache_all', false)) {
- $this->cache();
- }
- }
- public function __destruct()
- {
- unset($this->image);
- }
- public function __clone()
- {
- $this->image = $this->image ? clone $this->image : null;
- parent::__clone();
- }
- /**
- * Add meta file for the medium.
- *
- * @param string $filepath
- * @return $this
- */
- public function addMetaFile($filepath)
- {
- parent::addMetaFile($filepath);
- // Apply filters in meta file
- $this->reset();
- return $this;
- }
- /**
- * Clear out the alternatives
- */
- public function clearAlternatives()
- {
- $this->alternatives = [];
- }
- /**
- * Return PATH to image.
- *
- * @param bool $reset
- * @return string path to image
- */
- public function path($reset = true)
- {
- $output = $this->saveImage();
- if ($reset) {
- $this->reset();
- }
- return $output;
- }
- /**
- * Return URL to image.
- *
- * @param bool $reset
- * @return string
- */
- public function url($reset = true)
- {
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
- $saved_image_path = $this->saveImage();
- $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
- if ($locator->isStream($output)) {
- $output = $locator->findResource($output, false);
- }
- if (Utils::startsWith($output, $image_path)) {
- $image_dir = $locator->findResource('cache://images', false);
- $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
- }
- if ($reset) {
- $this->reset();
- }
- return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
- }
- /**
- * Simply processes with no extra methods. Useful for triggering events.
- *
- * @return $this
- */
- public function cache()
- {
- if (!$this->image) {
- $this->image();
- }
- return $this;
- }
- /**
- * Return srcset string for this Medium and its alternatives.
- *
- * @param bool $reset
- * @return string
- */
- public function srcset($reset = true)
- {
- if (empty($this->alternatives)) {
- if ($reset) {
- $this->reset();
- }
- return '';
- }
- $srcset = [];
- foreach ($this->alternatives as $ratio => $medium) {
- $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
- }
- $srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
- return implode(', ', $srcset);
- }
- /**
- * Allows the ability to override the image's pretty name stored in cache
- *
- * @param string $name
- */
- public function setImagePrettyName($name)
- {
- $this->set('prettyname', $name);
- if ($this->image) {
- $this->image->setPrettyName($name);
- }
- }
- public function getImagePrettyName()
- {
- if ($this->get('prettyname')) {
- return $this->get('prettyname');
- }
- $basename = $this->get('basename');
- if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
- $basename = $matches[1];
- }
- return $basename;
- }
- /**
- * Generate alternative image widths, using either an array of integers, or
- * a min width, a max width, and a step parameter to fill out the necessary
- * widths. Existing image alternatives won't be overwritten.
- *
- * @param int|int[] $min_width
- * @param int $max_width
- * @param int $step
- * @return $this
- */
- public function derivatives($min_width, $max_width = 2500, $step = 200)
- {
- if (!empty($this->alternatives)) {
- $max = max(array_keys($this->alternatives));
- $base = $this->alternatives[$max];
- } else {
- $base = $this;
- }
- $widths = [];
- if (func_num_args() === 1) {
- foreach ((array) func_get_arg(0) as $width) {
- if ($width < $base->get('width')) {
- $widths[] = $width;
- }
- }
- } else {
- $max_width = min($max_width, $base->get('width'));
- for ($width = $min_width; $width < $max_width; $width = $width + $step) {
- $widths[] = $width;
- }
- }
- foreach ($widths as $width) {
- // Only generate image alternatives that don't already exist
- if (array_key_exists((int) $width, $this->alternatives)) {
- continue;
- }
- $derivative = MediumFactory::fromFile($base->get('filepath'));
- // It's possible that MediumFactory::fromFile returns null if the
- // original image file no longer exists and this class instance was
- // retrieved from the page cache
- if (null !== $derivative) {
- $index = 2;
- $alt_widths = array_keys($this->alternatives);
- sort($alt_widths);
- foreach ($alt_widths as $i => $key) {
- if ($width > $key) {
- $index += max($i, 1);
- }
- }
- $basename = preg_replace('/(@\d+x){0,1}$/', "@{$width}w", $base->get('basename'), 1);
- $derivative->setImagePrettyName($basename);
- $ratio = $base->get('width') / $width;
- $height = $derivative->get('height') / $ratio;
- $derivative->resize($width, $height);
- $derivative->set('width', $width);
- $derivative->set('height', $height);
- $this->addAlternative($ratio, $derivative);
- }
- }
- return $this;
- }
- /**
- * Parsedown element for source display mode
- *
- * @param array $attributes
- * @param bool $reset
- * @return array
- */
- public function sourceParsedownElement(array $attributes, $reset = true)
- {
- empty($attributes['src']) && $attributes['src'] = $this->url(false);
- $srcset = $this->srcset($reset);
- if ($srcset) {
- empty($attributes['srcset']) && $attributes['srcset'] = $srcset;
- $attributes['sizes'] = $this->sizes();
- }
- return ['name' => 'img', 'attributes' => $attributes];
- }
- /**
- * Reset image.
- *
- * @return $this
- */
- public function reset()
- {
- parent::reset();
- if ($this->image) {
- $this->image();
- $this->medium_querystring = [];
- $this->filter();
- $this->clearAlternatives();
- }
- $this->format = 'guess';
- $this->quality = $this->default_quality;
- $this->debug_watermarked = false;
- return $this;
- }
- /**
- * Turn the current Medium into a Link
- *
- * @param bool $reset
- * @param array $attributes
- * @return Link
- */
- public function link($reset = true, array $attributes = [])
- {
- $attributes['href'] = $this->url(false);
- $srcset = $this->srcset(false);
- if ($srcset) {
- $attributes['data-srcset'] = $srcset;
- }
- return parent::link($reset, $attributes);
- }
- /**
- * Turn the current Medium into a Link with lightbox enabled
- *
- * @param int $width
- * @param int $height
- * @param bool $reset
- * @return Link
- */
- public function lightbox($width = null, $height = null, $reset = true)
- {
- if ($this->mode !== 'source') {
- $this->display('source');
- }
- if ($width && $height) {
- $this->__call('cropResize', [$width, $height]);
- }
- return parent::lightbox($width, $height, $reset);
- }
- /**
- * Sets or gets the quality of the image
- *
- * @param int $quality 0-100 quality
- * @return int|$this
- */
- public function quality($quality = null)
- {
- if ($quality) {
- if (!$this->image) {
- $this->image();
- }
- $this->quality = $quality;
- return $this;
- }
- return $this->quality;
- }
- /**
- * Sets image output format.
- *
- * @param string $format
- * @return $this
- */
- public function format($format)
- {
- if (!$this->image) {
- $this->image();
- }
- $this->format = $format;
- return $this;
- }
- /**
- * Set or get sizes parameter for srcset media action
- *
- * @param string $sizes
- * @return string
- */
- public function sizes($sizes = null)
- {
- if ($sizes) {
- $this->sizes = $sizes;
- return $this;
- }
- return empty($this->sizes) ? '100vw' : $this->sizes;
- }
- /**
- * Allows to set the width attribute from Markdown or Twig
- * Examples: 
- * 
- * 
- * 
- * {{ page.media['myimg.png'].width().height().html }}
- * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
- *
- * @param mixed $value A value or 'auto' or empty to use the width of the image
- * @return $this
- */
- public function width($value = 'auto')
- {
- if (!$value || $value === 'auto') {
- $this->attributes['width'] = $this->get('width');
- } else {
- $this->attributes['width'] = $value;
- }
- return $this;
- }
- /**
- * Allows to set the height attribute from Markdown or Twig
- * Examples: 
- * 
- * 
- * 
- * {{ page.media['myimg.png'].width().height().html }}
- * {{ page.media['myimg.png'].resize(100,200).width(100).height(200).html }}
- *
- * @param mixed $value A value or 'auto' or empty to use the height of the image
- * @return $this
- */
- public function height($value = 'auto')
- {
- if (!$value || $value === 'auto') {
- $this->attributes['height'] = $this->get('height');
- } else {
- $this->attributes['height'] = $value;
- }
- return $this;
- }
- /**
- * Handle this commonly used variant
- */
- public function cropZoom()
- {
- $this->__call('zoomCrop', func_get_args());
- return $this;
- }
- /**
- * Forward the call to the image processing method.
- *
- * @param string $method
- * @param mixed $args
- * @return $this|mixed
- */
- public function __call($method, $args)
- {
- if (!\in_array($method, self::$magic_actions, true)) {
- return parent::__call($method, $args);
- }
- // Always initialize image.
- if (!$this->image) {
- $this->image();
- }
- try {
- call_user_func_array([$this->image, $method], $args);
- foreach ($this->alternatives as $medium) {
- if (!$medium->image) {
- $medium->image();
- }
- $args_copy = $args;
- // regular image: resize 400x400 -> 200x200
- // --> @2x: resize 800x800->400x400
- if (isset(self::$magic_resize_actions[$method])) {
- foreach (self::$magic_resize_actions[$method] as $param) {
- if (isset($args_copy[$param])) {
- $args_copy[$param] *= $medium->get('ratio');
- }
- }
- }
- call_user_func_array([$medium, $method], $args_copy);
- }
- } catch (\BadFunctionCallException $e) {
- }
- return $this;
- }
- /**
- * Gets medium image, resets image manipulation operations.
- *
- * @return $this
- */
- protected function image()
- {
- $locator = Grav::instance()['locator'];
- $file = $this->get('filepath');
- // Use existing cache folder or if it doesn't exist, create it.
- $cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
- // Make sure we free previous image.
- unset($this->image);
- $this->image = ImageFile::open($file)
- ->setCacheDir($cacheDir)
- ->setActualCacheDir($cacheDir)
- ->setPrettyName($this->getImagePrettyName());
- return $this;
- }
- /**
- * Save the image with cache.
- *
- * @return string
- */
- protected function saveImage()
- {
- if (!$this->image) {
- return parent::path(false);
- }
- $this->filter();
- if (isset($this->result)) {
- return $this->result;
- }
- if (!$this->debug_watermarked && $this->get('debug')) {
- $ratio = $this->get('ratio');
- if (!$ratio) {
- $ratio = 1;
- }
- $locator = Grav::instance()['locator'];
- $overlay = $locator->findResource("system://assets/responsive-overlays/{$ratio}x.png") ?: $locator->findResource('system://assets/responsive-overlays/unknown.png');
- $this->image->merge(ImageFile::open($overlay));
- }
- return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]);
- }
- /**
- * Filter image by using user defined filter parameters.
- *
- * @param string $filter Filter to be used.
- */
- public function filter($filter = 'image.filters.default')
- {
- $filters = (array) $this->get($filter, []);
- foreach ($filters as $params) {
- $params = (array) $params;
- $method = array_shift($params);
- $this->__call($method, $params);
- }
- }
- /**
- * Return the image higher quality version
- *
- * @return ImageMedium the alternative version with higher quality
- */
- public function higherQualityAlternative()
- {
- if ($this->alternatives) {
- $max = reset($this->alternatives);
- foreach($this->alternatives as $alternative)
- {
- if($alternative->quality() > $max->quality())
- {
- $max = $alternative;
- }
- }
- return $max;
- }
- return $this;
- }
- }
|