├── src ├── helpers.php ├── Commands │ └── ClearCacheCommand.php ├── GlideServiceProvider.php ├── Http │ └── Controllers │ │ ├── GlideController.php │ │ └── GlideController │ │ └── GlideResponseFactory.php └── GlideImageGenerator.php ├── ide.json ├── routes └── web.php ├── config └── glide.php ├── pint.json ├── CHANGELOG.md ├── LICENSE.md ├── composer.json └── README.md /src/helpers.php: -------------------------------------------------------------------------------- 1 | where('source', '.*') 8 | ->name('glide.generate'); 9 | 10 | if ($domain = config('glide.route.domain')) { 11 | $route->domain($domain); 12 | } 13 | 14 | if (config('glide.route.signed')) { 15 | $route->middleware('signed'); 16 | } 17 | -------------------------------------------------------------------------------- /config/glide.php: -------------------------------------------------------------------------------- 1 | [ 5 | /** 6 | * The domain that will be used to generate the Glide URLs. 7 | */ 8 | 'domain' => null, 9 | 10 | /** 11 | * Whether the generated Glide URLs should be signed. 12 | * For some browsers this differing query string 13 | * might prevent browser caching of the image, 14 | * but major browsers appear to handle fine. 15 | */ 16 | 'signed' => false, 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /src/Commands/ClearCacheCommand.php: -------------------------------------------------------------------------------- 1 | getCachePath() 16 | ); 17 | 18 | $this->info('Cleared Glide cache'); 19 | 20 | return static::SUCCESS; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/GlideServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-glide') 15 | ->hasRoute('web') 16 | ->hasCommand(ClearCacheCommand::class) 17 | ->hasConfigFile(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "no_superfluous_phpdoc_tags": true, 5 | "concat_space": { 6 | "spacing": "one" 7 | }, 8 | "single_quote": true, 9 | "combine_consecutive_issets": true, 10 | "combine_consecutive_unsets": true, 11 | "explicit_string_variable": true, 12 | "global_namespace_import": true, 13 | "single_trait_insert_per_statement": true, 14 | "ordered_traits": true, 15 | "types_spaces": { 16 | "space": "single" 17 | }, 18 | "new_with_parentheses": { 19 | "anonymous_class": false, 20 | "named_class": true 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `ralphjsmit/laravel-glide` will be documented in this file. 4 | 5 | ## 2.0.0 - 2025-12-20 6 | 7 | - Feat: Glide V3 support by @lao9s. 8 | 9 | ## 1.3.4 - 2025-07-12 10 | 11 | - Feat: ability to sign Glide image URLs. 12 | 13 | ## 1.3.3 - 2025-07-03 14 | 15 | - Feat: support scoping Glide URL to domain 16 | 17 | ## 1.3.2 - 2025-06-20 18 | 19 | - Fix: allow merging style attribute. 20 | 21 | ## 1.3.1 - 2025-03-15 22 | 23 | - Fix: do not attempt to generate a `srcset` for provided SVG files. 24 | 25 | ## 1.3.0 - 2025-02-25 26 | 27 | - Feat: Laravel 12 support in #10 by @poldixd. 28 | 29 | ## 1.2.3 - 2024-12-26 30 | 31 | - Feat: ability to let images grow past their full size 32 | 33 | ## 1.2.1 - 2024-04-13 34 | 35 | - Chore: upgrade to league/glide-symfony because the Laravel wrapper is an empty wrapper and not doing anything specific. 36 | 37 | ## 1.2.0 - 2024-03-14 38 | 39 | - Laravel 11 Compatibility. 40 | 41 | ## 1.1.0 - 2023-11-01 42 | 43 | - Allow adding `loading="lazy"` attributes. 44 | 45 | ## 1.0.0 - 2023-10-27 46 | 47 | - Initial release. 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) ralphjsmit 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 | -------------------------------------------------------------------------------- /src/Http/Controllers/GlideController.php: -------------------------------------------------------------------------------- 1 | new GlideResponseFactory($request), 21 | 'source' => glide()->getSourcePath(), 22 | 'cache' => glide()->getCachePath(), 23 | 'base_url' => '', 24 | ]); 25 | 26 | $width = $request->integer('width'); 27 | 28 | try { 29 | return $server->getImageResponse($source, [ 30 | ...$width ? ['w' => $width] : [], 31 | 'fit' => 'crop', 32 | ]); 33 | } catch (FileNotFoundException) { 34 | abort(404); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Controllers/GlideController/GlideResponseFactory.php: -------------------------------------------------------------------------------- 1 | readStream($path); 19 | 20 | $response = new StreamedResponse(); 21 | $response->headers->set('Content-Type', $cache->mimeType($path)); 22 | $response->headers->set('Content-Length', $cache->fileSize($path)); 23 | $response->setPublic(); 24 | $response->setMaxAge(31536000); 25 | $response->setExpires(date_create()->modify('+1 years')); 26 | 27 | if ($this->request) { 28 | $response->setLastModified(date_create()->setTimestamp($cache->lastModified($path))); 29 | $response->isNotModified($this->request); 30 | } 31 | 32 | $response->setCallback(function () use ($stream) { 33 | if (ftell($stream) !== 0) { 34 | rewind($stream); 35 | } 36 | fpassthru($stream); 37 | fclose($stream); 38 | }); 39 | 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ralphjsmit/laravel-glide", 3 | "description": "Auto-magically generate responsive images from static image files.", 4 | "keywords": [ 5 | "ralphjsmit", 6 | "laravel", 7 | "laravel-glide" 8 | ], 9 | "homepage": "https://github.com/ralphjsmit/laravel-glide", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Ralph J. Smit", 14 | "email": "rjs@ralphjsmit.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^10.0|^11.0|^12.0", 21 | "illuminate/http": "^10.29|^11.0|^12.0", 22 | "illuminate/support": "^10.29|^11.0|^12.0", 23 | "illuminate/view": "^10.29|^11.0|^12.0", 24 | "league/glide": "^3.0", 25 | "spatie/laravel-package-tools": "^1.14.0" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.0" 29 | }, 30 | "autoload": { 31 | "files": [ 32 | "src/helpers.php" 33 | ], 34 | "psr-4": { 35 | "RalphJSmit\\Laravel\\Glide\\": "src/" 36 | } 37 | }, 38 | "scripts": { 39 | "format": "vendor/bin/pint" 40 | }, 41 | "config": { 42 | "sort-packages": true, 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true, 45 | "phpstan/extension-installer": true 46 | } 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "RalphJSmit\\Laravel\\Glide\\GlideServiceProvider" 52 | ] 53 | } 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true 57 | } 58 | -------------------------------------------------------------------------------- /src/GlideImageGenerator.php: -------------------------------------------------------------------------------- 1 | isGlideSupported($path); 19 | 20 | $attributes->setAttributes([ 21 | 'src' => $this->getSrcAttribute($path, $maxWidth), 22 | ...$isGlideSupported ? ['srcset' => $this->getSrcsetAttribute($path, $maxWidth)] : [], 23 | ...($isGlideSupported && $sizes !== null) ? ['sizes' => $sizes] : [], 24 | ...$lazy ? ['loading' => 'lazy'] : [], 25 | ]); 26 | 27 | if (! $grow) { 28 | $attributes = $attributes->style("max-width: {$this->getImageWidth($path)}px"); 29 | } 30 | 31 | return $attributes; 32 | } 33 | 34 | protected function getSrcAttribute(string $path, ?int $maxWidth): string 35 | { 36 | if (! $this->isGlideSupported($path)) { 37 | return asset($path); 38 | } 39 | 40 | if ($maxWidth === null) { 41 | return asset($path); 42 | } 43 | 44 | $imageWidth = $this->getImageWidth($path); 45 | 46 | // For generating the `src` url, we should not use values bigger than the image width, because 47 | // the browser will load these images at their original size as second request after picking 48 | // the optimal version. An upsized version should be a convenience thing and not a default. 49 | return $this->generateUrl($path, [ 50 | 'width' => $imageWidth ? min($imageWidth, $maxWidth) : $maxWidth, 51 | ]); 52 | } 53 | 54 | protected function getSrcsetAttribute(string $path, ?int $maxWidth): string 55 | { 56 | $scale = collect([ 57 | 400, 58 | 800, 59 | 1200, 60 | 1600, 61 | 2000, 62 | 2500, 63 | 3000, 64 | 3500, 65 | 4000, 66 | 5000, 67 | 6000, 68 | 7000, 69 | 8000, 70 | 9000, 71 | 10000, 72 | ]); 73 | 74 | $imageWidth = $this->getImageWidth($path); 75 | 76 | $scale = $scale 77 | ->when($maxWidth)->reject(fn (int $width) => $width > $maxWidth) 78 | // We will up-scale an image up to 2x it's original size. Above that it has no use anymore. 79 | ->when($imageWidth)->reject(fn (int $width) => $width > ($imageWidth * 2)); 80 | 81 | // Push a final version with exactly the correct max-width if the difference with the last item 82 | // in the scale is bigger than 50px. Otherwise, the additional provided type is not so useful. 83 | if ($maxWidth && ($maxWidth - $scale->last()) > 50) { 84 | $scale->push($maxWidth); 85 | } 86 | 87 | return $scale 88 | ->mapWithKeys(function (int $width) use ($path): array { 89 | return [$width => $this->generateUrl($path, ['width' => $width])]; 90 | }) 91 | ->map(fn (string $src, int $width) => "{$src} {$width}w") 92 | ->implode(', '); 93 | } 94 | 95 | protected function getImageWidth(string $path): ?int 96 | { 97 | return Cache::rememberForever("glide::image-generator.image-width.{$path}", function () use ($path) { 98 | return rescue(fn () => $this->getImageManager()->read(public_path($path))->width()); 99 | }); 100 | } 101 | 102 | protected function generateUrl(string $path, array $parameters): string 103 | { 104 | return config('glide.route.signed') 105 | ? Url::signedRoute('glide.generate', ['source' => $path, ...$parameters]) 106 | : route('glide.generate', ['source' => $path, ...$parameters]); 107 | } 108 | 109 | protected function isGlideSupported(string $path): bool 110 | { 111 | return ! Str::endsWith($path, ['.svg']); 112 | } 113 | 114 | protected function getImageManager(): ImageManager 115 | { 116 | if (extension_loaded('gd')) { 117 | return ImageManager::gd(); 118 | } 119 | 120 | if (extension_loaded('imagick')) { 121 | return ImageManager::imagick(); 122 | } 123 | 124 | throw new RuntimeException('No supported image driver (GD or Imagick) is installed.'); 125 | } 126 | 127 | public function getSourcePath(): string 128 | { 129 | return public_path(); 130 | } 131 | 132 | public function getCachePath(): string 133 | { 134 | return storage_path('framework/cache/glide'); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![laravel-glide](https://github.com/ralphjsmit/laravel-glide/blob/main/docs/images/laravel-glide.jpg) 2 | 3 | # Never worry about manually rescaling static images again! 4 | 5 | Currently, it's almost a requirement to **load images** on websites in such a way that they are **responsive** and not unnecessarily big. This means that every image should be **scaled down to a variety of sizes**, so that browser on smaller screens don't have to download a large image unnecessarily. This technique is accomplished by using `srcset` and `sizes` attributes on each `img` tag. 6 | 7 | However, if you **receive a 3.000 x 2.000px** image from your client, you **don't want** to put this into Figma or other tool, **generate 5 versions**, name them in a sensible way, manually put them in the correct public folder, etc. This is just a tremendous hassle, whereas usually you just want to **drop in the original image** in your project, refer to it via a `src` and be done with it. This package aims to solve this problem in a simple and sensible way. 8 | 9 | Instead of **manually needing to generate all these images**, we can use an image generator like [Glide](https://glide.thephpleague.com/). This package provides a simple way to reuse or scale images. 10 | 11 | ## Installation 12 | 13 | Run the following command to install the package: 14 | 15 | ```bash 16 | composer require ralphjsmit/laravel-glide 17 | ``` 18 | 19 | You do not need to publish a config file or anything else. 20 | 21 | ## Usage 22 | 23 | In order to demonstrate this package's usage, we'll use the following example. Previously, you **would include** an image like this: 24 | 25 | ```blade 26 | Some alt text 27 | ``` 28 | 29 | This loads the `my-huge-image.png` on it's full resolution on every screen size. **With this package**, you'd do this: 30 | 31 | ```blade 32 | src('img/my-huge-image.png') }} alt="Some alt text" /> 33 | ``` 34 | 35 | Under the hood, this will be **converted to the following** output: 36 | 37 | ```blade 38 | Some alt text 52 | ``` 53 | 54 | If your **browser** receives the above code, it will **determine the optimal image size**. Say that your browser is 800px wide at a resolution of `2x`, then it would be optimal to have an image of 1600px. Your browser will then look into the `srcset` and it will take the URL for the 1600px version (https://your-app.com/glide/img/my-huge-image.png?width=1600). The browser will then call this URL and **Glide will generate the 1600px image version** for the browser and return it. 55 | 56 | Glide will **cache all images**, so that it doesn't have to generate the same image over and over again. Even on the first image, Glide will still be vary fast always. 57 | 58 | Because the browser in this case only requests the 1600px, the other URLs are **not called** and therefore also **not processed** by Glide. This solution is therefore perfect, because it will only do the **minimum amount of work**. 59 | 60 | Your image will automatically be upscaled to a maximum of 2x the resolution provided in your original image. 61 | 62 | The `glide()->src()` function is even **auto-completed** if you use Laravel Idea to the files in your public-/asset-path. 63 | 64 | ### Setting a maximum width 65 | 66 | Say that your image is 2000px wide. However, you have displayed in such a way that it will **at its biggest only be a 1000px wide**. In that case, pass a second parameter to the `glide()->src()` function to set a maximum width: 67 | 68 | ```blade 69 | src('img/my-huge-image.png', 1000) }} alt="Some alt text" /> 70 | ``` 71 | 72 | This will output the image variations up to the last version that fits inside the maximum width, plus an image at exactly the maximum width, but not wider. Also, your original `src` will also inherit this maximum width: 73 | 74 | ```blade 75 | Some alt text 85 | ``` 86 | 87 | ### Specifying a `sizes` attribute as well 88 | 89 | You can provide a `sizes` attribute as well. This attribute is handy to tell the browser what **width an image will approximately have at a certain breakpoint**. You can give any value you want. This is an example of showing that on screens smaller than 500px, the image is approximately full-width, and on screens above (the default), it is approximately 50% of the screen width: 90 | 91 | ```blade 92 | src('img/my-huge-image.png', 1000, sizes: '(max-width: 500px) 100vw, 50vw') }} alt="Some alt text" /> 93 | ``` 94 | 95 | Which will result in: 96 | 97 | ```blade 98 | Some alt text 109 | ``` 110 | 111 | ### Eager loading images 112 | 113 | HTML allows for a native way to lazy load images if they are below the fold. This reduces the initial page size and speeds up the page load. Lazy loading for images is enabled by default for URLs outputted by Glide. 114 | 115 | However, if you need to eager load an image, you can pass `lazy: false` to disable lazy loading for an image. You should only do this for images that are above the fold on initial page load, otherwise it will slow down your page unnecessarily. 116 | 117 | ```blade 118 | src('img/my-huge-image.png', 1000, sizes: '(max-width: 500px) 100vw, 50vw', lazy: false) }} alt="Some alt text" /> 119 | ``` 120 | 121 | Which will result in: 122 | 123 | ```blade 124 | 133 | alt="Some alt text" 134 | /> 135 | 136 | ``` 137 | 138 | ### Clearing cache 139 | 140 | If you want to **clear the cache**, you can call the following command: 141 | 142 | ```bash 143 | php artisan glide:clear 144 | ``` 145 | 146 | This will **empty the entire Glide cache**. You can choose to put this in your deployment script on production if you often _modify_ your current images (adding new images has no effect on the cache, they will just be generated anew). Another option is to only run this command when you actually _modify_ an existing image and then run this command manually via SSH on the server in that situation. 147 | 148 | ## Glide Configuration 149 | 150 | Currently, the package **does not provide configuration** options and it just assumes **sensible defaults**. 151 | 152 | Since it is geared at auto-generating versions for static images, it will **assume** the `public_path()`/`asset()` as root folder for the images. 153 | 154 | The cache is positioned at the `storage/framework/cache/glide` folder. 155 | 156 | Currently, it is not possible to **modify these locations**. However, that would not be so hard to implement. If you have a use case for this, please let me know via the issues or provide a PR. 157 | 158 | ## Roadmap 159 | 160 | I hope this package will be useful to you! If you have any ideas or suggestions on how to make it more useful, please let me know (rjs@ralphjsmit.com) or via the issues. 161 | 162 | PRs are welcome, so feel free to fork and submit a pull request. I'll be happy to review your changes, think along and add them to the package. 163 | 164 | ## Credits 165 | 166 | This package was partially inspired by the archived [flowframe/laravel-glide](https://github.com/Flowframe/laravel-glide) by [Lars Klopstra](https://github.com/larsklopstra). 167 | 168 | ## General 169 | 170 | 🐞 If you spot a bug, please submit a detailed issue and I'll try to fix it as soon as possible. 171 | 172 | 🔐 If you discover a vulnerability, please e-mail rjs@ralphjsmit.com. 173 | 174 | 🙌 If you want to contribute, please submit a pull request. All PRs will be fully credited. If you're unsure whether I'd accept your idea, feel free to contact me! 175 | 176 | 🙋‍♂️ [Ralph J. Smit](https://ralphjsmit.com) 177 | --------------------------------------------------------------------------------