diff --git a/system/assets/responsive-overlays/1x.png b/system/assets/responsive-overlays/1x.png
new file mode 100644
index 000000000..1e3573fe3
Binary files /dev/null and b/system/assets/responsive-overlays/1x.png differ
diff --git a/system/assets/responsive-overlays/2x.png b/system/assets/responsive-overlays/2x.png
new file mode 100644
index 000000000..501b5b830
Binary files /dev/null and b/system/assets/responsive-overlays/2x.png differ
diff --git a/system/assets/responsive-overlays/3x.png b/system/assets/responsive-overlays/3x.png
new file mode 100644
index 000000000..8052b0940
Binary files /dev/null and b/system/assets/responsive-overlays/3x.png differ
diff --git a/system/assets/responsive-overlays/4x.png b/system/assets/responsive-overlays/4x.png
new file mode 100644
index 000000000..2676a1348
Binary files /dev/null and b/system/assets/responsive-overlays/4x.png differ
diff --git a/system/assets/responsive-overlays/unknown.png b/system/assets/responsive-overlays/unknown.png
new file mode 100644
index 000000000..09df3a748
Binary files /dev/null and b/system/assets/responsive-overlays/unknown.png differ
diff --git a/system/config/system.yaml b/system/config/system.yaml
index a7c1ada23..a3ac7e7a4 100644
--- a/system/config/system.yaml
+++ b/system/config/system.yaml
@@ -63,3 +63,6 @@ debugger:
twig: true # Enable debugging of Twig templates
shutdown:
close_connection: true # Close the connection before calling onShutdown(). false for debugging
+
+images:
+ debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example
diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
index ea2f5abf9..9ba39e423 100644
--- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
+++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php
@@ -104,6 +104,7 @@ trait ParsedownGravTrait
//get the url and parse it
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
+
$path_parts = pathinfo($url['path']);
// if there is no host set but there is a path, the file is local
@@ -145,27 +146,42 @@ trait ParsedownGravTrait
}
}
- // Get the URL for regular images, or an array of bits needed to put together
- // the lightbox HTML
- if (!isset($actions['lightbox'])) {
- $src = $medium->url();
- } else {
- $src = $medium->lightboxRaw();
- }
+ $data = $medium->htmlRaw();
// set the src element with the new generated url
- if (!isset($actions['lightbox']) && !is_array($src)) {
- $excerpt['element']['attributes']['src'] = $src;
+ 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' => array('rel' => $src['a_rel'], 'href' => $src['a_url']),
+ 'attributes' => $attributes,
'handler' => 'element',
'text' => array(
'name' => 'img',
- 'attributes' => array('src' => $src['img_url'], 'alt' => $alt, 'title' => $title)
- ),
+ 'attributes' => $img_attributes
+ )
);
// Set any custom classes on the lightbox element
diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php
index 97ac69962..ae5defa44 100644
--- a/system/src/Grav/Common/Page/Media.php
+++ b/system/src/Grav/Common/Page/Media.php
@@ -49,77 +49,81 @@ class Media extends Getters
// Find out the real filename, in case of we are at the metadata.
$filename = $info->getFilename();
- list($basename, $ext, $meta) = $this->getFileParts($filename);
+ list($basename, $ext, $meta, $alternative) = $this->getFileParts($filename);
- // Get medium instance creating it if it didn't exist.
- $medium = $this->get("{$basename}.{$ext}", true);
- if (!$medium) {
+ // Get medium instance if it already exists.
+ $medium = $this->get("{$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());
+ }
+ } 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);
+ }
+
+ $this->add("{$basename}.{$ext}", $medium);
+ }
+
+ foreach ($this->images() as $medium) {
+
+ $alternatives = $medium->getAlternatives();
+
+ if (empty($alternatives)) {
continue;
}
- //set file size
- $medium->set('size', $info->getSize());
+ $max = max(array_keys($alternatives));
- // Assign meta files to the medium.
- if ($meta) {
- $medium->addMetaFile($meta);
+ for ($i=2; $i < $max; $i++) {
+
+ if (isset($alternatives[$i])) {
+ continue;
+ }
+
+ $medium->addAlternative($i, $this->scaleMedium($alternatives[$max], $max, $i));
}
}
}
+
+
/**
- * Get medium by basename and extension.
+ * Get medium by filename.
*
* @param string $filename
- * @param bool $create
* @return Medium|null
*/
- public function get($filename, $create = false)
+ public function get($filename)
{
- if ($create && !isset($this->instances[$filename])) {
- $parts = explode('.', $filename);
- $ext = array_pop($parts);
- $basename = implode('.', $parts);
-
- /** @var Config $config */
- $config = self::$grav['config'];
-
- // Check if medium type has been configured.
- $params = $config->get("media.".strtolower($ext));
- if (!$params) {
- return null;
- }
-
- $filePath = $this->path . '/' . $filename;
-
- // 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' => $this->path,
- 'modified' => filemtime($filePath),
- );
-
- $lookup = array(
- USER_DIR . 'images/',
- SYSTEM_DIR . 'images/',
- );
- foreach ($lookup as $path) {
- if (is_file($path . $params['thumb'])) {
- $params['thumb'] = $path . $params['thumb'];
- break;
- }
- }
-
- $this->add(new Medium($params));
- }
-
return isset($this->instances[$filename]) ? $this->instances[$filename] : null;
}
@@ -178,24 +182,113 @@ 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::$grav['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),
+ );
+
+ $lookup = array(
+ USER_DIR . 'images/',
+ SYSTEM_DIR . 'images/',
+ );
+ 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::$grav['base_url_relative']) .'$|', '', GRAV_ROOT) . $file;
+
+ $medium->set('debug', $debug);
+
+ $size = filesize($file);
+
+ $medium = $this->createMedium($file);
+ $medium->set('size', $size);
+
+ return $medium;
+ }
+
+
/**
* @internal
*/
- protected function add($file)
+ protected function add($name, $file)
{
- $this->instances[$file->filename] = $file;
+ $this->instances[$name] = $file;
switch ($file->type) {
case 'image':
- $this->images[$file->filename] = $file;
+ $this->images[$name] = $file;
break;
case 'video':
- $this->videos[$file->filename] = $file;
+ $this->videos[$name] = $file;
break;
case 'audio':
- $this->audios[$file->filename] = $file;
+ $this->audios[$name] = $file;
break;
default:
- $this->files[$file->filename] = $file;
+ $this->files[$name] = $file;
}
}
@@ -210,6 +303,13 @@ class Media extends Getters
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
+ $alternative = false;
+
+ if (preg_match('/(.*)@(\d+x)$/', $name, $matches)) {
+ $name = $matches[1];
+ $alternative = $matches[2];
+ }
+
$extension = null;
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta') {
@@ -223,6 +323,15 @@ class Media extends Getters
}
$meta = implode('.', $fileParts);
- return array($name, $extension, $meta);
+ return array($name, $extension, $meta, $alternative);
+ }
+
+ protected function parseRatio($ratio)
+ {
+ if (!is_numeric($ratio)) {
+ $ratio = (float) trim($ratio, 'x');
+ }
+
+ return $ratio;
}
}
diff --git a/system/src/Grav/Common/Page/Medium.php b/system/src/Grav/Common/Page/Medium.php
index b963b0906..66d4c7b90 100644
--- a/system/src/Grav/Common/Page/Medium.php
+++ b/system/src/Grav/Common/Page/Medium.php
@@ -49,24 +49,49 @@ class Medium extends Data
protected $type = 'guess';
protected $quality = 85;
+ protected $debug_watermarked = false;
- public static $valid_actions = ['resize', 'forceResize', 'cropResize', 'crop', 'cropZoom',
+ public static $valid_actions = [
+ // Medium functions
+ 'format', 'lightbox', 'link', 'reset',
+
+ // Gregwar Image functions
+ 'resize', 'forceResize', 'cropResize', 'crop', 'cropZoom',
'negate', 'brightness', 'contrast', 'grayscale', 'emboss', 'smooth', 'sharp', 'edge', 'colorize', 'sepia' ];
+ public static $size_param_actions = [
+ 'resize' => [ 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 string
+ * @var array
*/
- protected $linkTarget;
+ protected $alternatives = array();
/**
* @var string
*/
- protected $linkAttributes;
+ protected $linkTarget;
+
+ /**
+ * @var string
+ */
+ protected $linkSrcset;
+
+ /**
+ * @var string
+ */
+ protected $linkAttributes = [];
public function __construct($items = array(), Blueprint $blueprint = null)
{
@@ -89,7 +114,7 @@ class Medium extends Data
$this->def('mime', 'application/octet-stream');
}
-
+ $this->set('debug', self::$grav['config']->get('system.images.debug'));
}
/**
@@ -113,7 +138,7 @@ class Medium extends Data
$config = self::$grav['config'];
if ($this->image) {
- $output = $this->image->cacheFile($this->type, $this->quality);
+ $output = $this->saveImage();
$this->reset();
$output = ROOT_DIR . $output;
} else {
@@ -122,6 +147,128 @@ class Medium extends Data
return $output;
}
+ /**
+ * Return URL to file.
+ *
+ * @return string
+ */
+ public function url($reset = true)
+ {
+ if ($this->image) {
+ $output = $this->saveImage();
+
+ if ($reset) $this->reset();
+ } else {
+ $relPath = preg_replace('|^' . ROOT_DIR . '|', '', $this->get('path'));
+ $output = $relPath . '/' . $this->get('filename');
+ }
+
+ return self::$grav['base_url'] . '/'. $output;
+ }
+
+
+ /**
+ * Return srcset string for this Medium and its alternatives
+ *
+ * @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
+ * @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 string $type
+ * @param int $quality
+ * @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;
+ }
+
+ public function htmlRaw($reset = true)
+ {
+ $output = [];
+
+ if ($this->image) {
+ $output['img_src'] = $this->url(false);
+ $output['img_srcset'] = $this->srcset($reset);
+ } else {
+ $output['text'] = $title;
+ }
+
+ if ($this->linkTarget) {
+ /** @var Config $config */
+ $config = self::$grav['config'];
+
+ $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
@@ -133,24 +280,6 @@ class Medium extends Data
return $this;
}
- /**
- * Return URL to file.
- *
- * @return string
- */
- public function url()
- {
- if ($this->image) {
- $output = $this->image->cacheFile($this->type, $this->quality);
- $this->reset();
- } else {
- $relPath = preg_replace('|^' . ROOT_DIR . '|', '', $this->get('path'));
- $output = $relPath . '/' . $this->get('filename');
- }
-
- return self::$grav['base_url'] . '/'. $output;
- }
-
/**
* Sets image output format.
*
@@ -170,91 +299,7 @@ class Medium extends Data
}
/**
- * Returns
tag from the medium.
- *
- * @param string $title
- * @param string $class
- * @param string $type
- * @param int $quality
- * @return string
- */
- public function img($title = null, $class = null, $type = null, $quality = 80)
- {
- if (!$this->image) {
- $this->image();
- }
-
- $output = $this->html($title, $class, $type, $quality);
-
- return $output;
- }
-
- /**
- * Return HTML markup from the medium.
- *
- * @param string $title
- * @param string $class
- * @param string $type
- * @param int $quality
- * @return string
- */
- public function html($title = null, $class = null, $type = null, $quality = 80)
- {
- $title = $title ? $title : $this->get('title');
- $class = $class ? $class : '';
-
- if ($this->image) {
- $type = $type ? $type : $this->type;
- $quality = $quality ? $quality : $this->quality;
-
- $url = $this->url($type, $quality);
- $this->reset();
-
- $output = '
';
- } else {
- $output = $title;
- }
-
- if ($this->linkTarget) {
- /** @var Config $config */
- $config = self::$grav['config'];
-
- $output = 'linkAttributes. ' class="'. $class . '">' . $output . '';
-
- $this->linkTarget = $this->linkAttributes = null;
- }
-
- return $output;
- }
-
- /**
- * Return lightbox HTML for the medium.
- *
- * @param int $width
- * @param int $height
- * @return $this
- */
- public function lightbox($width = null, $height = null)
- {
- $this->linkAttributes = ' rel="lightbox"';
-
- return $this->link($width, $height);
- }
-
- public function lightboxRaw($width = null, $height = null)
- {
- /** @var Config $config */
- $config = self::$grav['config'];
- $url = $this->url();
- $this->link($width, $height);
- $lightbox_url = self::$grav['base_url'] . '/'. $this->linkTarget;
-
- return array('a_url' => $lightbox_url, 'a_rel' => 'lightbox', 'img_url' => $url);
- }
-
- /**
- * Return link HTML for the medium.
+ * Enable link for the medium object
*
* @param int $width
* @param int $height
@@ -263,11 +308,12 @@ class Medium extends Data
public function link($width = null, $height = null)
{
if ($this->image) {
- $image = clone $this->image;
- if ($width && $height) {
- $image->cropResize($width, $height);
+ $this->linkTarget = $this->url(false);
+ $srcset = $this->srcset();
+
+ if ($srcset) {
+ $this->linkAttributes['data-srcset'] = $srcset;
}
- $this->linkTarget = $image->cacheFile($this->type, $this->quality);
} else {
// TODO: we need to find out URI in a bit better way.
$relPath = preg_replace('|^' . ROOT_DIR . '|', '', $this->get('path'));
@@ -277,6 +323,20 @@ class Medium extends Data
return $this;
}
+ /**
+ * Enable lightbox for the medium.
+ *
+ * @param int $width
+ * @param int $height
+ * @return $this
+ */
+ public function lightbox($width = null, $height = null)
+ {
+ $this->linkAttributes['rel'] = 'lightbox';
+
+ return $this->link($width, $height);
+ }
+
/**
* Reset image.
*
@@ -292,6 +352,7 @@ class Medium extends Data
}
$this->type = 'guess';
$this->quality = 80;
+ $this->debug_watermarked = false;
return $this;
}
@@ -316,6 +377,20 @@ class Medium extends Data
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;
}
@@ -344,6 +419,28 @@ class Medium extends Data
return $this;
}
+ protected function saveImage()
+ {
+ if (!$this->image) {
+ $this->image();
+ }
+
+ if ($this->get('debug') && !$this->debug_watermarked) {
+
+ $ratio = $this->get('ratio');
+ if (!$ratio) {
+ $ratio = 1;
+ }
+
+ $overlay = SYSTEM_DIR . '/assets/responsive-overlays/' . $ratio . 'x.png';
+ $overlay = file_exists($overlay) ? $overlay : SYSTEM_DIR . '/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.
*
@@ -365,6 +462,29 @@ class Medium extends Data
return $this;
}
+ /**
+ * Add alternative Medium to this Medium
+ *
+ * @param $type
+ * @param $alternative
+ * @return $this
+ */
+ 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.
*