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 = '
';
- } 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',