├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── lio.php ├── resources └── js │ └── svgo.config.js └── src ├── Contracts ├── HasArguments.php ├── HasConfig.php ├── Image.php └── Optimizer.php ├── Exceptions └── InvalidConfiguration.php ├── Facades └── ImageOptimizer.php ├── FilesystemImage.php ├── LioServiceProvider.php ├── LocalImage.php ├── Middlewares └── OptimizeUploadedImages.php ├── OptimizerChain.php ├── OptimizerChainFactory.php ├── Optimizers ├── CommandOptimizer.php ├── Cwebp.php ├── Gifsicle.php ├── Jpegoptim.php ├── Optipng.php ├── Pngquant.php ├── ReSmushOptimizer.php ├── Svgo.php ├── Svgo2.php ├── WithArgumentsOptimizer.php └── WithConfigOptimizer.php └── TempLocalImage.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `lio` will be documented in this file. 4 | 5 | ## 2.0.1 - 2022-06-15 6 | 7 | - 🐛 Fix reSmush optimizer bugs 8 | 9 | ## 2.0.0 - 2022-06-15 10 | 11 | - 💥 Refactor optimizers and change the config file 12 | - ✨ reSmush Optimizer 13 | 14 | ## 1.0.0 - 2022-05-26 15 | 16 | - 💥 Refactor Optimizers 17 | - ⬆️ Support for SVGO `2.x` 18 | 19 | ## 0.2.1 - 2022-02-23 20 | 21 | - Fix temp directory didn't delete bug 22 | 23 | ## 0.2.0 - 2022-02-22 24 | 25 | - Add `optimizeLocal ` method for optimizing local files 26 | - Add `OptimizeUploadedImages ` middleware 27 | 28 | ## 0.1.1 - 2022-02-21 29 | 30 | - Fix OptimizerChainFactory bug 31 | 32 | ## 0.1.0 - 2022-02-21 33 | 34 | - initial release 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) bvtterfly 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚨 THIS PACKAGE HAS BEEN ABANDONED 🚨 2 | 3 | I no longer use Laravel and cannot justify the time needed to maintain this package. That's why I have chosen to abandon it. Feel free to fork my code and maintain your own copy. 4 | 5 | # Easily optimize images using Laravel 6 | 7 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/bvtterfly/lio.svg?style=flat-square)](https://packagist.org/packages/bvtterfly/lio) 8 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/bvtterfly/lio/run-tests?label=tests)](https://github.com/bvtterfly/lio/actions?query=workflow%3Arun-tests+branch%3Amain) 9 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/bvtterfly/lio/Check%20&%20fix%20styling?label=code%20style)](https://github.com/bvtterfly/lio/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/bvtterfly/lio?style=flat-square)](https://packagist.org/packages/bvtterfly/lio) 11 | 12 | Lio can optimize PNGs, JPGs, SVGs, and GIFs by running them through a chain of various [image optimization tools](https://github.com/bvtterfly/lio#command-line-optimization-tools). 13 | 14 | This package is heavily based on `Spatie`'s `spatie/image-optimizer` and `spatie/laravel-image-optimizer` packages and can optimize local images like them. 15 | In addition, It optimizes images stored on the Laravel filesystem disks. 16 | 17 | Here's how you can use it: 18 | 19 | ```php 20 | use Bvtterfly\Lio\Facades\ImageOptimizer; 21 | // The image from your configured filesystem disk will be downloaded, optimized, and uploaded to the output path in 22 | ImageOptimizer::optimize($pathToImage, $pathToOptimizedImage); 23 | // The local image will be replaced with an optimized version which should be smaller 24 | ImageOptimizer::optimizeLocal($pathToImage); 25 | // if you use a second parameter the package will not modify the original 26 | ImageOptimizer::optimizeLocal($pathToImage, $pathToOptimizedImage); 27 | ``` 28 | If you don't like facades, just resolve a configured instance of `Bvtterfly\Lio\OptimizerChain` out of the container: 29 | 30 | ```php 31 | use Bvtterfly\Lio\OptimizerChain; 32 | app(OptimizerChain::class)->optimize($pathToImage, $pathToOptimizedImage); 33 | ``` 34 | 35 | ## Installation 36 | 37 | You can install the package via composer: 38 | 39 | ```bash 40 | composer require bvtterfly/lio 41 | ``` 42 | 43 | The package will automatically register itself. 44 | 45 | The package uses a bunch of binaries to optimize images. To learn which ones on how to install them, head over to the [image optimization tools](https://github.com/bvtterfly/lio#command-line-optimization-tools) section. 46 | 47 | The package comes with some sane defaults to optimize images. You can modify that configuration by publishing the config file. 48 | 49 | ```bash 50 | php artisan vendor:publish --tag="lio-config" 51 | ``` 52 | 53 | This is the contents of the published config file: 54 | 55 | ```php 56 | use Bvtterfly\Lio\Optimizers\Cwebp; 57 | use Bvtterfly\Lio\Optimizers\Gifsicle; 58 | use Bvtterfly\Lio\Optimizers\Jpegoptim; 59 | use Bvtterfly\Lio\Optimizers\Optipng; 60 | use Bvtterfly\Lio\Optimizers\Pngquant; 61 | use Bvtterfly\Lio\Optimizers\ReSmushOptimizer; 62 | use Bvtterfly\Lio\Optimizers\Svgo; 63 | use Bvtterfly\Lio\Optimizers\Svgo2; 64 | 65 | return [ 66 | /* 67 | * If set to `default` it uses your default filesystem disk. 68 | * You can set it to any filesystem disks configured in your application. 69 | */ 70 | 'disk' => 'default', 71 | 72 | /* 73 | * If set to `true` all output of the optimizer binaries will be appended to the default log channel. 74 | * You can also set this to a class that implements `Psr\Log\LoggerInterface` 75 | * or any log channels you configured in your application. 76 | */ 77 | 'log_optimizer_activity' => false, 78 | 79 | /* 80 | * Optimizers are responsible for optimizing your image 81 | */ 82 | 'optimizers' => [ 83 | Jpegoptim::class => [ 84 | '--max=85', 85 | '--strip-all', 86 | '--all-progressive', 87 | ], 88 | Pngquant::class => [ 89 | '--quality=85', 90 | '--force', 91 | '--skip-if-larger', 92 | ], 93 | Optipng::class => [ 94 | '-i0', 95 | '-o2', 96 | '-quiet', 97 | ], 98 | Svgo2::class => [], 99 | Gifsicle::class => [ 100 | '-b', 101 | '-O3', 102 | ], 103 | Cwebp::class => [ 104 | '-m 6', 105 | '-pass 10', 106 | '-mt', 107 | '-q 80', 108 | ], 109 | // Svgo::class => [ 110 | // '--disable={cleanupIDs,removeViewBox}', 111 | // ], 112 | // ReSmushOptimizer::class => [ 113 | // 'quality' => 92, 114 | // 'retry' => 3, 115 | // 'mime' => [ 116 | // 'image/png', 117 | // 'image/jpeg', 118 | // 'image/gif', 119 | // 'image/bmp', 120 | // 'image/tiff', 121 | // ], 122 | // 123 | // 'exif' => false, 124 | // 125 | // ], 126 | ], 127 | 128 | /* 129 | * The maximum time in seconds each optimizer is allowed to run separately. 130 | */ 131 | 'timeout' => 60, 132 | 133 | /* 134 | * The directories where your binaries are stored. 135 | * Only use this when your binaries are not accessible in the global environment. 136 | */ 137 | 'binaries_path' => [ 138 | 'jpegoptim' => '', 139 | 'optipng' => '', 140 | 'pngquant' => '', 141 | 'svgo' => '', 142 | 'gifsicle' => '', 143 | 'cwebp' => '', 144 | ], 145 | 146 | 147 | /* 148 | * The directory where the temporary files will be stored. 149 | */ 150 | 'temporary_directory' => storage_path('app/temp'), 151 | 152 | ]; 153 | ``` 154 | ### Command-Line Optimization tools 155 | 156 | The package will use these optimizers if they are present on your system: 157 | 158 | - [JpegOptim](http://freecode.com/projects/jpegoptim) 159 | - [Optipng](http://optipng.sourceforge.net/) 160 | - [Pngquant 2](https://pngquant.org/) 161 | - [SVGO 2](https://github.com/svg/svgo) 162 | - [Gifsicle](http://www.lcdf.org/gifsicle/) 163 | - [cwebp](https://developers.google.com/speed/webp/docs/precompiled) 164 | 165 | Here's how to install all the optimizers on Ubuntu: 166 | 167 | ```bash 168 | sudo apt-get install jpegoptim 169 | sudo apt-get install optipng 170 | sudo apt-get install pngquant 171 | sudo npm install -g svgo@2.8.x 172 | sudo apt-get install gifsicle 173 | sudo apt-get install webp 174 | ``` 175 | 176 | And here's how to install the binaries on MacOS (using [Homebrew](https://brew.sh/)): 177 | 178 | ```bash 179 | brew install jpegoptim 180 | brew install optipng 181 | brew install pngquant 182 | npm install -g svgo@2.8.x 183 | brew install gifsicle 184 | brew install webp 185 | ``` 186 | And here's how to install the binaries on Fedora/RHEL/CentOS: 187 | 188 | ```bash 189 | sudo dnf install epel-release 190 | sudo dnf install jpegoptim 191 | sudo dnf install optipng 192 | sudo dnf install pngquant 193 | sudo npm install -g svgo@2.8.x 194 | sudo dnf install gifsicle 195 | sudo dnf install libwebp-tools 196 | ``` 197 | > If You can't install and use above optimizers, You can still optimize your images using [reSmush Optimizer](https://github.com/bvtterfly/lio#resmush-optimizer). 198 | 199 | ## Which tools will do what? 200 | 201 | The package will automatically decide which tools to use on a particular image. 202 | 203 | ### JPGs 204 | 205 | JPGs will be made smaller by running them through [JpegOptim](http://freecode.com/projects/jpegoptim). These options are used: 206 | - `-m85`: this will store the image with 85% quality. This setting [seems to satisfy Google's Pagespeed compression rules](https://webmasters.stackexchange.com/questions/102094/google-pagespeed-how-to-satisfy-the-new-image-compression-rules) 207 | - `--strip-all`: this strips out all text information such as comments and EXIF data 208 | - `--all-progressive`: this will make sure the resulting image is a progressive one, meaning it can be downloaded using multiple passes of progressively higher details. 209 | 210 | ### PNGs 211 | 212 | PNGs will be made smaller by running them through two tools. The first one is [Pngquant 2](https://pngquant.org/), a lossy PNG compressor. We set no extra options, their defaults are used. After that we run the image through a second one: [Optipng](http://optipng.sourceforge.net/). These options are used: 213 | - `-i0`: this will result in a non-interlaced, progressive scanned image 214 | - `-o2`: this set the optimization level to two (multiple IDAT compression trials) 215 | 216 | ### SVGs 217 | 218 | SVGs will be minified by [SVGO 2](https://github.com/svg/svgo). SVGO's default configuration will be used, with the omission of the `cleanupIDs` plugin because that one is known to cause troubles when displaying multiple optimized SVGs on one page. 219 | 220 | Please be aware that SVGO can break your svg. You'll find more info on that in this [excellent blogpost](https://www.sarasoueidan.com/blog/svgo-tools/) by [Sara Soueidan](https://twitter.com/SaraSoueidan). 221 | 222 | The default SVGO optimizer (`Svgo2`) is only compatible with SVGO `2.x`. For custom SVGO configuration, you must create [your configuration file](https://github.com/svg/svgo#configuration) and pass its path to the config array: 223 | 224 | ```php 225 | Svgo2::class => [ 226 | 'path' => '/path/to/your/svgo/config.js' 227 | ] 228 | ``` 229 | 230 | If you installed SVGO `1.x` and can't upgrade to `2.x`, You can uncomment the `Svgo` optimizer in the config file: 231 | 232 | ```php 233 | Svgo::class => [ 234 | '--disable={cleanupIDs,removeViewBox}', 235 | ], 236 | // Svgo2::class => [], 237 | ``` 238 | 239 | ### GIFs 240 | 241 | GIFs will be optimized by [Gifsicle](http://www.lcdf.org/gifsicle/). These options will be used: 242 | - `-O3`: this sets the optimization level to Gifsicle's maximum, which produces the slowest but best results 243 | 244 | ### WEBPs 245 | 246 | WEBPs will be optimized by [Cwebp](https://developers.google.com/speed/webp/docs/cwebp). These options will be used: 247 | - `-m 6` for the slowest compression method in order to get the best compression. 248 | - `-pass 10` for maximizing the amount of analysis pass. 249 | - `-mt` multithreading for some speed improvements. 250 | - `-q 90` Quality factor that brings the least noticeable changes. 251 | 252 | (Settings are original taken from [here](https://medium.com/@vinhlh/how-i-apply-webp-for-optimizing-images-9b11068db349)) 253 | 254 | #### Set Binary Path 255 | 256 | If your binaries are not accessible in the global environment, You can set them using `binaries_path` option in the config file. 257 | 258 | ### reSmush Optimizer 259 | 260 | When you can't install command-line optimizer tools, you can comment them on the config file to disable them and uncomment the reSumsh optimizer to enable it. [reSmush](https://resmush.it/) provides a free API for optimizing images. However, it can only optimize up to 5MB of PNG, JPG, GIF, BMP, and TIF images. 261 | 262 | 263 | ## Usage 264 | You can resolve a configured instance of `Bvtterfly\Lio\OptimizerChain` out of the container: 265 | ```php 266 | use Bvtterfly\Lio\OptimizerChain; 267 | app(OptimizerChain::class)->optimize($pathToImage, $pathToOptimizedImage); 268 | ``` 269 | or using facade: 270 | 271 | ```php 272 | use Bvtterfly\Lio\Facades\ImageOptimizer; 273 | // The image from your configured filesystem disk will be downloaded, optimized, and uploaded to the output path in 274 | ImageOptimizer::optimize($pathToImage, $pathToOptimizedImage); 275 | ``` 276 | if your files are local you can using `optimizeLocal` method: 277 | 278 | ```php 279 | use Bvtterfly\Lio\Facades\ImageOptimizer; 280 | // The local image will be replaced with an optimized version which should be smaller 281 | ImageOptimizer::optimizeLocal($pathToImage); 282 | // if you use a second parameter the package will not modify the original 283 | ImageOptimizer::optimizeLocal($pathToImage, $pathToOptimizedImage); 284 | ``` 285 | 286 | ### Using the middleware 287 | 288 | If you want to optimize all uploaded images in requests to route automatically, You can use the `OptimizeUploadedImages` middleware. 289 | 290 | ```php 291 | Route::middleware(OptimizeUploadedImages::class)->group(function () { 292 | // all images will be optimized automatically 293 | Route::post('images', 'ImageController@store'); 294 | }); 295 | ``` 296 | 297 | ### Writing a custom optimizers 298 | 299 | You may want to write your own optimizer to optimize your images via other utilities. An optimizer is any class that implements the `Bvtterfly\Lio\Contracts\Optimizer` interface: 300 | 301 | ```php 302 | use Psr\Log\LoggerInterface; 303 | 304 | interface Optimizer 305 | { 306 | /** 307 | * Determines if the given image can be handled by the optimizer. 308 | * 309 | * @param Image $image 310 | * 311 | * @return bool 312 | */ 313 | public function canHandle(Image $image): bool; 314 | 315 | /** 316 | * Sets the path to the image that should be optimized. 317 | * 318 | * @param string $imagePath 319 | * 320 | * @return Optimizer 321 | */ 322 | public function setImagePath(string $imagePath): self; 323 | 324 | /** 325 | * Sets the logger for logging optimization process. 326 | * 327 | * @param LoggerInterface $logger 328 | * 329 | * @return Optimizer 330 | */ 331 | public function setLogger(LoggerInterface $logger): self; 332 | 333 | /** 334 | * Sets the amount of seconds optimizer may use. 335 | * 336 | * @param int $timeout 337 | * 338 | * @return Optimizer 339 | */ 340 | public function setTimeout(int $timeout): self; 341 | 342 | /** 343 | * Runs the optimizer. 344 | * 345 | * @return void 346 | */ 347 | public function run(): void; 348 | } 349 | ``` 350 | 351 | If you want to view an example implementation take a look at [the existing optimizers](https://github.com/bvtterfly/lio/tree/main/src/Optimizers) shipped with this package. 352 | You can add the fully qualified classname of your optimizer as a key in the `optimizers` array in the config file. 353 | 354 | ## Testing 355 | 356 | ```bash 357 | composer test 358 | ``` 359 | 360 | ## Changelog 361 | 362 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 363 | 364 | ## Contributing 365 | 366 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 367 | 368 | ## Credits 369 | 370 | - [ARI](https://github.com/bvtterfly) 371 | - [All Contributors](../../contributors) 372 | 373 | ## License 374 | 375 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 376 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bvtterfly/lio", 3 | "description": "Easily optimize images using Laravel", 4 | "keywords": [ 5 | "bvtterfly", 6 | "laravel", 7 | "laravel-image-optimizer", 8 | "lio" 9 | ], 10 | "homepage": "https://github.com/bvtterfly/lio", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "ARI", 15 | "email": "thearihdrn@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.0", 21 | "ext-fileinfo": "*", 22 | "guzzlehttp/guzzle": "^7.4", 23 | "illuminate/contracts": "^9.0", 24 | "spatie/laravel-package-tools": "^1.9.2", 25 | "spatie/temporary-directory": "^2.0" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.0", 29 | "nunomaduro/collision": "^6.0", 30 | "nunomaduro/larastan": "^2.0.1", 31 | "orchestra/testbench": "^7.0", 32 | "pestphp/pest": "^1.21", 33 | "pestphp/pest-plugin-laravel": "^1.1", 34 | "phpstan/extension-installer": "^1.1", 35 | "phpstan/phpstan-deprecation-rules": "^1.0", 36 | "phpstan/phpstan-phpunit": "^1.0", 37 | "phpunit/phpunit": "^9.5", 38 | "spatie/laravel-ray": "^1.26" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Bvtterfly\\Lio\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Bvtterfly\\Lio\\Tests\\": "tests" 48 | } 49 | }, 50 | "scripts": { 51 | "analyse": "vendor/bin/phpstan analyse", 52 | "test": "vendor/bin/pest", 53 | "test-coverage": "vendor/bin/pest --coverage", 54 | "format": "vendor/bin/pint" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "\\Bvtterfly\\Lio\\LioServiceProvider" 67 | ] 68 | } 69 | }, 70 | "minimum-stability": "dev", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /config/lio.php: -------------------------------------------------------------------------------- 1 | 'default', 18 | 19 | /* 20 | * If set to `true` all output of the optimizer binaries will be appended to the default log channel. 21 | * You can also set this to a class that implements `Psr\Log\LoggerInterface` 22 | * or any log channels you configured in your application. 23 | */ 24 | 'log_optimizer_activity' => false, 25 | 26 | /* 27 | * Optimizers are responsible for optimizing your image 28 | */ 29 | 'optimizers' => [ 30 | Jpegoptim::class => [ 31 | '--max=85', 32 | '--strip-all', 33 | '--all-progressive', 34 | ], 35 | Pngquant::class => [ 36 | '--quality=85', 37 | '--force', 38 | '--skip-if-larger', 39 | ], 40 | Optipng::class => [ 41 | '-i0', 42 | '-o2', 43 | '-quiet', 44 | ], 45 | Svgo2::class => [], 46 | Gifsicle::class => [ 47 | '-b', 48 | '-O3', 49 | ], 50 | Cwebp::class => [ 51 | '-m 6', 52 | '-pass 10', 53 | '-mt', 54 | '-q 80', 55 | ], 56 | // Svgo::class => [ 57 | // '--disable={cleanupIDs,removeViewBox}', 58 | // ], 59 | // ReSmushOptimizer::class => [ 60 | // 'quality' => 92, 61 | // 'retry' => 3, 62 | // 'mime' => [ 63 | // 'image/png', 64 | // 'image/jpeg', 65 | // 'image/gif', 66 | // 'image/bmp', 67 | // 'image/tiff', 68 | // ], 69 | // 70 | // 'exif' => false, 71 | // 72 | // ], 73 | ], 74 | 75 | /* 76 | * The maximum time in seconds each optimizer is allowed to run separately. 77 | */ 78 | 'timeout' => 60, 79 | 80 | /* 81 | * The directories where your binaries are stored. 82 | * Only use this when your binaries are not accessible in the global environment. 83 | */ 84 | 'binaries_path' => [ 85 | 'jpegoptim' => '', 86 | 'optipng' => '', 87 | 'pngquant' => '', 88 | 'svgo' => '', 89 | 'gifsicle' => '', 90 | 'cwebp' => '', 91 | ], 92 | 93 | /* 94 | * The directory where the temporary files will be stored. 95 | */ 96 | 'temporary_directory' => storage_path('app/temp'), 97 | 98 | ]; 99 | -------------------------------------------------------------------------------- /resources/js/svgo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | name: 'preset-default', 5 | params: { 6 | overrides: { 7 | // or disable plugins 8 | cleanupIDs: false, 9 | removeViewBox: false, 10 | }, 11 | }, 12 | }, 13 | ], 14 | }; -------------------------------------------------------------------------------- /src/Contracts/HasArguments.php: -------------------------------------------------------------------------------- 1 | exists($pathToImage)) { 17 | throw new InvalidArgumentException("`{$pathToImage}` does not exist"); 18 | } 19 | $this->pathToImage = $pathToImage; 20 | } 21 | 22 | public static function make(Filesystem $filesystem, $pathToImage): FilesystemImage 23 | { 24 | return new FilesystemImage($filesystem, $pathToImage); 25 | } 26 | 27 | public function update(TempLocalImage $tempImage, string $pathToOutput) 28 | { 29 | $this->disk->makeDirectory(dirname($pathToOutput)); 30 | $this->disk->put($pathToOutput, file_get_contents($tempImage->path())); 31 | } 32 | 33 | public function tempImage(): TempLocalImage 34 | { 35 | return TempLocalImage::make( 36 | $this->disk->get($this->pathToImage), 37 | $this->pathToImage 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/LioServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('lio') 14 | ->hasConfigFile(); 15 | } 16 | 17 | public function packageRegistered() 18 | { 19 | $this->app->singleton(OptimizerChain::class, function () { 20 | return OptimizerChainFactory::create(config('lio')); 21 | }); 22 | 23 | $this->app->alias(OptimizerChain::class, 'image-optimizer'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LocalImage.php: -------------------------------------------------------------------------------- 1 | pathToImage); 20 | } 21 | 22 | public function path(): string 23 | { 24 | return $this->pathToImage; 25 | } 26 | 27 | public function extension(): string 28 | { 29 | $extension = pathinfo($this->pathToImage, PATHINFO_EXTENSION); 30 | 31 | return strtolower($extension); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Middlewares/OptimizeUploadedImages.php: -------------------------------------------------------------------------------- 1 | allFiles()) 17 | ->flatten() 18 | ->filter(function (UploadedFile $file) { 19 | if (app()->environment('testing')) { 20 | return true; 21 | } 22 | 23 | return $file->isValid(); 24 | }) 25 | ->each(function (UploadedFile $file) use ($optimizerChain) { 26 | $optimizerChain->optimizeLocal($file->getPathname()); 27 | }); 28 | 29 | return $next($request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OptimizerChain.php: -------------------------------------------------------------------------------- 1 | optimizers; 24 | } 25 | 26 | public function getLogger(): LoggerInterface 27 | { 28 | return $this->logger; 29 | } 30 | 31 | public function getFilesystem(): Filesystem 32 | { 33 | return $this->filesystem; 34 | } 35 | 36 | public function addOptimizer(Optimizer $optimizer): static 37 | { 38 | $this->optimizers[] = $optimizer; 39 | 40 | return $this; 41 | } 42 | 43 | public function setOptimizers(array $optimizers): static 44 | { 45 | $this->optimizers = []; 46 | 47 | foreach ($optimizers as $optimizer) { 48 | $this->addOptimizer($optimizer); 49 | } 50 | 51 | return $this; 52 | } 53 | 54 | /** Sets the amount of seconds each separate optimizer may use. 55 | * @param int $timeoutInSeconds 56 | * @return $this 57 | */ 58 | public function setTimeout(int $timeoutInSeconds): static 59 | { 60 | $this->timeout = $timeoutInSeconds; 61 | 62 | return $this; 63 | } 64 | 65 | public function useLogger(LoggerInterface $logger): static 66 | { 67 | $this->logger = $logger; 68 | 69 | return $this; 70 | } 71 | 72 | public function useFilesystem(Filesystem $filesystem): static 73 | { 74 | $this->filesystem = $filesystem; 75 | 76 | return $this; 77 | } 78 | 79 | public function optimize(string $pathToImage, string $pathToOutput) 80 | { 81 | $fileSystemImagePath = $pathToImage; 82 | 83 | $image = FilesystemImage::make($this->filesystem, $fileSystemImagePath); 84 | 85 | $tempImage = $image->tempImage(); 86 | 87 | $pathToImage = $tempImage->path(); 88 | 89 | try { 90 | $this->optimizeImage($pathToImage, $tempImage); 91 | 92 | $image->update($tempImage, $pathToOutput); 93 | } catch (\Exception $e) { 94 | $this->logger->error("Optimizing {$fileSystemImagePath} failed!"); 95 | 96 | throw $e; 97 | } finally { 98 | $tempImage->delete(); 99 | } 100 | } 101 | 102 | public function optimizeLocal(string $pathToImage, string $pathToOutput = null) 103 | { 104 | if ($pathToOutput) { 105 | copy($pathToImage, $pathToOutput); 106 | 107 | $pathToImage = $pathToOutput; 108 | } 109 | 110 | $image = new LocalImage($pathToImage); 111 | 112 | $this->optimizeImage($pathToImage, $image); 113 | } 114 | 115 | /** 116 | * @param string $pathToImage 117 | * @param Image $image 118 | * @return void 119 | */ 120 | protected function optimizeImage(string $pathToImage, Image $image): void 121 | { 122 | $this->logger->info("Start optimizing {$pathToImage}"); 123 | 124 | foreach ($this->optimizers as $optimizer) { 125 | $this->runOptimizer($optimizer, $image); 126 | } 127 | } 128 | 129 | protected function runOptimizer(Optimizer $optimizer, Image $image) 130 | { 131 | if (! $optimizer->canHandle($image)) { 132 | return; 133 | } 134 | 135 | $optimizerClass = get_class($optimizer); 136 | 137 | $this->logger->info("Using optimizer: `{$optimizerClass}`"); 138 | 139 | $optimizer->setImagePath($image->path()); 140 | $optimizer->setTimeout($this->timeout); 141 | $optimizer->setLogger($this->logger); 142 | 143 | $optimizer->run(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/OptimizerChainFactory.php: -------------------------------------------------------------------------------- 1 | useFilesystem(self::getFilesystem($config)) 26 | ->useLogger($logger) 27 | ->setOptimizers(self::getOptimizers($config, $logger)) 28 | ->setTimeout($config['timeout']); 29 | } 30 | 31 | /** 32 | * @throws InvalidConfiguration 33 | */ 34 | protected static function getLogger($config): LoggerInterface 35 | { 36 | $configuredLogger = $config['log_optimizer_activity']; 37 | 38 | if (class_exists($configuredLogger)) { 39 | self::ensureLogger($configuredLogger); 40 | 41 | return new $configuredLogger(); 42 | } 43 | 44 | /** @var LogManager $logManager */ 45 | $logManager = app(LogManager::class); 46 | 47 | if (is_bool($configuredLogger)) { 48 | $configuredLogger = $configuredLogger ? $logManager->getDefaultDriver() : 'null'; 49 | } 50 | 51 | return $logManager->channel($configuredLogger); 52 | } 53 | 54 | private static function getOptimizers(array $config, LoggerInterface $logger): array 55 | { 56 | return collect($config['optimizers']) 57 | ->map(function (mixed $value, mixed $key) use ($logger) { 58 | [$optimizer, $options] = self::getOptimizerAndOptions($key, $value); 59 | 60 | self::ensureOptimizer($optimizer); 61 | 62 | if (is_string($optimizer)) { 63 | $optimizer = self::createOptimizer($optimizer, $options); 64 | } 65 | 66 | /** @var Optimizer $optimizer */ 67 | $optimizer->setLogger($logger); 68 | 69 | return $optimizer; 70 | }) 71 | ->toArray(); 72 | } 73 | 74 | private static function getFilesystem(array $config): Filesystem 75 | { 76 | $disk = $config['disk']; 77 | 78 | /** @var Factory $factory */ 79 | $factory = app(Factory::class); 80 | 81 | if ($disk === 'default') { 82 | return $factory->disk(); 83 | } 84 | 85 | return $factory->disk($disk); 86 | } 87 | 88 | /** 89 | * @param mixed $key 90 | * @param mixed $value 91 | * @return array 92 | */ 93 | private static function getOptimizerAndOptions(mixed $key, mixed $value): array 94 | { 95 | $options = []; 96 | if (is_int($key)) { 97 | return [$value, $options]; 98 | } 99 | 100 | $optimizer = $key; 101 | 102 | if (is_array($value)) { 103 | $options = $value; 104 | } 105 | 106 | return [$optimizer, $options]; 107 | } 108 | 109 | /** 110 | * @throws InvalidConfiguration 111 | */ 112 | private static function ensureOptimizer(mixed $optimizer): void 113 | { 114 | if ( 115 | ! is_a($optimizer, Optimizer::class, true) 116 | ) { 117 | $optimizerClass = is_object($optimizer) ? get_class($optimizer) : $optimizer; 118 | 119 | throw InvalidConfiguration::notAnOptimizer($optimizerClass); 120 | } 121 | } 122 | 123 | /** 124 | * @throws InvalidConfiguration 125 | */ 126 | private static function ensureLogger(string $logger): void 127 | { 128 | if (! is_a($logger, LoggerInterface::class, true)) { 129 | throw InvalidConfiguration::notAnLogger($logger); 130 | } 131 | } 132 | 133 | /** 134 | * @param class-string $optimizerClass 135 | * @param array $options 136 | * @return Optimizer 137 | * 138 | * @throws BindingResolutionException 139 | */ 140 | private static function createOptimizer(string $optimizerClass, array $options): Optimizer 141 | { 142 | $optimizer = app()->make($optimizerClass); 143 | if ($optimizer instanceof HasArguments && count($options)) { 144 | $optimizer->setArguments($options); 145 | } 146 | if ($optimizer instanceof HasConfig && count($options)) { 147 | $optimizer->setConfig($options); 148 | } 149 | 150 | return $optimizer; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Optimizers/CommandOptimizer.php: -------------------------------------------------------------------------------- 1 | binaryName; 23 | } 24 | 25 | public function setLogger(LoggerInterface $logger): Optimizer 26 | { 27 | $this->logger = $logger; 28 | 29 | return $this; 30 | } 31 | 32 | abstract public function canHandle(Image $image): bool; 33 | 34 | public function setImagePath(string $imagePath): static 35 | { 36 | $this->imagePath = $imagePath; 37 | 38 | return $this; 39 | } 40 | 41 | public function setTimeout(int $timeout): static 42 | { 43 | $this->timeout = $timeout; 44 | 45 | return $this; 46 | } 47 | 48 | public function getBinaryPath(): string 49 | { 50 | $binaryPath = config("lio.binaries_path.{$this->binaryName}"); 51 | if (strlen($binaryPath) > 0 && substr($binaryPath, -1) !== DIRECTORY_SEPARATOR) { 52 | $binaryPath = $binaryPath.DIRECTORY_SEPARATOR; 53 | } 54 | 55 | return $binaryPath; 56 | } 57 | 58 | abstract public function getCommand(): string; 59 | 60 | public function run(): void 61 | { 62 | $command = $this->getCommand(); 63 | 64 | $this->logger?->info("Executing `{$command}`"); 65 | 66 | $process = Process::fromShellCommandline($command); 67 | 68 | $process 69 | ->setTimeout($this->timeout) 70 | ->run(); 71 | 72 | $this->logResult($process); 73 | } 74 | 75 | protected function logResult(Process $process) 76 | { 77 | if (! $process->isSuccessful()) { 78 | $this->logger?->error("Process errored with `{$process->getErrorOutput()}`"); 79 | 80 | return; 81 | } 82 | 83 | $this->logger?->info("Process successfully ended with output `{$process->getOutput()}`"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Optimizers/Cwebp.php: -------------------------------------------------------------------------------- 1 | mime() === 'image/webp'; 14 | } 15 | 16 | public function getCommand(): string 17 | { 18 | return "\"{$this->getBinaryPath()}{$this->binaryName}\" {$this->getArgumentString()}" 19 | .' '.escapeshellarg($this->imagePath) 20 | .' -o '.escapeshellarg($this->imagePath); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Optimizers/Gifsicle.php: -------------------------------------------------------------------------------- 1 | mime() === 'image/gif'; 14 | } 15 | 16 | public function getCommand(): string 17 | { 18 | return "\"{$this->getBinaryPath()}{$this->binaryName}\" {$this->getArgumentString()}" 19 | .' -i '.escapeshellarg($this->imagePath) 20 | .' -o '.escapeshellarg($this->imagePath); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Optimizers/Jpegoptim.php: -------------------------------------------------------------------------------- 1 | mime() === 'image/jpeg'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Optimizers/Optipng.php: -------------------------------------------------------------------------------- 1 | mime() === 'image/png'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Optimizers/Pngquant.php: -------------------------------------------------------------------------------- 1 | mime() === 'image/png'; 14 | } 15 | 16 | public function getCommand(): string 17 | { 18 | return "\"{$this->getBinaryPath()}{$this->binaryName}\" {$this->getArgumentString()}" 19 | .' '.escapeshellarg($this->imagePath) 20 | .' --output='.escapeshellarg($this->imagePath); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Optimizers/ReSmushOptimizer.php: -------------------------------------------------------------------------------- 1 | setConfig($configs); 45 | } 46 | 47 | public function canHandle(Image $image): bool 48 | { 49 | return in_array($image->mime(), $this->getMime()) && filesize($image->path()) < self::MAX_FILE_SIZE; 50 | } 51 | 52 | public function setImagePath(string $imagePath): Optimizer 53 | { 54 | $this->imagePath = $imagePath; 55 | 56 | return $this; 57 | } 58 | 59 | public function setLogger(LoggerInterface $logger): Optimizer 60 | { 61 | $this->logger = $logger; 62 | 63 | return $this; 64 | } 65 | 66 | public function setTimeout(int $timeout): Optimizer 67 | { 68 | $this->timeout = $timeout; 69 | 70 | return $this; 71 | } 72 | 73 | private function upload(): ?Response 74 | { 75 | $file = fopen($this->imagePath, 'r'); 76 | 77 | $params = http_build_query([ 78 | 'qlty' => $this->getQuality(), 79 | 'exif' => $this->getExif(), 80 | ]); 81 | 82 | return rescue( 83 | fn () => Http::attach('files', $file, basename($this->imagePath)) 84 | ->timeout($this->timeout) 85 | ->retry($this->getRetry()) 86 | ->post(self::ENDPOINT.'?'.$params), 87 | fn ($e) => $e instanceof RequestException ? $e->response : null 88 | ); 89 | } 90 | 91 | public function run(): void 92 | { 93 | $this->logger?->info('Uploading image to reSmush'); 94 | 95 | $result = $this->upload(); 96 | 97 | if (! $result?->successful()) { 98 | $this->logger?->error('Failed to upload image: '.$result->body()); 99 | 100 | return; 101 | } 102 | 103 | $json = $result->json(); 104 | 105 | $this->logger?->info('Downloading optimized image from reSmush'); 106 | 107 | $destinationPath = Arr::get($json, 'dest'); 108 | 109 | $downloadResponse = rescue( 110 | fn () => Http::timeout($this->timeout)->retry($this->getRetry())->get($destinationPath), 111 | fn ($e) => $e instanceof RequestException ? $e->response : null 112 | ); 113 | 114 | if ($downloadResponse?->successful()) { 115 | file_put_contents($this->imagePath, $downloadResponse->body()); 116 | $this->logger->info('Image Optimized successfully'); 117 | } else { 118 | $this->logger->error('Failed to download image from: '.$destinationPath); 119 | $this->logger->error('Error: '.$downloadResponse?->body()); 120 | } 121 | } 122 | 123 | public function setConfig(array $config = []) 124 | { 125 | $this->config = $config; 126 | } 127 | 128 | public function getQuality(): int 129 | { 130 | return Arr::get($this->config, 'quality') ?? self::DEFAULT_QUALITY; 131 | } 132 | 133 | public function getMime(): array 134 | { 135 | return Arr::get($this->config, 'mime') ?? self::SUPPORTED_MIMES; 136 | } 137 | 138 | public function getExif(): bool 139 | { 140 | return Arr::get($this->config, 'exif') ?? self::DEFAULT_EXIF; 141 | } 142 | 143 | public function getRetry(): int 144 | { 145 | return Arr::get($this->config, 'retry') ?? self::DEFAULT_RETRY; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Optimizers/Svgo.php: -------------------------------------------------------------------------------- 1 | extension() !== 'svg') { 14 | return false; 15 | } 16 | 17 | return in_array($image->mime(), [ 18 | 'text/html', 19 | 'image/svg', 20 | 'image/svg+xml', 21 | 'text/plain', 22 | ]); 23 | } 24 | 25 | public function getCommand(): string 26 | { 27 | return "\"{$this->getBinaryPath()}{$this->binaryName}\" {$this->getArgumentString()}" 28 | .' --input='.escapeshellarg($this->imagePath) 29 | .' --output='.escapeshellarg($this->imagePath); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Optimizers/Svgo2.php: -------------------------------------------------------------------------------- 1 | extension() !== 'svg') { 15 | return false; 16 | } 17 | 18 | return in_array($image->mime(), [ 19 | 'text/html', 20 | 'image/svg', 21 | 'image/svg+xml', 22 | 'text/plain', 23 | ]); 24 | } 25 | 26 | public function getCommand(): string 27 | { 28 | return "\"{$this->getBinaryPath()}{$this->binaryName}\"" 29 | .' --config '.escapeshellarg($this->getConfigPath()) 30 | .' '.escapeshellarg($this->imagePath) 31 | .' -o '.escapeshellarg($this->imagePath); 32 | } 33 | 34 | private function getConfigPath() 35 | { 36 | return Arr::get($this->config, 'path') ?? $this->getDefaultConfigPath(); 37 | } 38 | 39 | private function getDefaultConfigPath(): string 40 | { 41 | return implode(DIRECTORY_SEPARATOR, [ 42 | __DIR__, 43 | '..', 44 | '..', 45 | 'resources', 46 | 'js', 47 | 'svgo.config.js', 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Optimizers/WithArgumentsOptimizer.php: -------------------------------------------------------------------------------- 1 | setArguments($arguments); 14 | } 15 | 16 | public function setArguments(array $arguments = []) 17 | { 18 | $this->arguments = $arguments; 19 | } 20 | 21 | public function getCommand(): string 22 | { 23 | return "\"{$this->getBinaryPath()}{$this->binaryName}\" {$this->getArgumentString()} ".escapeshellarg($this->imagePath); 24 | } 25 | 26 | protected function getArgumentString(): string 27 | { 28 | return implode(' ', $this->arguments); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Optimizers/WithConfigOptimizer.php: -------------------------------------------------------------------------------- 1 | setConfig($config); 14 | } 15 | 16 | public function setConfig(array $config = []) 17 | { 18 | $this->config = $config; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/TempLocalImage.php: -------------------------------------------------------------------------------- 1 | force() 27 | ->create(); 28 | file_put_contents($temporaryDirectory->path($filename), $content); 29 | 30 | return new TempLocalImage($filename, $temporaryDirectory); 31 | } 32 | 33 | public function mime(): string 34 | { 35 | return mime_content_type($this->path()); 36 | } 37 | 38 | public function path(): string 39 | { 40 | return $this->temporaryDirectory->path($this->filename); 41 | } 42 | 43 | public function extension(): string 44 | { 45 | $extension = pathinfo($this->path(), PATHINFO_EXTENSION); 46 | 47 | return strtolower($extension); 48 | } 49 | 50 | public function delete() 51 | { 52 | $this->temporaryDirectory->delete(); 53 | } 54 | 55 | public function __destruct() 56 | { 57 | $this->delete(); 58 | } 59 | } 60 | --------------------------------------------------------------------------------