Added stream support for images (![Sepia Image](image://image.jpg?sepia))

Added stream support for links (`[Download PDF](user://data/pdf/my.pdf)`)
This commit is contained in:
Matias Griese
2016-12-07 19:51:06 +02:00
parent 6c1a76b901
commit 83fdecbdd1
6 changed files with 425 additions and 208 deletions

View File

@@ -5,7 +5,9 @@
* Add `ignore_empty` property to be used on array fields, if positive only save options with a value
* Use new `permissions` field in user account
* Add `range(int start, int end, int step)` twig function to generate an array of numbers between start and end, inclusive
* New retina Media image derivatives array support (`![](image.jpg?derivatives=[640,1024,1440]`) [#1147](https://github.com/getgrav/grav/pull/1147)
* New retina Media image derivatives array support (`![](image.jpg?derivatives=[640,1024,1440])`) [#1147](https://github.com/getgrav/grav/pull/1147)
* Added stream support for images (`![Sepia Image](image://image.jpg?sepia)`)
* Added stream support for links (`[Download PDF](user://data/pdf/my.pdf)`)
1. [](#improved)
* Added alias `selfupdate` to the `self-upgrade` `bin/gpm` CLI command
* Synced `webserver-configs/htaccess.txt` with `.htaccess`

View File

@@ -10,9 +10,9 @@ namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Page\Medium\Medium;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts
{
@@ -115,11 +115,11 @@ class Excerpts
*/
public static function processLinkExcerpt($excerpt, $page, $type = 'link')
{
$url = $excerpt['element']['attributes']['href'];
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['href']));
$url_parts = parse_url(htmlspecialchars_decode(urldecode($url)));
$url_parts = static::parseUrl($url);
// if there is a query, then parse it and build action calls
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
@@ -129,19 +129,19 @@ class Excerpts
return $carry;
}, []);
// valid attributes supported
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// loop through actions for the image and call them
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes)) {
// support both class and classes
// support both class and classes.
if ($attrib == 'classes') {
$attrib = 'class';
}
@@ -154,25 +154,33 @@ class Excerpts
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// if no query elements left, unset query
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// set path to / if not set
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// if special scheme, just return
if(isset($url_parts['scheme']) && !Utils::startsWith($url_parts['scheme'], 'http')) {
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
}
return $excerpt;
}
// handle paths and such
// Handle paths and such.
$url_parts = Uri::convertUrl($page, $url_parts, $type);
// build the URL from the component parts and set it on the element
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
@@ -187,62 +195,65 @@ class Excerpts
*/
public static function processImageExcerpt($excerpt, $page)
{
$url = $excerpt['element']['attributes']['src'];
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
$url_parts = static::parseUrl($url);
$url_parts = parse_url(htmlspecialchars_decode(urldecode($url)));
$media = null;
$filename = null;
if (isset($url_parts['scheme']) && !Utils::startsWith($url_parts['scheme'], 'http')) {
$stream_path = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path'];
$url_parts['path'] = $stream_path;
unset($url_parts['host']);
unset($url_parts['scheme']);
}
if (!empty($url_parts['stream'])) {
$filename = $url_parts['scheme'] . '://' . (isset($url_parts['path']) ? $url_parts['path'] : '');
$this_host = isset($url_parts['host']) && $url_parts['host'] == Grav::instance()['uri']->host();
$media = $page->media();
// if there is no host set but there is a path, the file is local
if ((!isset($url_parts['host']) || $this_host) && isset($url_parts['path'])) {
} else {
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https']))
&& (empty($url_parts['host']) || $url_parts['host'] == Grav::instance()['uri']->host());
$path_parts = pathinfo($url_parts['path']);
$media = null;
if ($local_file) {
$filename = basename($url_parts['path']);
$folder = dirname($url_parts['path']);
// get the local path to page media if possible
if ($path_parts['dirname'] == $page->url(false, false, false)) {
// get the media objects for this page
$media = $page->media();
} else {
// see if this is an external page to this one
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/');
$ext_page = Grav::instance()['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->media();
// Get the local path to page media if possible.
if ($folder === $page->url(false, false, false)) {
// Get the media objects for this page.
$media = $page->media();
} else {
Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
// see if this is an external page to this one
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
$ext_page = Grav::instance()['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->media();
} else {
Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
}
}
}
}
// if there is a media file that matches the path referenced..
if ($media && isset($media->all()[$path_parts['basename']])) {
// get the medium object
/** @var Medium $medium */
$medium = $media->all()[$path_parts['basename']];
// If there is a media file that matches the path referenced..
if ($media && $filename && isset($media[$filename])) {
// Get the medium object.
/** @var Medium $medium */
$medium = $media[$filename];
// Process operations
$medium = static::processMediaActions($medium, $url_parts);
// Process operations
$medium = static::processMediaActions($medium, $url_parts);
$alt = isset($excerpt['element']['attributes']['alt']) ? $excerpt['element']['attributes']['alt'] : '';
$title = isset($excerpt['element']['attributes']['title']) ? $excerpt['element']['attributes']['title'] : '';
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
$id = isset($excerpt['element']['attributes']['id']) ? $excerpt['element']['attributes']['id'] : '';
$alt = isset($excerpt['element']['attributes']['alt']) ? $excerpt['element']['attributes']['alt'] : '';
$title = isset($excerpt['element']['attributes']['title']) ? $excerpt['element']['attributes']['title'] : '';
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
$id = isset($excerpt['element']['attributes']['id']) ? $excerpt['element']['attributes']['id'] : '';
$excerpt['element'] = $medium->parseDownElement($title, $alt, $class, $id, true);
$excerpt['element'] = $medium->parseDownElement($title, $alt, $class, $id, true);
} else {
// not a current page media file, see if it needs converting to relative
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
return $excerpt;
@@ -300,4 +311,40 @@ class Excerpts
return $medium;
}
/**
* Variation of parse_url() which works also with local streams.
*
* @param string $url
* @return array|bool
*/
protected static function parseUrl($url)
{
$url_parts = parse_url($url);
if (isset($url_parts['scheme'])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Special handling for the streams.
if ($locator->schemeExists($url_parts['scheme'])) {
if (isset($url_parts['host'])) {
// Merge host and path into a path.
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
unset($url_parts['host']);
}
$url_parts['stream'] = true;
}
}
return $url_parts;
}
protected static function resolveStream($url)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->isStream($url) ? ($locator->findResource($url, false) ?: $locator->findResource($url, false, true)) : $url;
}
}

View File

@@ -8,51 +8,78 @@
namespace Grav\Common\Page;
use Grav\Common\Getters;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\GlobalMedia;
use Grav\Common\Page\Medium\MediumFactory;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Media extends Getters
class Media extends AbstractMedia
{
protected $gettersVariable = 'instances';
protected static $global;
protected $path;
protected $instances = [];
protected $images = [];
protected $videos = [];
protected $audios = [];
protected $files = [];
/**
* @param $path
*/
public function __construct($path)
{
$this->path = $path;
if (!isset(static::$global)) {
// Add fallback to global media.
static::$global = new GlobalMedia($path);
}
$this->init();
}
/**
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: isset(static::$global[$offset]);
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: static::$global[$offset];
}
/**
* Initialize class.
*/
protected function init()
{
// Handle special cases where page doesn't exist in filesystem.
if (!is_dir($path)) {
if (!is_dir($this->path)) {
return;
}
$this->path = $path;
$iterator = new \FilesystemIterator($path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$iterator = new \FilesystemIterator($this->path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$media = [];
/** @var \DirectoryIterator $info */
foreach ($iterator as $path => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename() === '.DS_Store') {
if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename()[0] === '.') {
continue;
}
// Find out what type we're dealing with
list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename());
$media["{$basename}.{$ext}"] = isset($media["{$basename}.{$ext}"]) ? $media["{$basename}.{$ext}"] : [];
if ($type === 'alternative') {
$media["{$basename}.{$ext}"][$type] = isset($media["{$basename}.{$ext}"][$type]) ? $media["{$basename}.{$ext}"][$type] : [];
$media["{$basename}.{$ext}"][$type][$extra] = [ 'file' => $path, 'size' => $info->getSize() ];
} else {
$media["{$basename}.{$ext}"][$type] = [ 'file' => $path, 'size' => $info->getSize() ];
@@ -124,134 +151,4 @@ class Media extends Getters
$this->add($name, $medium);
}
}
/**
* Get medium by filename.
*
* @param string $filename
* @return Medium|null
*/
public function get($filename)
{
return isset($this->instances[$filename]) ? $this->instances[$filename] : null;
}
/**
* Get a list of all media.
*
* @return array|Medium[]
*/
public function all()
{
ksort($this->instances, SORT_NATURAL | SORT_FLAG_CASE);
return $this->instances;
}
/**
* Get a list of all image media.
*
* @return array|Medium[]
*/
public function images()
{
ksort($this->images, SORT_NATURAL | SORT_FLAG_CASE);
return $this->images;
}
/**
* Get a list of all video media.
*
* @return array|Medium[]
*/
public function videos()
{
ksort($this->videos, SORT_NATURAL | SORT_FLAG_CASE);
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|Medium[]
*/
public function audios()
{
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
return $this->audios;
}
/**
* Get a list of all file media.
*
* @return array|Medium[]
*/
public function files()
{
ksort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
return $this->files;
}
/**
* @internal
*/
protected function add($name, $file)
{
$this->instances[$name] = $file;
switch ($file->type) {
case 'image':
$this->images[$name] = $file;
break;
case 'video':
$this->videos[$name] = $file;
break;
case 'audio':
$this->audios[$name] = $file;
break;
default:
$this->files[$name] = $file;
}
}
/**
* Get filename, extension and meta part.
*
* @param string $filename
* @return array
*/
protected function getFileParts($filename)
{
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
$type = 'base';
$extra = null;
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
$name = $matches[1];
$extension = $matches[3];
$extra = (int) $matches[2];
$type = 'alternative';
if ($extra === 1) {
$type = 'base';
$extra = null;
}
} else {
$extension = null;
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta' && $part != 'thumb') {
if (isset($extension)) {
$name .= '.' . $extension;
}
$extension = $part;
} else {
$type = $part;
$extra = '.' . $part . '.' . implode('.', $fileParts);
break;
}
}
}
return array($name, $extension, $type, $extra);
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Getters;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
abstract class AbstractMedia extends Getters
{
protected $gettersVariable = 'instances';
protected $instances = [];
protected $images = [];
protected $videos = [];
protected $audios = [];
protected $files = [];
/**
* Get medium by filename.
*
* @param string $filename
* @return Medium|null
*/
public function get($filename)
{
return $this->offsetGet($filename);
}
/**
* Get a list of all media.
*
* @return array|Medium[]
*/
public function all()
{
ksort($this->instances, SORT_NATURAL | SORT_FLAG_CASE);
return $this->instances;
}
/**
* Get a list of all image media.
*
* @return array|Medium[]
*/
public function images()
{
ksort($this->images, SORT_NATURAL | SORT_FLAG_CASE);
return $this->images;
}
/**
* Get a list of all video media.
*
* @return array|Medium[]
*/
public function videos()
{
ksort($this->videos, SORT_NATURAL | SORT_FLAG_CASE);
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|Medium[]
*/
public function audios()
{
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
return $this->audios;
}
/**
* Get a list of all file media.
*
* @return array|Medium[]
*/
public function files()
{
ksort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
return $this->files;
}
/**
* @param string $name
* @param Medium $file
*/
protected function add($name, $file)
{
$this->instances[$name] = $file;
switch ($file->type) {
case 'image':
$this->images[$name] = $file;
break;
case 'video':
$this->videos[$name] = $file;
break;
case 'audio':
$this->audios[$name] = $file;
break;
default:
$this->files[$name] = $file;
}
}
/**
* Get filename, extension and meta part.
*
* @param string $filename
* @return array
*/
protected function getFileParts($filename)
{
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
$name = $matches[1];
$extension = $matches[3];
$extra = (int) $matches[2];
$type = 'alternative';
if ($extra === 1) {
$type = 'base';
$extra = null;
}
} else {
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
$extension = null;
$extra = null;
$type = 'base';
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta' && $part != 'thumb') {
if (isset($extension)) {
$name .= '.' . $extension;
}
$extension = $part;
} else {
$type = $part;
$extra = '.' . $part . '.' . implode('.', $fileParts);
break;
}
}
}
return array($name, $extension, $type, $extra);
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class GlobalMedia extends AbstractMedia
{
/**
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: !empty($this->resolveStream($offset));
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: $this->addMedium($offset);
}
/**
* @param string $filename
* @return string|null
*/
protected function resolveStream($filename)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->isStream($filename) ? ($locator->findResource($filename) ?: null) : null;
}
/**
* @param string $stream
* @return Medium|null
*/
protected function addMedium($stream)
{
$filename = $this->resolveStream($stream);
if (!$filename) {
return null;
}
$path = dirname($filename);
list($basename, $ext,, $extra) = $this->getFileParts(basename($filename));
$medium = MediumFactory::fromFile($filename);
if (empty($medium)) {
return null;
}
$medium->set('size', filesize($filename));
$scale = (int) ($extra ?: 1);
if ($scale !== 1) {
$altMedium = $medium;
// Create scaled down regular sized image.
$medium = MediumFactory::scaledFromMedium($altMedium, $scale, 1)['file'];
if (empty($medium)) {
return null;
}
// Add original sized image as alternative.
$medium->addAlternative($scale, $altMedium['file']);
// Locate or generate smaller retina images.
for ($i = $scale-1; $i > 1; $i--) {
$altFilename = "{$path}/{$basename}@{$i}x.{$ext}";
if (file_exists($altFilename)) {
$scaled = MediumFactory::fromFile($altFilename);
} else {
$scaled = MediumFactory::scaledFromMedium($altMedium, $scale, $i)['file'];
}
if ($scaled) {
$medium->addAlternative($i, $scaled);
}
}
}
$meta = "{$path}/{$basename}.{$ext}.yaml";
if (file_exists($meta)) {
$medium->addMetaFile($meta);
}
$meta = "{$path}/{$basename}.{$ext}.meta.yaml";
if (file_exists($meta)) {
$medium->addMetaFile($meta);
}
$thumb = "{$path}/{$basename}.thumb.{$ext}";
if (file_exists($thumb)) {
$medium->set('thumbnails.page', $thumb);
}
$this->add($stream, $medium);
return $medium;
}
}

View File

@@ -150,8 +150,8 @@ class Medium extends Data implements RenderableInterface
/**
* Get/set querystring for the file's url
*
* @param string $hash
* @param boolean $withHash
* @param string $querystring
* @param boolean $withQuestionmark
* @return string
*/
public function querystring($querystring = null, $withQuestionmark = true)