├── .editorconfig ├── CHANGELOG.md ├── LICENSE ├── README.md ├── adapters ├── gd.php ├── imagick.php └── interface.php ├── blueprints.yaml ├── resize-images.php └── resize-images.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.{md,yml,yaml}] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.2 2 | ## 03/03/2017 3 | 4 | 1. [](#bugfix) 5 | * Fixed issue that arose when `system.images.cache_all` was enabled with a more reliable method for determining image source paths. 6 | 7 | # v0.2.1 8 | ## 03/03/2017 9 | 10 | 1. [](#new) 11 | * Added option to enable removing of original image after it has been resized. 12 | 13 | # v0.2.0 14 | ## 03/03/2017 15 | 16 | 1. [](#bugfix) 17 | * Fixed adapter fallback logic and print a warning when no adapter is installed. 18 | 19 | # v0.1.0 20 | ## 10/14/2016 21 | 22 | 1. [](#new) 23 | * Initial release 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fredrik Ekelund 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Grav](http://getgrav.org) Resize Images Plugin 2 | 3 | **This plugin is unmaintained. Feel free to fork and continue development elsewhere.** 4 | 5 | > Resize images at upload time in the Grav admin 6 | 7 | Grav provides some nifty built-in features for editing images on the fly through 8 | the use of [Gregwar/Image](https://github.com/Gregwar/Image). But there's no 9 | support yet for automatically generating responsive image alternatives at upload 10 | time rather than at request time. This plugin fixes that! It will automatically 11 | resize images that are uploaded through the [Grav 12 | admin](https://github.com/getgrav/grav-plugin-admin) to a set of predetermined 13 | widths. This means improved performance for Grav, and less manual resizing work 14 | for you. Win-win! :tada: 15 | 16 | Moreover, this plugin doesn't support just GD, but also Imagick, which means 17 | you'll get higher quality results than with the 18 | [ImageMedium#derivatives](https://learn.getgrav.org/content/media#sizes-with-media-queries) 19 | method that can be used to generate image alternatives in theme templates. 20 | 21 | Images that already have responsive alternatives won't be resized. 22 | 23 | ## Configuration 24 | 25 | You can customize the set of widths that your images will be resized to. By 26 | default they are 640, 1000, 1500, 2500, 3500 pixels in width. Images will never 27 | be scaled up, however, so only the widths that are smaller than the original 28 | image's will be used. 29 | 30 | For every width, you're also able to set the JPEG compression quality. A good 31 | rule of thumb is to lower that number at higher widths - the result will still 32 | be good! 33 | 34 | This plugin won't convert PNG's to JPEG's, so the quality number only applies to 35 | JPEG images. 36 | 37 | To generate variations of existing images go into the admin panel and re-save the pages where those images live. Every time a page is saved (whether it's new or old), this plugin will go through all images (again, whether they are new or old) in that page, check if they have responsive variants and generate new ones if necessary. 38 | 39 | ## Installation 40 | 41 | Download the [ZIP 42 | archive](https://github.com/fredrikekelund/grav-plugin-resize-images/archive/master.zip) 43 | from GitHub and extract it to the `user/plugins` directory in your Grav 44 | installation. 45 | 46 | ## CLI 47 | 48 | I'm aiming to add support for CLI commands to this plugin as well, to make it 49 | easy to generate responsive image alternatives for already uploaded images. 50 | -------------------------------------------------------------------------------- /adapters/gd.php: -------------------------------------------------------------------------------- 1 | original_width = $size[0]; 35 | $this->original_height = $size[1]; 36 | 37 | if (preg_match('/jpe?g/', $extension)) { 38 | $this->image = imagecreatefromjpeg($source); 39 | $this->format = 'JPEG'; 40 | } else if ($extension == 'png') { 41 | $this->image = imagecreatefrompng($source); 42 | $this->format = 'PNG'; 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Gets the image format 50 | * @return string - Either 'JPEG' or 'PNG' 51 | */ 52 | public function getFormat() 53 | { 54 | return $this->format; 55 | } 56 | 57 | /** 58 | * Resizes the image to the specified dimensions 59 | * @param float $width 60 | * @param float $height 61 | * @return GDAdapter - Returns $this 62 | */ 63 | public function resize($width, $height) 64 | { 65 | $this->target = imagecreatetruecolor($width, $height); 66 | $format = $this->getFormat(); 67 | 68 | if ($format == 'PNG') { 69 | $transparent = imagecolorallocatealpha($this->target, 255, 255, 255, 127); 70 | 71 | imagealphablending($this->target, false); 72 | imagesavealpha($this->target, true); 73 | imagefilledrectangle($this->target, 0, 0, $width, $height, $transparent); 74 | } 75 | 76 | imagecopyresampled($this->target, $this->image, 0, 0, 0, 0, $width, $height, $this->original_width, $this->original_height); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Sets JPEG quality of target image 83 | * @param int $quality 84 | * @return GDAdapter - Returns $this 85 | */ 86 | public function setQuality($quality) 87 | { 88 | $this->quality = $quality; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Generates image and saves it to disk 95 | * @param string $filename - Target filename for image 96 | * @return bool - Returns true if successful, false otherwise 97 | */ 98 | public function save($filename) 99 | { 100 | $format = $this->getFormat(); 101 | 102 | if ($format == 'JPEG') { 103 | $result = imagejpeg($this->target, $filename, $this->quality); 104 | } else if ($format == 'PNG') { 105 | $result = imagepng($this->target, $filename, 9); 106 | } 107 | 108 | imagedestroy($this->image); 109 | imagedestroy($this->target); 110 | 111 | return $result; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /adapters/imagick.php: -------------------------------------------------------------------------------- 1 | image = new \Imagick($source); 23 | $this->format = strtolower($this->image->getImageFormat()); 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Gets the image format 30 | * @return string - Either 'JPEG' or 'PNG' 31 | */ 32 | public function getFormat() 33 | { 34 | return $this->format; 35 | } 36 | 37 | /** 38 | * Resizes the image to the specified dimensions 39 | * @param float $width 40 | * @param float $height 41 | * @return ImagickAdapter - Returns $this 42 | */ 43 | public function resize($width, $height) 44 | { 45 | $this->image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Sets JPEG quality of target image 52 | * @param int $quality 53 | * @return ImagickAdapter - Returns $this 54 | */ 55 | public function setQuality($quality) 56 | { 57 | $this->image->setImageCompressionQuality($quality); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Generates image and saves it to disk 64 | * @param string $filename - Target filename for image 65 | * @return bool - Returns true if successful, false otherwise 66 | */ 67 | public function save($filename) 68 | { 69 | $format = $this->getFormat(); 70 | 71 | if ($format == 'jpeg') { 72 | $this->image->setImageCompression(\Imagick::COMPRESSION_JPEG); 73 | } else if ($format == 'png') { 74 | $this->image->setImageCompression(\Imagick::COMPRESSION_ZIP); 75 | } 76 | 77 | $result = $this->image->writeImage($filename); 78 | $this->image->clear(); 79 | 80 | return (bool) $result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /adapters/interface.php: -------------------------------------------------------------------------------- 1 | ['onAdminSave', 0] 34 | ]; 35 | } 36 | 37 | /** 38 | * Determine whether a particular dependency is installed. 39 | * @param string $adapter Either 'gd' or 'imagick' 40 | * @return bool 41 | */ 42 | protected function dependencyCheck($adapter = 'gd') 43 | { 44 | if ($adapter === 'gd') { 45 | return extension_loaded('gd'); 46 | } 47 | 48 | if ($adapter === 'imagick') { 49 | return class_exists('\Imagick'); 50 | } 51 | } 52 | 53 | /** 54 | * Determine which adapter is preferred and whether or not it's available. 55 | * Construct an instance of that adapter and return it. 56 | * @param string $source - Source image path 57 | * @return mixed - Either an instance of ImagickAdapter, GDAdapter or false if none of the extensions were available 58 | */ 59 | protected function getImageAdapter($source) 60 | { 61 | $imagick_exists = $this->dependencyCheck('imagick'); 62 | $gd_exists = $this->dependencyCheck('gd'); 63 | 64 | if ($this->adapter === 'imagick') { 65 | if ($imagick_exists) { 66 | return new ImagickAdapter($source); 67 | } else if ($gd_exists) { 68 | return new GDAdapter($source); 69 | } 70 | } else if ($this->adapter === 'gd') { 71 | if ($gd_exists) { 72 | return new GDAdapter($source); 73 | } else if ($imagick_exists) { 74 | return new ImagickAdapter($source); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Resizes an image using either Imagick or GD 81 | * @param string $source - Source image path 82 | * @param string $target - Target image path 83 | * @param float $width - Target width 84 | * @param float $height - Target height 85 | * @param int [$quality=95] - Compression quality for target image 86 | * @return bool - Returns true on success, otherwise false 87 | */ 88 | protected function resizeImage($source, $target, $width, $height, $quality = 95) 89 | { 90 | $adapter = $this->getImageAdapter($source); 91 | $adapter->resize($width, $height); 92 | $adapter->setQuality($quality); 93 | 94 | return $adapter->save($target); 95 | } 96 | 97 | /** 98 | * Called when a page is saved from the admin plugin. Will generate 99 | * responsive image alternatives for image that don't have any. 100 | */ 101 | public function onAdminSave($event) 102 | { 103 | $page = $event['object']; 104 | 105 | if (!$page instanceof Page) { 106 | return false; 107 | } 108 | 109 | if (!$this->dependencyCheck('imagick') && !$this->dependencyCheck('gd')) { 110 | $this->grav['admin']->setMessage('Neither Imagick nor GD seem to be installed. The resize-images plugin needs one of them to work.', 'warning'); 111 | return; 112 | } 113 | 114 | $this->sizes = (array) $this->config->get('plugins.resize-images.sizes'); 115 | $this->adapter = $this->config->get('plugins.resize-images.adapter', 'imagick'); 116 | 117 | foreach ($page->media()->images() as $filename => $medium) { 118 | $srcset = $medium->srcset(false); 119 | 120 | if ($srcset != '') { 121 | continue; 122 | } 123 | 124 | // We can't rely on the path returned from the image's own path 125 | // method, since it points to the directory where the image is saved 126 | // rather than where the original is stored. This means it could 127 | // point to the global image cache directory. 128 | $page_path = $page->path(); 129 | $source_path = "$page_path/$filename"; 130 | $info = pathinfo($source_path); 131 | $count = 0; 132 | 133 | foreach ($this->sizes as $i => $size) { 134 | if ($size['width'] >= $medium->width) { 135 | continue; 136 | } 137 | 138 | $count++; 139 | $dest_path = "{$info['dirname']}/{$info['filename']}@{$count}x.{$info['extension']}"; 140 | $width = $size['width']; 141 | $quality = $size['quality']; 142 | $height = ($width / $medium->width) * $medium->height; 143 | 144 | $this->resizeImage($source_path, $dest_path, $width, $height, $quality, $medium->width, $medium->height); 145 | } 146 | 147 | $remove_original = $this->config->get('plugins.resize-images.remove_original'); 148 | 149 | if ($count > 0) { 150 | $original_index = $count + 1; 151 | 152 | if ($remove_original) { 153 | unlink($source_path); 154 | } else { 155 | rename($source_path, "{$info['dirname']}/{$info['filename']}@{$original_index}x.{$info['extension']}"); 156 | } 157 | 158 | rename("{$info['dirname']}/{$info['filename']}@1x.{$info['extension']}", $source_path); 159 | } 160 | 161 | $message = "Resized $filename $count times"; 162 | 163 | if ($remove_original) { 164 | $message .= ' (and removed the original image)'; 165 | } 166 | 167 | $this->grav['admin']->setMessage($message, 'info'); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /resize-images.yaml: -------------------------------------------------------------------------------- 1 | enabled: '1' 2 | adapter: imagick 3 | remove_original: '0' 4 | sizes: 5 | - 6 | width: 640 7 | quality: 92 8 | - 9 | width: 1000 10 | quality: 90 11 | - 12 | width: 1500 13 | quality: 87 14 | - 15 | width: 2500 16 | quality: 85 17 | - 18 | width: 3500 19 | quality: 82 20 | --------------------------------------------------------------------------------