diff --git a/composer.json b/composer.json index 0aeeb3bdd..045dda3e2 100644 --- a/composer.json +++ b/composer.json @@ -103,9 +103,9 @@ "api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md", "api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md", "post-create-project-cmd": "bin/grav install", - "phpstan": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=340M", - "phpstan-framework": "vendor/bin/phpstan analyse -l 7 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=128M", - "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=300M", + "phpstan": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=340M system/src", + "phpstan-framework": "vendor/bin/phpstan analyse -l 7 -c ./tests/phpstan/phpstan.neon --memory-limit=128M system/src/Grav/Framework", + "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=300M user/plugins", "test": "vendor/bin/codecept run unit", "test-windows": "vendor\\bin\\codecept run unit" }, diff --git a/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php new file mode 100644 index 000000000..a2122c7f7 --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/AudioMediaInterface.php @@ -0,0 +1,25 @@ +get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null); } diff --git a/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php new file mode 100644 index 000000000..d008eea97 --- /dev/null +++ b/system/src/Grav/Common/Media/Interfaces/MediaPlayerInterface.php @@ -0,0 +1,56 @@ +attributes['controlsList'] = $controlsList; + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + $location = $this->url($reset); + + return [ + 'name' => 'audio', + 'rawHtml' => 'Your browser does not support the audio tag.', + 'attributes' => $attributes + ]; + } +} diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php new file mode 100644 index 000000000..3735707f2 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -0,0 +1,381 @@ + [0, 1], + 'forceResize' => [0, 1], + 'cropResize' => [0, 1], + 'crop' => [0, 1, 2, 3], + 'zoomCrop' => [0, 1] + ]; + + /** @var string */ + protected $sizes = '100vw'; + + + /** + * 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); + } + } + + /** + * @return string + */ + 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; + } + + /** + * Simply processes with no extra methods. Useful for triggering events. + * + * @return $this + */ + public function cache() + { + if (!$this->image) { + $this->image(); + } + + return $this; + } + + /** + * 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 += $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)?$/', "@{$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; + } + + /** + * Clear out the alternatives. + */ + public function clearAlternatives() + { + $this->alternatives = []; + } + + /** + * 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: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ 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: ![Example](myimg.png?width=200&height=400) + * ![Example](myimg.png?resize=100,200&width=100&height=200) + * ![Example](myimg.png?width=auto&height=auto) + * ![Example](myimg.png?width&height) + * {{ 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; + } + + /** + * Filter image by using user defined filter parameters. + * + * @param string $filter Filter to be used. + * @return $this + */ + 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 $this; + } + + /** + * Return the image higher quality version + * + * @return ImageMediaInterface the alternative version with higher quality + */ + public function higherQualityAlternative() + { + if ($this->alternatives) { + /** @var ImageMedium $max */ + $max = reset($this->alternatives); + /** @var ImageMedium $alternative */ + foreach ($this->alternatives as $alternative) { + if ($alternative->quality() > $max->quality()) { + $max = $alternative; + } + } + + return $max; + } + + 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; + } + + $this->format($this->get('extension')); + + 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')]); + } +} diff --git a/system/src/Grav/Common/Media/Traits/MediaFileTrait.php b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php new file mode 100644 index 000000000..9b2aa084a --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaFileTrait.php @@ -0,0 +1,134 @@ +path(false); + + return file_exists($path); + } + + /** + * Get file modification time for the medium. + * + * @return int|null + */ + public function modified() + { + $path = $this->path(false); + if (!file_exists($path)) { + return null; + } + + return filemtime($path) ?: null; + } + + /** + * Get size of the medium. + * + * @return int + */ + public function size() + { + $path = $this->path(false); + if (!file_exists($path)) { + return 0; + } + + return filesize($path) ?: 0; + } + + /** + * Return PATH to file. + * + * @param bool $reset + * @return string path to file + */ + public function path($reset = true) + { + if ($reset) { + $this->reset(); + } + + return $this->get('filepath'); + } + + /** + * Return the relative path to file + * + * @param bool $reset + * @return string + */ + public function relativePath($reset = true) + { + if ($reset) { + $this->reset(); + } + + $path = $this->path(false); + $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $path) ?: $path; + + /** @var UniformResourceLocator $locator */ + $locator = $this->getGrav()['locator']; + if ($locator->isStream($output)) { + $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true)); + } + + return $output; + } + + /** + * Return URL to file. + * + * @param bool $reset + * @return string + */ + public function url($reset = true) + { + $path = $this->relativePath($reset); + + return trim($this->getGrav()['base_url'] . '/' . $this->urlQuerystring($path), '\\'); + } + + /** + * Get the URL with full querystring + * + * @param string $url + * @return string + */ + abstract public function urlQuerystring($url); + + /** + * Reset medium. + * + * @return $this + */ + abstract public function reset(); + + /** + * @return Grav + */ + abstract protected function getGrav(): Grav; +} diff --git a/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php new file mode 100644 index 000000000..269002515 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php @@ -0,0 +1,591 @@ +getItems()); + } + + /** + * Set querystring to file modification timestamp (or value provided as a parameter). + * + * @param string|int|null $timestamp + * @return $this + */ + public function setTimestamp($timestamp = null) + { + if (null !== $timestamp) { + $this->timestamp = (string)($timestamp); + } elseif ($this instanceof MediaFileInterface) { + $this->timestamp = (string)$this->modified(); + } else { + $this->timestamp = ''; + } + + return $this; + } + + /** + * Returns an array containing just the metadata + * + * @return array + */ + public function metadata() + { + return $this->metadata; + } + + /** + * Add meta file for the medium. + * + * @param string $filepath + */ + abstract public function addMetaFile($filepath); + + /** + * Add alternative Medium to this Medium. + * + * @param int|float $ratio + * @param MediaObjectInterface $alternative + */ + public function addAlternative($ratio, MediaObjectInterface $alternative) + { + if (!is_numeric($ratio) || $ratio === 0) { + return; + } + + $alternative->set('ratio', $ratio); + $width = $alternative->get('width'); + + $this->alternatives[$width] = $alternative; + } + + /** + * Return string representation of the object (html). + * + * @return string + */ + abstract public function __toString(); + + /** + * Get/set querystring for the file's url + * + * @param string|null $querystring + * @param bool $withQuestionmark + * @return string + */ + public function querystring($querystring = null, $withQuestionmark = true) + { + if (null !== $querystring) { + $this->medium_querystring[] = ltrim($querystring, '?&'); + foreach ($this->alternatives as $alt) { + $alt->querystring($querystring, $withQuestionmark); + } + } + + if (empty($this->medium_querystring)) { + return ''; + } + + // join the strings + $querystring = implode('&', $this->medium_querystring); + // explode all strings + $query_parts = explode('&', $querystring); + // Join them again now ensure the elements are unique + $querystring = implode('&', array_unique($query_parts)); + + return $withQuestionmark ? ('?' . $querystring) : $querystring; + } + + /** + * Get the URL with full querystring + * + * @param string $url + * @return string + */ + public function urlQuerystring($url) + { + $querystring = $this->querystring(); + if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) { + $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp); + } + + return ltrim($url . $querystring . $this->urlHash(), '/'); + } + + /** + * Get/set hash for the file's url + * + * @param string $hash + * @param bool $withHash + * @return string + */ + public function urlHash($hash = null, $withHash = true) + { + if ($hash) { + $this->set('urlHash', ltrim($hash, '#')); + } + + $hash = $this->get('urlHash', ''); + + return $withHash && !empty($hash) ? '#' . $hash : $hash; + } + + /** + * Get an element (is array) that can be rendered by the Parsedown engine + * + * @param string|null $title + * @param string|null $alt + * @param string|null $class + * @param string|null $id + * @param bool $reset + * @return array + */ + public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) + { + $attributes = $this->attributes; + $items = $this->getItems(); + + $style = ''; + foreach ($this->styleAttributes as $key => $value) { + if (is_numeric($key)) { // Special case for inline style attributes, refer to style() method + $style .= $value; + } else { + $style .= $key . ': ' . $value . ';'; + } + } + if ($style) { + $attributes['style'] = $style; + } + + if (empty($attributes['title'])) { + if (!empty($title)) { + $attributes['title'] = $title; + } elseif (!empty($items['title'])) { + $attributes['title'] = $items['title']; + } + } + + if (empty($attributes['alt'])) { + if (!empty($alt)) { + $attributes['alt'] = $alt; + } elseif (!empty($items['alt'])) { + $attributes['alt'] = $items['alt']; + } elseif (!empty($items['alt_text'])) { + $attributes['alt'] = $items['alt_text']; + } else { + $attributes['alt'] = ''; + } + } + + if (empty($attributes['class'])) { + if (!empty($class)) { + $attributes['class'] = $class; + } elseif (!empty($items['class'])) { + $attributes['class'] = $items['class']; + } + } + + if (empty($attributes['id'])) { + if (!empty($id)) { + $attributes['id'] = $id; + } elseif (!empty($items['id'])) { + $attributes['id'] = $items['id']; + } + } + + switch ($this->mode) { + case 'text': + $element = $this->textParsedownElement($attributes, false); + break; + case 'thumbnail': + $thumbnail = $this->getThumbnail(); + $element = $thumbnail ? $thumbnail->sourceParsedownElement($attributes, false) : []; + break; + case 'source': + $element = $this->sourceParsedownElement($attributes, false); + break; + default: + $element = []; + } + + if ($reset) { + $this->reset(); + } + + $this->display('source'); + + return $element; + } + + /** + * Reset medium. + * + * @return $this + */ + public function reset() + { + $this->attributes = []; + + return $this; + } + + /** + * Switch display mode. + * + * @param string $mode + * + * @return MediaObjectInterface|null + */ + public function display($mode = 'source') + { + if ($this->mode === $mode) { + return $this; + } + + $this->mode = $mode; + if ($mode === 'thumbnail') { + $thumbnail = $this->getThumbnail(); + + return $thumbnail ? $thumbnail->reset() : null; + } + + return $this->reset(); + } + + /** + * Helper method to determine if this media item has a thumbnail or not + * + * @param string $type; + * + * @return bool + */ + public function thumbnailExists($type = 'page') + { + $thumbs = $this->get('thumbnails'); + + return isset($thumbs[$type]); + } + + /** + * Switch thumbnail. + * + * @param string $type + * + * @return $this + */ + public function thumbnail($type = 'auto') + { + if ($type !== 'auto' && !\in_array($type, $this->thumbnailTypes, true)) { + return $this; + } + + if ($this->thumbnailType !== $type) { + $this->_thumbnail = null; + } + + $this->thumbnailType = $type; + + return $this; + } + + /** + * Return URL to file. + * + * @param bool $reset + * @return string + */ + abstract public function url($reset = true); + + /** + * Turn the current Medium into a Link + * + * @param bool $reset + * @param array $attributes + * @return MediaLinkInterface + */ + public function link($reset = true, array $attributes = []) + { + if ($this->mode !== 'source') { + $this->display('source'); + } + + foreach ($this->attributes as $key => $value) { + empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value; + } + + empty($attributes['href']) && $attributes['href'] = $this->url(); + + return $this->createLink($attributes); + } + + /** + * Turn the current Medium into a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param bool $reset + * @return MediaLinkInterface + */ + public function lightbox($width = null, $height = null, $reset = true) + { + $attributes = ['rel' => 'lightbox']; + + if ($width && $height) { + $attributes['data-width'] = $width; + $attributes['data-height'] = $height; + } + + return $this->link($reset, $attributes); + } + + /** + * Add a class to the element from Markdown or Twig + * Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2) + * + * @return $this + */ + public function classes() + { + $classes = func_get_args(); + if (!empty($classes)) { + $this->attributes['class'] = implode(',', $classes); + } + + return $this; + } + + /** + * Add an id to the element from Markdown or Twig + * Example: ![Example](myimg.png?id=primary-img) + * + * @param string $id + * @return $this + */ + public function id($id) + { + if (is_string($id)) { + $this->attributes['id'] = trim($id); + } + + return $this; + } + + /** + * Allows to add an inline style attribute from Markdown or Twig + * Example: ![Example](myimg.png?style=float:left) + * + * @param string $style + * @return $this + */ + public function style($style) + { + $this->styleAttributes[] = rtrim($style, ';') . ';'; + + return $this; + } + + /** + * Allow any action to be called on this medium from twig or markdown + * + * @param string $method + * @param mixed $args + * @return $this + */ + public function __call($method, $args) + { + $count = \count($args); + if ($count > 1 || ($count === 1 && !empty($args[0]))) { + $method .= '=' . implode(',', array_map(static function ($a) { + if (is_array($a)) { + $a = '[' . implode(',', $a) . ']'; + } + + return rawurlencode($a); + }, $args)); + } + + if (!empty($method)) { + $this->querystring($this->querystring(null, false) . '&' . $method); + } + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + return $this->textParsedownElement($attributes, $reset); + } + + /** + * Parsedown element for text display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function textParsedownElement(array $attributes, $reset = true) + { + if ($reset) { + $this->reset(); + } + + $text = $attributes['title'] ?? ''; + if ($text === '') { + $text = $attributes['alt'] ?? ''; + if ($text === '') { + $text = $this->get('filename'); + } + } + + return [ + 'name' => 'p', + 'attributes' => $attributes, + 'text' => $text + ]; + } + + /** + * Get the thumbnail Medium object + * + * @return ThumbnailImageMedium|null + */ + protected function getThumbnail() + { + if (null === $this->_thumbnail) { + $types = $this->thumbnailTypes; + + if ($this->thumbnailType !== 'auto') { + array_unshift($types, $this->thumbnailType); + } + + foreach ($types as $type) { + $thumb = $this->get("thumbnails.{$type}", false); + + if ($thumb) { + $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : $this->createThumbnail($thumb); + $thumb->parent = $this; + $this->_thumbnail = $thumb; + break; + } + } + } + + return $this->_thumbnail; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $this->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + abstract public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + abstract public function set($name, $value, $separator = null); + + /** + * @param string $thumb + */ + abstract protected function createThumbnail($thumb); + + /** + * @param array $attributes + * @return MediaLinkInterface + */ + abstract protected function createLink(array $attributes); + + /** + * @return array + */ + abstract protected function getItems(): array; +} diff --git a/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php new file mode 100644 index 000000000..8aade2a36 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/MediaPlayerTrait.php @@ -0,0 +1,111 @@ +attributes['controls'] = true; + } else { + unset($this->attributes['controls']); + } + + return $this; + } + + /** + * Allows to set the loop attribute + * + * @param bool $status + * @return $this + */ + public function loop($status = false) + { + if ($status) { + $this->attributes['loop'] = true; + } else { + unset($this->attributes['loop']); + } + + return $this; + } + + /** + * Allows to set the autoplay attribute + * + * @param bool $status + * @return $this + */ + public function autoplay($status = false) + { + if ($status) { + $this->attributes['autoplay'] = true; + } else { + unset($this->attributes['autoplay']); + } + + return $this; + } + + /** + * Allows to set the muted attribute + * + * @param bool $status + * @return $this + */ + public function muted($status = false) + { + if ($status) { + $this->attributes['muted'] = true; + } else { + unset($this->attributes['muted']); + } + + return $this; + } + + /** + * Allows to set the preload behaviour + * + * @param string|null $preload + * @return $this + */ + public function preload($preload = null) + { + $validPreloadAttrs = ['auto', 'metadata', 'none']; + + if (null === $preload) { + unset($this->attributes['preload']); + } elseif (\in_array($preload, $validPreloadAttrs, true)) { + $this->attributes['preload'] = $preload; + } + + return $this; + } + + /** + * Reset player. + */ + public function resetPlayer() + { + $this->attributes['controls'] = true; + } +} diff --git a/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php new file mode 100644 index 000000000..d7ab1cf99 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/StaticResizeTrait.php @@ -0,0 +1,28 @@ +styleAttributes['width'] = $width . 'px'; + $this->styleAttributes['height'] = $height . 'px'; + + return $this; + } +} diff --git a/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php new file mode 100644 index 000000000..9497a17a8 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/ThumbnailMediaTrait.php @@ -0,0 +1,142 @@ +bubble('parsedownElement', [$title, $alt, $class, $id, $reset]); + } + + /** + * Return HTML markup from the medium. + * + * @param string|null $title + * @param string|null $alt + * @param string|null $class + * @param string|null $id + * @param bool $reset + * @return string + */ + public function html($title = null, $alt = null, $class = null, $id = null, $reset = true) + { + return $this->bubble('html', [$title, $alt, $class, $id, $reset]); + } + + /** + * Switch display mode. + * + * @param string $mode + * + * @return MediaLinkInterface|MediaObjectInterface|null + */ + public function display($mode = 'source') + { + return $this->bubble('display', [$mode], false); + } + + /** + * Switch thumbnail. + * + * @param string $type + * + * @return MediaLinkInterface|MediaObjectInterface + */ + public function thumbnail($type = 'auto') + { + $this->bubble('thumbnail', [$type], false); + + return $this->bubble('getThumbnail', [], false); + } + + /** + * Turn the current Medium into a Link + * + * @param bool $reset + * @param array $attributes + * @return MediaLinkInterface + */ + public function link($reset = true, array $attributes = []) + { + return $this->bubble('link', [$reset, $attributes], false); + } + + /** + * Turn the current Medium into a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param bool $reset + * @return MediaLinkInterface + */ + public function lightbox($width = null, $height = null, $reset = true) + { + return $this->bubble('lightbox', [$width, $height, $reset], false); + } + + /** + * Bubble a function call up to either the superclass function or the parent Medium instance + * + * @param string $method + * @param array $arguments + * @param bool $testLinked + * @return mixed + */ + protected function bubble($method, array $arguments = [], $testLinked = true) + { + if (!$testLinked || $this->linked) { + $parent = $this->parent; + if (null === $parent) { + return $this; + } + + $closure = [$parent, $method]; + + if (!is_callable($closure)) { + throw new \BadMethodCallException(get_class($parent) . '::' . $method . '() not found.'); + } + + return $closure(...$arguments); + } + + return parent::{$method}(...$arguments); + } +} diff --git a/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php new file mode 100644 index 000000000..2de9c0769 --- /dev/null +++ b/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php @@ -0,0 +1,64 @@ +attributes['poster'] = $urlImage; + + return $this; + } + + /** + * Allows to set the playsinline attribute + * + * @param bool $status + * @return $this + */ + public function playsinline($status = false) + { + if ($status) { + $this->attributes['playsinline'] = true; + } else { + unset($this->attributes['playsinline']); + } + + return $this; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param bool $reset + * @return array + */ + protected function sourceParsedownElement(array $attributes, $reset = true) + { + $location = $this->url($reset); + + return [ + 'name' => 'video', + 'rawHtml' => 'Your browser does not support the video tag.', + 'attributes' => $attributes + ]; + } +} diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index db8e90eb8..d6a2e605a 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -29,7 +29,7 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac /** @var array */ protected $items = []; - /** @var string */ + /** @var string|null */ protected $path; /** @var array */ protected $images = []; @@ -45,7 +45,7 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac /** * Return media path. * - * @return string + * @return string|null */ public function getPath() { @@ -192,12 +192,14 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac protected function orderMedia($media) { if (null === $this->media_order) { - /** @var Pages $pages */ - $pages = Grav::instance()['pages']; - $page = $pages->get($this->getPath()); - - if ($page && isset($page->header()->media_order)) { - $this->media_order = array_map('trim', explode(',', $page->header()->media_order)); + $path = $this->getPath(); + if (null !== $path) { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + $page = $pages->get($path); + if ($page && isset($page->header()->media_order)) { + $this->media_order = array_map('trim', explode(',', $page->header()->media_order)); + } } } diff --git a/system/src/Grav/Common/Page/Medium/AudioMedium.php b/system/src/Grav/Common/Page/Medium/AudioMedium.php index dd988a11c..2e4f33afd 100644 --- a/system/src/Grav/Common/Page/Medium/AudioMedium.php +++ b/system/src/Grav/Common/Page/Medium/AudioMedium.php @@ -9,127 +9,12 @@ namespace Grav\Common\Page\Medium; -class AudioMedium extends Medium +use Grav\Common\Media\Interfaces\AudioMediaInterface; +use Grav\Common\Media\Traits\AudioMediaTrait; + +class AudioMedium extends Medium implements AudioMediaInterface { - use StaticResizeTrait; - - /** - * Parsedown element for source display mode - * - * @param array $attributes - * @param bool $reset - * @return array - */ - protected function sourceParsedownElement(array $attributes, $reset = true) - { - $location = $this->url($reset); - - return [ - 'name' => 'audio', - 'rawHtml' => 'Your browser does not support the audio tag.', - 'attributes' => $attributes - ]; - } - - /** - * Allows to set or remove the HTML5 default controls - * - * @param bool $display - * @return $this - */ - public function controls($display = true) - { - if ($display) { - $this->attributes['controls'] = true; - } else { - unset($this->attributes['controls']); - } - - return $this; - } - - /** - * Allows to set the preload behaviour - * - * @param string $preload - * @return $this - */ - public function preload($preload) - { - $validPreloadAttrs = ['auto', 'metadata', 'none']; - - if (\in_array($preload, $validPreloadAttrs, true)) { - $this->attributes['preload'] = $preload; - } - - return $this; - } - - /** - * Allows to set the controlsList behaviour - * Separate multiple values with a hyphen - * - * @param string $controlsList - * @return $this - */ - public function controlsList($controlsList) - { - $controlsList = str_replace('-', ' ', $controlsList); - $this->attributes['controlsList'] = $controlsList; - - return $this; - } - - /** - * Allows to set the muted attribute - * - * @param bool $status - * @return $this - */ - public function muted($status = false) - { - if ($status) { - $this->attributes['muted'] = true; - } else { - unset($this->attributes['muted']); - } - - return $this; - } - - /** - * Allows to set the loop attribute - * - * @param bool $status - * @return $this - */ - public function loop($status = false) - { - if ($status) { - $this->attributes['loop'] = true; - } else { - unset($this->attributes['loop']); - } - - return $this; - } - - /** - * Allows to set the autoplay attribute - * - * @param bool $status - * @return $this - */ - public function autoplay($status = false) - { - if ($status) { - $this->attributes['autoplay'] = true; - } else { - unset($this->attributes['autoplay']); - } - - return $this; - } + use AudioMediaTrait; /** * Reset medium. @@ -140,7 +25,7 @@ class AudioMedium extends Medium { parent::reset(); - $this->attributes['controls'] = true; + $this->resetPlayer(); return $this; } diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index b250e4da0..4d5cea819 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -10,49 +10,16 @@ namespace Grav\Common\Page\Medium; use Grav\Common\Data\Blueprint; -use Grav\Common\Grav; +use Grav\Common\Media\Interfaces\ImageManipulateInterface; +use Grav\Common\Media\Interfaces\ImageMediaInterface; +use Grav\Common\Media\Interfaces\MediaLinkInterface; +use Grav\Common\Media\Traits\ImageMediaTrait; use Grav\Common\Utils; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; -class ImageMedium extends Medium +class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulateInterface { - /** @var array */ - protected $thumbnailTypes = ['page', 'media', 'default']; - - /** @var ImageFile|null */ - 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'; + use ImageMediaTrait; /** * Construct. @@ -64,13 +31,15 @@ class ImageMedium extends Medium { parent::__construct($items, $blueprint); - $config = Grav::instance()['config']; + $this->thumbnailTypes = ['page', 'media', 'default']; $path = $this->get('filepath'); if (!$path || !file_exists($path) || !filesize($path)) { return; } + $config = $this->getGrav()['config']; + $image_info = getimagesize($path); $this->def('width', $image_info[0]); @@ -96,243 +65,13 @@ class ImageMedium extends Medium public function __clone() { - $this->image = $this->image ? clone $this->image : null; + if ($this->image) { + $this->image = clone $this->image; + } 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. * @@ -357,12 +96,123 @@ class ImageMedium extends Medium return $this; } + /** + * 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; + } + + /** + * 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) + { + $grav = $this->getGrav(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $image_path = (string)($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) ?: $saved_image_path; + + if ($locator->isStream($output)) { + $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true)); + } + + 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['base_url'] . '/' . $this->urlQuerystring($output), '\\'); + } + + /** + * 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); + } + + /** + * 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]; + } + /** * Turn the current Medium into a Link * * @param bool $reset * @param array $attributes - * @return Link + * @return MediaLinkInterface */ public function link($reset = true, array $attributes = []) { @@ -381,7 +231,7 @@ class ImageMedium extends Medium * @param int $width * @param int $height * @param bool $reset - * @return Link + * @return MediaLinkInterface */ public function lightbox($width = null, $height = null, $reset = true) { @@ -396,108 +246,6 @@ class ImageMedium extends Medium 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: ![Example](myimg.png?width=200&height=400) - * ![Example](myimg.png?resize=100,200&width=100&height=200) - * ![Example](myimg.png?width=auto&height=auto) - * ![Example](myimg.png?width&height) - * {{ 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: ![Example](myimg.png?width=200&height=400) - * ![Example](myimg.png?resize=100,200&width=100&height=200) - * ![Example](myimg.png?width=auto&height=auto) - * ![Example](myimg.png?width&height) - * {{ 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; - } - /** * Forward the call to the image processing method. * @@ -511,7 +259,7 @@ class ImageMedium extends Medium $method = 'zoomCrop'; } - if (!\in_array($method, self::$magic_actions, true)) { + if (!\in_array($method, static::$magic_actions, true)) { return parent::__call($method, $args); } @@ -521,127 +269,28 @@ class ImageMedium extends Medium } try { - call_user_func_array([$this->image, $method], $args); + $this->image->{$method}(...$args); + /** @var ImageMediaInterface $medium */ 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(static::$magic_resize_actions[$method])) { + foreach (static::$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); + // Do the same call for alternative media. + $medium->__call($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; - } - - $this->format($this->get('extension')); - - 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. - * @return $this - */ - 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 $this; - } - - /** - * Return the image higher quality version - * - * @return Medium|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; - } } diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php index e326da7c6..fbebb2b8e 100644 --- a/system/src/Grav/Common/Page/Medium/Link.php +++ b/system/src/Grav/Common/Page/Medium/Link.php @@ -9,25 +9,37 @@ namespace Grav\Common\Page\Medium; -class Link implements RenderableInterface +use Grav\Common\Media\Interfaces\MediaLinkInterface; +use Grav\Common\Media\Interfaces\MediaObjectInterface; + +class Link implements RenderableInterface, MediaLinkInterface { use ParsedownHtmlTrait; /** @var array */ protected $attributes = []; - /** @var Medium|null */ + /** @var MediaObjectInterface */ protected $source; /** * Construct. * @param array $attributes - * @param Medium $medium + * @param MediaObjectInterface $medium */ - public function __construct(array $attributes, Medium $medium) + public function __construct(array $attributes, MediaObjectInterface $medium) { $this->attributes = $attributes; - $this->source = $medium->reset()->thumbnail('auto')->display('thumbnail'); - $this->source->linked = true; + + $source = $medium->reset()->thumbnail('auto')->display('thumbnail'); + + // FIXME: Thumbnail can be null, maybe we should not allow that? + if (null === $source) { + throw new \RuntimeException('Media has no thumbnail set'); + } + + $source->set('linked', true); + + $this->source = $source; } /** @@ -47,7 +59,7 @@ class Link implements RenderableInterface return [ 'name' => 'a', 'attributes' => $this->attributes, - 'handler' => is_string($innerElement) ? 'line' : 'element', + 'handler' => is_array($innerElement) ? 'element' : 'line', 'text' => $innerElement ]; } @@ -61,10 +73,16 @@ class Link implements RenderableInterface */ public function __call($method, $args) { - $this->source = call_user_func_array(array($this->source, $method), $args); + $object = $this->source; + $callable = [$object, $method]; + if (!is_callable($callable)) { + throw new \BadMethodCallException(get_class($object) . '::' . $method . '() not found.'); + } + + $this->source = call_user_func_array($callable, $args); // Don't start nesting links, if user has multiple link calls in his // actions, we will drop the previous links. - return $this->source instanceof Link ? $this->source : $this; + return $this->source instanceof MediaLinkInterface ? $this->source : $this; } } diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php index 53bb78920..bcc342e10 100644 --- a/system/src/Grav/Common/Page/Medium/Medium.php +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -13,8 +13,10 @@ use Grav\Common\File\CompiledYamlFile; use Grav\Common\Grav; use Grav\Common\Data\Data; use Grav\Common\Data\Blueprint; -use Grav\Common\Media\Interfaces\MediaObjectInterface; -use Grav\Common\Utils; +use Grav\Common\Media\Interfaces\MediaFileInterface; +use Grav\Common\Media\Interfaces\MediaLinkInterface; +use Grav\Common\Media\Traits\MediaFileTrait; +use Grav\Common\Media\Traits\MediaObjectTrait; /** * Class Medium @@ -22,40 +24,12 @@ use Grav\Common\Utils; * * @property string $mime */ -class Medium extends Data implements RenderableInterface, MediaObjectInterface +class Medium extends Data implements RenderableInterface, MediaFileInterface { + use MediaObjectTrait; + use MediaFileTrait; use ParsedownHtmlTrait; - /** @var string */ - protected $mode = 'source'; - - /** @var Medium|null */ - protected $_thumbnail; - - /** @var array */ - protected $thumbnailTypes = ['page', 'default']; - - /** @var string|null */ - protected $thumbnailType; - - /** @var Medium[] */ - protected $alternatives = []; - - /** @var array */ - protected $attributes = []; - - /** @var array */ - protected $styleAttributes = []; - - /** @var array */ - protected $metadata = []; - - /** @var array */ - protected $medium_querystring = []; - - /** @var string */ - protected $timestamp; - /** * Construct. * @@ -79,93 +53,6 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface // Allows future compatibility as parent::__clone() works. } - /** - * Create a copy of this media object - * - * @return Medium - */ - public function copy() - { - return clone $this; - } - - /** - * Return just metadata from the Medium object - * - * @return Data - */ - public function meta() - { - return new Data($this->items); - } - - /** - * Check if this medium exists or not - * - * @return bool - */ - public function exists() - { - $path = $this->get('filepath'); - if (file_exists($path)) { - return true; - } - return false; - } - - /** - * Get file modification time for the medium. - * - * @return int|null - */ - public function modified() - { - $path = $this->get('filepath'); - - if (!file_exists($path)) { - return null; - } - - return filemtime($path) ?: null; - } - - /** - * @return int - */ - public function size() - { - $path = $this->get('filepath'); - - if (!file_exists($path)) { - return 0; - } - - return filesize($path) ?: 0; - } - - /** - * Set querystring to file modification timestamp (or value provided as a parameter). - * - * @param string|int|null $timestamp - * @return $this - */ - public function setTimestamp($timestamp = null) - { - $this->timestamp = (string)($timestamp ?? $this->modified()); - - return $this; - } - - /** - * Returns an array containing just the metadata - * - * @return array - */ - public function metadata() - { - return $this->metadata; - } - /** * Add meta file for the medium. * @@ -177,24 +64,6 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface $this->merge($this->metadata); } - /** - * Add alternative Medium to this Medium. - * - * @param int|float $ratio - * @param Medium $alternative - */ - public function addAlternative($ratio, Medium $alternative) - { - if (!is_numeric($ratio) || $ratio === 0) { - return; - } - - $alternative->set('ratio', $ratio); - $width = $alternative->get('width'); - - $this->alternatives[$width] = $alternative; - } - /** * Return string representation of the object (html). * @@ -206,465 +75,35 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface } /** - * Return PATH to file. - * - * @param bool $reset - * @return string path to file + * @param string $thumb */ - public function path($reset = true) + protected function createThumbnail($thumb) { - if ($reset) { - $this->reset(); - } - - return $this->get('filepath'); + return MediumFactory::fromFile($thumb, ['type' => 'thumbnail']); } /** - * Return the relative path to file - * - * @param bool $reset - * @return mixed + * @param array $attributes + * @return MediaLinkInterface */ - public function relativePath($reset = true) + protected function createLink(array $attributes) { - $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath')); - - $locator = Grav::instance()['locator']; - if ($locator->isStream($output)) { - $output = $locator->findResource($output, false); - } - - if ($reset) { - $this->reset(); - } - - return str_replace(GRAV_ROOT, '', $output); - } - - /** - * Return URL to file. - * - * @param bool $reset - * @return string - */ - public function url($reset = true) - { - $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath')); - - $locator = Grav::instance()['locator']; - if ($locator->isStream($output)) { - $output = $locator->findResource($output, false); - } - - if ($reset) { - $this->reset(); - } - - return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\'); - } - - /** - * Get/set querystring for the file's url - * - * @param string|null $querystring - * @param bool $withQuestionmark - * @return string - */ - public function querystring($querystring = null, $withQuestionmark = true) - { - if (null !== $querystring) { - $this->medium_querystring[] = ltrim($querystring, '?&'); - foreach ($this->alternatives as $alt) { - $alt->querystring($querystring, $withQuestionmark); - } - } - - if (empty($this->medium_querystring)) { - return ''; - } - - // join the strings - $querystring = implode('&', $this->medium_querystring); - // explode all strings - $query_parts = explode('&', $querystring); - // Join them again now ensure the elements are unique - $querystring = implode('&', array_unique($query_parts)); - - return $withQuestionmark ? ('?' . $querystring) : $querystring; - } - - /** - * Get the URL with full querystring - * - * @param string $url - * @return string - */ - public function urlQuerystring($url) - { - $querystring = $this->querystring(); - if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) { - $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp); - } - - return ltrim($url . $querystring . $this->urlHash(), '/'); - } - - /** - * Get/set hash for the file's url - * - * @param string $hash - * @param bool $withHash - * @return string - */ - public function urlHash($hash = null, $withHash = true) - { - if ($hash) { - $this->set('urlHash', ltrim($hash, '#')); - } - - $hash = $this->get('urlHash', ''); - - return $withHash && !empty($hash) ? '#' . $hash : $hash; - } - - /** - * Get an element (is array) that can be rendered by the Parsedown engine - * - * @param string|null $title - * @param string|null $alt - * @param string|null $class - * @param string|null $id - * @param bool $reset - * @return array - */ - public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) - { - $attributes = $this->attributes; - - $style = ''; - foreach ($this->styleAttributes as $key => $value) { - if (is_numeric($key)) { // Special case for inline style attributes, refer to style() method - $style .= $value; - } else { - $style .= $key . ': ' . $value . ';'; - } - } - if ($style) { - $attributes['style'] = $style; - } - - if (empty($attributes['title'])) { - if (!empty($title)) { - $attributes['title'] = $title; - } elseif (!empty($this->items['title'])) { - $attributes['title'] = $this->items['title']; - } - } - - if (empty($attributes['alt'])) { - if (!empty($alt)) { - $attributes['alt'] = $alt; - } elseif (!empty($this->items['alt'])) { - $attributes['alt'] = $this->items['alt']; - } elseif (!empty($this->items['alt_text'])) { - $attributes['alt'] = $this->items['alt_text']; - } else { - $attributes['alt'] = ''; - } - } - - if (empty($attributes['class'])) { - if (!empty($class)) { - $attributes['class'] = $class; - } elseif (!empty($this->items['class'])) { - $attributes['class'] = $this->items['class']; - } - } - - if (empty($attributes['id'])) { - if (!empty($id)) { - $attributes['id'] = $id; - } elseif (!empty($this->items['id'])) { - $attributes['id'] = $this->items['id']; - } - } - - switch ($this->mode) { - case 'text': - $element = $this->textParsedownElement($attributes, false); - break; - case 'thumbnail': - $element = $this->getThumbnail()->sourceParsedownElement($attributes, false); - break; - case 'source': - $element = $this->sourceParsedownElement($attributes, false); - break; - default: - $element = []; - } - - if ($reset) { - $this->reset(); - } - - $this->display('source'); - - return $element; - } - - /** - * Parsedown element for source display mode - * - * @param array $attributes - * @param bool $reset - * @return array - */ - protected function sourceParsedownElement(array $attributes, $reset = true) - { - return $this->textParsedownElement($attributes, $reset); - } - - /** - * Parsedown element for text display mode - * - * @param array $attributes - * @param bool $reset - * @return array - */ - protected function textParsedownElement(array $attributes, $reset = true) - { - $text = empty($attributes['title']) ? empty($attributes['alt']) ? $this->get('filename') : $attributes['alt'] : $attributes['title']; - - $element = [ - 'name' => 'p', - 'attributes' => $attributes, - 'text' => $text - ]; - - if ($reset) { - $this->reset(); - } - - return $element; - } - - /** - * Reset medium. - * - * @return $this - */ - public function reset() - { - $this->attributes = []; - return $this; - } - - /** - * Switch display mode. - * - * @param string $mode - * - * @return self|null - */ - public function display($mode = 'source') - { - if ($this->mode === $mode) { - return $this; - } - - - $this->mode = $mode; - if ($mode === 'thumbnail') { - $thumbnail = $this->getThumbnail(); - - return $thumbnail ? $thumbnail->reset() : null; - } - - return $this->reset(); - } - - /** - * Helper method to determine if this media item has a thumbnail or not - * - * @param string $type; - * - * @return bool - */ - public function thumbnailExists($type = 'page') - { - $thumbs = $this->get('thumbnails'); - if (isset($thumbs[$type])) { - return true; - } - return false; - } - - /** - * Switch thumbnail. - * - * @param string $type - * - * @return $this - */ - public function thumbnail($type = 'auto') - { - if ($type !== 'auto' && !\in_array($type, $this->thumbnailTypes, true)) { - return $this; - } - - if ($this->thumbnailType !== $type) { - $this->_thumbnail = null; - } - - $this->thumbnailType = $type; - - return $this; - } - - - /** - * Turn the current Medium into a Link - * - * @param bool $reset - * @param array $attributes - * @return Link - */ - public function link($reset = true, array $attributes = []) - { - if ($this->mode !== 'source') { - $this->display('source'); - } - - foreach ($this->attributes as $key => $value) { - empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value; - } - - empty($attributes['href']) && $attributes['href'] = $this->url(); - return new Link($attributes, $this); } /** - * Turn the current Medium into a Link with lightbox enabled - * - * @param int $width - * @param int $height - * @param bool $reset - * @return Link + * @return Grav */ - public function lightbox($width = null, $height = null, $reset = true) + protected function getGrav(): Grav { - $attributes = ['rel' => 'lightbox']; - - if ($width && $height) { - $attributes['data-width'] = $width; - $attributes['data-height'] = $height; - } - - return $this->link($reset, $attributes); + return Grav::instance(); } /** - * Add a class to the element from Markdown or Twig - * Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2) - * - * @return $this + * @return array */ - public function classes() + protected function getItems(): array { - $classes = func_get_args(); - if (!empty($classes)) { - $this->attributes['class'] = implode(',', $classes); - } - - return $this; - } - - /** - * Add an id to the element from Markdown or Twig - * Example: ![Example](myimg.png?id=primary-img) - * - * @param string $id - * @return $this - */ - public function id($id) - { - if (is_string($id)) { - $this->attributes['id'] = trim($id); - } - - return $this; - } - - /** - * Allows to add an inline style attribute from Markdown or Twig - * Example: ![Example](myimg.png?style=float:left) - * - * @param string $style - * @return $this - */ - public function style($style) - { - $this->styleAttributes[] = rtrim($style, ';') . ';'; - return $this; - } - - /** - * Allow any action to be called on this medium from twig or markdown - * - * @param string $method - * @param mixed $args - * @return $this - */ - public function __call($method, $args) - { - $qs = $method; - if (\count($args) > 1 || (\count($args) === 1 && !empty($args[0]))) { - $qs .= '=' . implode(',', array_map(function ($a) { - if (is_array($a)) { - $a = '[' . implode(',', $a) . ']'; - } - return rawurlencode($a); - }, $args)); - } - - if (!empty($qs)) { - $this->querystring($this->querystring(null, false) . '&' . $qs); - } - - return $this; - } - - /** - * Get the thumbnail Medium object - * - * @return ThumbnailImageMedium|null - */ - protected function getThumbnail() - { - if (null === $this->_thumbnail) { - $types = $this->thumbnailTypes; - - if ($this->thumbnailType !== 'auto') { - array_unshift($types, $this->thumbnailType); - } - - foreach ($types as $type) { - $thumb = $this->get('thumbnails.' . $type, false); - - if ($thumb) { - $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']); - $thumb->parent = $this; - } - - if ($thumb) { - $this->_thumbnail = $thumb; - break; - } - } - } - - return $this->_thumbnail; + return $this->items; } } diff --git a/system/src/Grav/Common/Page/Medium/StaticImageMedium.php b/system/src/Grav/Common/Page/Medium/StaticImageMedium.php index aaa291487..076121a2f 100644 --- a/system/src/Grav/Common/Page/Medium/StaticImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/StaticImageMedium.php @@ -9,7 +9,10 @@ namespace Grav\Common\Page\Medium; -class StaticImageMedium extends Medium +use Grav\Common\Media\Interfaces\ImageMediaInterface; +use Grav\Common\Media\Traits\StaticResizeTrait; + +class StaticImageMedium extends Medium implements ImageMediaInterface { use StaticResizeTrait; @@ -22,7 +25,9 @@ class StaticImageMedium extends Medium */ protected function sourceParsedownElement(array $attributes, $reset = true) { - empty($attributes['src']) && $attributes['src'] = $this->url($reset); + if (empty($attributes['src'])) { + $attributes['src'] = $this->url($reset); + } return ['name' => 'img', 'attributes' => $attributes]; } diff --git a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php index a5036e85e..6d43915d9 100644 --- a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php +++ b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php @@ -9,6 +9,13 @@ namespace Grav\Common\Page\Medium; +user_error('Grav\Common\Page\Medium\StaticResizeTrait is deprecated since Grav 1.7, use Grav\Common\Media\Traits\StaticResizeTrait instead', E_USER_DEPRECATED); + +/** + * Trait StaticResizeTrait + * @package Grav\Common\Page\Medium + * @deprecated 1.7 Use `Grav\Common\Media\Traits\StaticResizeTrait` instead + */ trait StaticResizeTrait { /** diff --git a/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php index a9e9970a2..3faa34fe2 100644 --- a/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php @@ -9,120 +9,9 @@ namespace Grav\Common\Page\Medium; +use Grav\Common\Media\Traits\ThumbnailMediaTrait; + class ThumbnailImageMedium extends ImageMedium { - /** @var Medium|null */ - public $parent; - - /** @var bool */ - public $linked = false; - - /** - * Return srcset string for this Medium and its alternatives. - * - * @param bool $reset - * @return string - */ - public function srcset($reset = true) - { - return ''; - } - - /** - * Get an element (is array) that can be rendered by the Parsedown engine - * - * @param string|null $title - * @param string|null $alt - * @param string|null $class - * @param string|null $id - * @param bool $reset - * @return array - */ - public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) - { - return $this->bubble('parsedownElement', [$title, $alt, $class, $id, $reset]); - } - - /** - * Return HTML markup from the medium. - * - * @param string|null $title - * @param string|null $alt - * @param string|null $class - * @param string|null $id - * @param bool $reset - * @return string - */ - public function html($title = null, $alt = null, $class = null, $id = null, $reset = true) - { - return $this->bubble('html', [$title, $alt, $class, $id, $reset]); - } - - /** - * Switch display mode. - * - * @param string $mode - * - * @return array|Link|Medium - */ - public function display($mode = 'source') - { - return $this->bubble('display', [$mode], false); - } - - /** - * Switch thumbnail. - * - * @param string $type - * - * @return array|Link|Medium - */ - public function thumbnail($type = 'auto') - { - $this->bubble('thumbnail', [$type], false); - - return $this->bubble('getThumbnail', [], false); - } - - /** - * Turn the current Medium into a Link - * - * @param bool $reset - * @param array $attributes - * @return Link - */ - public function link($reset = true, array $attributes = []) - { - return $this->bubble('link', [$reset, $attributes], false); - } - - /** - * 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) - { - return $this->bubble('lightbox', [$width, $height, $reset], false); - } - - /** - * Bubble a function call up to either the superclass function or the parent Medium instance - * - * @param string $method - * @param array $arguments - * @param bool $testLinked - * @return array|Link|Medium - */ - protected function bubble($method, array $arguments = [], $testLinked = true) - { - if (!$testLinked || $this->linked) { - return $this->parent ? call_user_func_array(array($this->parent, $method), $arguments) : $this; - } - - return call_user_func_array(array($this, 'parent::' . $method), $arguments); - } + use ThumbnailMediaTrait; } diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php index 47844852f..2d8a7ec9a 100644 --- a/system/src/Grav/Common/Page/Medium/VideoMedium.php +++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php @@ -9,142 +9,12 @@ namespace Grav\Common\Page\Medium; -class VideoMedium extends Medium +use Grav\Common\Media\Interfaces\VideoMediaInterface; +use Grav\Common\Media\Traits\VideoMediaTrait; + +class VideoMedium extends Medium implements VideoMediaInterface { - use StaticResizeTrait; - - /** - * Parsedown element for source display mode - * - * @param array $attributes - * @param bool $reset - * @return array - */ - protected function sourceParsedownElement(array $attributes, $reset = true) - { - $location = $this->url($reset); - - return [ - 'name' => 'video', - 'rawHtml' => 'Your browser does not support the video tag.', - 'attributes' => $attributes - ]; - } - - /** - * Allows to set or remove the HTML5 default controls - * - * @param bool $display - * @return $this - */ - public function controls($display = true) - { - if ($display) { - $this->attributes['controls'] = true; - } else { - unset($this->attributes['controls']); - } - - return $this; - } - - /** - * Allows to set the video's poster image - * - * @param string $urlImage - * @return $this - */ - public function poster($urlImage) - { - $this->attributes['poster'] = $urlImage; - - return $this; - } - - /** - * Allows to set the loop attribute - * - * @param bool $status - * @return $this - */ - public function loop($status = false) - { - if ($status) { - $this->attributes['loop'] = true; - } else { - unset($this->attributes['loop']); - } - - return $this; - } - - /** - * Allows to set the autoplay attribute - * - * @param bool $status - * @return $this - */ - public function autoplay($status = false) - { - if ($status) { - $this->attributes['autoplay'] = ''; - } else { - unset($this->attributes['autoplay']); - } - - return $this; - } - - /** - * Allows ability to set the preload option - * - * @param string|null $status - * @return $this - */ - public function preload($status = null) - { - if ($status) { - $this->attributes['preload'] = $status; - } else { - unset($this->attributes['preload']); - } - - return $this; - } - - /** - * Allows to set the playsinline attribute - * - * @param bool $status - * @return $this - */ - public function playsinline($status = false) - { - if ($status) { - $this->attributes['playsinline'] = true; - } else { - unset($this->attributes['playsinline']); - } - - return $this; - } - - /** - * Allows to set the muted attribute - * - * @param bool $status - * @return $this - */ - public function muted($status = false) - { - if ($status) { - $this->attributes['muted'] = true; - } else { - unset($this->attributes['muted']); - } - - return $this; - } + use VideoMediaTrait; /** * Reset medium. @@ -155,7 +25,7 @@ class VideoMedium extends Medium { parent::reset(); - $this->attributes['controls'] = true; + $this->resetPlayer(); return $this; }