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 = '' . $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; + } + + 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 = '' . $title . ''; - } 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. *