├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── image-optimizer.php ├── resources └── lang │ └── en │ └── image-optimizer.php └── src ├── Components ├── BaseFileUpload.php └── SpatieMediaLibraryFileUpload.php ├── Facades └── ImageOptimizer.php └── ImageOptimizerServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `image-optimizer` will be documented in this file. 4 | 5 | ## v1.6.0 - 2025-03-08 6 | 7 | ### What's Changed 8 | 9 | * Laravel 12 compatibility by @FinnPaes in https://github.com/joshembling/image-optimizer/pull/42 10 | 11 | ### New Contributors 12 | 13 | * @FinnPaes made their first contribution in https://github.com/joshembling/image-optimizer/pull/42 14 | 15 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.5.0...v1.6.0 16 | 17 | ## v1.5.0 - 2025-02-24 18 | 19 | ### What's Changed 20 | 21 | * Add Conditional Image Resizing Functions by @Autive in https://github.com/joshembling/image-optimizer/pull/40 22 | 23 | ### New Contributors 24 | 25 | * @Autive made their first contribution in https://github.com/joshembling/image-optimizer/pull/40 26 | 27 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.4...v1.5.0 28 | 29 | ## v1.4.4 - 2025-01-17 30 | 31 | ### What's Changed 32 | 33 | * Fixed maxParallelUploads attribute bug by @lucasvieira2902 in https://github.com/joshembling/image-optimizer/pull/39 34 | 35 | ### New Contributors 36 | 37 | * @lucasvieira2902 made their first contribution in https://github.com/joshembling/image-optimizer/pull/39 38 | 39 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.3...v1.4.4 40 | 41 | ## v1.4.3 - 2024-11-05 42 | 43 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.2...v1.4.3 44 | 45 | ## v1.4.2 - 2024-09-17 46 | 47 | ### What's Changed 48 | 49 | * handle afterStateUpdated as array by @cleeimpro in https://github.com/joshembling/image-optimizer/pull/33 50 | 51 | ### New Contributors 52 | 53 | * @cleeimpro made their first contribution in https://github.com/joshembling/image-optimizer/pull/33 54 | 55 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.1...v1.4.2 56 | 57 | ## v1.4.1 - 2024-06-22 58 | 59 | ### What's Changed 60 | 61 | * fix: undefined class facade ImageOptimizer by @ahmadrio in https://github.com/joshembling/image-optimizer/pull/24 62 | 63 | ### New Contributors 64 | 65 | * @ahmadrio made their first contribution in https://github.com/joshembling/image-optimizer/pull/24 66 | 67 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.0...v1.4.1 68 | 69 | ## v1.4.0 - 2024-03-17 70 | 71 | - Adds support for Laravel 11 72 | - Minimum PHP requirement now 8.2 73 | 74 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.3.1...v1.4.0 75 | 76 | ## v1.3.1 - 2024-02-03 77 | 78 | - Updates documentation around versioning 79 | 80 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.3.0...v1.3.1 81 | 82 | ## v1.3.0 - 2024-01-28 83 | 84 | - Fixes https://github.com/joshembling/image-optimizer/issues/9 with upgrades to latest Filament changes in v3.2^ 85 | 86 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.2.0...v1.3.0 87 | 88 | ## 1.2.0 - 2023-10-17 89 | 90 | Requires `intervention/image` directly. 91 | 92 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.1.1...v1.2.0 93 | 94 | ## 1.1.1 - 2023-09-27 95 | 96 | Fixes FileUpload class import 97 | 98 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.1.0...v1.1.1 99 | 100 | ## 1.1.0 - 2023-09-27 101 | 102 | Fixes Spatie Media Library support 103 | 104 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.4...v1.1.0 105 | 106 | ## 1.0.3 - 2023-09-23 107 | 108 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.2...v1.0.3 109 | 110 | ## 1.0.2 - 2023-09-23 111 | 112 | Package name and action changes 113 | 114 | ### What's Changed 115 | 116 | - Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/joshembling/image-optimizer/pull/1 117 | - Revert "Bump actions/checkout from 3 to 4" by @joshembling in https://github.com/joshembling/image-optimizer/pull/2 118 | 119 | ### New Contributors 120 | 121 | - @dependabot made their first contribution in https://github.com/joshembling/image-optimizer/pull/1 122 | - @joshembling made their first contribution in https://github.com/joshembling/image-optimizer/pull/2 123 | 124 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.1...v1.0.2 125 | 126 | ## 1.0.1 - 2023-09-23 127 | 128 | Documentation update 129 | 130 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.0...v1.0.1 131 | 132 | ## 1.0.0 - 202X-XX-XX 133 | 134 | - initial release 135 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) joshembling 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 | > [!CAUTION] 2 | > There are no plans to extend this plugin's lifetime beyond Filament v3. Please do not plan to use this in production if you are thinking of upgrading to Filament v4 when it is released in the summer of 2025. 3 | 4 | # Optimize your Filament images before they reach your database. 5 | 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/joshembling/image-optimizer.svg?style=flat-square)](https://packagist.org/packages/joshembling/image-optimizer) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/joshembling/image-optimizer.svg?style=flat-square)](https://packagist.org/packages/joshembling/image-optimizer) 8 | 9 | When you currently upload an image using the native Filament component `FileUpload`, the original file is saved without any compression or conversion. 10 | 11 | Additionally, if you upload an image and use conversions with `SpatieMediaLibraryFileUpload`, the original file is saved with its corresponding versions provided on your model. 12 | 13 | What if you'd rather convert and reduce the image(s) before reaching your database/S3 bucket? Especially in the case where you know you'll never need to save the original image sizes the user has uploaded. 14 | 15 | 🤳 **This is where Filament Image Optimizer comes in**. 16 | 17 | You use the same components as you have been doing and have access to two additional methods for maximum optimization, saving you a lot of disk space in the process. 🎉 18 | 19 | ## Contents 20 | 21 | - [Contents](#contents) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [Optimizing Images](#optimizing-images) 25 | - [Resizing Images](#resizing-images) 26 | - [Combining Methods](#combining-methods) 27 | - [Multiple Images](#multiple-images) 28 | - [Examples](#examples) 29 | - [Debugging](#debugging) 30 | - [Changelog](#changelog) 31 | - [Contributing](#contributing) 32 | - [Security Vulnerabilities](#security-vulnerabilities) 33 | - [Credits](#credits) 34 | - [Licence](#license) 35 | 36 | ## Installation 37 | 38 | You can install the package via composer, which currently works with the latest Filament version (^3.2) and Laravel 10, 11 & 12: 39 | 40 | ```bash 41 | composer require joshembling/image-optimizer 42 | ``` 43 | 44 | If you are using Filament 3.0 or 3.1 install with: 45 | ```bash 46 | composer require joshembling/image-optimizer:v1.2 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Filament version 52 | 53 | You must be using [Filament v3.x](https://filamentphp.com/docs/3.x/panels/installation) to have access to this plugin. 54 | 55 | For specific versions that match your PHP, Laravel, Filament and Image Optimizer installations please see the table below: 56 | 57 | | PHP | Laravel version | Filament version | Image Optimizer version | 58 | | ----- | ----- | -----| ----- | 59 | | ^8.1 | ^10.0 | ^3.0 | 1.2 | 60 | | ^8.1 | ^10.0 | ^3.1 | 1.2 | 61 | | ^8.1 | ^10.0 | ^3.2 | ~1.3 | 62 | | ^8.2 | ^10.0, ^11.0 | ^3.2 | ^1.4 | 63 | | ^8.2 | ^10.0, ^11.0, ^12.0 | ^3.2 | ^1.6 | 64 | 65 | ### Server 66 | 67 | [GD Library](https://www.php.net/manual/en/image.installation.php) must be installed on your server to compress images. 68 | 69 | ### Optimizing images 70 | 71 | Before uploading your image, you may choose to optimize it by converting to your chosen format. The file saved to your disk will be the converted version only. 72 | 73 | E.g. I want to convert my image to 'webp': 74 | 75 | `````php 76 | use Filament\Forms\Components\FileUpload; 77 | 78 | FileUpload::make('attachment') 79 | ->image() 80 | ->optimize('webp'), 81 | ````` 82 | 83 | You can do exactly the same using `SpatieMediaLibraryFileUpload`: 84 | 85 | `````php 86 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 87 | 88 | SpatieMediaLibraryFileUpload::make('attachment') 89 | ->image() 90 | ->optimize('webp'), 91 | ````` 92 | 93 | ### Resizing images 94 | 95 | You may also want to resize an image by passing in a percentage you would like to reduce the image by. This will also maintain aspect ratio. 96 | 97 | E.g. I'd like to reduce my image (1280px x 720px) by 50%: 98 | 99 | `````php 100 | use Filament\Forms\Components\FileUpload; 101 | 102 | FileUpload::make('attachment') 103 | ->image() 104 | ->resize(50), 105 | ````` 106 | 107 | Uploaded image size is 640px x 360px. 108 | 109 | You can do the same using `SpatieMediaLibraryFileUpload`: 110 | 111 | `````php 112 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 113 | 114 | SpatieMediaLibraryFileUpload::make('attachment') 115 | ->image() 116 | ->resize(50), 117 | ````` 118 | 119 | ### Add maximum width and/or height 120 | 121 | You can also add a maximum width and/or height to the image. This will resize the image to the maximum width and/or height, maintaining the aspect ratio. 122 | 123 | `````php 124 | use Filament\Forms\Components\FileUpload; 125 | 126 | FileUpload::make('attachment') 127 | ->image() 128 | ->maxWidth(1024) 129 | ->maxHeight(768), 130 | ````` 131 | 132 | ### Combining methods 133 | 134 | You can combine these two methods for maximum optimization. 135 | 136 | `````php 137 | use Filament\Forms\Components\FileUpload; 138 | 139 | FileUpload::make('attachment') 140 | ->image() 141 | ->optimize('webp') 142 | ->resize(50), 143 | ````` 144 | 145 | `````php 146 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 147 | 148 | SpatieMediaLibraryFileUpload::make('attachment') 149 | ->image() 150 | ->optimize('webp') 151 | ->resize(50), 152 | ````` 153 | 154 | ### Multiple images 155 | 156 | You can also do this with multiple images - all images will be converted to the same format and reduced with the same percentage passed in. Just chain on `multiple()` to your upload: 157 | 158 | `````php 159 | use Filament\Forms\Components\FileUpload; 160 | 161 | FileUpload::make('attachment') 162 | ->image() 163 | ->multiple() 164 | ->optimize('jpg') 165 | ->resize(50), 166 | ````` 167 | 168 | `````php 169 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 170 | 171 | SpatieMediaLibraryFileUpload::make('attachment') 172 | ->image() 173 | ->multiple() 174 | ->optimize('jpg') 175 | ->resize(50), 176 | ````` 177 | 178 | ### Examples 179 | 180 | ![Before](images/before.jpg) 181 | 182 | ![After](images/after.jpg) 183 | 184 | ### Debugging 185 | 186 | - If you see a 'not found' exception, including "Method `optimize`" or "Method `resize`", ensure you run `composer update` so that your lock file is in sync with your `composer.json`. 187 | 188 | - You might see a 'Waiting for size' message and an infinite loading state on the component and the likely cause of this is a CORS issue. This can be quickly be resolved by ensuring you are serving and upload images from the same domain. Check your Javascript console for more information. 189 | 190 | ## Changelog 191 | 192 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 193 | 194 | ## Contributing 195 | 196 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 197 | 198 | ## Security Vulnerabilities 199 | 200 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 201 | 202 | ## Credits 203 | 204 | - [Josh Embling](https://github.com/joshembling) 205 | - [All Contributors](../../contributors) 206 | 207 | ## License 208 | 209 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 210 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joshembling/image-optimizer", 3 | "description": "Optimize your Filament images before they reach your database.", 4 | "keywords": [ 5 | "joshembling", 6 | "laravel", 7 | "filament", 8 | "filamentphp", 9 | "image", 10 | "optimization", 11 | "image-optimizer" 12 | ], 13 | "homepage": "https://github.com/joshembling/image-optimizer", 14 | "support": { 15 | "issues": "https://github.com/joshembling/image-optimizer/issues", 16 | "source": "https://github.com/joshembling/image-optimizer" 17 | }, 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Josh Embling", 22 | "email": "joshembling@gmail.com", 23 | "role": "Developer" 24 | } 25 | ], 26 | "require": { 27 | "php": "^8.2", 28 | "filament/forms": "^3.3", 29 | "illuminate/contracts": "^10.0|^11.0|^12.0", 30 | "intervention/image": "^2.7", 31 | "spatie/laravel-package-tools": "^1.19.0" 32 | }, 33 | "require-dev": { 34 | "laravel/pint": "^1.0", 35 | "nunomaduro/collision": "^7.9", 36 | "orchestra/testbench": "^8.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Joshembling\\ImageOptimizer\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Joshembling\\ImageOptimizer\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 50 | "format": "vendor/bin/pint" 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Joshembling\\ImageOptimizer\\ImageOptimizerServiceProvider" 59 | ], 60 | "aliases": { 61 | "ImageOptimizer": "Joshembling\\ImageOptimizer\\Facades\\ImageOptimizer" 62 | } 63 | } 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /config/image-optimizer.php: -------------------------------------------------------------------------------- 1 | | Arrayable | Closure | null 28 | */ 29 | protected array | Arrayable | Closure | null $acceptedFileTypes = null; 30 | 31 | protected bool | Closure $isDeletable = true; 32 | 33 | protected bool | Closure $isDownloadable = false; 34 | 35 | protected bool | Closure $isOpenable = false; 36 | 37 | protected bool | Closure $isPreviewable = true; 38 | 39 | protected bool | Closure $isReorderable = false; 40 | 41 | protected string | Closure | null $directory = null; 42 | 43 | protected string | Closure | null $diskName = null; 44 | 45 | protected bool | Closure $isMultiple = false; 46 | 47 | protected int | Closure | null $maxSize = null; 48 | 49 | protected int | Closure | null $minSize = null; 50 | 51 | protected int | Closure | null $maxParallelUploads = null; 52 | 53 | protected int | Closure | null $maxFiles = null; 54 | 55 | protected int | Closure | null $minFiles = null; 56 | 57 | protected string | Closure | null $optimize = null; 58 | 59 | protected int | Closure | null $resize = null; 60 | 61 | protected int | Closure | null $maxImageWidth = null; 62 | 63 | protected int | Closure | null $maxImageHeight = null; 64 | 65 | protected bool | Closure $shouldPreserveFilenames = false; 66 | 67 | protected bool | Closure $shouldMoveFiles = false; 68 | 69 | protected bool | Closure $shouldStoreFiles = true; 70 | 71 | protected bool | Closure $shouldFetchFileInformation = true; 72 | 73 | protected string | Closure | null $fileNamesStatePath = null; 74 | 75 | protected string | Closure $visibility = 'public'; 76 | 77 | protected ?Closure $deleteUploadedFileUsing = null; 78 | 79 | protected ?Closure $getUploadedFileNameForStorageUsing = null; 80 | 81 | protected ?Closure $getUploadedFileUsing = null; 82 | 83 | protected ?Closure $reorderUploadedFilesUsing = null; 84 | 85 | protected ?Closure $saveUploadedFileUsing = null; 86 | 87 | protected function setUp(): void 88 | { 89 | parent::setUp(); 90 | 91 | $this->afterStateHydrated(static function (BaseFileUpload $component, string | array | null $state): void { 92 | if (blank($state)) { 93 | $component->state([]); 94 | 95 | return; 96 | } 97 | 98 | $shouldFetchFileInformation = $component->shouldFetchFileInformation(); 99 | 100 | $files = collect(Arr::wrap($state)) 101 | ->filter(static function (string $file) use ($component, $shouldFetchFileInformation): bool { 102 | if (blank($file)) { 103 | return false; 104 | } 105 | 106 | if (! $shouldFetchFileInformation) { 107 | return true; 108 | } 109 | 110 | try { 111 | return $component->getDisk()->exists($file); 112 | } catch (UnableToCheckFileExistence $exception) { 113 | return false; 114 | } 115 | }) 116 | ->mapWithKeys(static fn (string $file): array => [((string) Str::uuid()) => $file]) 117 | ->all(); 118 | 119 | $component->state($files); 120 | }); 121 | 122 | $this->afterStateUpdated(static function (BaseFileUpload $component, $state) { 123 | if ($state instanceof TemporaryUploadedFile) { 124 | return; 125 | } 126 | 127 | if (blank($state)) { 128 | return; 129 | } 130 | 131 | if (is_array($state)) { 132 | return; 133 | } 134 | 135 | $component->state([(string) Str::uuid() => $state]); 136 | }); 137 | 138 | $this->beforeStateDehydrated(static function (BaseFileUpload $component): void { 139 | $component->saveUploadedFiles(); 140 | }); 141 | 142 | $this->dehydrateStateUsing(static function (BaseFileUpload $component, ?array $state): string | array | null | TemporaryUploadedFile { 143 | $files = array_values($state ?? []); 144 | 145 | if ($component->isMultiple()) { 146 | return $files; 147 | } 148 | 149 | return $files[0] ?? null; 150 | }); 151 | 152 | $this->getUploadedFileUsing(static function (BaseFileUpload $component, string $file, string | array | null $storedFileNames): ?array { 153 | /** @var FilesystemAdapter $storage */ 154 | $storage = $component->getDisk(); 155 | 156 | $shouldFetchFileInformation = $component->shouldFetchFileInformation(); 157 | 158 | if ($shouldFetchFileInformation) { 159 | try { 160 | if (! $storage->exists($file)) { 161 | return null; 162 | } 163 | } catch (UnableToCheckFileExistence $exception) { 164 | return null; 165 | } 166 | } 167 | 168 | $url = null; 169 | 170 | if ($component->getVisibility() === 'private') { 171 | try { 172 | $url = $storage->temporaryUrl( 173 | $file, 174 | now()->addMinutes(5), 175 | ); 176 | } catch (Throwable $exception) { 177 | // This driver does not support creating temporary URLs. 178 | } 179 | } 180 | 181 | $url ??= $storage->url($file); 182 | 183 | return [ 184 | 'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file), 185 | 'size' => $shouldFetchFileInformation ? $storage->size($file) : 0, 186 | 'type' => $shouldFetchFileInformation ? $storage->mimeType($file) : null, 187 | 'url' => $url, 188 | ]; 189 | }); 190 | 191 | $this->getUploadedFileNameForStorageUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file) { 192 | return $component->shouldPreserveFilenames() ? $file->getClientOriginalName() : (Str::ulid() . '.' . $file->getClientOriginalExtension()); 193 | }); 194 | 195 | $this->saveUploadedFileUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file): ?string { 196 | try { 197 | if (! $file->exists()) { 198 | return null; 199 | } 200 | } catch (UnableToCheckFileExistence $exception) { 201 | return null; 202 | } 203 | 204 | $compressedImage = null; 205 | $filename = $component->getUploadedFileNameForStorage($file); 206 | $optimize = $component->getOptimization(); 207 | $resize = $component->getResize(); 208 | $maxImageWidth = $component->getMaxImageWidth(); 209 | $maxImageHeight = $component->getMaxImageHeight(); 210 | $shouldResize = false; 211 | $imageHeight = null; 212 | $imageWidth = null; 213 | // $originalBinaryFile = $file->get(); 214 | 215 | if ( 216 | str_contains($file->getMimeType(), 'image') && 217 | ($optimize || $resize || $maxImageWidth || $maxImageHeight) 218 | ) { 219 | $image = InterventionImage::make($file); 220 | 221 | if ($optimize) { 222 | $quality = $optimize === 'jpeg' || 223 | $optimize === 'jpg' ? 70 : null; 224 | } 225 | 226 | if ($maxImageWidth && $image->width() > $maxImageWidth) { 227 | $shouldResize = true; 228 | $imageWidth = $maxImageWidth; 229 | } 230 | 231 | if ($maxImageHeight && $image->height() > $maxImageHeight) { 232 | $shouldResize = true; 233 | $imageHeight = $maxImageHeight; 234 | } 235 | 236 | if ($resize) { 237 | $shouldResize = true; 238 | 239 | if ($image->height() > $image->width()) { 240 | $imageHeight = $image->height() - ($image->height() * ($resize / 100)); 241 | } else { 242 | $imageWidth = $image->width() - ($image->width() * ($resize / 100)); 243 | } 244 | } 245 | 246 | if ($shouldResize) { 247 | $image->resize($imageWidth, $imageHeight, function ($constraint) { 248 | $constraint->aspectRatio(); 249 | }); 250 | } 251 | 252 | if ($optimize) { 253 | $compressedImage = $image->encode($optimize, $quality); 254 | } else { 255 | $compressedImage = $image->encode(); 256 | } 257 | 258 | $filename = self::formatFileName($filename, $optimize); 259 | } 260 | 261 | if ($compressedImage) { 262 | Storage::disk($component->getDiskName())->put( 263 | $component->getDirectory() . '/' . $filename, 264 | $compressedImage->getEncoded() 265 | ); 266 | 267 | return $component->getDirectory() . '/' . $filename; 268 | } 269 | 270 | if ( 271 | $component->shouldMoveFiles() && 272 | ($component->getDiskName() == (fn (): string => $this->disk)->call($file)) 273 | ) { 274 | $newPath = trim($component->getDirectory() . '/' . $component->getUploadedFileNameForStorage($file), '/'); 275 | 276 | $component->getDisk()->move((fn (): string => $this->path)->call($file), $newPath); 277 | 278 | return $newPath; 279 | } 280 | 281 | $storeMethod = $component->getVisibility() === 'public' ? 'storePubliclyAs' : 'storeAs'; 282 | 283 | return $file->{$storeMethod}( 284 | $component->getDirectory(), 285 | $component->getUploadedFileNameForStorage($file), 286 | $component->getDiskName() 287 | ); 288 | }); 289 | } 290 | 291 | protected function callAfterStateUpdatedHook(Closure $hook): void 292 | { 293 | $state = $this->getState(); 294 | 295 | $this->evaluate($hook, [ 296 | 'state' => $this->isMultiple() ? $state : Arr::first($state ?? []), 297 | 'old' => $this->isMultiple() ? $this->getOldState() : Arr::first($this->getOldState() ?? []), 298 | ]); 299 | } 300 | 301 | public function callAfterStateUpdated(): static 302 | { 303 | $state = $this->getState(); 304 | 305 | foreach ($this->afterStateUpdated as $callback) { 306 | $this->evaluate($callback, [ 307 | 'state' => $this->isMultiple() ? $state : Arr::first($state ?? []), 308 | ]); 309 | } 310 | 311 | return $this; 312 | } 313 | 314 | /** 315 | * @param array | Arrayable | Closure $types 316 | */ 317 | public function acceptedFileTypes(array | Arrayable | Closure $types): static 318 | { 319 | $this->acceptedFileTypes = $types; 320 | 321 | $this->rule(static function (BaseFileUpload $component) { 322 | $types = implode(',', ($component->getAcceptedFileTypes() ?? [])); 323 | 324 | return "mimetypes:{$types}"; 325 | }); 326 | 327 | return $this; 328 | } 329 | 330 | public function deletable(bool | Closure $condition = true): static 331 | { 332 | $this->isDeletable = $condition; 333 | 334 | return $this; 335 | } 336 | 337 | public function directory(string | Closure | null $directory): static 338 | { 339 | $this->directory = $directory; 340 | 341 | return $this; 342 | } 343 | 344 | public function disk(string | Closure | null $name): static 345 | { 346 | $this->diskName = $name; 347 | 348 | return $this; 349 | } 350 | 351 | public function downloadable(bool | Closure $condition = true): static 352 | { 353 | $this->isDownloadable = $condition; 354 | 355 | return $this; 356 | } 357 | 358 | public function openable(bool | Closure $condition = true): static 359 | { 360 | $this->isOpenable = $condition; 361 | 362 | return $this; 363 | } 364 | 365 | public function reorderable(bool | Closure $condition = true): static 366 | { 367 | $this->isReorderable = $condition; 368 | 369 | return $this; 370 | } 371 | 372 | public function previewable(bool | Closure $condition = true): static 373 | { 374 | $this->isPreviewable = $condition; 375 | 376 | return $this; 377 | } 378 | 379 | /** 380 | * @deprecated Use `downloadable()` instead. 381 | */ 382 | public function enableDownload(bool | Closure $condition = true): static 383 | { 384 | $this->downloadable($condition); 385 | 386 | return $this; 387 | } 388 | 389 | /** 390 | * @deprecated Use `openable()` instead. 391 | */ 392 | public function enableOpen(bool | Closure $condition = true): static 393 | { 394 | $this->openable($condition); 395 | 396 | return $this; 397 | } 398 | 399 | /** 400 | * @deprecated Use `reorderable()` instead. 401 | */ 402 | public function enableReordering(bool | Closure $condition = true): static 403 | { 404 | $this->reorderable($condition); 405 | 406 | return $this; 407 | } 408 | 409 | /** 410 | * @deprecated Use `previewable()` instead. 411 | */ 412 | public function disablePreview(bool | Closure $condition = true): static 413 | { 414 | $this->previewable(fn (BaseFileUpload $component): bool => ! $component->evaluate($condition)); 415 | 416 | return $this; 417 | } 418 | 419 | public function storeFileNamesIn(string | Closure | null $statePath): static 420 | { 421 | $this->fileNamesStatePath = $statePath; 422 | 423 | return $this; 424 | } 425 | 426 | public function preserveFilenames(bool | Closure $condition = true): static 427 | { 428 | $this->shouldPreserveFilenames = $condition; 429 | 430 | return $this; 431 | } 432 | 433 | public function moveFiles(bool | Closure $condition = true): static 434 | { 435 | $this->shouldMoveFiles = $condition; 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * @deprecated Use `moveFiles()` instead. 442 | */ 443 | public function moveFile(bool | Closure $condition = true): static 444 | { 445 | $this->moveFiles($condition); 446 | 447 | return $this; 448 | } 449 | 450 | public function maxSize(int | Closure | null $size): static 451 | { 452 | $this->maxSize = $size; 453 | 454 | $this->rule(static function (BaseFileUpload $component): string { 455 | $size = $component->getMaxSize(); 456 | 457 | return "max:{$size}"; 458 | }); 459 | 460 | return $this; 461 | } 462 | 463 | public function minSize(int | Closure | null $size): static 464 | { 465 | $this->minSize = $size; 466 | 467 | $this->rule(static function (BaseFileUpload $component): string { 468 | $size = $component->getMinSize(); 469 | 470 | return "min:{$size}"; 471 | }); 472 | 473 | return $this; 474 | } 475 | 476 | public function maxParallelUploads(int | Closure | null $count): static 477 | { 478 | $this->maxParallelUploads = $count; 479 | 480 | return $this; 481 | } 482 | 483 | public function maxFiles(int | Closure | null $count): static 484 | { 485 | $this->maxFiles = $count; 486 | 487 | return $this; 488 | } 489 | 490 | public function minFiles(int | Closure | null $count): static 491 | { 492 | $this->minFiles = $count; 493 | 494 | return $this; 495 | } 496 | 497 | public function multiple(bool | Closure $condition = true): static 498 | { 499 | $this->isMultiple = $condition; 500 | 501 | return $this; 502 | } 503 | 504 | public function optimize(string | Closure | null $optimize): static 505 | { 506 | $this->optimize = $optimize; 507 | 508 | return $this; 509 | } 510 | 511 | public function resize(int | Closure | null $reductionPercentage): static 512 | { 513 | $this->resize = $reductionPercentage; 514 | 515 | return $this; 516 | } 517 | 518 | public function maxImageWidth(int | Closure | null $width): static 519 | { 520 | $this->maxImageWidth = $width; 521 | 522 | return $this; 523 | } 524 | 525 | public function maxImageHeight(int | Closure | null $height): static 526 | { 527 | $this->maxImageHeight = $height; 528 | 529 | return $this; 530 | } 531 | 532 | public function storeFiles(bool | Closure $condition = true): static 533 | { 534 | $this->shouldStoreFiles = $condition; 535 | 536 | return $this; 537 | } 538 | 539 | /** 540 | * @deprecated Use `storeFiles()` instead. 541 | */ 542 | public function storeFile(bool | Closure $condition = true): static 543 | { 544 | $this->storeFiles($condition); 545 | 546 | return $this; 547 | } 548 | 549 | public function visibility(string | Closure | null $visibility): static 550 | { 551 | $this->visibility = $visibility; 552 | 553 | return $this; 554 | } 555 | 556 | public function deleteUploadedFileUsing(?Closure $callback): static 557 | { 558 | $this->deleteUploadedFileUsing = $callback; 559 | 560 | return $this; 561 | } 562 | 563 | public function getUploadedFileUsing(?Closure $callback): static 564 | { 565 | $this->getUploadedFileUsing = $callback; 566 | 567 | return $this; 568 | } 569 | 570 | public function reorderUploadedFilesUsing(?Closure $callback): static 571 | { 572 | $this->reorderUploadedFilesUsing = $callback; 573 | 574 | return $this; 575 | } 576 | 577 | public function saveUploadedFileUsing(?Closure $callback): static 578 | { 579 | $this->saveUploadedFileUsing = $callback; 580 | 581 | return $this; 582 | } 583 | 584 | public function isDeletable(): bool 585 | { 586 | return (bool) $this->evaluate($this->isDeletable); 587 | } 588 | 589 | public function isDownloadable(): bool 590 | { 591 | return (bool) $this->evaluate($this->isDownloadable); 592 | } 593 | 594 | public function isOpenable(): bool 595 | { 596 | return (bool) $this->evaluate($this->isOpenable); 597 | } 598 | 599 | public function isPreviewable(): bool 600 | { 601 | return (bool) $this->evaluate($this->isPreviewable); 602 | } 603 | 604 | public function isReorderable(): bool 605 | { 606 | return (bool) $this->evaluate($this->isReorderable); 607 | } 608 | 609 | /** 610 | * @return array | null 611 | */ 612 | public function getAcceptedFileTypes(): ?array 613 | { 614 | $types = $this->evaluate($this->acceptedFileTypes); 615 | 616 | if ($types instanceof Arrayable) { 617 | $types = $types->toArray(); 618 | } 619 | 620 | return $types; 621 | } 622 | 623 | public function getDirectory(): ?string 624 | { 625 | return $this->evaluate($this->directory); 626 | } 627 | 628 | public function getDisk(): Filesystem 629 | { 630 | return Storage::disk($this->getDiskName()); 631 | } 632 | 633 | public function getDiskName(): string 634 | { 635 | return $this->evaluate($this->diskName) ?? config('filament.default_filesystem_disk'); 636 | } 637 | 638 | public function getMaxFiles(): ?int 639 | { 640 | return $this->evaluate($this->maxFiles); 641 | } 642 | 643 | public function getMinFiles(): ?int 644 | { 645 | return $this->evaluate($this->minFiles); 646 | } 647 | 648 | public function getMaxSize(): ?int 649 | { 650 | return $this->evaluate($this->maxSize); 651 | } 652 | 653 | public function getMinSize(): ?int 654 | { 655 | return $this->evaluate($this->minSize); 656 | } 657 | 658 | public function getOptimization(): ?string 659 | { 660 | return $this->evaluate($this->optimize); 661 | } 662 | 663 | public function getResize(): ?int 664 | { 665 | return $this->evaluate($this->resize); 666 | } 667 | 668 | public function getMaxImageWidth(): ?int 669 | { 670 | return $this->evaluate($this->maxImageWidth); 671 | } 672 | 673 | public function getMaxImageHeight(): ?int 674 | { 675 | return $this->evaluate($this->maxImageHeight); 676 | } 677 | 678 | public function getMaxParallelUploads(): ?int 679 | { 680 | return $this->evaluate($this->maxParallelUploads); 681 | } 682 | 683 | public function getVisibility(): string 684 | { 685 | return $this->evaluate($this->visibility); 686 | } 687 | 688 | public function shouldPreserveFilenames(): bool 689 | { 690 | return (bool) $this->evaluate($this->shouldPreserveFilenames); 691 | } 692 | 693 | public function shouldMoveFiles(): bool 694 | { 695 | return $this->evaluate($this->shouldMoveFiles); 696 | } 697 | 698 | public function shouldFetchFileInformation(): bool 699 | { 700 | return (bool) $this->evaluate($this->shouldFetchFileInformation); 701 | } 702 | 703 | public function shouldStoreFiles(): bool 704 | { 705 | return $this->evaluate($this->shouldStoreFiles); 706 | } 707 | 708 | public function getFileNamesStatePath(): ?string 709 | { 710 | if (! $this->fileNamesStatePath) { 711 | return null; 712 | } 713 | 714 | return $this->generateRelativeStatePath($this->fileNamesStatePath); 715 | } 716 | 717 | /** 718 | * @return array 719 | */ 720 | public function getValidationRules(): array 721 | { 722 | $rules = [ 723 | $this->getRequiredValidationRule(), 724 | 'array', 725 | ]; 726 | 727 | if (filled($count = $this->getMaxFiles())) { 728 | $rules[] = "max:{$count}"; 729 | } 730 | 731 | if (filled($count = $this->getMinFiles())) { 732 | $rules[] = "min:{$count}"; 733 | } 734 | 735 | $rules[] = function (string $attribute, array $value, Closure $fail): void { 736 | $files = array_filter($value, fn (TemporaryUploadedFile | string $file): bool => $file instanceof TemporaryUploadedFile); 737 | 738 | $name = $this->getName(); 739 | 740 | $validator = Validator::make( 741 | [$name => $files], 742 | ["{$name}.*" => ['file', ...parent::getValidationRules()]], 743 | [], 744 | ["{$name}.*" => $this->getValidationAttribute()], 745 | ); 746 | 747 | if (! $validator->fails()) { 748 | return; 749 | } 750 | 751 | $fail($validator->errors()->first()); 752 | }; 753 | 754 | return $rules; 755 | } 756 | 757 | public function deleteUploadedFile(string $fileKey): static 758 | { 759 | $file = $this->removeUploadedFile($fileKey); 760 | 761 | if (blank($file)) { 762 | return $this; 763 | } 764 | 765 | $callback = $this->deleteUploadedFileUsing; 766 | 767 | if (! $callback) { 768 | return $this; 769 | } 770 | 771 | $this->evaluate($callback, [ 772 | 'file' => $file, 773 | ]); 774 | 775 | return $this; 776 | } 777 | 778 | public function removeUploadedFile(string $fileKey): string | TemporaryUploadedFile | null 779 | { 780 | $files = $this->getState(); 781 | $file = $files[$fileKey] ?? null; 782 | 783 | if (! $file) { 784 | return null; 785 | } 786 | 787 | if (is_string($file)) { 788 | $this->removeStoredFileName($file); 789 | } elseif ($file instanceof TemporaryUploadedFile) { 790 | $file->delete(); 791 | } 792 | 793 | unset($files[$fileKey]); 794 | 795 | $this->state($files); 796 | 797 | return $file; 798 | } 799 | 800 | public function removeStoredFileName(string $file): void 801 | { 802 | $statePath = $this->fileNamesStatePath; 803 | 804 | if (blank($statePath)) { 805 | return; 806 | } 807 | 808 | $this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $statePath) { 809 | if (! $component->isMultiple()) { 810 | $set($statePath, null); 811 | 812 | return; 813 | } 814 | 815 | $fileNames = $get($statePath) ?? []; 816 | 817 | if (array_key_exists($file, $fileNames)) { 818 | unset($fileNames[$file]); 819 | } 820 | 821 | $set($statePath, $fileNames); 822 | }); 823 | } 824 | 825 | /** 826 | * @param array $fileKeys 827 | */ 828 | public function reorderUploadedFiles(array $fileKeys): void 829 | { 830 | if (! $this->isReorderable) { 831 | return; 832 | } 833 | 834 | $fileKeys = array_flip($fileKeys); 835 | 836 | $state = collect($this->getState()) 837 | ->sortBy(static fn ($file, $fileKey) => $fileKeys[$fileKey] ?? null) // $fileKey may not be present in $fileKeys if it was added to the state during the reorder call 838 | ->all(); 839 | 840 | $this->state($state); 841 | } 842 | 843 | /** 844 | * @return array | null 845 | */ 846 | public function getUploadedFiles(): ?array 847 | { 848 | $urls = []; 849 | 850 | foreach ($this->getState() ?? [] as $fileKey => $file) { 851 | if ($file instanceof TemporaryUploadedFile) { 852 | $urls[$fileKey] = null; 853 | 854 | continue; 855 | } 856 | 857 | $callback = $this->getUploadedFileUsing; 858 | 859 | if (! $callback) { 860 | return [$fileKey => null]; 861 | } 862 | 863 | $urls[$fileKey] = $this->evaluate($callback, [ 864 | 'file' => $file, 865 | 'storedFileNames' => $this->getStoredFileNames(), 866 | ]) ?: null; 867 | } 868 | 869 | return $urls; 870 | } 871 | 872 | public function saveUploadedFiles(): void 873 | { 874 | if (blank($this->getState())) { 875 | $this->state([]); 876 | 877 | return; 878 | } 879 | 880 | if (! $this->shouldStoreFiles()) { 881 | return; 882 | } 883 | 884 | $state = array_filter(array_map(function (TemporaryUploadedFile | string $file) { 885 | if (! $file instanceof TemporaryUploadedFile) { 886 | return $file; 887 | } 888 | 889 | $callback = $this->saveUploadedFileUsing; 890 | 891 | if (! $callback) { 892 | $file->delete(); 893 | 894 | return $file; 895 | } 896 | 897 | $storedFile = $this->evaluate($callback, [ 898 | 'file' => $file, 899 | ]); 900 | 901 | if ($storedFile === null) { 902 | return null; 903 | } 904 | 905 | $this->storeFileName($storedFile, $file->getClientOriginalName()); 906 | 907 | $file->delete(); 908 | 909 | return $storedFile; 910 | }, Arr::wrap($this->getState()))); 911 | 912 | if ($this->isReorderable && ($callback = $this->reorderUploadedFilesUsing)) { 913 | $state = $this->evaluate($callback, [ 914 | 'state' => $state, 915 | ]); 916 | } 917 | 918 | $this->state($state); 919 | } 920 | 921 | public function storeFileName(string $file, string $fileName): void 922 | { 923 | $statePath = $this->fileNamesStatePath; 924 | 925 | if (blank($statePath)) { 926 | return; 927 | } 928 | 929 | $this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $fileName, $statePath) { 930 | if (! $component->isMultiple()) { 931 | $set($statePath, $fileName); 932 | 933 | return; 934 | } 935 | 936 | $fileNames = $get($statePath) ?? []; 937 | $fileNames[$file] = $fileName; 938 | 939 | $set($statePath, $fileNames); 940 | }); 941 | } 942 | 943 | /** 944 | * @return string | array | null 945 | */ 946 | public function getStoredFileNames(): string | array | null 947 | { 948 | $state = null; 949 | $statePath = $this->fileNamesStatePath; 950 | 951 | if (filled($statePath)) { 952 | $state = $this->evaluate(fn (Get $get) => $get($statePath)); 953 | } 954 | 955 | if (blank($state) && $this->isMultiple()) { 956 | return []; 957 | } 958 | 959 | return $state; 960 | } 961 | 962 | public function isMultiple(): bool 963 | { 964 | return (bool) $this->evaluate($this->isMultiple); 965 | } 966 | 967 | public function getUploadedFileNameForStorageUsing(Closure $callback): static 968 | { 969 | $this->getUploadedFileNameForStorageUsing = $callback; 970 | 971 | return $this; 972 | } 973 | 974 | public function getUploadedFileNameForStorage(TemporaryUploadedFile $file): string 975 | { 976 | return $this->evaluate($this->getUploadedFileNameForStorageUsing, [ 977 | 'file' => $file, 978 | ]); 979 | } 980 | 981 | /** 982 | * @return array 983 | */ 984 | public function getStateToDehydrate(): array 985 | { 986 | $state = parent::getStateToDehydrate(); 987 | 988 | if ($fileNamesStatePath = $this->getFileNamesStatePath()) { 989 | $state = [ 990 | ...$state, 991 | $fileNamesStatePath => $this->getStoredFileNames(), 992 | ]; 993 | } 994 | 995 | return $state; 996 | } 997 | 998 | /** 999 | * @param array> $rules 1000 | */ 1001 | public function dehydrateValidationRules(array &$rules): void 1002 | { 1003 | parent::dehydrateValidationRules($rules); 1004 | 1005 | if ($fileNamesStatePath = $this->getFileNamesStatePath()) { 1006 | $rules[$fileNamesStatePath] = ['nullable']; 1007 | } 1008 | } 1009 | 1010 | public static function formatFilename(string $filename, ?string $format): string 1011 | { 1012 | if (! $format) { 1013 | return $filename; 1014 | } 1015 | 1016 | $extension = strrpos($filename, '.'); 1017 | 1018 | if ($extension !== false) { 1019 | return substr($filename, 0, $extension + 1) . $format; 1020 | } 1021 | 1022 | return $filename; 1023 | } 1024 | } 1025 | -------------------------------------------------------------------------------- /src/Components/SpatieMediaLibraryFileUpload.php: -------------------------------------------------------------------------------- 1 | | Closure | null 31 | */ 32 | protected array | Closure | null $customHeaders = null; 33 | 34 | /** 35 | * @var array | Closure | null 36 | */ 37 | protected array | Closure | null $customProperties = null; 38 | 39 | /** 40 | * @var array> | Closure | null 41 | */ 42 | protected array | Closure | null $manipulations = null; 43 | 44 | /** 45 | * @var array | Closure | null 46 | */ 47 | protected array | Closure | null $properties = null; 48 | 49 | protected ?Closure $filterMedia = null; 50 | 51 | protected function setUp(): void 52 | { 53 | parent::setUp(); 54 | 55 | $this->loadStateFromRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component, HasMedia $record): void { 56 | /** @var Model&HasMedia $record */ 57 | $files = $record->load('media')->getMedia($component->getCollection()) 58 | ->when( 59 | ! $component->isMultiple(), 60 | fn (Collection $files): Collection => $files->take(1) 61 | ->when( 62 | $component->hasMediaFilter(), 63 | fn (Collection $files) => $component->getFilteredMedia($files) 64 | ), 65 | ) 66 | ->mapWithKeys(function (Media $file): array { 67 | $uuid = $file->getAttributeValue('uuid'); 68 | 69 | return [$uuid => $uuid]; 70 | }) 71 | ->toArray(); 72 | 73 | $component->state($files); 74 | }); 75 | 76 | $this->afterStateHydrated(static function (BaseFileUpload $component, string | array | null $state): void { 77 | if (is_array($state)) { 78 | return; 79 | } 80 | 81 | $component->state([]); 82 | }); 83 | 84 | $this->beforeStateDehydrated(null); 85 | 86 | $this->dehydrated(false); 87 | 88 | $this->getUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, string $file): ?array { 89 | if (! $component->getRecord()) { 90 | return null; 91 | } 92 | 93 | /** @var ?Media $media */ 94 | $media = $component->getRecord()->getRelationValue('media')->firstWhere('uuid', $file); 95 | 96 | $url = null; 97 | 98 | if ($component->getVisibility() === 'private') { 99 | try { 100 | $url = $media?->getTemporaryUrl( 101 | now()->addMinutes(5), 102 | ); 103 | } catch (Throwable $exception) { 104 | // This driver does not support creating temporary URLs. 105 | } 106 | } 107 | 108 | if ($component->getConversion() && $media->hasGeneratedConversion($component->getConversion())) { 109 | $url ??= $media?->getUrl($component->getConversion()); 110 | } 111 | 112 | $url ??= $media?->getUrl(); 113 | 114 | return [ 115 | 'name' => $media->getAttributeValue('name') ?? $media->getAttributeValue('file_name'), 116 | 'size' => $media->getAttributeValue('size'), 117 | 'type' => $media->getAttributeValue('mime_type'), 118 | 'url' => $url, 119 | ]; 120 | }); 121 | 122 | $this->saveRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component) { 123 | $component->deleteAbandonedFiles(); 124 | $component->saveUploadedFiles(); 125 | }); 126 | 127 | $this->saveUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, TemporaryUploadedFile $file, ?Model $record): ?string { 128 | if (! method_exists($record, 'addMediaFromString')) { 129 | return $file; 130 | } 131 | 132 | try { 133 | if (! $file->exists()) { 134 | return null; 135 | } 136 | } catch (UnableToCheckFileExistence $exception) { 137 | return null; 138 | } 139 | 140 | $compressedImage = null; 141 | $filename = $component->getUploadedFileNameForStorage($file); 142 | $originalBinaryFile = $file->get(); 143 | $optimize = $component->getOptimization(); 144 | $resize = $component->getResize(); 145 | $maxImageWidth = $component->getMaxImageWidth(); 146 | $maxImageHeight = $component->getMaxImageHeight(); 147 | $shouldResize = false; 148 | $imageHeight = null; 149 | $imageWidth = null; 150 | 151 | if ( 152 | str_contains($file->getMimeType(), 'image') && 153 | ($optimize || $resize || $maxImageWidth || $maxImageHeight) 154 | ) { 155 | $image = InterventionImage::make($originalBinaryFile); 156 | 157 | if ($optimize) { 158 | $quality = $optimize === 'jpeg' || 159 | $optimize === 'jpg' ? 70 : null; 160 | } 161 | 162 | if ($maxImageWidth && $image->width() > $maxImageWidth) { 163 | $shouldResize = true; 164 | $imageWidth = $maxImageWidth; 165 | } 166 | 167 | if ($maxImageHeight && $image->height() > $maxImageHeight) { 168 | $shouldResize = true; 169 | $imageHeight = $maxImageHeight; 170 | } 171 | 172 | if ($resize) { 173 | $shouldResize = true; 174 | 175 | if ($image->height() > $image->width()) { 176 | $imageHeight = $image->height() - ($image->height() * ($resize / 100)); 177 | } else { 178 | $imageWidth = $image->width() - ($image->width() * ($resize / 100)); 179 | } 180 | } 181 | 182 | if ($shouldResize) { 183 | $image->resize($imageWidth, $imageHeight, function ($constraint) { 184 | $constraint->aspectRatio(); 185 | }); 186 | } 187 | 188 | if ($optimize) { 189 | $compressedImage = $image->encode($optimize, $quality); 190 | } else { 191 | $compressedImage = $image->encode(); 192 | } 193 | 194 | $filename = self::formatFileName($filename, $optimize); 195 | } 196 | 197 | /** @var FileAdder $mediaAdder */ 198 | $mediaAdder = $record->addMediaFromString($compressedImage ?? $originalBinaryFile); 199 | 200 | $media = $mediaAdder 201 | ->addCustomHeaders($component->getCustomHeaders()) 202 | ->usingFileName($filename) 203 | ->usingName($component->getMediaName($file) ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) 204 | ->storingConversionsOnDisk($component->getConversionsDisk() ?? '') 205 | ->withCustomProperties($component->getCustomProperties()) 206 | ->withManipulations($component->getManipulations()) 207 | ->withResponsiveImagesIf($component->hasResponsiveImages()) 208 | ->withProperties($component->getProperties()) 209 | ->toMediaCollection($component->getCollection(), $component->getDiskName()); 210 | 211 | return $media->getAttributeValue('uuid'); 212 | }); 213 | 214 | $this->reorderUploadedFilesUsing(static function (SpatieMediaLibraryFileUpload $component, array $state): array { 215 | $uuids = array_filter(array_values($state)); 216 | 217 | $mediaClass = config('media-library.media_model', Media::class); 218 | 219 | $mappedIds = $mediaClass::query()->whereIn('uuid', $uuids)->pluck('id', 'uuid')->toArray(); 220 | 221 | $mediaClass::setNewOrder([ 222 | ...array_flip($uuids), 223 | ...$mappedIds, 224 | ]); 225 | 226 | return $state; 227 | }); 228 | } 229 | 230 | public function collection(string | Closure | null $collection): static 231 | { 232 | $this->collection = $collection; 233 | 234 | return $this; 235 | } 236 | 237 | public function conversion(string | Closure | null $conversion): static 238 | { 239 | $this->conversion = $conversion; 240 | 241 | return $this; 242 | } 243 | 244 | public function conversionsDisk(string | Closure | null $disk): static 245 | { 246 | $this->conversionsDisk = $disk; 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * @param array | Closure | null $headers 253 | */ 254 | public function customHeaders(array | Closure | null $headers): static 255 | { 256 | $this->customHeaders = $headers; 257 | 258 | return $this; 259 | } 260 | 261 | /** 262 | * @param array | Closure | null $properties 263 | */ 264 | public function customProperties(array | Closure | null $properties): static 265 | { 266 | $this->customProperties = $properties; 267 | 268 | return $this; 269 | } 270 | 271 | public function filterMedia(?Closure $filterMedia): static 272 | { 273 | $this->filterMedia = $filterMedia; 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * @param array> | Closure | null $manipulations 280 | */ 281 | public function manipulations(array | Closure | null $manipulations): static 282 | { 283 | $this->manipulations = $manipulations; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * @param array | Closure | null $properties 290 | */ 291 | public function properties(array | Closure | null $properties): static 292 | { 293 | $this->properties = $properties; 294 | 295 | return $this; 296 | } 297 | 298 | public function responsiveImages(bool | Closure $condition = true): static 299 | { 300 | $this->hasResponsiveImages = $condition; 301 | 302 | return $this; 303 | } 304 | 305 | public function deleteAbandonedFiles(): void 306 | { 307 | /** @var Model&HasMedia $record */ 308 | $record = $this->getRecord(); 309 | 310 | $record 311 | ->getMedia($this->getCollection()) 312 | ->whereNotIn('uuid', array_keys($this->getState() ?? [])) 313 | ->when($this->hasMediaFilter(), fn (Collection $files) => $this->getFilteredMedia($files)) 314 | ->each(fn (Media $media) => $media->delete()); 315 | } 316 | 317 | public function getCollection(): string 318 | { 319 | return $this->evaluate($this->collection) ?? 'default'; 320 | } 321 | 322 | public function getConversion(): ?string 323 | { 324 | return $this->evaluate($this->conversion); 325 | } 326 | 327 | public function getConversionsDisk(): ?string 328 | { 329 | return $this->evaluate($this->conversionsDisk); 330 | } 331 | 332 | /** 333 | * @return array 334 | */ 335 | public function getCustomHeaders(): array 336 | { 337 | return $this->evaluate($this->customHeaders) ?? []; 338 | } 339 | 340 | /** 341 | * @return array 342 | */ 343 | public function getCustomProperties(): array 344 | { 345 | return $this->evaluate($this->customProperties) ?? []; 346 | } 347 | 348 | /** 349 | * @return array> 350 | */ 351 | public function getManipulations(): array 352 | { 353 | return $this->evaluate($this->manipulations) ?? []; 354 | } 355 | 356 | /** 357 | * @return array 358 | */ 359 | public function getProperties(): array 360 | { 361 | return $this->evaluate($this->properties) ?? []; 362 | } 363 | 364 | public function getFilteredMedia(Collection $media): Collection 365 | { 366 | return $this->evaluate($this->filterMedia, [ 367 | 'media' => $media, 368 | ]) ?? $media; 369 | } 370 | 371 | public function hasMediaFilter(): bool 372 | { 373 | return $this->filterMedia instanceof Closure; 374 | } 375 | 376 | public function hasResponsiveImages(): bool 377 | { 378 | return (bool) $this->evaluate($this->hasResponsiveImages); 379 | } 380 | 381 | public function mediaName(string | Closure | null $name): static 382 | { 383 | $this->mediaName = $name; 384 | 385 | return $this; 386 | } 387 | 388 | public function getMediaName(TemporaryUploadedFile $file): ?string 389 | { 390 | return $this->evaluate($this->mediaName, [ 391 | 'file' => $file, 392 | ]); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Facades/ImageOptimizer.php: -------------------------------------------------------------------------------- 1 | alias(BaseFileUpload::class, CustomBaseFileUpload::class); 22 | AliasLoader::getInstance()->alias(SpatieMediaLibraryFileUpload::class, CustomSpatieMediaLibraryFileUpload::class); 23 | } 24 | 25 | public function configurePackage(Package $package): void 26 | { 27 | $package->name(static::$name) 28 | ->hasInstallCommand(function (InstallCommand $command) { 29 | $command 30 | ->publishConfigFile() 31 | ->publishMigrations() 32 | ->askToRunMigrations() 33 | ->askToStarRepoOnGitHub('joshembling/image-optimizer'); 34 | }); 35 | 36 | $configFileName = $package->shortName(); 37 | 38 | if (file_exists($package->basePath("/../config/{$configFileName}.php"))) { 39 | $package->hasConfigFile(); 40 | } 41 | 42 | if (file_exists($package->basePath('/../resources/lang'))) { 43 | $package->hasTranslations(); 44 | } 45 | } 46 | 47 | public function packageBooted(): void 48 | { 49 | // Handle Stubs 50 | if (app()->runningInConsole()) { 51 | foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) { 52 | $this->publishes([ 53 | $file->getRealPath() => base_path("stubs/image-optimizer/{$file->getFilename()}"), 54 | ], 'image-optimizer-stubs'); 55 | } 56 | } 57 | } 58 | } 59 | --------------------------------------------------------------------------------