diff --git a/CHANGELOG.md b/CHANGELOG.md index a58998227..b7f03a163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v0.9.21 +## 04/07/2015 + +1. [](#new) + * Major Media functionality enhancements: SVG, Animated GIF, Video support! + * Added ability to configure default image quality in system configuration + * Added `sizes` attributes for custom retina image breakpoints +2. [](#improved) + * Don't scale @1x retina images + * Add filter to Iterator class + * Updated various composer packages + * Various PSR fixes + # v0.9.20 ## 03/24/2015 diff --git a/composer.json b/composer.json index eae309e79..53bcf0113 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "doctrine/cache": "~1.3", "maximebf/debugbar": "dev-master", "filp/whoops": "1.2.*@dev", - "monolog/monolog": "~1.1", + "monolog/monolog": "~1.0", "gregwar/image": "~2.0", "ircmaxell/password-compat": "1.0.*", "mrclay/minify": "dev-master", diff --git a/system/config/media.yaml b/system/config/media.yaml index 057d8be91..62dfa75eb 100644 --- a/system/config/media.yaml +++ b/system/config/media.yaml @@ -20,10 +20,15 @@ png: thumb: media/thumb-png.png mime: image/png gif: - type: image + type: animated thumb: media/thumb-gif.png mime: image/gif +svg: + type: vector + thumb: media/thumb-gif.png + mime: image/svg+xml + mp4: type: video thumb: media/thumb-mp4.png diff --git a/system/config/streams.yaml b/system/config/streams.yaml index a38b5805e..c06ec3547 100644 --- a/system/config/streams.yaml +++ b/system/config/streams.yaml @@ -8,6 +8,7 @@ schemes: type: ReadOnlyStream paths: - user://images + - system://images page: type: ReadOnlyStream diff --git a/system/config/system.yaml b/system/config/system.yaml index aaafeef79..59f4b36a9 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -69,4 +69,5 @@ debugger: close_connection: true # Close the connection before calling onShutdown(). false for debugging images: + default_image_quality: 85 # Default image quality to use when resampling images (85%) debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example diff --git a/system/defines.php b/system/defines.php index f915c3815..4413c4a24 100644 --- a/system/defines.php +++ b/system/defines.php @@ -2,7 +2,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '0.9.20'); +define('GRAV_VERSION', '0.9.21'); define('DS', '/'); // Directories and Paths diff --git a/system/src/Grav/Common/GPM/Local/Collection.php b/system/src/Grav/Common/GPM/AbstractCollection.php similarity index 50% rename from system/src/Grav/Common/GPM/Local/Collection.php rename to system/src/Grav/Common/GPM/AbstractCollection.php index 9e3cf3638..7c1974d22 100644 --- a/system/src/Grav/Common/GPM/Local/Collection.php +++ b/system/src/Grav/Common/GPM/AbstractCollection.php @@ -1,19 +1,19 @@ items as $name => $theme) { - $items[$name] = $theme->toArray(); + foreach ($this->items as $name => $package) { + $items[$name] = $package->toArray(); } return json_encode($items); @@ -23,8 +23,8 @@ class Collection extends Iterator { $items = []; - foreach ($this->items as $name => $theme) { - $items[$name] = $theme->toArray(); + foreach ($this->items as $name => $package) { + $items[$name] = $package->toArray(); } return $items; diff --git a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php new file mode 100644 index 000000000..46c9e894a --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php @@ -0,0 +1,34 @@ +items as $name => $package) { + $items[$name] = $package->toArray(); + } + + return json_encode($items); + } + + public function toArray() + { + $items = []; + + foreach ($this->items as $name => $package) { + $items[$name] = $package->toArray(); + } + + return $items; + } +} diff --git a/system/src/Grav/Common/GPM/Common/CachedCollection.php b/system/src/Grav/Common/GPM/Common/CachedCollection.php new file mode 100644 index 000000000..fd6ae9420 --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/CachedCollection.php @@ -0,0 +1,21 @@ + $item) { + $this->append([$name => $item]); + } + } +} diff --git a/system/src/Grav/Common/GPM/Common/Package.php b/system/src/Grav/Common/GPM/Common/Package.php new file mode 100644 index 000000000..ff9a087c9 --- /dev/null +++ b/system/src/Grav/Common/GPM/Common/Package.php @@ -0,0 +1,42 @@ +data = $package; + + if ($type) { + $this->data->set('package_type', $type); + } + } + + public function getData() { + return $this->data; + } + + public function __get($key) { + return $this->data->get($key); + } + + public function __isset($key) { + return isset($this->data->$key); + } + + public function __toString() { + return $this->toJson(); + } + + public function toJson() { + return $this->data->toJson(); + } + + public function toArray() { + return $this->data->toArray(); + } + +} diff --git a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php new file mode 100644 index 000000000..314829378 --- /dev/null +++ b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php @@ -0,0 +1,16 @@ + $data) { + $this->items[$name] = new Package($data, $this->type); + } + } +} diff --git a/system/src/Grav/Common/GPM/Local/Package.php b/system/src/Grav/Common/GPM/Local/Package.php index 4ba7bc0b4..6af4a4e1a 100644 --- a/system/src/Grav/Common/GPM/Local/Package.php +++ b/system/src/Grav/Common/GPM/Local/Package.php @@ -2,39 +2,24 @@ namespace Grav\Common\GPM\Local; use Grav\Common\Data\Data; +use Grav\Common\GPM\Common\Package as BasePackage; -/** - * Class Package - * @package Grav\Common\GPM\Local - */ -class Package +class Package extends BasePackage { - /** - * @var Data - */ - protected $data; - /** - * @var \Grav\Common\Data\Blueprint - */ - protected $blueprints; + protected $settings; - /** - * @param Data $package - * @param bool $package_type - */ - public function __construct(Data $package, $package_type = false) + public function __construct(Data $package, $package_type = null) { - $this->data = $package; - $this->blueprints = $this->data->blueprints(); + $data = new Data($package->blueprints()->toArray()); + parent::__construct($data, $package_type); - if ($package_type) { - $html_description = \Parsedown::instance()->line($this->blueprints->get('description')); - $this->blueprints->set('package_type', $package_type); - $this->blueprints->set('slug', $this->blueprints->name); - $this->blueprints->set('description_html', $html_description); - $this->blueprints->set('description_plain', strip_tags($html_description)); - $this->blueprints->set('symlink', is_link(USER_DIR . $package_type . DS . $this->blueprints->name)); - } + $this->settings = $package->toArray(); + + $html_description = \Parsedown::instance()->line($this->description); + $this->data->set('slug', $this->name); + $this->data->set('description_html', $html_description); + $this->data->set('description_plain', strip_tags($html_description)); + $this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->name)); } /** @@ -42,47 +27,6 @@ class Package */ public function isEnabled() { - return $this->data['enabled']; - } - - /** - * @return Data - */ - public function getData() - { - return $this->data; - } - - /** - * @param $key - * @return mixed - */ - public function __get($key) - { - return $this->blueprints->get($key); - } - - /** - * @return string - */ - public function __toString() - { - return $this->toJson(); - } - - /** - * @return string - */ - public function toJson() - { - return $this->blueprints->toJson(); - } - - /** - * @return array - */ - public function toArray() - { - return $this->blueprints->toArray(); + return $this->settings['enabled']; } } diff --git a/system/src/Grav/Common/GPM/Local/Packages.php b/system/src/Grav/Common/GPM/Local/Packages.php index 4498e6895..a8ed6a455 100644 --- a/system/src/Grav/Common/GPM/Local/Packages.php +++ b/system/src/Grav/Common/GPM/Local/Packages.php @@ -1,28 +1,17 @@ new Plugins(), - 'themes' => new Themes() - ]; - } + $items = [ + 'plugins' => new Plugins(), + 'themes' => new Themes() + ]; - $this->plugins = self::$cache[__METHOD__]['plugins']; - $this->themes = self::$cache[__METHOD__]['themes']; - - $this->append(['plugins' => $this->plugins]); - $this->append(['themes' => $this->themes]); + parent::__construct($items); } } diff --git a/system/src/Grav/Common/GPM/Local/Plugins.php b/system/src/Grav/Common/GPM/Local/Plugins.php index b52efea93..b1c3c64a7 100644 --- a/system/src/Grav/Common/GPM/Local/Plugins.php +++ b/system/src/Grav/Common/GPM/Local/Plugins.php @@ -5,22 +5,18 @@ namespace Grav\Common\GPM\Local; * Class Plugins * @package Grav\Common\GPM\Local */ -class Plugins extends Collection +class Plugins extends AbstractPackageCollection { /** * @var string */ - private $type = 'plugins'; + protected $type = 'plugins'; /** * Local Plugins Constructor */ public function __construct() { - $grav = self::getGrav(); - - foreach ($grav['plugins']->all() as $name => $data) { - $this->items[$name] = new Package($data, $this->type); - } + parent::__construct(self::getGrav()['plugins']->all()); } } diff --git a/system/src/Grav/Common/GPM/Local/Themes.php b/system/src/Grav/Common/GPM/Local/Themes.php index 673490144..64d23b903 100644 --- a/system/src/Grav/Common/GPM/Local/Themes.php +++ b/system/src/Grav/Common/GPM/Local/Themes.php @@ -1,15 +1,22 @@ all() as $name => $data) { - $this->items[$name] = new Package($data, $this->type); - } + parent::__construct(self::getGrav()['themes']->all()); } } diff --git a/system/src/Grav/Common/GPM/PackageInterface.php b/system/src/Grav/Common/GPM/PackageInterface.php new file mode 100644 index 000000000..ad856c08f --- /dev/null +++ b/system/src/Grav/Common/GPM/PackageInterface.php @@ -0,0 +1,58 @@ +findResource('cache://gpm', true, true); @@ -35,28 +35,11 @@ class Collection extends Iterator { $this->repository = $repository; $this->raw = $this->cache->fetch(md5($this->repository)); - } - public function toJson() - { - $items = []; - - foreach ($this->items as $name => $theme) { - $items[$name] = $theme->toArray(); + $this->fetch($refresh, $callback); + foreach (json_decode($this->raw, true) as $slug => $data) { + $this->items[$slug] = new Package($data, $this->type); } - - return json_encode($items); - } - - public function toArray() - { - $items = []; - - foreach ($this->items as $name => $theme) { - $items[$name] = $theme->toArray(); - } - - return $items; } public function fetch($refresh = false, $callback = null) diff --git a/system/src/Grav/Common/GPM/Remote/Grav.php b/system/src/Grav/Common/GPM/Remote/Grav.php index 8e6510060..ed5582d57 100644 --- a/system/src/Grav/Common/GPM/Remote/Grav.php +++ b/system/src/Grav/Common/GPM/Remote/Grav.php @@ -1,9 +1,11 @@ repository); + $cache_dir = self::getGrav()['locator']->findResource('cache://gpm', true, true); + $this->cache = new FilesystemCache($cache_dir); + $this->raw = $this->cache->fetch(md5($this->repository)); $this->fetch($refresh, $callback); - $this->data = json_decode($this->raw); - $this->version = @$this->data->version ?: '-'; - $this->date = @$this->data->date ?: '-'; + $this->data = json_decode($this->raw, true); + $this->version = @$this->data['version'] ?: '-'; + $this->date = @$this->data['date'] ?: '-'; - foreach ($this->data->assets as $slug => $data) { + foreach ($this->data['assets'] as $slug => $data) { $this->items[$slug] = new Package($data); } } diff --git a/system/src/Grav/Common/GPM/Remote/Package.php b/system/src/Grav/Common/GPM/Remote/Package.php index 45da29478..9b0367f66 100644 --- a/system/src/Grav/Common/GPM/Remote/Package.php +++ b/system/src/Grav/Common/GPM/Remote/Package.php @@ -1,32 +1,12 @@ data = $package; - if ($package_type) { - $this->data->package_type = $package_type; - } - } +use Grav\Common\Data\Data; +use Grav\Common\GPM\Common\Package as BasePackage; - public function getData() { - return $this->data; +class Package extends BasePackage { + public function __construct($package, $package_type = null) { + $data = new Data($package); + parent::__construct($data, $package_type); } - - public function __get($key) { - return $this->data->$key; - } - - public function __toString() { - return $this->toJson(); - } - - public function toJson() { - return json_encode($this->data); - } - - public function toArray() { - return $this->data; - } - } diff --git a/system/src/Grav/Common/GPM/Remote/Packages.php b/system/src/Grav/Common/GPM/Remote/Packages.php index f78e9c67e..75f397fa3 100644 --- a/system/src/Grav/Common/GPM/Remote/Packages.php +++ b/system/src/Grav/Common/GPM/Remote/Packages.php @@ -1,28 +1,17 @@ new Plugins($refresh, $callback), - 'themes' => new Themes($refresh, $callback) - ]; - } + $items = [ + 'plugins' => new Plugins($refresh, $callback), + 'themes' => new Themes($refresh, $callback) + ]; - $this->plugins = self::$cache[__METHOD__]['plugins']->toArray(); - $this->themes = self::$cache[__METHOD__]['themes']->toArray(); - - $this->append(['plugins' => $this->plugins]); - $this->append(['themes' => $this->themes]); + parent::__construct($items); } } diff --git a/system/src/Grav/Common/GPM/Remote/Plugins.php b/system/src/Grav/Common/GPM/Remote/Plugins.php index fa71fbab6..ec6d64521 100644 --- a/system/src/Grav/Common/GPM/Remote/Plugins.php +++ b/system/src/Grav/Common/GPM/Remote/Plugins.php @@ -1,21 +1,24 @@ repository); - - $this->fetch($refresh, $callback); - $this->data = json_decode($this->raw); - - foreach ($this->data as $slug => $data) { - $this->items[$slug] = new Package($data, $this->type); - } + parent::__construct($this->repository, $refresh, $callback); } } diff --git a/system/src/Grav/Common/GPM/Remote/Themes.php b/system/src/Grav/Common/GPM/Remote/Themes.php index fcb5a34c4..759b7e10a 100644 --- a/system/src/Grav/Common/GPM/Remote/Themes.php +++ b/system/src/Grav/Common/GPM/Remote/Themes.php @@ -1,21 +1,24 @@ repository); - - $this->fetch($refresh, $callback); - $this->data = json_decode($this->raw); - - foreach ($this->data as $slug => $data) { - $this->items[$slug] = new Package($data, $this->type); - } + parent::__construct($this->repository, $refresh, $callback); } } diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php index 211b2a415..530415870 100644 --- a/system/src/Grav/Common/GPM/Response.php +++ b/system/src/Grav/Common/GPM/Response.php @@ -78,7 +78,6 @@ class Response $method = 'get' . ucfirst(strtolower(self::$method)); self::$callback = $callback; - return static::$method($uri, $options, $callback); } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 38ff62fa1..e0a91a06a 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -10,7 +10,7 @@ use Grav\Common\Service\StreamsServiceProvider; use RocketTheme\Toolbox\DI\Container; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\EventDispatcher; -use Grav\Common\Page\Medium; +use Grav\Common\Page\Medium\Medium; /** * Grav diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php index 9105c0630..c9b0ee776 100644 --- a/system/src/Grav/Common/Iterator.php +++ b/system/src/Grav/Common/Iterator.php @@ -197,4 +197,23 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable return $this; } + + /** + * Filter elements from the list + * @param callable|null $callback A function the receives ($value, $key) and must return a boolean to indicate filter status + * @return $this + */ + public function filter(callable $callback = null) + { + foreach ($this->items as $key => $value) { + if ( + ($callback && !call_user_func($callback, $value, $key)) || + (!$callback && !(bool) $value) + ) { + unset($this->items[$key]); + } + } + + return $this; + } } diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php index c507606bd..dd4380b5c 100644 --- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -4,7 +4,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Config\Config; use Grav\Common\Debugger; use Grav\Common\GravTrait; -use Grav\Common\Page\Medium; +use Grav\Common\Page\Medium\Medium; use Grav\Common\Uri; /** @@ -43,6 +43,17 @@ trait ParsedownGravTrait $this->setSpecialChars($defaults['special_chars']); } + /** + * Make the element function publicly accessible, Medium uses this to render from Twig + * + * @param array $Element + * @return string markup + */ + public function elementToHtml(array $Element) + { + return $this->element($Element); + } + /** * Setter for special chars * @@ -108,6 +119,7 @@ trait ParsedownGravTrait $alt = $excerpt['element']['attributes']['alt'] ?: ''; $title = $excerpt['element']['attributes']['title'] ?: ''; + $class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : ''; //get the url and parse it $url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src'])); @@ -136,69 +148,32 @@ trait ParsedownGravTrait } // if there is a media file that matches the path referenced.. - if ($media && isset($media->images()[$url['path']])) { + if ($media && isset($media->all()[$url['path']])) { // get the medium object - $medium = $media->images()[$url['path']]; + $medium = $media->all()[$url['path']]; // if there is a query, then parse it and build action calls if (isset($url['query'])) { - parse_str($url['query'], $actions); + $actions = array_reduce(explode('&', $url['query']), function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = isset($parts[1]) ? $parts[1] : null; + $carry[] = [ 'method' => $parts[0], 'params' => $value ]; + + return $carry; + }, []); } // loop through actions for the image and call them - foreach ($actions as $action => $params) { - // as long as it's a valid action - if (in_array($action, Medium::$valid_actions)) { - call_user_func_array(array(&$medium, $action), explode(',', $params)); - } + foreach ($actions as $action) { + $medium = call_user_func_array(array($medium, $action['method']), explode(',', $action['params'])); } - $data = $medium->htmlRaw(); - - // set the src element with the new generated url - if (!isset($actions['lightbox'])) { - $excerpt['element']['attributes']['src'] = $data['img_src']; - - if ($data['img_srcset']) { - $excerpt['element']['attributes']['srcset'] = $data['img_srcset'];; - $excerpt['element']['attributes']['sizes'] = '100vw'; - } - - } else { - // Create the custom lightbox element - - $attributes = $data['a_attributes']; - $attributes['href'] = $data['a_href']; - - $img_attributes = [ - 'src' => $data['img_src'], - 'alt' => $alt, - 'title' => $title - ]; - - if ($data['img_srcset']) { - $img_attributes['srcset'] = $data['img_srcset']; - $img_attributes['sizes'] = '100vw'; - } - - $element = array( - 'name' => 'a', - 'attributes' => $attributes, - 'handler' => 'element', - 'text' => array( - 'name' => 'img', - 'attributes' => $img_attributes - ) - ); - - // Set any custom classes on the lightbox element - if (isset($excerpt['element']['attributes']['class'])) { - $element['attributes']['class'] = $excerpt['element']['attributes']['class']; - } - - // Set the lightbox element on the Excerpt - $excerpt['element'] = $element; + if (isset($url['fragment'])) { + $medium->urlHash($url['fragment']); } + + $excerpt['element'] = $medium->parseDownElement($title, $alt, $class); + } else { // not a current page media file, see if it needs converting to relative $excerpt['element']['attributes']['src'] = Uri::buildUrl($url); @@ -227,7 +202,7 @@ trait ParsedownGravTrait $url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href'])); // if there is no scheme, the file is local - if (!isset($url['scheme']) and (count($url) > 0)) { + if (!isset($url['scheme']) && (count($url) > 0)) { // convert the URl is required $excerpt['element']['attributes']['href'] = $this->convertUrl(Uri::buildUrl($url)); } diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php index e6438327a..1495eddbd 100644 --- a/system/src/Grav/Common/Page/Media.php +++ b/system/src/Grav/Common/Page/Media.php @@ -5,6 +5,8 @@ use Grav\Common\Getters; use Grav\Common\Grav; use Grav\Common\Config\Config; use Grav\Common\GravTrait; +use Grav\Common\Page\Medium\Medium; +use Grav\Common\Page\Medium\MediumFactory; /** * Media is a holder object that contains references to the media of page. This object is created and @@ -40,6 +42,8 @@ class Media extends Getters $iterator = new \DirectoryIterator($path); + $media = []; + /** @var \DirectoryIterator $info */ foreach ($iterator as $info) { // Ignore folders and Markdown files. @@ -47,75 +51,74 @@ class Media extends Getters continue; } - // Find out the real filename, in case of we are at the metadata. - $filename = $info->getFilename(); - list($basename, $ext, $meta, $alternative) = $this->getFileParts($filename); + // Find out what type we're dealing with + list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename()); - // Get medium instance if it already exists. - $medium = $this->get("{$basename}.{$ext}"); + $media["{$basename}.{$ext}"] = isset($media["{$basename}.{$ext}"]) ? $media["{$basename}.{$ext}"] : []; - if (!$alternative) { - - $medium = $medium ? $medium : $this->createMedium($info->getPathname()); - - if (!$medium) { - continue; - } - - if ($meta) { - $medium->addMetaFile($meta); - } else { - $medium->set('size', $info->getSize()); - } + if ($type === 'alternative') { + $media["{$basename}.{$ext}"][$type] = isset($media["{$basename}.{$ext}"][$type]) ? $media["{$basename}.{$ext}"][$type] : []; + $media["{$basename}.{$ext}"][$type][$extra] = $info->getPathname(); } else { - - $altMedium = $this->createMedium($info->getPathname()); - - if (!$altMedium) { - continue; - } - - $altMedium->set('size', $info->getSize()); - - if (!$medium) { - $medium = $this->createMedium("{$path}/${basename}.${ext}"); - - if ($medium) { - $medium->set('size', filesize("{$path}/${basename}.${ext}")); - } - } - - $medium = $medium ? $medium : $this->scaleMedium($altMedium, $alternative, 1); - - $medium->addAlternative($this->parseRatio($alternative), $altMedium); + $media["{$basename}.{$ext}"][$type] = $info->getPathname(); } - - $this->add("{$basename}.{$ext}", $medium); } - foreach ($this->images() as $medium) { + foreach ($media as $name => $types) { + // First prepare the alternatives in case there is no base medium + if (!empty($types['alternative'])) { + foreach ($types['alternative'] as $ratio => &$file) { + $file = MediumFactory::fromFile($file); + } + } - $alternatives = $medium->getAlternatives(); + // Create the base medium + if (!empty($types['base'])) { + $medium = MediumFactory::fromFile($types['base']); + } else if (!empty($types['alternative'])) { + $altMedium = reset($types['alternative']); + $ratio = key($types['alternative']); - if (empty($alternatives)) { + $medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1); + } + + if (!$medium) { continue; } - $max = max(array_keys($alternatives)); + if (!empty($types['meta'])) { + $medium->addMetaFile($types['meta']); + } - for ($i=2; $i < $max; $i++) { + if (!empty($types['thumb'])) { + // We will not turn it into medium yet because user might never request the thumbnail + // not wasting any resources on that, maybe we should do this for medium in general? + $medium->set('thumbnails.page', $types['thumb']); + } - if (isset($alternatives[$i])) { - continue; + // Build missing alternatives + if (!empty($types['alternative'])) { + $alternatives = $types['alternative']; + + $max = max(array_keys($alternatives)); + + for ($i=2; $i < $max; $i++) { + if (isset($alternatives[$i])) { + continue; + } + + $types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max], $max, $i); } - $medium->addAlternative($i, $this->scaleMedium($alternatives[$max], $max, $i)); + foreach ($types['alternative'] as $ratio => $altMedium) { + $medium->addAlternative($ratio, $altMedium); + } } + + $this->add($name, $medium); } } - - /** * Get medium by filename. * @@ -182,94 +185,6 @@ class Media extends Getters return $this->files; } - /** - * Create a Medium object from a file - * - * @param string $file - * - * @return Medium|null - */ - protected function createMedium($file) - { - if (!file_exists($file)) { - return null; - } - - $path = dirname($file); - $filename = basename($file); - $parts = explode('.', $filename); - $ext = array_pop($parts); - $basename = implode('.', $parts); - - /** @var Config $config */ - $config = self::getGrav()['config']; - - // Check if medium type has been configured. - $params = $config->get("media.".strtolower($ext)); - if (!$params) { - return null; - } - - // Add default settings for undefined variables. - $params += $config->get('media.defaults'); - $params += array( - 'type' => 'file', - 'thumb' => 'media/thumb.png', - 'mime' => 'application/octet-stream', - 'name' => $filename, - 'filename' => $filename, - 'basename' => $basename, - 'extension' => $ext, - 'path' => $path, - 'modified' => filemtime($file), - ); - - $locator = self::getGrav()['locator']; - - $lookup = $locator->findResources('image://'); - foreach ($lookup as $lookupPath) { - if (is_file($lookupPath . $params['thumb'])) { - $params['thumb'] = $lookupPath . $params['thumb']; - break; - } - } - - return new Medium($params); - } - - protected function scaleMedium($medium, $from, $to) - { - $from = $this->parseRatio($from); - $to = $this->parseRatio($to); - - if ($to > $from) { - return $medium; - } - - $ratio = $to / $from; - $width = (int) ($medium->get('width') * $ratio); - $height = (int) ($medium->get('height') * $ratio); - - $basename = $medium->get('basename'); - $basename = str_replace('@'.$from.'x', '@'.$to.'x', $basename); - - $debug = $medium->get('debug'); - $medium->set('debug', false); - - $file = $medium->resize($width, $height)->setPrettyName($basename)->url(); - $file = preg_replace('|'. preg_quote(self::getGrav()['base_url_relative']) .'$|', '', GRAV_ROOT) . $file; - - $medium->set('debug', $debug); - - $size = filesize($file); - - $medium = $this->createMedium($file); - $medium->set('size', $size); - - return $medium; - } - - /** * @internal */ @@ -302,35 +217,35 @@ class Media extends Getters $fileParts = explode('.', $filename); $name = array_shift($fileParts); - $alternative = false; + $type = 'base'; + $extra = null; - if (preg_match('/(.*)@(\d+x)$/', $name, $matches)) { + if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) { $name = $matches[1]; - $alternative = $matches[2]; - } + $extension = $matches[3]; + $extra = (int) $matches[2]; + $type = 'alternative'; - $extension = null; - while (($part = array_shift($fileParts)) !== null) { - if ($part != 'meta') { - if (isset($extension)) { - $name .= '.' . $extension; + 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; } - $extension = $part; - } else { - break; } } - $meta = implode('.', $fileParts); - return array($name, $extension, $meta, $alternative); - } - - protected function parseRatio($ratio) - { - if (!is_numeric($ratio)) { - $ratio = (float) trim($ratio, 'x'); - } - - return $ratio; + return array($name, $extension, $type, $extra); } } diff --git a/system/src/Grav/Common/Page/Medium.php b/system/src/Grav/Common/Page/Medium.php deleted file mode 100644 index 224317b79..000000000 --- a/system/src/Grav/Common/Page/Medium.php +++ /dev/null @@ -1,521 +0,0 @@ - [ 0, 1 ], - 'forceResize' => [ 0, 1 ], - 'cropResize' => [ 0, 1 ], - 'crop' => [ 0, 1, 2, 3 ], - 'cropResize' => [ 0, 1 ], - 'zoomCrop' => [ 0, 1 ] - ]; - - /** - * @var array - */ - protected $meta = array(); - - /** - * @var array - */ - protected $alternatives = array(); - - /** - * @var string - */ - protected $linkTarget; - - /** - * @var string - */ - protected $linkSrcset; - - /** - * @var string - */ - protected $linkAttributes = []; - - /** - * Construct. - * - * @param array $items - * @param Blueprint $blueprint - */ - public function __construct($items = array(), Blueprint $blueprint = null) - { - parent::__construct($items, $blueprint); - - $file_path = $this->get('path') . '/' . $this->get('filename'); - $file_parts = pathinfo($file_path); - - $this->set('thumb', $file_path); - $this->set('extension', $file_parts['extension']); - $this->set('filename', $this->get('filename')); - - if ($this->get('type') == 'image') { - $image_info = getimagesize($file_path); - $this->def('width', $image_info[0]); - $this->def('height', $image_info[1]); - $this->def('mime', $image_info['mime']); - $this->reset(); - } else { - $this->def('mime', 'application/octet-stream'); - } - - $this->set('debug', self::getGrav()['config']->get('system.images.debug')); - } - - /** - * Return string representation of the object (html or url). - * - * @return string - */ - public function __toString() - { - return $this->linkImage ? $this->html() : $this->url(); - } - - /** - * Return PATH to file. - * - * @return string path to file - */ - public function path() - { - if ($this->image) { - $output = $this->saveImage(); - $this->reset(); - $output = GRAV_ROOT . '/' . $output; - } else { - $output = $this->get('path') . '/' . $this->get('filename'); - } - return $output; - } - - /** - * Return URL to file. - * - * @param bool $reset - * @return string - */ - public function url($reset = true) - { - if ($this->image) { - $output = '/' . $this->saveImage(); - - if ($reset) { - $this->reset(); - } - } else { - $output = preg_replace('|^' . GRAV_ROOT . '|', '', $this->get('path')) . '/' . $this->get('filename'); - } - - return self::getGrav()['base_url'] . $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 = [ $this->url($reset) . ' ' . $this->get('width') . 'w' ]; - - foreach ($this->alternatives as $ratio => $medium) { - $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w'; - } - - return implode(', ', $srcset); - } - - /** - * Returns tag from the medium. - * - * @param string $title - * @param string $class - * @param string $type - * @param int $quality - * @param bool $reset - * @return string - */ - public function img($title = null, $class = null, $type = null, $quality = 80, $reset = true) - { - if (!$this->image) { - $this->image(); - } - - $output = $this->html($title, $class, $type, $quality, $reset); - - return $output; - } - - /** - * Return HTML markup from the medium. - * - * @param string $title - * @param string $class - * @param bool $reset - * @return string - */ - public function html($title = null, $class = null, $reset = true) - { - $data = $this->htmlRaw($reset); - - $title = $title ? $title : $this->get('title'); - $class = $class ? $class : ''; - - if ($this->image) { - $attributes = $data['img_srcset'] ? ' srcset="' . $data['img_srcset'] . '" sizes="100vw"' : ''; - $output = '' . $title . ''; - } else { - $output = $data['text']; - } - - if (isset($data['a_href'])) { - $attributes = ''; - foreach ($data['a_attributes'] as $prop => $value) { - $attributes .= " {$prop}=\"{$value}\""; - } - - $output = '' . $output . ''; - } - - return $output; - } - - /** - * Return HTML array from medium. - * - * @param bool $reset - * @param string $title - * - * @return array - */ - public function htmlRaw($reset = true, $title = '') - { - $output = []; - - if ($this->image) { - $output['img_src'] = $this->url(false); - $output['img_srcset'] = $this->srcset($reset); - } else { - $output['text'] = $title; - } - - if ($this->linkTarget) { - $output['a_href'] = $this->linkTarget; - $output['a_attributes'] = $this->linkAttributes; - - $this->linkTarget = null; - $this->linkAttributes = []; - } - - return $output; - } - - /** - * Sets the quality of the image - * @param Int $quality 0-100 quality - * @return Medium - */ - public function quality($quality) - { - $this->quality = $quality; - return $this; - } - - /** - * Sets image output format. - * - * @param string $type - * @param int $quality - * @return $this - */ - public function format($type = null, $quality = 80) - { - if (!$this->image) { - $this->image(); - } - - $this->type = $type; - $this->quality = $quality; - return $this; - } - - /** - * Enable link for the medium object. - * - * @param null $width - * @param null $height - * @return $this - */ - public function link($width = null, $height = null) - { - if ($this->image) { - if ($width && $height) { - $this->cropResize($width, $height); - } - - $this->linkTarget = $this->url(false); - $srcset = $this->srcset(); - - if ($srcset) { - $this->linkAttributes['data-srcset'] = $srcset; - } - } else { - // TODO: we need to find out URI in a bit better way. - $this->linkTarget = self::getGrav()['base_url'] . preg_replace('|^' . GRAV_ROOT . '|', '', $this->get('path')) . '/' . $this->get('filename'); - } - - return $this; - } - - /** - * Enable lightbox for the medium. - * - * @param null $width - * @param null $height - * @return Medium - */ - public function lightbox($width = null, $height = null) - { - $this->linkAttributes['rel'] = 'lightbox'; - - return $this->link($width, $height); - } - - /** - * Reset image. - * - * @return $this - */ - public function reset() - { - $this->image = null; - - if ($this->get('type') == 'image') { - $this->image(); - $this->filter(); - } - $this->type = 'guess'; - $this->quality = 80; - $this->debug_watermarked = false; - - return $this; - } - - /** - * Forward the call to the image processing method. - * - * @param string $method - * @param mixed $args - * @return $this|mixed - */ - public function __call($method, $args) - { - if ($method == 'cropZoom') { - $method = 'zoomCrop'; - } - - // Always initialize image. - if (!$this->image) { - $this->image(); - } - - try { - $result = call_user_func_array(array($this->image, $method), $args); - - foreach ($this->alternatives as $ratio => $medium) { - $args_copy = $args; - - if (isset(self::$size_param_actions[$method])) { - foreach (self::$size_param_actions[$method] as $param) { - if (isset($args_copy[$param])) { - $args_copy[$param] = (int) $args_copy[$param] * $ratio; - } - } - } - - call_user_func_array(array($medium, $method), $args_copy); - } - } catch (\BadFunctionCallException $e) { - $result = null; - } - - // Returns either current object or result of the action. - return $result instanceof ImageFile ? $this : $result; - } - - /** - * Gets medium image, resets image manipulation operations. - * - * @param string $variable - * @return $this - */ - public function image($variable = 'thumb') - { - $locator = self::getGrav()['locator']; - - // TODO: add default file - $file = $this->get($variable); - $this->image = ImageFile::open($file) - ->setCacheDir($locator->findResource('cache://images', false)) - ->setActualCacheDir($locator->findResource('cache://images', true)) - ->setPrettyName(basename($this->get('basename'))); - - $this->filter(); - - return $this; - } - - /** - * Save the image with cache. - * - * @return mixed|string - */ - protected function saveImage() - { - if (!$this->image) { - $this->image(); - } - - if ($this->get('debug') && !$this->debug_watermarked) { - $ratio = $this->get('ratio'); - if (!$ratio) { - $ratio = 1; - } - - $locator = self::getGrav()['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->type, $this->quality); - } - - /** - * Add meta file for the medium. - * - * @param $type - * @return $this - */ - public function addMetaFile($type) - { - $this->meta[$type] = $type; - - $path = $this->get('path') . '/' . $this->get('filename') . '.meta.' . $type; - if ($type == 'yaml') { - $this->merge(CompiledYamlFile::instance($path)->content()); - } elseif (in_array($type, array('jpg', 'jpeg', 'png', 'gif'))) { - $this->set('thumb', $path); - } - $this->reset(); - - return $this; - } - - /** - * Add alternative Medium to this Medium. - * - * @param $ratio - * @param Medium $alternative - */ - public function addAlternative($ratio, Medium $alternative) - { - if (!is_numeric($ratio) || $ratio === 0) { - return; - } - - $alternative->set('ratio', $ratio); - - $this->alternatives[(float) $ratio] = $alternative; - } - - public function getAlternatives() - { - return $this->alternatives; - } - - /** - * Filter image by using user defined filter parameters. - * - * @param string $filter Filter to be used. - */ - public function filter($filter = 'image.filters.default') - { - $filters = (array) $this->get($filter, array()); - foreach ($filters as $params) { - $params = (array) $params; - $method = array_shift($params); - $this->__call($method, $params); - } - } -} diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php new file mode 100644 index 000000000..ede065f09 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -0,0 +1,397 @@ + [ 0, 1 ], + 'forceResize' => [ 0, 1 ], + 'cropResize' => [ 0, 1 ], + 'crop' => [ 0, 1, 2, 3 ], + 'cropResize' => [ 0, 1 ], + 'zoomCrop' => [ 0, 1 ] + ]; + + /** + * Construct. + * + * @param array $items + * @param Blueprint $blueprint + */ + public function __construct($items = [], Blueprint $blueprint = null) + { + parent::__construct($items, $blueprint); + + $image_info = getimagesize($this->get('filepath')); + $this->def('width', $image_info[0]); + $this->def('height', $image_info[1]); + $this->def('mime', $image_info['mime']); + $this->def('debug', self::$grav['config']->get('system.images.debug')); + + $this->set('thumbnails.media', $this->get('filepath')); + + $this->default_quality = self::$grav['config']->get('system.images.default_image_quality', 85); + + $this->reset(); + } + + /** + * Add meta file for the medium. + * + * @param $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) + { + $output = preg_replace('|^' . GRAV_ROOT . '|', '', $this->saveImage()); + + if ($reset) { + $this->reset(); + } + + return self::$grav['base_url'] . $output . $this->urlHash(); + } + + + /** + * 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 = [ $this->url($reset) . ' ' . $this->get('width') . 'w' ]; + + foreach ($this->alternatives as $ratio => $medium) { + $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w'; + } + + return implode(', ', $srcset); + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param boolean $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; + empty($attributes['sizes']) && $attributes['sizes'] = $this->sizes(); + } + + return [ 'name' => 'img', 'attributes' => $attributes ]; + } + + /** + * Reset image. + * + * @return $this + */ + public function reset() + { + parent::reset(); + + if ($this->image) { + $this->image(); + $this->filter(); + } + + $this->format = 'guess'; + $this->quality = $this->default_quality; + + $this->debug_watermarked = false; + + return $this; + } + + /** + * Turn the current Medium into a Link + * + * @param boolean $reset + * @param array $attributes + * @return Link + */ + public function link($reset = true, array $attributes = []) + { + $attributes['href'] = $this->url(false); + $srcset = $this->srcset(false); + if ($srcset) { + $attributes['data-srcset'] = $srcset; + } + + return parent::link($reset, $attributes); + } + + /** + * Turn the current Medium inta a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param boolean $reset + * @return Link + */ + public function lightbox($width = null, $height = null, $reset = true) + { + if ($this->mode !== 'source') { + $this->display('source'); + } + + if ($width && $height) { + $this->cropResize($width, $height); + } + + return parent::lightbox($width, $height, $reset); + } + + /** + * Sets the quality of the image + * + * @param int $quality 0-100 quality + * @return Medium + */ + public function quality($quality) + { + if (!$this->image) { + $this->image(); + } + + $this->quality = $quality; + return $this; + } + + /** + * 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 $this + */ + public function sizes($sizes = null) + { + + if ($sizes) { + $this->attributes['sizes'] = $sizes; + return $this; + } + + return empty($this->attributes['sizes']) ? '100vw' : $this->attributes['sizes']; + } + + /** + * Forward the call to the image processing method. + * + * @param string $method + * @param mixed $args + * @return $this|mixed + */ + public function __call($method, $args) + { + if ($method == 'cropZoom') { + $method = 'zoomCrop'; + } + + if (!in_array($method, self::$magic_actions)) { + return $this; + } + + // Always initialize image. + if (!$this->image) { + $this->image(); + } + + try { + $result = call_user_func_array([$this->image, $method], $args); + + foreach ($this->alternatives as $ratio => $medium) { + $args_copy = $args; + + // regular image: resize 400x400 -> 200x200 + // --> @2x: resize 800x800->400x400 + if (isset(self::$magic_resize_actions[$method])) { + foreach (self::$magic_resize_actions[$method] as $param) { + if (isset($args_copy[$param])) { + $args_copy[$param] = (int) $args_copy[$param] * $ratio; + } + } + } + + call_user_func_array([$medium, $method], $args_copy); + } + } catch (\BadFunctionCallException $e) { + } + + return $this; + } + + /** + * Gets medium image, resets image manipulation operations. + * + * @return $this + */ + protected function image() + { + $locator = self::$grav['locator']; + + $file = $this->get('filepath'); + $cacheDir = $locator->findResource('cache://images', true); + + $this->image = ImageFile::open($file) + ->setCacheDir($cacheDir) + ->setActualCacheDir($cacheDir) + ->setPrettyName(basename($this->get('basename'))); + + $this->filter(); + + return $this; + } + + /** + * Save the image with cache. + * + * @return mixed|string + */ + protected function saveImage() + { + if (!$this->image) { + return parent::path(false); + } + + if ($this->get('debug') && !$this->debug_watermarked) { + $ratio = $this->get('ratio'); + if (!$ratio) { + $ratio = 1; + } + + $locator = self::$grav['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)); + } + + $result = $this->image->cacheFile($this->format, $this->quality); + + return $result; + } + + /** + * Filter image by using user defined filter parameters. + * + * @param string $filter Filter to be used. + */ + public function filter($filter = 'image.filters.default') + { + $filters = (array) $this->get($filter, []); + foreach ($filters as $params) { + $params = (array) $params; + $method = array_shift($params); + $this->__call($method, $params); + } + } +} diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php new file mode 100644 index 000000000..1262bddf9 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/Link.php @@ -0,0 +1,65 @@ +attributes = $attributes; + $this->source = $medium->reset()->thumbnail('auto')->display('thumbnail'); + $this->source->linked = true; + } + + /** + * Get an element (is array) that can be rendered by the Parsedown engine + * + * @param string $title + * @param string $alt + * @param string $class + * @param boolean $reset + * @return array + */ + public function parsedownElement($title = null, $alt = null, $class = null, $reset = true) + { + $innerElement = $this->source->parsedownElement($title, $alt, $class, $reset); + + return [ + 'name' => 'a', + 'attributes' => $this->attributes, + 'handler' => is_string($innerElement) ? 'line' : 'element', + 'text' => $innerElement + ]; + } + + /** + * Forward the call to the source element + * + * @param string $method + * @param mixed $args + * @return $this|mixed + */ + public function __call($method, $args) + { + $this->source = call_user_func_array(array($this->source, $method), $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 LinkMedium ? $this->source : $this; + } +} diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php new file mode 100644 index 000000000..e001ec3c6 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -0,0 +1,374 @@ +def('mime', 'application/octet-stream'); + $this->reset(); + } + + /** + * Add meta file for the medium. + * + * @param $filepath + */ + public function addMetaFile($filepath) + { + $this->merge(CompiledYamlFile::instance($filepath)->content()); + } + + /** + * Add alternative Medium to this Medium. + * + * @param $ratio + * @param Medium $alternative + */ + public function addAlternative($ratio, Medium $alternative) + { + if (!is_numeric($ratio) || $ratio === 0) { + return; + } + + $alternative->set('ratio', $ratio); + $this->alternatives[(float) $ratio] = $alternative; + } + + /** + * Return string representation of the object (html). + * + * @return string + */ + public function __toString() + { + return $this->html(); + } + + /** + * 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 URL to file. + * + * @param bool $reset + * @return string + */ + public function url($reset = true) + { + $output = preg_replace('|^' . GRAV_ROOT . '|', '', $this->get('filepath')); + + if ($reset) { + $this->reset(); + } + + return self::$grav['base_url'] . $output . $this->urlHash(); + } + + /** + * Get/set hash for the file's url + * + * @param string $hash + * @param boolean $withHash + * @return string + */ + public function urlHash($hash = null, $withHash = true) + { + if ($hash) { + $this->set('urlHash', ltrim($hash, '#')); + } + + $hash = $this->get('urlHash', ''); + + if ($withHash && !empty($hash)) { + return '#' . $hash; + } else { + return $hash; + } + } + + /** + * Get an element (is array) that can be rendered by the Parsedown engine + * + * @param string $title + * @param string $alt + * @param string $class + * @param boolean $reset + * @return array + */ + public function parsedownElement($title = null, $alt = null, $class = null, $reset = true) + { + $attributes = $this->attributes; + + $style = ''; + foreach ($this->styleAttributes as $key => $value) { + $style .= $key . ': ' . $value . ';'; + } + $attributes['style'] = $style; + + !empty($title) && empty($attributes['title']) && $attributes['title'] = $title; + !empty($alt) && empty($attributes['alt']) && $attributes['alt'] = $alt; + !empty($class) && empty($attributes['class']) && $attributes['class'] = $class; + + 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; + } + + if ($reset) { + $this->reset(); + } + + $this->display('source'); + + return $element; + } + + /** + * Parsedown element for source display mode + * + * @param array $attributes + * @param boolean $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 boolean $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 $this + */ + public function display($mode = 'source') + { + if ($this->mode === $mode) { + return $this; + } + + + $this->mode = $mode; + + return $mode === 'thumbnail' ? $this->getThumbnail()->reset() : $this->reset(); + } + + /** + * Switch thumbnail. + * + * @param string $type + * + * @return $this + */ + public function thumbnail($type = 'auto') + { + if ($type !== 'auto' && !in_array($type, $this->thumbnailTypes)) { + return $this; + } + + if ($this->thumbnailType !== $type) { + $this->_thumbnail = null; + } + + $this->thumbnailType = $type; + + return $this; + } + + /** + * Turn the current Medium into a Link + * + * @param boolean $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 inta a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param boolean $reset + * @return Link + */ + 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); + } + + /** + * 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) + { + return $this; + } + + /** + * Get the thumbnail Medium object + * + * @return ThumbnailImageMedium + */ + protected function getThumbnail() + { + if (!$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 ThumbnailMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']); + $thumb->parent = $this; + } + + if ($thumb) { + $this->_thumbnail = $thumb; + break; + } + } + } + + return $this->_thumbnail; + } +} diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php new file mode 100644 index 000000000..9ff05d4b9 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php @@ -0,0 +1,146 @@ +get("media.".strtolower($ext)); + if (!$media_params) { + return null; + } + + $params += $media_params; + + // Add default settings for undefined variables. + $params += $config->get('media.defaults'); + $params += [ + 'type' => 'file', + 'thumb' => 'media/thumb.png', + 'mime' => 'application/octet-stream', + 'filepath' => $file, + 'filename' => $filename, + 'basename' => $basename, + 'extension' => $ext, + 'path' => $path, + 'modified' => filemtime($file), + 'thumbnails' => [] + ]; + + $locator = self::getGrav()['locator']; + + $lookup = $locator->findResources('image://'); + foreach ($lookup as $lookupPath) { + if (is_file($lookupPath . '/' . $params['thumb'])) { + $params['thumbnails']['default'] = $lookupPath . '/' . $params['thumb']; + break; + } + } + + return static::fromArray($params); + } + + /** + * Create Medium from array of parameters + * + * @param array $items + * @param Blueprint|null $blueprint + * @return Medium + */ + public static function fromArray(array $items = [], Blueprint $blueprint = null) + { + $type = isset($items['type']) ? $items['type'] : null; + + switch ($type) { + case 'image': + return new ImageMedium($items, $blueprint); + break; + case 'thumbnail': + return new ThumbnailImageMedium($items, $blueprint); + break; + case 'animated': + case 'vector': + return new StaticImageMedium($items, $blueprint); + break; + case 'video': + return new VideoMedium($items, $blueprint); + break; + default: + return new Medium($items, $blueprint); + break; + } + } + + /** + * Create a new ImageMedium by scaling another ImageMedium object. + * + * @param ImageMedium $medium + * @param int $from + * @param int $to + * @return Medium + */ + public static function scaledFromMedium($medium, $from, $to) + { + if (! $medium instanceof ImageMedium) { + return $medium; + } + + if ($to > $from) { + return $medium; + } + + $ratio = $to / $from; + $width = (int) ($medium->get('width') * $ratio); + $height = (int) ($medium->get('height') * $ratio); + + $basename = $medium->get('basename'); + $basename = str_replace('@'.$from.'x', '@'.$to.'x', $basename); + + $debug = $medium->get('debug'); + $medium->set('debug', false); + + $file = $medium->resize($width, $height)->setPrettyName($basename)->url(); + $file = preg_replace('|'. preg_quote(self::getGrav()['base_url_relative']) .'$|', '', GRAV_ROOT) . $file; + + $medium->set('debug', $debug); + + $size = filesize($file); + + $medium = self::fromFile($file); + $medium->set('size', $size); + + return $medium; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php new file mode 100644 index 000000000..2567705d7 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php @@ -0,0 +1,31 @@ +parsedownElement($title, $alt, $class, $reset); + + if (!$this->parsedown) { + $this->parsedown = new Parsedown(null); + } + + return $this->parsedown->elementToHtml($element); + } +} diff --git a/system/src/Grav/Common/Page/Medium/RenderableInterface.php b/system/src/Grav/Common/Page/Medium/RenderableInterface.php new file mode 100644 index 000000000..f1ebf0dfc --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/RenderableInterface.php @@ -0,0 +1,34 @@ +url($reset); + + return [ 'name' => 'image', 'attributes' => $attributes ]; + } +} diff --git a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php new file mode 100644 index 000000000..df42dcf14 --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php @@ -0,0 +1,20 @@ +styleAttributes['width'] = $width . 'px'; + $this->styleAttributes['height'] = $height . 'px'; + + return $this; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php new file mode 100644 index 000000000..3f40200cb --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/ThumbnailImageMedium.php @@ -0,0 +1,121 @@ +bubble('parsedownElement', [$title, $alt, $class, $reset]); + } + + /** + * Return HTML markup from the medium. + * + * @param string $title + * @param string $alt + * @param string $class + * @param bool $reset + * @return string + */ + public function html($title = null, $alt = null, $class = null, $reset = true) + { + return $this->bubble('html', [$title, $alt, $class, $reset]); + } + + /** + * Switch display mode. + * + * @param string $mode + * + * @return $this + */ + public function display($mode = 'source') + { + return $this->bubble('display', [$mode], false); + } + + /** + * Switch thumbnail. + * + * @param string $type + * + * @return $this + */ + public function thumbnail($type = 'auto') + { + $this->bubble('thumbnail', [$type], false); + return $this->bubble('getThumbnail', [], false); + } + + /** + * Turn the current Medium into a Link + * + * @param boolean $reset + * @param array $attributes + * @return Link + */ + public function link($reset = true, array $attributes = []) + { + return $this->bubble('link', [$reset, $attributes], false); + } + + /** + * Turn the current Medium inta a Link with lightbox enabled + * + * @param int $width + * @param int $height + * @param boolean $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 boolean $testLinked + * @return 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; + } else { + return call_user_func_array(array($this, 'parent::' . $method), $arguments); + } + } +} diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php new file mode 100644 index 000000000..e67d3fbfc --- /dev/null +++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php @@ -0,0 +1,46 @@ +url($reset); + + return [ + 'name' => 'video', + 'text' => 'Your browser does not support the video tag.', + 'attributes' => $attributes + ]; + } + + /** + * Reset medium. + * + * @return $this + */ + public function reset() + { + parent::reset(); + + $this->attributes['controls'] = true; + return $this; + } +} diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 44a5665e2..95b558f18 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -960,7 +960,6 @@ class Page // Build an array of meta objects.. foreach ((array)$page_header->metadata as $key => $value) { - // If this is a property type metadata: "og", "twitter", "facebook" etc if (is_array($value)) { foreach ($value as $property => $prop_value) { diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index bbff00e8d..fd70981d6 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -523,7 +523,6 @@ class Pages /** @var \DirectoryIterator $file */ foreach ($iterator as $file) { - if ($file->isDot()) { continue; } @@ -531,14 +530,12 @@ class Pages $name = $file->getFilename(); if ($file->isFile()) { - // Update the last modified if it's newer than already found if ($file->getBasename() !== '.DS_Store' && ($modified = $file->getMTime()) > $last_modified) { $last_modified = $modified; } if (Utils::endsWith($name, CONTENT_EXT)) { - $page->init($file); $content_exists = true; @@ -547,7 +544,6 @@ class Pages } } } elseif ($file->isDir()) { - if (!$page->path()) { $page->path($file->getPath()); } @@ -593,7 +589,6 @@ class Pages // Build routes and taxonomy map. /** @var $page Page */ foreach ($this->instances as $page) { - $parent = $page->parent(); if ($parent) { @@ -643,7 +638,6 @@ class Pages } foreach ($pages as $key => $info) { - $child = isset($this->instances[$key]) ? $this->instances[$key] : null; if (!$child) { throw new \RuntimeException("Page does not exist: {$key}"); diff --git a/system/src/Grav/Console/Gpm/InfoCommand.php b/system/src/Grav/Console/Gpm/InfoCommand.php index 721583032..53290b8ec 100644 --- a/system/src/Grav/Console/Gpm/InfoCommand.php +++ b/system/src/Grav/Console/Gpm/InfoCommand.php @@ -78,12 +78,12 @@ class InfoCommand extends Command $this->output->writeln(''); $packageURL = ''; - if (isset($foundPackage->author->url)) { - $packageURL = '<' . $foundPackage->author->url . '>'; + if (isset($foundPackage->author['url'])) { + $packageURL = '<' . $foundPackage->author['url'] . '>'; } $this->output->writeln("" . str_pad("Author", - 12) . ": " . $foundPackage->author->name . ' <' . $foundPackage->author->email . '> ' . $packageURL); + 12) . ": " . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL); foreach (array( 'version',