diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2659611 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +composer.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3fac30b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Martin Folkers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5be173d --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Kirby WebP +[![Release](https://img.shields.io/github/release/S1SYPHOS/kirby-webp.svg?color="brightgreen")](https://github.com/S1SYPHOS/kirby-webp/releases) [![License](https://img.shields.io/github/license/S1SYPHOS/kirby-webp.svg)](https://github.com/S1SYPHOS/kirby-webp/blob/master/LICENSE) [![Issues](https://img.shields.io/github/issues/S1SYPHOS/kirby-webp.svg)](https://github.com/S1SYPHOS/kirby-webp/issues) + +This plugin generates `.webp` images alongside your uploaded `.jp(e)g` & `.png` versions - so **you** don't have to! + +**Table of contents** +- [1. What is it good for?](#whats-is-it-good-for) +- [2. Getting started](#getting-started) +- [3. Configuration](#configuration) +- [4. Troubleshooting](#troubleshooting) +- [5. Credits / License](#credits--license) + +## What is it good for? +Absolutely .. smaller image size! + +## Getting started +Use one of the following methods to install & use `kirby-webp`: + +### Git submodule + +If you know your way around Git, you can download this plugin as a [submodule](https://github.com/blog/2104-working-with-submodules): + +```text +git submodule add https://github.com/S1SYPHOS/kirby-webp.git site/plugins/kirby-webp +``` + +### Clone or download + +1. [Clone](https://github.com/S1SYPHOS/kirby-webp.git) or [download](https://github.com/S1SYPHOS/kirby-webp/archive/master.zip) this repository. +2. Unzip / Move the folder to `site/plugins`. + +### Activate the plugin +Activate the plugin with the following line in your `config.php`: + +```text +c::set('plugin.kirby-webp', true); +``` + +## Configuration +After uploading some images, you are now officially ready to serve their newly generated WebP versions. + +### Apache +If you're using [Apache](http://httpd.apache.org/) as your webserver, add the following lines to your `.htaccess` (right after `RewriteBase`): + +```text + + RewriteEngine On + + # Checking for WebP browser support .. + RewriteCond %{HTTP_ACCEPT} image/webp + + # .. and if there's a WebP version for the requested image + RewriteCond %{DOCUMENT_ROOT}/$1.webp -f + + # Well, then go for it & serve WebP instead + RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp,E=accept:1] + + + + Header append Vary Accept env=REDIRECT_accept + + + + AddType image/webp .webp + +``` + +### NGINX +If you're using [NGINX](https://nginx.org/en/) as your webserver, add the following lines to your virtual host setup (for more information, go [here](https://github.com/uhop/grunt-tight-sprite/wiki/Recipe:-serve-WebP-with-nginx-conditionally) or [there](https://optimus.keycdn.com/support/configuration-to-deliver-webp)): + +```text +// First, make sure that NGINX' `mime.types` file includes 'image/webp webp' +include /etc/nginx/mime.types; + +// Checking if HTTP's `ACCEPT` header contains 'webp' +map $http_accept $webp_suffix { + default ""; + "~*webp" ".webp"; +} + +server { + // ... + + // Checking if there's a WebP version for the requested image .. + location ~* ^.+\.(jpe?g|png)$ { + add_header Vary Accept; + // .. and if so, serving it + try_files $1$webp_ext $uri =404; + } +} +``` + +## Troubleshooting +Despite stating that `An unexpected error occurred`, WebP generation after renaming / updating images works - `panel.file.replace` doesn't work at all .. PRs are always welcome :champagne: + +Because of that, only `panel.file.upload` is included by default. If you wish to investigate this further or don't care too much about the errror, go head with `c::set('plugin.webp.actions', ['upload', 'update', 'replace']);` in your `config.php`. + +## Credits / License +`kirby-webp` is based on Bjørn Rosell's `convert-webp` library. It is licensed under the [MIT License](LICENSE), but **using Kirby in production** requires you to [buy a license](https://getkirby.com/buy). Are you ready for the [next step](https://getkirby.com/next)? + +## Special Thanks +I'd like to thank everybody that's making great software - you people are awesome. Also I'm always thankful for feedback and bug reports :) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d02e402 --- /dev/null +++ b/composer.json @@ -0,0 +1,11 @@ +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/S1SYPHOS/webp-convert" + } + ], + "require": { + "rosell-dk/webp-convert": "dev-master" + } +} diff --git a/core/generate_webp.php b/core/generate_webp.php new file mode 100644 index 0000000..5b9d587 --- /dev/null +++ b/core/generate_webp.php @@ -0,0 +1,67 @@ +option('thumb.quality') ? + 'actions' => ['upload'], + 'quality' => 90, // Desired WebP compression quality + 'stripMetadata' => TRUE, + 'serveConverted' => FALSE, + 'serveOriginalOnFail' => TRUE, + 'preferredConverters' => ['gd', 'webp', 'imagick'] // TODO: include 'thumbs.driver' + ]; + + // If config settings exist, return the config with fallback + if(isset($settings) && array_key_exists($name, $settings)) { + return c::get($prefix . $name, $settings[$name]); + } + } +} + +foreach (settings::actions() as $action) { + kirby()->hook('panel.file.' . $action, 'generateWebP'); +} + +function generateWebP($file) { + + try { + + // Checking file type since only images are processed + if ($file->type() == 'image') { + + // Defining image-related options + $input = $file->dir() . '/' . $file->filename(); + $output = $file->dir() . '/' . $file->name() . '.webp'; + $quality = settings::quality(); + $strip = settings::stripMetadata(); + + // Defining WebPConvert-related options + WebPConvert::$serve_converted_image = settings::serveConverted(); + WebPConvert::$serve_original_image_on_fail = settings::serveOriginalOnFail(); + WebPConvert::set_preferred_converters(settings::preferredConverters()); + + // Generating WebP image & placing it alongside the original version + WebPConvert::convert($input, $output, $quality, $strip); + } + } catch (Exception $e) { + return response::error($e->getMessage()); + } +} diff --git a/kirby-webp.php b/kirby-webp.php new file mode 100644 index 0000000..68e0c8b --- /dev/null +++ b/kirby-webp.php @@ -0,0 +1,19 @@ + + * @link http://twobrain.io + * @version 0.1.0 + * @license MIT + */ + +if(!c::get('plugin.kirby-webp')) return; + +// Initialising composer's autoloader +require_once __DIR__ . DS . 'vendor' . DS . 'autoload.php'; + +// Loading settings & core +include_once __DIR__ . DS . 'core' . DS . 'generate_webp.php'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7a2c1b --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "kirby-webp", + "description": "WebP generation for Kirby", + "author": "S1SYPHOS ", + "version": "0.1.0", + "type": "kirby-plugin", + "license": "MIT" +} diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..4f3d7e3 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath.'\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($vendorDir . '/rosell-dk/webp-convert'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..b265c64 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,9 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitf7b42677ece290731e6e707bbc57bdfe::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..96ba82a --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,26 @@ + + array ( + 'WebPConvert' => + array ( + 0 => __DIR__ . '/..' . '/rosell-dk/webp-convert', + ), + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixesPsr0 = ComposerStaticInitf7b42677ece290731e6e707bbc57bdfe::$prefixesPsr0; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..419b813 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,30 @@ +[ + { + "name": "rosell-dk/webp-convert", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/S1SYPHOS/webp-convert.git", + "reference": "52028f520ea92209e26471922b1ef7ad32115331" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/S1SYPHOS/webp-convert/zipball/52028f520ea92209e26471922b1ef7ad32115331", + "reference": "52028f520ea92209e26471922b1ef7ad32115331", + "shasum": "" + }, + "time": "2018-02-24T18:55:46+00:00", + "type": "library", + "installation-source": "source", + "autoload": { + "psr-0": { + "WebPConvert": "" + } + }, + "description": "Convert jpeg/png to webp with PHP", + "support": { + "source": "https://github.com/S1SYPHOS/webp-convert/tree/master" + } + } +] diff --git a/vendor/rosell-dk/webp-convert b/vendor/rosell-dk/webp-convert new file mode 160000 index 0000000..52028f5 --- /dev/null +++ b/vendor/rosell-dk/webp-convert @@ -0,0 +1 @@ +Subproject commit 52028f520ea92209e26471922b1ef7ad32115331