├── LICENSE.md ├── README.md ├── composer.json ├── config └── media-library.php ├── database └── migrations │ └── create_media_table.php.stub ├── resources └── views │ ├── image.blade.php │ ├── placeholderSvg.blade.php │ ├── responsiveImage.blade.php │ └── responsiveImageWithPlaceholder.blade.php └── src ├── Conversions ├── Actions │ ├── PerformConversionAction.php │ └── PerformManipulationsAction.php ├── Commands │ └── RegenerateCommand.php ├── Conversion.php ├── ConversionCollection.php ├── Events │ ├── ConversionHasBeenCompletedEvent.php │ └── ConversionWillStartEvent.php ├── FileManipulator.php ├── ImageGenerators │ ├── Avif.php │ ├── Image.php │ ├── ImageGenerator.php │ ├── ImageGeneratorFactory.php │ ├── Pdf.php │ ├── Svg.php │ ├── Video.php │ └── Webp.php ├── Jobs │ └── PerformConversionsJob.php └── Manipulations.php ├── Downloaders ├── DefaultDownloader.php ├── Downloader.php └── HttpFacadeDownloader.php ├── Enums └── CollectionPosition.php ├── HasMedia.php ├── InteractsWithMedia.php ├── MediaCollections ├── Commands │ ├── CleanCommand.php │ └── ClearCommand.php ├── Contracts │ └── MediaLibraryRequest.php ├── Events │ ├── CollectionHasBeenClearedEvent.php │ └── MediaHasBeenAddedEvent.php ├── Exceptions │ ├── DiskCannotBeAccessed.php │ ├── DiskDoesNotExist.php │ ├── FileCannotBeAdded.php │ ├── FileDoesNotExist.php │ ├── FileIsTooBig.php │ ├── FileNameNotAllowed.php │ ├── FileUnacceptableForCollection.php │ ├── FunctionalityNotAvailable.php │ ├── InvalidBase64Data.php │ ├── InvalidConversion.php │ ├── InvalidFileRemover.php │ ├── InvalidPathGenerator.php │ ├── InvalidUrl.php │ ├── InvalidUrlGenerator.php │ ├── MediaCannotBeDeleted.php │ ├── MediaCannotBeUpdated.php │ ├── MimeTypeNotAllowed.php │ ├── RequestDoesNotHaveFile.php │ ├── UnknownType.php │ └── UnreachableUrl.php ├── File.php ├── FileAdder.php ├── FileAdderFactory.php ├── Filesystem.php ├── HtmlableMedia.php ├── MediaCollection.php ├── MediaRepository.php └── Models │ ├── Collections │ └── MediaCollection.php │ ├── Concerns │ ├── CustomMediaProperties.php │ ├── HasUuid.php │ └── IsSorted.php │ ├── Media.php │ └── Observers │ └── MediaObserver.php ├── MediaLibraryServiceProvider.php ├── ResponsiveImages ├── Events │ └── ResponsiveImagesGeneratedEvent.php ├── Exceptions │ └── InvalidTinyJpg.php ├── Jobs │ └── GenerateResponsiveImagesJob.php ├── RegisteredResponsiveImages.php ├── ResponsiveImage.php ├── ResponsiveImageGenerator.php ├── TinyPlaceholderGenerator │ ├── Blurred.php │ └── TinyPlaceholderGenerator.php └── WidthCalculator │ ├── FileSizeOptimizedWidthCalculator.php │ └── WidthCalculator.php └── Support ├── Factories └── TemporaryUploadFactory.php ├── File.php ├── FileNamer ├── DefaultFileNamer.php └── FileNamer.php ├── FileRemover ├── DefaultFileRemover.php ├── FileBaseFileRemover.php ├── FileRemover.php └── FileRemoverFactory.php ├── ImageFactory.php ├── MediaLibraryPro.php ├── MediaStream.php ├── PathGenerator ├── DefaultPathGenerator.php ├── PathGenerator.php └── PathGeneratorFactory.php ├── RemoteFile.php ├── TemporaryDirectory.php └── UrlGenerator ├── BaseUrlGenerator.php ├── DefaultUrlGenerator.php ├── UrlGenerator.php └── UrlGeneratorFactory.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 |
2 | 3 | 4 | 5 | Logo for laravel-medialibrary 6 | 7 | 8 | 9 |

Associate files with Eloquent models

10 | 11 | [![Latest Version](https://img.shields.io/github/release/spatie/laravel-medialibrary.svg?style=flat-square)](https://github.com/spatie/laravel-medialibrary/releases) 12 | [![run-tests](https://github.com/spatie/laravel-medialibrary/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/laravel-medialibrary/actions/workflows/run-tests.yml) 13 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-medialibrary.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-medialibrary) 14 | 15 |
16 | 17 | This package can associate all sorts of files with Eloquent models. It provides a 18 | simple API to work with. To learn all about it, head over to [the extensive documentation](https://spatie.be/docs/laravel-medialibrary). 19 | 20 | Here are a few short examples of what you can do: 21 | 22 | ```php 23 | $newsItem = News::find(1); 24 | $newsItem->addMedia($pathToFile)->toMediaCollection('images'); 25 | ``` 26 | 27 | It can handle your uploads directly: 28 | 29 | ```php 30 | $newsItem->addMedia($request->file('image'))->toMediaCollection('images'); 31 | ``` 32 | 33 | Want to store some large files on another filesystem? No problem: 34 | 35 | ```php 36 | $newsItem->addMedia($smallFile)->toMediaCollection('downloads', 'local'); 37 | $newsItem->addMedia($bigFile)->toMediaCollection('downloads', 's3'); 38 | ``` 39 | 40 | The storage of the files is handled by [Laravel's Filesystem](https://laravel.com/docs/filesystem), 41 | so you can use any filesystem you like. Additionally, the package can create image manipulations 42 | on images and pdfs that have been added in the media library. 43 | 44 | Spatie is a webdesign agency in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 45 | 46 | ## Support us 47 | 48 | [](https://spatie.be/github-ad-click/laravel-medialibrary) 49 | 50 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 51 | 52 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 53 | 54 | ## Documentation 55 | 56 | You'll find the documentation on [https://spatie.be/docs/laravel-medialibrary](https://spatie.be/docs/laravel-medialibrary/v11). 57 | 58 | Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the media library? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-medialibrary/issues), we'll try to address it as soon as possible. 59 | 60 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 61 | 62 | ## Testing 63 | 64 | You can run the tests with: 65 | 66 | ```bash 67 | ./vendor/bin/pest 68 | ``` 69 | 70 | You can run the Github actions locally with [act](https://github.com/nektos/act). To run the tests locally, run: 71 | 72 | ``` 73 | act -j run-tests 74 | ``` 75 | 76 | To run tests for a specific PHP/Laravel version, run: 77 | 78 | ``` 79 | act -j run-tests --matrix php:8.3 --matrix laravel:"11.*" --matrix dependency-version:prefer-stable 80 | ``` 81 | 82 | Available `matrix` options are available in the [workflow file](.github/workflows/run-tests.yml). 83 | 84 | ## Upgrading 85 | 86 | Please see [UPGRADING](UPGRADING.md) for details. 87 | 88 | ### Changelog 89 | 90 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 91 | 92 | ## Contributing 93 | 94 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 95 | 96 | ## Security 97 | 98 | If you discover any security related issues, please email [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 99 | 100 | ## Credits 101 | 102 | - [Freek Van der Herten](https://github.com/freekmurze) 103 | - [All Contributors](../../contributors) 104 | 105 | A big thank you to [Nicolas Beauvais](https://github.com/nicolasbeauvais) for helping out with the issues on this repo. 106 | 107 | Special thanks to [Caneco](https://twitter.com/caneco) for the original logo. 108 | 109 | ## Alternatives 110 | 111 | - [laravel-mediable](https://github.com/plank/laravel-mediable) 112 | - [laravel-stapler](https://github.com/CodeSleeve/laravel-stapler) 113 | - [media-manager](https://github.com/talvbansal/media-manager) 114 | 115 | ## License 116 | 117 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-medialibrary", 3 | "description": "Associate files with Eloquent models", 4 | "license": "MIT", 5 | "keywords": [ 6 | "spatie", 7 | "laravel-medialibrary", 8 | "media", 9 | "conversion", 10 | "images", 11 | "downloads", 12 | "cms", 13 | "laravel" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Freek Van der Herten", 18 | "email": "freek@spatie.be", 19 | "homepage": "https://spatie.be", 20 | "role": "Developer" 21 | } 22 | ], 23 | "homepage": "https://github.com/spatie/laravel-medialibrary", 24 | "require": { 25 | "php": "^8.2", 26 | "ext-exif": "*", 27 | "ext-fileinfo": "*", 28 | "ext-json": "*", 29 | "composer/semver": "^3.4", 30 | "illuminate/bus": "^10.2|^11.0|^12.0", 31 | "illuminate/conditionable": "^10.2|^11.0|^12.0", 32 | "illuminate/console": "^10.2|^11.0|^12.0", 33 | "illuminate/database": "^10.2|^11.0|^12.0", 34 | "illuminate/pipeline": "^10.2|^11.0|^12.0", 35 | "illuminate/support": "^10.2|^11.0|^12.0", 36 | "maennchen/zipstream-php": "^3.1", 37 | "spatie/image": "^3.3.2", 38 | "spatie/laravel-package-tools": "^1.16.1", 39 | "spatie/temporary-directory": "^2.2", 40 | "symfony/console": "^6.4.1|^7.0" 41 | }, 42 | "require-dev": { 43 | "ext-imagick": "*", 44 | "ext-pdo_sqlite": "*", 45 | "ext-zip": "*", 46 | "aws/aws-sdk-php": "^3.293.10", 47 | "guzzlehttp/guzzle": "^7.8.1", 48 | "league/flysystem-aws-s3-v3": "^3.22", 49 | "mockery/mockery": "^1.6.7", 50 | "larastan/larastan": "^2.7|^3.0", 51 | "orchestra/testbench": "^7.0|^8.17|^9.0|^10.0", 52 | "pestphp/pest": "^2.28|^3.5", 53 | "phpstan/extension-installer": "^1.3.1", 54 | "spatie/laravel-ray": "^1.33", 55 | "spatie/pdf-to-image": "^2.2|^3.0", 56 | "spatie/pest-plugin-snapshots": "^2.1" 57 | }, 58 | "conflict": { 59 | "php-ffmpeg/php-ffmpeg": "<0.6.1" 60 | }, 61 | "suggest": { 62 | "league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage", 63 | "php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails", 64 | "spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs" 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true, 68 | "autoload": { 69 | "psr-4": { 70 | "Spatie\\MediaLibrary\\": "src" 71 | } 72 | }, 73 | "autoload-dev": { 74 | "psr-4": { 75 | "Spatie\\MediaLibrary\\Tests\\": "tests" 76 | } 77 | }, 78 | "config": { 79 | "allow-plugins": { 80 | "pestphp/pest-plugin": true, 81 | "phpstan/extension-installer": true 82 | }, 83 | "sort-packages": true 84 | }, 85 | "extra": { 86 | "laravel": { 87 | "providers": [ 88 | "Spatie\\MediaLibrary\\MediaLibraryServiceProvider" 89 | ] 90 | } 91 | }, 92 | "scripts": { 93 | "analyse": "vendor/bin/phpstan analyse", 94 | "baseline": "vendor/bin/phpstan analyse --generate-baseline", 95 | "test": "vendor/bin/pest" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/media-library.php: -------------------------------------------------------------------------------- 1 | env('MEDIA_DISK', 'public'), 10 | 11 | /* 12 | * The maximum file size of an item in bytes. 13 | * Adding a larger file will result in an exception. 14 | */ 15 | 'max_file_size' => 1024 * 1024 * 10, // 10MB 16 | 17 | /* 18 | * This queue connection will be used to generate derived and responsive images. 19 | * Leave empty to use the default queue connection. 20 | */ 21 | 'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'), 22 | 23 | /* 24 | * This queue will be used to generate derived and responsive images. 25 | * Leave empty to use the default queue. 26 | */ 27 | 'queue_name' => env('MEDIA_QUEUE', ''), 28 | 29 | /* 30 | * By default all conversions will be performed on a queue. 31 | */ 32 | 'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true), 33 | 34 | /* 35 | * Should database transactions be run after database commits? 36 | */ 37 | 'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true), 38 | 39 | /* 40 | * The fully qualified class name of the media model. 41 | */ 42 | 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, 43 | 44 | /* 45 | * The fully qualified class name of the media observer. 46 | */ 47 | 'media_observer' => Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver::class, 48 | 49 | /* 50 | * When enabled, media collections will be serialised using the default 51 | * laravel model serialization behaviour. 52 | * 53 | * Keep this option disabled if using Media Library Pro components (https://medialibrary.pro) 54 | */ 55 | 'use_default_collection_serialization' => false, 56 | 57 | /* 58 | * The fully qualified class name of the model used for temporary uploads. 59 | * 60 | * This model is only used in Media Library Pro (https://medialibrary.pro) 61 | */ 62 | 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, 63 | 64 | /* 65 | * When enabled, Media Library Pro will only process temporary uploads that were uploaded 66 | * in the same session. You can opt to disable this for stateless usage of 67 | * the pro components. 68 | */ 69 | 'enable_temporary_uploads_session_affinity' => true, 70 | 71 | /* 72 | * When enabled, Media Library pro will generate thumbnails for uploaded file. 73 | */ 74 | 'generate_thumbnails_for_temporary_uploads' => true, 75 | 76 | /* 77 | * This is the class that is responsible for naming generated files. 78 | */ 79 | 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, 80 | 81 | /* 82 | * The class that contains the strategy for determining a media file's path. 83 | */ 84 | 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, 85 | 86 | /* 87 | * The class that contains the strategy for determining how to remove files. 88 | */ 89 | 'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class, 90 | 91 | /* 92 | * Here you can specify which path generator should be used for the given class. 93 | */ 94 | 'custom_path_generators' => [ 95 | // Model::class => PathGenerator::class 96 | // or 97 | // 'model_morph_alias' => PathGenerator::class 98 | ], 99 | 100 | /* 101 | * When urls to files get generated, this class will be called. Use the default 102 | * if your files are stored locally above the site root or on s3. 103 | */ 104 | 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, 105 | 106 | /* 107 | * Moves media on updating to keep path consistent. Enable it only with a custom 108 | * PathGenerator that uses, for example, the media UUID. 109 | */ 110 | 'moves_media_on_update' => false, 111 | 112 | /* 113 | * Whether to activate versioning when urls to files get generated. 114 | * When activated, this attaches a ?v=xx query string to the URL. 115 | */ 116 | 'version_urls' => false, 117 | 118 | /* 119 | * The media library will try to optimize all converted images by removing 120 | * metadata and applying a little bit of compression. These are 121 | * the optimizers that will be used by default. 122 | */ 123 | 'image_optimizers' => [ 124 | Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ 125 | '-m85', // set maximum quality to 85% 126 | '--force', // ensure that progressive generation is always done also if a little bigger 127 | '--strip-all', // this strips out all text information such as comments and EXIF data 128 | '--all-progressive', // this will make sure the resulting image is a progressive one 129 | ], 130 | Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ 131 | '--force', // required parameter for this package 132 | ], 133 | Spatie\ImageOptimizer\Optimizers\Optipng::class => [ 134 | '-i0', // this will result in a non-interlaced, progressive scanned image 135 | '-o2', // this set the optimization level to two (multiple IDAT compression trials) 136 | '-quiet', // required parameter for this package 137 | ], 138 | Spatie\ImageOptimizer\Optimizers\Svgo::class => [ 139 | '--disable=cleanupIDs', // disabling because it is known to cause troubles 140 | ], 141 | Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ 142 | '-b', // required parameter for this package 143 | '-O3', // this produces the slowest but best results 144 | ], 145 | Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ 146 | '-m 6', // for the slowest compression method in order to get the best compression. 147 | '-pass 10', // for maximizing the amount of analysis pass. 148 | '-mt', // multithreading for some speed improvements. 149 | '-q 90', // quality factor that brings the least noticeable changes. 150 | ], 151 | Spatie\ImageOptimizer\Optimizers\Avifenc::class => [ 152 | '-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63). 153 | '-j all', // number of jobs (worker threads, "all" uses all available cores). 154 | '--min 0', // min quantizer for color (0-63). 155 | '--max 63', // max quantizer for color (0-63). 156 | '--minalpha 0', // min quantizer for alpha (0-63). 157 | '--maxalpha 63', // max quantizer for alpha (0-63). 158 | '-a end-usage=q', // rate control mode set to Constant Quality mode. 159 | '-a tune=ssim', // SSIM as tune the encoder for distortion metric. 160 | ], 161 | ], 162 | 163 | /* 164 | * These generators will be used to create an image of media files. 165 | */ 166 | 'image_generators' => [ 167 | Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, 168 | Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, 169 | Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class, 170 | Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, 171 | Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, 172 | Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, 173 | ], 174 | 175 | /* 176 | * The path where to store temporary files while performing image conversions. 177 | * If set to null, storage_path('media-library/temp') will be used. 178 | */ 179 | 'temporary_directory_path' => null, 180 | 181 | /* 182 | * The engine that should perform the image conversions. 183 | * Should be either `gd` or `imagick`. 184 | */ 185 | 'image_driver' => env('IMAGE_DRIVER', 'gd'), 186 | 187 | /* 188 | * FFMPEG & FFProbe binaries paths, only used if you try to generate video 189 | * thumbnails and have installed the php-ffmpeg/php-ffmpeg composer 190 | * dependency. 191 | */ 192 | 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), 193 | 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), 194 | 195 | /* 196 | * Here you can override the class names of the jobs used by this package. Make sure 197 | * your custom jobs extend the ones provided by the package. 198 | */ 199 | 'jobs' => [ 200 | 'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, 201 | 'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class, 202 | ], 203 | 204 | /* 205 | * When using the addMediaFromUrl method you may want to replace the default downloader. 206 | * This is particularly useful when the url of the image is behind a firewall and 207 | * need to add additional flags, possibly using curl. 208 | */ 209 | 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, 210 | 211 | /* 212 | * When using the addMediaFromUrl method the SSL is verified by default. 213 | * This is option disables SSL verification when downloading remote media. 214 | * Please note that this is a security risk and should only be false in a local environment. 215 | */ 216 | 'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true), 217 | 218 | 'remote' => [ 219 | /* 220 | * Any extra headers that should be included when uploading media to 221 | * a remote disk. Even though supported headers may vary between 222 | * different drivers, a sensible default has been provided. 223 | * 224 | * Supported by S3: CacheControl, Expires, StorageClass, 225 | * ServerSideEncryption, Metadata, ACL, ContentEncoding 226 | */ 227 | 'extra_headers' => [ 228 | 'CacheControl' => 'max-age=604800', 229 | ], 230 | ], 231 | 232 | 'responsive_images' => [ 233 | /* 234 | * This class is responsible for calculating the target widths of the responsive 235 | * images. By default we optimize for filesize and create variations that each are 30% 236 | * smaller than the previous one. More info in the documentation. 237 | * 238 | * https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images 239 | */ 240 | 'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class, 241 | 242 | /* 243 | * By default rendering media to a responsive image will add some javascript and a tiny placeholder. 244 | * This ensures that the browser can already determine the correct layout. 245 | * When disabled, no tiny placeholder is generated. 246 | */ 247 | 'use_tiny_placeholders' => true, 248 | 249 | /* 250 | * This class will generate the tiny placeholder used for progressive image loading. By default 251 | * the media library will use a tiny blurred jpg image. 252 | */ 253 | 'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class, 254 | ], 255 | 256 | /* 257 | * When enabling this option, a route will be registered that will enable 258 | * the Media Library Pro Vue and React components to move uploaded files 259 | * in a S3 bucket to their right place. 260 | */ 261 | 'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false), 262 | 263 | /* 264 | * When converting Media instances to response the media library will add 265 | * a `loading` attribute to the `img` tag. Here you can set the default 266 | * value of that attribute. 267 | * 268 | * Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction. 269 | * 270 | * More info: https://css-tricks.com/native-lazy-loading/ 271 | */ 272 | 'default_loading_attribute_value' => null, 273 | 274 | /* 275 | * You can specify a prefix for that is used for storing all media. 276 | * If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory. 277 | */ 278 | 'prefix' => env('MEDIA_PREFIX', ''), 279 | 280 | /* 281 | * When forcing lazy loading, media will be loaded even if you don't eager load media and you have 282 | * disabled lazy loading globally in the service provider. 283 | */ 284 | 'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true), 285 | ]; 286 | -------------------------------------------------------------------------------- /database/migrations/create_media_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->morphs('model'); 15 | $table->uuid()->nullable()->unique(); 16 | $table->string('collection_name'); 17 | $table->string('name'); 18 | $table->string('file_name'); 19 | $table->string('mime_type')->nullable(); 20 | $table->string('disk'); 21 | $table->string('conversions_disk')->nullable(); 22 | $table->unsignedBigInteger('size'); 23 | $table->json('manipulations'); 24 | $table->json('custom_properties'); 25 | $table->json('generated_conversions'); 26 | $table->json('responsive_images'); 27 | $table->unsignedInteger('order_column')->nullable()->index(); 28 | 29 | $table->nullableTimestamps(); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /resources/views/image.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/placeholderSvg.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/responsiveImage.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/responsiveImageWithPlaceholder.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Conversions/Actions/PerformConversionAction.php: -------------------------------------------------------------------------------- 1 | convert($copiedOriginalFile, $conversion); 23 | 24 | if (! $copiedOriginalFile) { 25 | return; 26 | } 27 | 28 | event(new ConversionWillStartEvent($media, $conversion, $copiedOriginalFile)); 29 | 30 | $manipulationResult = (new PerformManipulationsAction)->execute($media, $conversion, $copiedOriginalFile); 31 | 32 | $newFileName = $conversion->getConversionFile($media); 33 | 34 | $renamedFile = $this->renameInLocalDirectory($manipulationResult, $newFileName); 35 | 36 | if ($conversion->shouldGenerateResponsiveImages()) { 37 | /** @var ResponsiveImageGenerator $responsiveImageGenerator */ 38 | $responsiveImageGenerator = app(ResponsiveImageGenerator::class); 39 | 40 | $responsiveImageGenerator->generateResponsiveImagesForConversion( 41 | $media, 42 | $conversion, 43 | $renamedFile 44 | ); 45 | } 46 | 47 | app(Filesystem::class)->copyToMediaLibrary($renamedFile, $media, 'conversions'); 48 | 49 | $media->markAsConversionGenerated($conversion->getName()); 50 | 51 | event(new ConversionHasBeenCompletedEvent($media, $conversion)); 52 | } 53 | 54 | protected function renameInLocalDirectory( 55 | string $fileNameWithDirectory, 56 | string $newFileNameWithoutDirectory 57 | ): string { 58 | $targetFile = pathinfo($fileNameWithDirectory, PATHINFO_DIRNAME).'/'.$newFileNameWithoutDirectory; 59 | 60 | rename($fileNameWithDirectory, $targetFile); 61 | 62 | return $targetFile; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Conversions/Actions/PerformManipulationsAction.php: -------------------------------------------------------------------------------- 1 | getManipulations()->isEmpty()) { 21 | return $imageFile; 22 | } 23 | 24 | $conversionTempFile = $this->getConversionTempFileName($media, $conversion, $imageFile); 25 | 26 | File::copy($imageFile, $conversionTempFile); 27 | 28 | $supportedFormats = ['jpg', 'jpeg', 'pjpg', 'png', 'gif', 'webp']; 29 | if ($conversion->shouldKeepOriginalImageFormat() && in_array($media->extension, $supportedFormats)) { 30 | $conversion->format($media->extension); 31 | } 32 | 33 | $image = Image::useImageDriver(config('media-library.image_driver')) 34 | ->loadFile($conversionTempFile) 35 | ->format('jpg'); 36 | 37 | try { 38 | $conversion->getManipulations()->apply($image); 39 | 40 | $image->save(); 41 | } catch (UnsupportedImageFormat) { 42 | 43 | } 44 | 45 | return $conversionTempFile; 46 | } 47 | 48 | protected function getConversionTempFileName( 49 | Media $media, 50 | Conversion $conversion, 51 | string $imageFile, 52 | ): string { 53 | $directory = pathinfo($imageFile, PATHINFO_DIRNAME); 54 | 55 | $extension = $media->extension; 56 | 57 | if ($extension === '') { 58 | $extension = 'jpg'; 59 | } 60 | 61 | $fileName = Str::random(32)."{$conversion->getName()}.{$extension}"; 62 | 63 | return "{$directory}/{$fileName}"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Conversions/Commands/RegenerateCommand.php: -------------------------------------------------------------------------------- 1 | mediaRepository = $mediaRepository; 39 | 40 | $this->fileManipulator = $fileManipulator; 41 | 42 | if (! $this->confirmToProceed()) { 43 | return; 44 | } 45 | 46 | $mediaFiles = $this->getMediaToBeRegenerated(); 47 | 48 | $progressBar = $this->output->createProgressBar($mediaFiles->count()); 49 | 50 | if (config('media-library.queue_connection_name') === 'sync') { 51 | set_time_limit(0); 52 | } 53 | 54 | $mediaFiles->each(function (Media $media) use ($progressBar) { 55 | try { 56 | $this->fileManipulator->createDerivedFiles( 57 | $media, 58 | Arr::wrap($this->option('only')), 59 | $this->option('only-missing'), 60 | $this->option('with-responsive-images'), 61 | $this->option('queue-all'), 62 | ); 63 | } catch (Exception $exception) { 64 | $this->errorMessages[$media->getKey()] = $exception->getMessage(); 65 | } 66 | 67 | $progressBar->advance(); 68 | }); 69 | 70 | $progressBar->finish(); 71 | 72 | if (count($this->errorMessages)) { 73 | $this->warn('All done, but with some error messages:'); 74 | 75 | foreach ($this->errorMessages as $mediaId => $message) { 76 | $this->warn("Media id {$mediaId}: `{$message}`"); 77 | } 78 | } 79 | 80 | $this->newLine(2); 81 | 82 | $this->info('All done!'); 83 | } 84 | 85 | public function getMediaToBeRegenerated(): LazyCollection 86 | { 87 | // Get this arg first as it can also be passed to the greater-than-id branch 88 | $modelType = $this->argument('modelType'); 89 | 90 | $startingFromId = (int) $this->option('starting-from-id'); 91 | if ($startingFromId !== 0) { 92 | $excludeStartingId = (bool) $this->option('exclude-starting-id') ?: false; 93 | 94 | return $this->mediaRepository->getByIdGreaterThan($startingFromId, $excludeStartingId, is_string($modelType) ? $modelType : ''); 95 | } 96 | 97 | if (is_string($modelType)) { 98 | return $this->mediaRepository->getByModelType($modelType); 99 | } 100 | 101 | $mediaIds = $this->getMediaIds(); 102 | if (count($mediaIds) > 0) { 103 | return $this->mediaRepository->getByIds($mediaIds); 104 | } 105 | 106 | return $this->mediaRepository->all(); 107 | } 108 | 109 | protected function getMediaIds(): array 110 | { 111 | $mediaIds = $this->option('ids'); 112 | 113 | if (! is_array($mediaIds)) { 114 | $mediaIds = explode(',', (string) $mediaIds); 115 | } 116 | 117 | if (count($mediaIds) === 1 && Str::contains((string) $mediaIds[0], ',')) { 118 | $mediaIds = explode(',', (string) $mediaIds[0]); 119 | } 120 | 121 | return $mediaIds; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Conversions/Conversion.php: -------------------------------------------------------------------------------- 1 | manipulations = new Manipulations; 43 | $this->manipulations->optimize($optimizerChain)->format('jpg'); 44 | 45 | $this->fileNamer = app(config('media-library.file_namer')); 46 | 47 | $this->loadingAttributeValue = config('media-library.default_loading_attribute_value'); 48 | 49 | $this->performOnQueue = config('media-library.queue_conversions_by_default', true); 50 | } 51 | 52 | public static function create(string $name): self 53 | { 54 | return new static($name); 55 | } 56 | 57 | public function getName(): string 58 | { 59 | return $this->name; 60 | } 61 | 62 | public function getPerformOnCollections(): array 63 | { 64 | if (! count($this->performOnCollections)) { 65 | return ['default']; 66 | } 67 | 68 | return $this->performOnCollections; 69 | } 70 | 71 | public function extractVideoFrameAtSecond(float $timeCode): self 72 | { 73 | $this->extractVideoFrameAtSecond = $timeCode; 74 | 75 | return $this; 76 | } 77 | 78 | public function getExtractVideoFrameAtSecond(): float 79 | { 80 | return $this->extractVideoFrameAtSecond; 81 | } 82 | 83 | public function keepOriginalImageFormat(): self 84 | { 85 | $this->keepOriginalImageFormat = true; 86 | 87 | return $this; 88 | } 89 | 90 | public function shouldKeepOriginalImageFormat(): bool 91 | { 92 | return $this->keepOriginalImageFormat; 93 | } 94 | 95 | public function getManipulations(): Manipulations 96 | { 97 | return $this->manipulations; 98 | } 99 | 100 | public function removeManipulation(string $manipulationName): self 101 | { 102 | $this->manipulations->removeManipulation($manipulationName); 103 | 104 | return $this; 105 | } 106 | 107 | public function withoutManipulations(): self 108 | { 109 | $this->manipulations = new Manipulations; 110 | 111 | return $this; 112 | } 113 | 114 | public function __call($name, $arguments): self 115 | { 116 | $this->manipulations->$name(...$arguments); 117 | 118 | return $this; 119 | } 120 | 121 | public function setManipulations($manipulations): self 122 | { 123 | if ($manipulations instanceof Manipulations) { 124 | $this->manipulations = $this->manipulations->mergeManipulations($manipulations); 125 | } 126 | 127 | if (is_callable($manipulations)) { 128 | $manipulations($this->manipulations); 129 | } 130 | 131 | return $this; 132 | } 133 | 134 | public function addAsFirstManipulations(Manipulations $manipulations): self 135 | { 136 | $newManipulations = $manipulations->toArray(); 137 | 138 | $currentManipulations = $this->manipulations->toArray(); 139 | 140 | $allManipulations = array_merge($newManipulations, Arr::except($currentManipulations, array_keys($newManipulations))); 141 | 142 | $this->manipulations = new Manipulations($allManipulations); 143 | 144 | return $this; 145 | } 146 | 147 | public function performOnCollections(...$collectionNames): self 148 | { 149 | $this->performOnCollections = $collectionNames; 150 | 151 | return $this; 152 | } 153 | 154 | public function shouldBePerformedOn(string $collectionName): bool 155 | { 156 | // if no collections were specified, perform conversion on all collections 157 | if (! count($this->performOnCollections)) { 158 | return true; 159 | } 160 | 161 | if (in_array('*', $this->performOnCollections)) { 162 | return true; 163 | } 164 | 165 | return in_array($collectionName, $this->performOnCollections); 166 | } 167 | 168 | public function queued(): self 169 | { 170 | $this->performOnQueue = true; 171 | 172 | return $this; 173 | } 174 | 175 | public function nonQueued(): self 176 | { 177 | $this->performOnQueue = false; 178 | 179 | return $this; 180 | } 181 | 182 | public function nonOptimized(): self 183 | { 184 | $this->removeManipulation('optimize'); 185 | 186 | return $this; 187 | } 188 | 189 | public function withResponsiveImages(bool $withResponsiveImages = true): self 190 | { 191 | $this->generateResponsiveImages = $withResponsiveImages; 192 | 193 | return $this; 194 | } 195 | 196 | public function withWidthCalculator(WidthCalculator $widthCalculator): self 197 | { 198 | $this->widthCalculator = $widthCalculator; 199 | 200 | return $this; 201 | } 202 | 203 | public function getWidthCalculator(): ?WidthCalculator 204 | { 205 | return $this->widthCalculator; 206 | } 207 | 208 | public function shouldGenerateResponsiveImages(): bool 209 | { 210 | return $this->generateResponsiveImages; 211 | } 212 | 213 | public function shouldBeQueued(): bool 214 | { 215 | return $this->performOnQueue; 216 | } 217 | 218 | public function getResultExtension(string $originalFileExtension = ''): string 219 | { 220 | if ($this->shouldKeepOriginalImageFormat()) { 221 | if (in_array(strtolower($originalFileExtension), ['jpg', 'jpeg', 'pjpg', 'png', 'gif', 'webp', 'avif'])) { 222 | return $originalFileExtension; 223 | } 224 | } 225 | 226 | if ($manipulationArgument = Arr::get($this->manipulations->getManipulationArgument('format'), 0)) { 227 | return $manipulationArgument; 228 | } 229 | 230 | return $originalFileExtension; 231 | } 232 | 233 | public function getConversionFile(Media $media): string 234 | { 235 | $fileName = $this->fileNamer->conversionFileName($media->file_name, $this); 236 | 237 | $fileExtension = $this->fileNamer->extensionFromBaseImage($media->file_name); 238 | $extension = $this->getResultExtension($fileExtension) ?: $fileExtension; 239 | 240 | return "{$fileName}.{$extension}"; 241 | } 242 | 243 | public function useLoadingAttributeValue(string $value): self 244 | { 245 | $this->loadingAttributeValue = $value; 246 | 247 | return $this; 248 | } 249 | 250 | public function getLoadingAttributeValue(): ?string 251 | { 252 | return $this->loadingAttributeValue; 253 | } 254 | 255 | public function pdfPageNumber(int $pageNumber): self 256 | { 257 | $this->pdfPageNumber = $pageNumber; 258 | 259 | return $this; 260 | } 261 | 262 | public function getPdfPageNumber(): int 263 | { 264 | return $this->pdfPageNumber; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Conversions/ConversionCollection.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ConversionCollection extends Collection 18 | { 19 | protected Media $media; 20 | 21 | public static function createForMedia(Media $media): self 22 | { 23 | return (new static)->setMedia($media); 24 | } 25 | 26 | /** 27 | * @return $this 28 | */ 29 | public function setMedia(Media $media): self 30 | { 31 | $this->media = $media; 32 | 33 | $this->items = []; 34 | 35 | $this->addConversionsFromRelatedModel($media); 36 | 37 | $this->addManipulationsFromDb($media); 38 | 39 | return $this; 40 | } 41 | 42 | public function getByName(string $name): Conversion 43 | { 44 | $conversion = $this->first(fn (Conversion $conversion) => $conversion->getName() === $name); 45 | 46 | if (! $conversion) { 47 | throw InvalidConversion::unknownName($name); 48 | } 49 | 50 | return $conversion; 51 | } 52 | 53 | protected function addConversionsFromRelatedModel(Media $media): void 54 | { 55 | $modelName = Arr::get(Relation::morphMap(), $media->model_type, $media->model_type); 56 | 57 | if (! class_exists($modelName)) { 58 | return; 59 | } 60 | 61 | /** @var \Spatie\MediaLibrary\HasMedia $model */ 62 | $model = new $modelName; 63 | 64 | /* 65 | * In some cases the user might want to get the actual model 66 | * instance so conversion parameters can depend on model 67 | * properties. This will causes extra queries. 68 | */ 69 | if ($model->registerMediaConversionsUsingModelInstance && $media->model) { 70 | $model = $media->model; 71 | 72 | $model->mediaConversions = []; 73 | } 74 | 75 | $model->registerAllMediaConversions($media); 76 | 77 | $this->items = $model->mediaConversions; 78 | } 79 | 80 | protected function addManipulationsFromDb(Media $media): void 81 | { 82 | collect(Arr::except($media->manipulations, '*'))->each(function ($manipulation, $conversionName) { 83 | $manipulations = new Manipulations($manipulation); 84 | 85 | $this->addManipulationToConversion($manipulations, $conversionName); 86 | }); 87 | 88 | if (array_key_exists('*', $media->manipulations)) { 89 | $globalManipulations = new Manipulations($media->manipulations['*']); 90 | 91 | $this->addManipulationToConversion($globalManipulations, '*'); 92 | } 93 | } 94 | 95 | public function getConversions(string $collectionName = ''): self 96 | { 97 | if ($collectionName === '') { 98 | return $this; 99 | } 100 | 101 | return $this->filter(fn (Conversion $conversion) => $conversion->shouldBePerformedOn($collectionName)); 102 | } 103 | 104 | protected function addManipulationToConversion(Manipulations $manipulations, string $conversionName): void 105 | { 106 | /** @var Conversion|null $conversion */ 107 | $conversion = $this->first(function (Conversion $conversion) use ($conversionName) { 108 | if (! $conversion->shouldBePerformedOn($this->media->collection_name)) { 109 | return false; 110 | } 111 | 112 | if ($conversion->getName() !== $conversionName) { 113 | return false; 114 | } 115 | 116 | return true; 117 | }); 118 | 119 | if ($conversion) { 120 | $conversion->addAsFirstManipulations($manipulations); 121 | } 122 | 123 | if ($conversionName === '*') { 124 | $this->each( 125 | fn (Conversion $conversion) => $conversion->addAsFirstManipulations(clone $manipulations) 126 | ); 127 | } 128 | } 129 | 130 | public function getConversionsFiles(string $collectionName = ''): self 131 | { 132 | return $this 133 | ->getConversions($collectionName) 134 | ->map(fn (Conversion $conversion) => $conversion->getConversionFile($this->media)); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Conversions/Events/ConversionHasBeenCompletedEvent.php: -------------------------------------------------------------------------------- 1 | canConvertMedia($media)) { 25 | return; 26 | } 27 | 28 | [$queuedConversions, $conversions] = ConversionCollection::createForMedia($media) 29 | ->filter(function (Conversion $conversion) use ($onlyConversionNames) { 30 | if (count($onlyConversionNames) === 0) { 31 | return true; 32 | } 33 | 34 | return in_array($conversion->getName(), $onlyConversionNames); 35 | }) 36 | ->filter(fn (Conversion $conversion) => $conversion->shouldBePerformedOn($media->collection_name)) 37 | ->partition(fn (Conversion $conversion) => $queueAll || $conversion->shouldBeQueued()); 38 | 39 | $this 40 | ->performConversions($conversions, $media, $onlyMissing) 41 | ->dispatchQueuedConversions($media, $queuedConversions, $onlyMissing) 42 | ->generateResponsiveImages($media, $withResponsiveImages); 43 | } 44 | 45 | public function performConversions( 46 | ConversionCollection $conversions, 47 | Media $media, 48 | bool $onlyMissing = false 49 | ): self { 50 | $conversions = $conversions 51 | ->when( 52 | $onlyMissing, 53 | fn (ConversionCollection $conversions) => $conversions->reject(function (Conversion $conversion) use ($media) { 54 | $relativePath = $media->getPath($conversion->getName()); 55 | 56 | if ($rootPath = config("filesystems.disks.{$media->disk}.root")) { 57 | $relativePath = str_replace($rootPath, '', $relativePath); 58 | } 59 | 60 | return Storage::disk($media->disk)->exists($relativePath); 61 | }) 62 | ); 63 | 64 | if ($conversions->isEmpty()) { 65 | return $this; 66 | } 67 | 68 | $temporaryDirectory = TemporaryDirectory::create(); 69 | 70 | $copiedOriginalFile = app(Filesystem::class)->copyFromMediaLibrary( 71 | $media, 72 | $temporaryDirectory->path(Str::random(32).'.'.$media->extension) 73 | ); 74 | 75 | $conversions->each(fn (Conversion $conversion) => (new PerformConversionAction)->execute($conversion, $media, $copiedOriginalFile)); 76 | 77 | $temporaryDirectory->delete(); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @return $this 84 | */ 85 | protected function dispatchQueuedConversions( 86 | Media $media, 87 | ConversionCollection $conversions, 88 | bool $onlyMissing = false 89 | ): self { 90 | if ($conversions->isEmpty()) { 91 | return $this; 92 | } 93 | 94 | $performConversionsJobClass = config( 95 | 'media-library.jobs.perform_conversions', 96 | PerformConversionsJob::class 97 | ); 98 | 99 | /** @var PerformConversionsJob $job */ 100 | $job = (new $performConversionsJobClass($conversions, $media, $onlyMissing)) 101 | ->onConnection(config('media-library.queue_connection_name')) 102 | ->onQueue(config('media-library.queue_name')); 103 | 104 | config('media-library.queue_conversions_after_database_commit') 105 | ? dispatch($job)->afterCommit() 106 | : dispatch($job); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * @return $this 113 | */ 114 | protected function generateResponsiveImages(Media $media, bool $withResponsiveImages): self 115 | { 116 | if (! $withResponsiveImages) { 117 | return $this; 118 | } 119 | 120 | if (! count($media->responsive_images)) { 121 | return $this; 122 | } 123 | 124 | $generateResponsiveImagesJobClass = config( 125 | 'media-library.jobs.generate_responsive_images', 126 | GenerateResponsiveImagesJob::class 127 | ); 128 | 129 | /** @var GenerateResponsiveImagesJob $job */ 130 | $job = (new $generateResponsiveImagesJobClass($media)) 131 | ->onConnection(config('media-library.queue_connection_name')) 132 | ->onQueue(config('media-library.queue_name')); 133 | 134 | config('media-library.queue_conversions_after_database_commit') 135 | ? dispatch($job)->afterCommit() 136 | : dispatch($job); 137 | 138 | return $this; 139 | } 140 | 141 | protected function canConvertMedia(Media $media): bool 142 | { 143 | $imageGenerator = ImageGeneratorFactory::forMedia($media); 144 | 145 | return $imageGenerator ? true : false; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/Avif.php: -------------------------------------------------------------------------------- 1 | requirementsAreInstalled()) { 19 | return false; 20 | } 21 | 22 | $validExtension = $this->canHandleExtension(strtolower($media->extension)); 23 | 24 | $validMimeType = $this->canHandleMime(strtolower($media->mime_type)); 25 | 26 | if ($this->shouldMatchBothExtensionsAndMimeTypes()) { 27 | return $validExtension && $validMimeType; 28 | } 29 | 30 | return $validExtension || $validMimeType; 31 | } 32 | 33 | public function shouldMatchBothExtensionsAndMimeTypes(): bool 34 | { 35 | return false; 36 | } 37 | 38 | public function canHandleMime(string $mime = ''): bool 39 | { 40 | return $this->supportedMimeTypes()->contains($mime); 41 | } 42 | 43 | public function canHandleExtension(string $extension = ''): bool 44 | { 45 | return $this->supportedExtensions()->contains($extension); 46 | } 47 | 48 | public function getType(): string 49 | { 50 | return strtolower(class_basename(static::class)); 51 | } 52 | 53 | abstract public function requirementsAreInstalled(): bool; 54 | 55 | abstract public function supportedExtensions(): Collection; 56 | 57 | abstract public function supportedMimeTypes(): Collection; 58 | } 59 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/ImageGeneratorFactory.php: -------------------------------------------------------------------------------- 1 | map(function ($imageGeneratorClassName, $key) { 14 | $imageGeneratorConfig = []; 15 | 16 | if (! is_numeric($key)) { 17 | $imageGeneratorConfig = $imageGeneratorClassName; 18 | $imageGeneratorClassName = $key; 19 | } 20 | 21 | return app($imageGeneratorClassName, $imageGeneratorConfig); 22 | }); 23 | } 24 | 25 | public static function forExtension(?string $extension): ?ImageGenerator 26 | { 27 | if (is_null($extension)) { 28 | return null; 29 | } 30 | 31 | return static::getImageGenerators() 32 | ->first(fn (ImageGenerator $imageGenerator) => $imageGenerator->canHandleExtension(strtolower($extension))); 33 | } 34 | 35 | public static function forMimeType(?string $mimeType): ?ImageGenerator 36 | { 37 | if (is_null($mimeType)) { 38 | return null; 39 | } 40 | 41 | return static::getImageGenerators() 42 | ->first(fn (ImageGenerator $imageGenerator) => $imageGenerator->canHandleMime($mimeType)); 43 | } 44 | 45 | public static function forMedia(Media $media): ?ImageGenerator 46 | { 47 | return static::getImageGenerators() 48 | ->first(fn (ImageGenerator $imageGenerator) => $imageGenerator->canConvert($media)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/Pdf.php: -------------------------------------------------------------------------------- 1 | getPdfPageNumber() : 1; 18 | 19 | if ($this->usesPdfToImageV3()) { 20 | (new \Spatie\PdfToImage\Pdf($file))->selectPage($pageNumber)->save($imageFile); 21 | } else { 22 | (new \Spatie\PdfToImage\Pdf($file))->setPage($pageNumber)->saveImage($imageFile); 23 | } 24 | 25 | return $imageFile; 26 | } 27 | 28 | private function usesPdfToImageV3(): bool 29 | { 30 | return InstalledVersions::satisfies(new VersionParser, 'spatie/pdf-to-image', '^3.0'); 31 | } 32 | 33 | public function requirementsAreInstalled(): bool 34 | { 35 | if (! class_exists(Imagick::class)) { 36 | return false; 37 | } 38 | 39 | if (! class_exists(\Spatie\PdfToImage\Pdf::class)) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | public function supportedExtensions(): Collection 47 | { 48 | return collect(['pdf']); 49 | } 50 | 51 | public function supportedMimeTypes(): Collection 52 | { 53 | return collect(['application/pdf']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/Svg.php: -------------------------------------------------------------------------------- 1 | setBackgroundColor(new ImagickPixel('none')); 18 | $image->readImage($file); 19 | 20 | $image->setImageFormat('png32'); 21 | 22 | file_put_contents($imageFile, $image); 23 | 24 | return $imageFile; 25 | } 26 | 27 | public function requirementsAreInstalled(): bool 28 | { 29 | return class_exists(Imagick::class); 30 | } 31 | 32 | public function supportedExtensions(): Collection 33 | { 34 | return collect(['svg']); 35 | } 36 | 37 | public function supportedMimeTypes(): Collection 38 | { 39 | return collect(['image/svg+xml']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/Video.php: -------------------------------------------------------------------------------- 1 | config('media-library.ffmpeg_path'), 17 | 'ffprobe.binaries' => config('media-library.ffprobe_path'), 18 | ]); 19 | 20 | $video = $ffmpeg->open($file); 21 | 22 | if (! ($video instanceof FFMpegVideo)) { 23 | return null; 24 | } 25 | 26 | $duration = $ffmpeg->getFFProbe()->format($file)->get('duration'); 27 | 28 | $seconds = $conversion ? $conversion->getExtractVideoFrameAtSecond() : 0; 29 | $seconds = $duration <= $seconds ? 0 : $seconds; 30 | 31 | $imageFile = pathinfo($file, PATHINFO_DIRNAME).'/'.pathinfo($file, PATHINFO_FILENAME).'.jpg'; 32 | 33 | $frame = $video->frame(TimeCode::fromSeconds($seconds)); 34 | $frame->save($imageFile); 35 | 36 | return $imageFile; 37 | } 38 | 39 | public function requirementsAreInstalled(): bool 40 | { 41 | return class_exists('\\FFMpeg\\FFMpeg'); 42 | } 43 | 44 | public function supportedExtensions(): Collection 45 | { 46 | return collect(['webm', 'mov', 'mp4']); 47 | } 48 | 49 | public function supportedMimeTypes(): Collection 50 | { 51 | return collect(['video/webm', 'video/mpeg', 'video/mp4', 'video/quicktime']); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Conversions/ImageGenerators/Webp.php: -------------------------------------------------------------------------------- 1 | performConversions( 30 | $this->conversions, 31 | $this->media, 32 | $this->onlyMissing 33 | ); 34 | 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Conversions/Manipulations.php: -------------------------------------------------------------------------------- 1 | manipulations = $manipulations; 22 | } 23 | 24 | public function __call(string $method, array $parameters): self 25 | { 26 | $this->addManipulation($method, $parameters); 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * @return $this 33 | */ 34 | public function addManipulation(string $name, array $parameters = []): self 35 | { 36 | $this->manipulations[$name] = $parameters; 37 | 38 | return $this; 39 | } 40 | 41 | public function getManipulationArgument(string $manipulationName): null|string|array 42 | { 43 | return $this->manipulations[$manipulationName] ?? null; 44 | } 45 | 46 | public function getFirstManipulationArgument(string $manipulationName): null|string|int 47 | { 48 | $manipulationArgument = $this->getManipulationArgument($manipulationName); 49 | 50 | if (! is_array($manipulationArgument)) { 51 | return null; 52 | } 53 | 54 | return $manipulationArgument[0]; 55 | } 56 | 57 | public function isEmpty(): bool 58 | { 59 | return count($this->manipulations) === 0; 60 | } 61 | 62 | public function apply(ImageDriver $image): void 63 | { 64 | foreach ($this->manipulations as $manipulationName => $parameters) { 65 | $parameters = $this->transformParameters($manipulationName, $parameters); 66 | $image->$manipulationName(...$parameters); 67 | } 68 | } 69 | 70 | /** 71 | * @return $this 72 | */ 73 | public function mergeManipulations(self $manipulations): self 74 | { 75 | foreach ($manipulations->toArray() as $name => $parameters) { 76 | $this->manipulations[$name] = array_merge($this->manipulations[$name] ?? [], $parameters ?: []); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @return $this 84 | */ 85 | public function removeManipulation(string $name): self 86 | { 87 | unset($this->manipulations[$name]); 88 | 89 | return $this; 90 | } 91 | 92 | public function toArray(): array 93 | { 94 | return $this->manipulations; 95 | } 96 | 97 | public function transformParameters(int|string $manipulationName, mixed $parameters): mixed 98 | { 99 | switch ($manipulationName) { 100 | case 'border': 101 | if (isset($parameters['type']) && ! $parameters['type'] instanceof BorderType) { 102 | $parameters['type'] = BorderType::from($parameters['type']); 103 | } 104 | break; 105 | case 'watermark': 106 | if (isset($parameters['fit']) && ! $parameters['fit'] instanceof Fit) { 107 | $parameters['fit'] = Fit::from($parameters['fit']); 108 | } 109 | // Fallthrough intended for position 110 | case 'resizeCanvas': 111 | case 'insert': 112 | if (isset($parameters['position']) && ! $parameters['position'] instanceof AlignPosition) { 113 | $parameters['position'] = AlignPosition::from($parameters['position']); 114 | } 115 | break; 116 | case 'resize': 117 | case 'width': 118 | case 'height': 119 | if (isset($parameters['constraints']) && is_array($parameters['constraints'])) { 120 | foreach ($parameters['constraints'] as &$constraint) { 121 | if (! $constraint instanceof Constraint) { 122 | $constraint = Constraint::from($constraint); 123 | } 124 | } 125 | } 126 | break; 127 | case 'crop': 128 | if (isset($parameters['position']) && ! $parameters['position'] instanceof CropPosition) { 129 | $parameters['position'] = CropPosition::from($parameters['position']); 130 | } 131 | break; 132 | case 'fit': 133 | if (isset($parameters['fit']) && ! $parameters['fit'] instanceof Fit) { 134 | $parameters['fit'] = Fit::from($parameters['fit']); 135 | } 136 | break; 137 | case 'flip': 138 | if (isset($parameters['flip']) && ! $parameters['flip'] instanceof FlipDirection) { 139 | $parameters['flip'] = FlipDirection::from($parameters['flip']); 140 | } 141 | break; 142 | case 'orientation': 143 | if (isset($parameters['orientation']) && ! $parameters['orientation'] instanceof Orientation) { 144 | $parameters['orientation'] = Orientation::from($parameters['orientation']); 145 | } 146 | break; 147 | default: 148 | break; 149 | } 150 | 151 | return $parameters; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Downloaders/DefaultDownloader.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'verify_peer' => config('media-library.media_downloader_ssl'), 14 | 'verify_peer_name' => config('media-library.media_downloader_ssl'), 15 | ], 16 | 'http' => [ 17 | 'header' => 'User-Agent: Spatie MediaLibrary', 18 | ], 19 | ]); 20 | 21 | if (! $stream = @fopen($url, 'r', false, $context)) { 22 | throw UnreachableUrl::create($url); 23 | } 24 | 25 | $temporaryFile = tempnam(sys_get_temp_dir(), 'media-library'); 26 | 27 | file_put_contents($temporaryFile, $stream); 28 | 29 | fclose($stream); 30 | 31 | return $temporaryFile; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Downloaders/Downloader.php: -------------------------------------------------------------------------------- 1 | throw(fn () => throw new UnreachableUrl($url)) 16 | ->sink($temporaryFile) 17 | ->get($url); 18 | 19 | return $temporaryFile; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Enums/CollectionPosition.php: -------------------------------------------------------------------------------- 1 | mediaRepository = $mediaRepository; 48 | $this->fileManipulator = $fileManipulator; 49 | $this->fileSystem = $fileSystem; 50 | 51 | if (! $this->confirmToProceed()) { 52 | return; 53 | } 54 | 55 | $this->isDryRun = $this->option('dry-run'); 56 | $this->rateLimit = (int) $this->option('rate-limit'); 57 | 58 | if ($this->option('delete-orphaned')) { 59 | $this->deleteOrphanedMediaItems(); 60 | } 61 | 62 | if (! $this->option('skip-conversions')) { 63 | $this->deleteFilesGeneratedForDeprecatedConversions(); 64 | } 65 | 66 | $this->deleteOrphanedDirectories(); 67 | 68 | $this->info('All done!'); 69 | } 70 | 71 | /** @return LazyCollection */ 72 | public function getMediaItems(): LazyCollection 73 | { 74 | $modelType = $this->argument('modelType'); 75 | $collectionName = $this->argument('collectionName'); 76 | 77 | if (is_string($modelType) && is_string($collectionName)) { 78 | return $this->mediaRepository->getByModelTypeAndCollectionName( 79 | $modelType, 80 | $collectionName 81 | ); 82 | } 83 | 84 | if (is_string($modelType)) { 85 | return $this->mediaRepository->getByModelType($modelType); 86 | } 87 | 88 | if (is_string($collectionName)) { 89 | return $this->mediaRepository->getByCollectionName($collectionName); 90 | } 91 | 92 | return $this->mediaRepository->all(); 93 | } 94 | 95 | protected function deleteOrphanedMediaItems(): void 96 | { 97 | $this->getOrphanedMediaItems()->each(function (Media $media): void { 98 | if ($this->isDryRun) { 99 | $this->info("Orphaned Media[id={$media->id}] found"); 100 | 101 | return; 102 | } 103 | 104 | $media->delete(); 105 | 106 | if ($this->rateLimit) { 107 | usleep((1 / $this->rateLimit) * 1_000_000); 108 | } 109 | 110 | $this->info("Orphaned Media[id={$media->id}] has been removed"); 111 | }); 112 | } 113 | 114 | /** @return LazyCollection */ 115 | protected function getOrphanedMediaItems(): LazyCollection 116 | { 117 | $collectionName = $this->argument('collectionName'); 118 | 119 | if (is_string($collectionName)) { 120 | return $this->mediaRepository->getOrphansByCollectionName($collectionName); 121 | } 122 | 123 | return $this->mediaRepository->getOrphans(); 124 | } 125 | 126 | protected function deleteFilesGeneratedForDeprecatedConversions(): void 127 | { 128 | $this->getMediaItems()->each(function (Media $media) { 129 | $this->deleteConversionFilesForDeprecatedConversions($media); 130 | 131 | if ($media->responsive_images) { 132 | $this->deleteDeprecatedResponsiveImages($media); 133 | } 134 | 135 | if ($this->rateLimit) { 136 | usleep((1 / $this->rateLimit) * 1_000_000 * 2); 137 | } 138 | }); 139 | } 140 | 141 | protected function deleteConversionFilesForDeprecatedConversions(Media $media): void 142 | { 143 | $conversionFilePaths = ConversionCollection::createForMedia($media)->getConversionsFiles($media->collection_name); 144 | 145 | $conversionPath = PathGeneratorFactory::create($media)->getPathForConversions($media); 146 | $currentFilePaths = $this->fileSystem->disk($media->disk)->files($conversionPath); 147 | 148 | collect($currentFilePaths) 149 | ->reject(fn (string $currentFilePath) => $conversionFilePaths->contains(basename($currentFilePath))) 150 | ->reject(fn (string $currentFilePath) => $media->file_name === basename($currentFilePath)) 151 | ->each(function (string $currentFilePath) use ($media) { 152 | if (! $this->isDryRun) { 153 | $this->fileSystem->disk($media->disk)->delete($currentFilePath); 154 | 155 | $this->markConversionAsRemoved($media, $currentFilePath); 156 | } 157 | 158 | $this->info("Deprecated conversion file `{$currentFilePath}` ".($this->isDryRun ? 'found' : 'has been removed')); 159 | }); 160 | } 161 | 162 | protected function deleteDeprecatedResponsiveImages(Media $media): void 163 | { 164 | $conversionNamesWithResponsiveImages = ConversionCollection::createForMedia($media) 165 | ->filter(fn (Conversion $conversion) => $conversion->shouldGenerateResponsiveImages()) 166 | ->map(fn (Conversion $conversion) => $conversion->getName()) 167 | ->push('media_library_original'); 168 | 169 | /** @var array $responsiveImagesGeneratedFor */ 170 | $responsiveImagesGeneratedFor = array_keys($media->responsive_images); 171 | 172 | collect($responsiveImagesGeneratedFor) 173 | ->map(fn (string $generatedFor) => $media->responsiveImages($generatedFor)) 174 | ->reject(fn (RegisteredResponsiveImages $responsiveImages) => $conversionNamesWithResponsiveImages->contains($responsiveImages->generatedFor)) 175 | ->each(function (RegisteredResponsiveImages $responsiveImages) { 176 | if (! $this->isDryRun) { 177 | $responsiveImages->delete(); 178 | } 179 | }); 180 | } 181 | 182 | protected function deleteOrphanedDirectories(): void 183 | { 184 | $diskName = $this->argument('disk') ?: config('media-library.disk_name'); 185 | 186 | if (is_null(config("filesystems.disks.{$diskName}"))) { 187 | throw DiskDoesNotExist::create($diskName); 188 | } 189 | 190 | $prefix = config('media-library.prefix', ''); 191 | 192 | if ($prefix !== '') { 193 | $prefix = trim($prefix, '/').'/'; 194 | } 195 | 196 | $mediaIds = $this->mediaRepository->allIds(); 197 | 198 | /** @var array */ 199 | $directories = $this->fileSystem->disk($diskName)->directories($prefix); 200 | 201 | collect($directories) 202 | ->map(fn (string $directory) => str_replace($prefix, '', $directory)) 203 | ->filter(fn (string $directory) => is_numeric($directory)) 204 | ->reject(fn (string $directory) => $mediaIds->contains((int) $directory)) 205 | ->each(function (string $directory) use ($diskName, $prefix) { 206 | $directory = $prefix.$directory; 207 | 208 | if (! $this->isDryRun) { 209 | $this->fileSystem->disk($diskName)->deleteDirectory($directory); 210 | } 211 | 212 | if ($this->rateLimit) { 213 | usleep((1 / $this->rateLimit) * 1_000_000); 214 | } 215 | 216 | $this->info("Orphaned media directory `{$directory}` ".($this->isDryRun ? 'found' : 'has been removed')); 217 | }); 218 | } 219 | 220 | protected function markConversionAsRemoved(Media $media, string $conversionPath): void 221 | { 222 | $conversionFile = pathinfo($conversionPath, PATHINFO_FILENAME); 223 | 224 | $generatedConversionName = null; 225 | 226 | $media->getGeneratedConversions() 227 | ->dot() 228 | ->filter( 229 | fn (bool $isGenerated, string $generatedConversionName) => Str::contains($conversionFile, $generatedConversionName) 230 | ) 231 | ->each( 232 | fn (bool $isGenerated, string $conversionName) => $media->markAsConversionNotGenerated($conversionName) 233 | ); 234 | 235 | $media->save(); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/MediaCollections/Commands/ClearCommand.php: -------------------------------------------------------------------------------- 1 | mediaRepository = $mediaRepository; 25 | 26 | if (! $this->confirmToProceed()) { 27 | return; 28 | } 29 | 30 | $mediaItems = $this->getMediaItems(); 31 | 32 | $progressBar = $this->output->createProgressBar($mediaItems->count()); 33 | 34 | $mediaItems->each(function (Media $media) use ($progressBar) { 35 | $media->delete(); 36 | $progressBar->advance(); 37 | }); 38 | 39 | $progressBar->finish(); 40 | 41 | $this->info('All done!'); 42 | } 43 | 44 | /** @return LazyCollection */ 45 | public function getMediaItems(): LazyCollection 46 | { 47 | $modelType = $this->argument('modelType'); 48 | $collectionName = $this->argument('collectionName'); 49 | 50 | if (is_string($modelType) && is_string($collectionName)) { 51 | return $this->mediaRepository->getByModelTypeAndCollectionName( 52 | $modelType, 53 | $collectionName 54 | ); 55 | } 56 | 57 | if (is_string($modelType)) { 58 | return $this->mediaRepository->getByModelType($modelType); 59 | } 60 | 61 | if (is_string($collectionName)) { 62 | return $this->mediaRepository->getByCollectionName($collectionName); 63 | } 64 | 65 | return $this->mediaRepository->all(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MediaCollections/Contracts/MediaLibraryRequest.php: -------------------------------------------------------------------------------- 1 | name}` of model `{$modelType}` with id `{$hasMedia->getKey()}`"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MediaCollections/Exceptions/FunctionalityNotAvailable.php: -------------------------------------------------------------------------------- 1 | getKey()}"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MediaCollections/Exceptions/MediaCannotBeUpdated.php: -------------------------------------------------------------------------------- 1 | getKey()} is not part of collection `{$collectionName}`"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MediaCollections/Exceptions/MimeTypeNotAllowed.php: -------------------------------------------------------------------------------- 1 | file_name, $media->size, $media->mime_type); 10 | } 11 | 12 | public function __construct( 13 | public string $name, 14 | public int $size, 15 | public string $mimeType 16 | ) {} 17 | 18 | public function __toString(): string 19 | { 20 | return "name: {$this->name}, size: {$this->size}, mime: {$this->mimeType}"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MediaCollections/FileAdderFactory.php: -------------------------------------------------------------------------------- 1 | setSubject($subject) 21 | ->setFile($file); 22 | } 23 | 24 | public static function createFromDisk(Model $subject, string $key, string $disk): FileAdder 25 | { 26 | /** @var FileAdder $fileAdder */ 27 | $fileAdder = app(FileAdder::class); 28 | 29 | return $fileAdder 30 | ->setSubject($subject) 31 | ->setFile(new RemoteFile($key, $disk)); 32 | } 33 | 34 | public static function createFromRequest(Model $subject, string $key): FileAdder 35 | { 36 | return static::createMultipleFromRequest($subject, [$key])->first(); 37 | } 38 | 39 | public static function createMultipleFromRequest(Model $subject, array $keys = []): Collection 40 | { 41 | return collect($keys) 42 | ->map(function (string $key) use ($subject) { 43 | $key = trim(basename($key), './'); 44 | 45 | if (! request()->hasFile($key)) { 46 | throw RequestDoesNotHaveFile::create($key); 47 | } 48 | 49 | $files = request()->file($key); 50 | 51 | if (! is_array($files)) { 52 | return static::create($subject, $files); 53 | } 54 | 55 | return array_map(fn ($file) => static::create($subject, $file), $files); 56 | })->flatten(); 57 | } 58 | 59 | public static function createAllFromRequest(Model $subject): Collection 60 | { 61 | $fileKeys = array_keys(request()->allFiles()); 62 | 63 | return static::createMultipleFromRequest($subject, $fileKeys); 64 | } 65 | 66 | public static function createForPendingMedia(Model $subject, PendingMediaItem $pendingMedia): FileAdder 67 | { 68 | /** @var FileAdder $fileAdder */ 69 | $fileAdder = app(FileAdder::class); 70 | 71 | return $fileAdder 72 | ->setSubject($subject) 73 | ->setFile($pendingMedia->temporaryUpload) 74 | ->setName($pendingMedia->name) 75 | ->setOrder($pendingMedia->order); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/MediaCollections/Filesystem.php: -------------------------------------------------------------------------------- 1 | copyToMediaLibrary($file, $media, null, $targetFileName); 31 | } catch (DiskCannotBeAccessed $exception) { 32 | return false; 33 | } 34 | 35 | event(new MediaHasBeenAddedEvent($media)); 36 | 37 | app(FileManipulator::class)->createDerivedFiles($media); 38 | 39 | return true; 40 | } 41 | 42 | public function addRemote(RemoteFile $file, Media $media, ?string $targetFileName = null): bool 43 | { 44 | try { 45 | $this->copyToMediaLibraryFromRemote($file, $media, null, $targetFileName); 46 | } catch (DiskCannotBeAccessed $exception) { 47 | return false; 48 | } 49 | 50 | event(new MediaHasBeenAddedEvent($media)); 51 | 52 | app(FileManipulator::class)->createDerivedFiles($media); 53 | 54 | return true; 55 | } 56 | 57 | public function prepareCopyFileOnDisk(RemoteFile $file, Media $media, string $destination): void 58 | { 59 | $this->copyFileOnDisk($file->getKey(), $destination, $media->disk); 60 | } 61 | 62 | public function copyToMediaLibraryFromRemote(RemoteFile $file, Media $media, ?string $type = null, ?string $targetFileName = null): void 63 | { 64 | $destinationFileName = $targetFileName ?: $file->getFilename(); 65 | 66 | $destination = $this->getMediaDirectory($media, $type).$destinationFileName; 67 | 68 | $diskDriverName = (in_array($type, ['conversions', 'responsiveImages'])) 69 | ? $media->getConversionsDiskDriverName() 70 | : $media->getDiskDriverName(); 71 | 72 | if ($this->shouldCopyFileOnDisk($file, $media, $diskDriverName)) { 73 | $this->prepareCopyFileOnDisk($file, $media, $destination); 74 | 75 | return; 76 | } 77 | 78 | $storage = Storage::disk($file->getDisk()); 79 | 80 | $headers = $diskDriverName === 'local' 81 | ? [] 82 | : $this->getRemoteHeadersForFile( 83 | $file->getKey(), 84 | $media->getCustomHeaders(), 85 | $storage->mimeType($file->getKey()) 86 | ); 87 | 88 | $this->streamFileToDisk( 89 | $storage->getDriver()->readStream($file->getKey()), 90 | $destination, 91 | $media->disk, 92 | $headers 93 | ); 94 | } 95 | 96 | protected function shouldCopyFileOnDisk(RemoteFile $file, Media $media, string $diskDriverName): bool 97 | { 98 | if ($file->getDisk() !== $media->disk) { 99 | return false; 100 | } 101 | 102 | if ($diskDriverName === 'local') { 103 | return true; 104 | } 105 | 106 | if (count($media->getCustomHeaders()) > 0) { 107 | return false; 108 | } 109 | 110 | if ((is_countable(config('media-library.remote.extra_headers')) ? count(config('media-library.remote.extra_headers')) : 0) > 0) { 111 | return false; 112 | } 113 | 114 | return true; 115 | } 116 | 117 | protected function copyFileOnDisk(string $file, string $destination, string $disk): void 118 | { 119 | $this->filesystem->disk($disk) 120 | ->copy($file, $destination); 121 | } 122 | 123 | protected function streamFileToDisk($stream, string $destination, string $disk, array $headers): void 124 | { 125 | $this->filesystem->disk($disk) 126 | ->getDriver()->writeStream( 127 | $destination, 128 | $stream, 129 | $headers 130 | ); 131 | } 132 | 133 | public function copyToMediaLibrary(string $pathToFile, Media $media, ?string $type = null, ?string $targetFileName = null): void 134 | { 135 | $destinationFileName = $targetFileName ?: pathinfo($pathToFile, PATHINFO_BASENAME); 136 | 137 | $destination = $this->getMediaDirectory($media, $type).$destinationFileName; 138 | 139 | $file = fopen($pathToFile, 'r'); 140 | 141 | $diskName = (in_array($type, ['conversions', 'responsiveImages'])) 142 | ? $media->conversions_disk 143 | : $media->disk; 144 | 145 | $diskDriverName = (in_array($type, ['conversions', 'responsiveImages'])) 146 | ? $media->getConversionsDiskDriverName() 147 | : $media->getDiskDriverName(); 148 | 149 | if ($diskDriverName === 'local') { 150 | $success = $this->filesystem 151 | ->disk($diskName) 152 | ->put($destination, $file); 153 | 154 | fclose($file); 155 | 156 | if (! $success) { 157 | throw DiskCannotBeAccessed::create($diskName); 158 | } 159 | 160 | return; 161 | } 162 | 163 | $success = $this->filesystem 164 | ->disk($diskName) 165 | ->put( 166 | $destination, 167 | $file, 168 | $this->getRemoteHeadersForFile($pathToFile, $media->getCustomHeaders()), 169 | ); 170 | 171 | if (is_resource($file)) { 172 | fclose($file); 173 | } 174 | 175 | if (! $success) { 176 | throw DiskCannotBeAccessed::create($diskName); 177 | } 178 | } 179 | 180 | public function addCustomRemoteHeaders(array $customRemoteHeaders): void 181 | { 182 | $this->customRemoteHeaders = $customRemoteHeaders; 183 | } 184 | 185 | public function getRemoteHeadersForFile( 186 | string $file, 187 | array $mediaCustomHeaders = [], 188 | ?string $mimeType = null 189 | ): array { 190 | $mimeTypeHeader = ['ContentType' => $mimeType ?: File::getMimeType($file)]; 191 | 192 | $extraHeaders = config('media-library.remote.extra_headers'); 193 | 194 | return array_merge( 195 | $mimeTypeHeader, 196 | $extraHeaders, 197 | $this->customRemoteHeaders, 198 | $mediaCustomHeaders 199 | ); 200 | } 201 | 202 | public function getStream(Media $media) 203 | { 204 | $sourceFile = $this->getMediaDirectory($media).'/'.$media->file_name; 205 | 206 | return $this->filesystem->disk($media->disk)->readStream($sourceFile); 207 | } 208 | 209 | public function getConversionStream(Media $media, string $conversion) 210 | { 211 | $sourceFile = $media->getPathRelativeToRoot($conversion); 212 | 213 | return $this->filesystem->disk($media->conversions_disk)->readStream($sourceFile); 214 | } 215 | 216 | public function copyFromMediaLibrary(Media $media, string $targetFile): string 217 | { 218 | file_put_contents($targetFile, $this->getStream($media)); 219 | 220 | return $targetFile; 221 | } 222 | 223 | public function removeAllFiles(Media $media): void 224 | { 225 | $fileRemover = FileRemoverFactory::create($media); 226 | 227 | $fileRemover->removeAllFiles($media); 228 | } 229 | 230 | public function removeFile(Media $media, string $path): void 231 | { 232 | $fileRemover = FileRemoverFactory::create($media); 233 | 234 | $fileRemover->removeFile($path, $media->disk); 235 | } 236 | 237 | public function removeResponsiveImages(Media $media, string $conversionName = 'media_library_original'): void 238 | { 239 | /** @var FileNamer $fileNamer */ 240 | $fileNamer = app(config('media-library.file_namer')); 241 | $mediaFilename = $fileNamer->responsiveFileName($media->name); 242 | 243 | $responsiveImagesDirectory = $this->getResponsiveImagesDirectory($media); 244 | 245 | $allFilePaths = $this->filesystem->disk($media->disk)->allFiles($responsiveImagesDirectory); 246 | 247 | $responsiveImagePaths = array_filter( 248 | $allFilePaths, 249 | static fn (string $path) => Str::contains($path, $mediaFilename.'___'.$conversionName) 250 | ); 251 | 252 | $this->filesystem->disk($media->disk)->delete($responsiveImagePaths); 253 | } 254 | 255 | public function syncFileNames(Media $media): void 256 | { 257 | $this->renameMediaFile($media); 258 | 259 | $this->renameConversionFiles($media); 260 | } 261 | 262 | public function syncMediaPath(Media $media): void 263 | { 264 | $factory = PathGeneratorFactory::create($media); 265 | 266 | $oldMedia = (clone $media)->fill($media->getOriginal()); 267 | 268 | $oldPath = $factory->getPath($oldMedia); 269 | $newPath = $factory->getPath($media); 270 | 271 | if ($oldPath === $newPath) { 272 | return; 273 | } 274 | 275 | // If the media is stored on S3, we need to move all files in the directory 276 | if ($media->getDiskDriverName() === 's3') { 277 | $allFiles = $this->filesystem->disk($media->disk)->allFiles($oldPath); 278 | 279 | foreach ($allFiles as $file) { 280 | $newFilePath = str_replace($oldPath, $newPath, $file); 281 | $this->filesystem->disk($media->disk)->move($file, $newFilePath); 282 | } 283 | 284 | return; 285 | } 286 | 287 | $this->filesystem->disk($media->disk)->move($oldPath, $newPath); 288 | } 289 | 290 | protected function renameMediaFile(Media $media): void 291 | { 292 | $newFileName = $media->file_name; 293 | $oldFileName = $media->getOriginal('file_name'); 294 | 295 | $mediaDirectory = $this->getMediaDirectory($media); 296 | 297 | $oldFile = "{$mediaDirectory}/{$oldFileName}"; 298 | $newFile = "{$mediaDirectory}/{$newFileName}"; 299 | 300 | $this->filesystem->disk($media->disk)->move($oldFile, $newFile); 301 | } 302 | 303 | protected function renameConversionFiles(Media $media): void 304 | { 305 | $mediaWithOldFileName = config('media-library.media_model')::find($media->getKey()); 306 | $mediaWithOldFileName->file_name = $mediaWithOldFileName->getOriginal('file_name'); 307 | 308 | $conversionDirectory = $this->getConversionDirectory($media); 309 | 310 | $conversionCollection = ConversionCollection::createForMedia($media); 311 | 312 | foreach ($media->getMediaConversionNames() as $conversionName) { 313 | $conversion = $conversionCollection->getByName($conversionName); 314 | 315 | $oldFile = $conversionDirectory.$conversion->getConversionFile($mediaWithOldFileName); 316 | $newFile = $conversionDirectory.$conversion->getConversionFile($media); 317 | 318 | $disk = $this->filesystem->disk($media->conversions_disk); 319 | 320 | // A media conversion file might be missing, waiting to be generated, failed etc. 321 | if (! $disk->exists($oldFile)) { 322 | continue; 323 | } 324 | 325 | $disk->move($oldFile, $newFile); 326 | } 327 | } 328 | 329 | public function getMediaDirectory(Media $media, ?string $type = null): string 330 | { 331 | $directory = null; 332 | $pathGenerator = PathGeneratorFactory::create($media); 333 | 334 | if (! $type) { 335 | $directory = $pathGenerator->getPath($media); 336 | } 337 | 338 | if ($type === 'conversions') { 339 | $directory = $pathGenerator->getPathForConversions($media); 340 | } 341 | 342 | if ($type === 'responsiveImages') { 343 | $directory = $pathGenerator->getPathForResponsiveImages($media); 344 | } 345 | 346 | return $directory; 347 | } 348 | 349 | public function getConversionDirectory(Media $media): string 350 | { 351 | return $this->getMediaDirectory($media, 'conversions'); 352 | } 353 | 354 | public function getResponsiveImagesDirectory(Media $media): string 355 | { 356 | return $this->getMediaDirectory($media, 'responsiveImages'); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/MediaCollections/HtmlableMedia.php: -------------------------------------------------------------------------------- 1 | extraAttributes = $attributes; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * @return $this 44 | */ 45 | public function conversion(string $conversionName): self 46 | { 47 | $this->conversionName = $conversionName; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return $this 54 | */ 55 | public function lazy(): self 56 | { 57 | $this->loadingAttributeValue = ('lazy'); 58 | 59 | return $this; 60 | } 61 | 62 | public function toHtml(): string 63 | { 64 | $imageGenerator = ImageGeneratorFactory::forMedia($this->media) ?? new Image; 65 | 66 | if (! $imageGenerator->canHandleMime($this->media->mime_type)) { 67 | return ''; 68 | } 69 | 70 | $attributeString = collect($this->extraAttributes) 71 | ->map(fn ($value, $name) => $name.'="'.$value.'"')->implode(' '); 72 | 73 | if (strlen($attributeString)) { 74 | $attributeString = ' '.$attributeString; 75 | } 76 | 77 | $loadingAttributeValue = config('media-library.default_loading_attribute_value'); 78 | 79 | if ($this->conversionName !== '') { 80 | $conversionObject = ConversionCollection::createForMedia($this->media)->getByName($this->conversionName); 81 | 82 | $loadingAttributeValue = $conversionObject->getLoadingAttributeValue(); 83 | } 84 | 85 | if ($this->loadingAttributeValue !== '') { 86 | $loadingAttributeValue = $this->loadingAttributeValue; 87 | } 88 | 89 | $viewName = 'image'; 90 | $width = ''; 91 | $height = ''; 92 | 93 | if ($this->media->hasResponsiveImages($this->conversionName)) { 94 | $viewName = config('media-library.responsive_images.use_tiny_placeholders') 95 | ? 'responsiveImageWithPlaceholder' 96 | : 'responsiveImage'; 97 | 98 | $responsiveImage = $this->media->responsiveImages($this->conversionName)->files->first(); 99 | 100 | $width = $responsiveImage->width(); 101 | $height = $responsiveImage->height(); 102 | } 103 | 104 | $media = $this->media; 105 | $conversion = $this->conversionName; 106 | 107 | return view("media-library::{$viewName}", compact( 108 | 'media', 109 | 'conversion', 110 | 'attributeString', 111 | 'loadingAttributeValue', 112 | 'width', 113 | 'height', 114 | ))->render(); 115 | } 116 | 117 | public function __toString(): string 118 | { 119 | return $this->toHtml(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/MediaCollections/MediaCollection.php: -------------------------------------------------------------------------------- 1 | */ 32 | public array $fallbackUrls = []; 33 | 34 | /** @var array */ 35 | public array $fallbackPaths = []; 36 | 37 | public function __construct( 38 | public string $name 39 | ) { 40 | $this->mediaConversionRegistrations = function () {}; 41 | 42 | $this->acceptsFile = fn () => true; 43 | } 44 | 45 | public static function create($name): self 46 | { 47 | return new static($name); 48 | } 49 | 50 | /** 51 | * @return $this 52 | */ 53 | public function useDisk(string $diskName): self 54 | { 55 | $this->diskName = $diskName; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return $this 62 | */ 63 | public function storeConversionsOnDisk(string $conversionsDiskName): self 64 | { 65 | $this->conversionsDiskName = $conversionsDiskName; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return $this 72 | */ 73 | public function acceptsFile(callable $acceptsFile): self 74 | { 75 | $this->acceptsFile = $acceptsFile; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * @return $this 82 | */ 83 | public function acceptsMimeTypes(array $mimeTypes): self 84 | { 85 | $this->acceptsMimeTypes = $mimeTypes; 86 | 87 | return $this; 88 | } 89 | 90 | public function singleFile(): self 91 | { 92 | return $this->onlyKeepLatest(1); 93 | } 94 | 95 | /** 96 | * @return $this 97 | */ 98 | public function onlyKeepLatest(int $maximumNumberOfItemsInCollection): self 99 | { 100 | if ($maximumNumberOfItemsInCollection < 1) { 101 | throw new InvalidArgumentException("You should pass a value higher than 0. `{$maximumNumberOfItemsInCollection}` given."); 102 | } 103 | 104 | $this->singleFile = ($maximumNumberOfItemsInCollection === 1); 105 | 106 | $this->collectionSizeLimit = $maximumNumberOfItemsInCollection; 107 | 108 | return $this; 109 | } 110 | 111 | public function registerMediaConversions(callable $mediaConversionRegistrations): void 112 | { 113 | $this->mediaConversionRegistrations = $mediaConversionRegistrations; 114 | } 115 | 116 | /** 117 | * @return $this 118 | */ 119 | public function useFallbackUrl(string $url, string $conversionName = ''): self 120 | { 121 | if ($conversionName === '') { 122 | $conversionName = 'default'; 123 | } 124 | 125 | $this->fallbackUrls[$conversionName] = $url; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @return $this 132 | */ 133 | public function useFallbackPath(string $path, string $conversionName = ''): self 134 | { 135 | if ($conversionName === '') { 136 | $conversionName = 'default'; 137 | } 138 | 139 | $this->fallbackPaths[$conversionName] = $path; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * @return $this 146 | */ 147 | public function withResponsiveImages(): self 148 | { 149 | $this->generateResponsiveImages = true; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return $this 156 | */ 157 | public function withResponsiveImagesIf($condition): self 158 | { 159 | $this->generateResponsiveImages = (bool) (is_callable($condition) ? $condition() : $condition); 160 | 161 | return $this; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/MediaCollections/MediaRepository.php: -------------------------------------------------------------------------------- 1 | applyFilterToMediaCollection($model->loadMedia($collectionName), $filter); 28 | } 29 | 30 | /** 31 | * Apply given filters on media. 32 | */ 33 | protected function applyFilterToMediaCollection( 34 | Collection $media, 35 | array|callable $filter 36 | ): Collection { 37 | if (is_array($filter)) { 38 | $filter = $this->getDefaultFilterFunction($filter); 39 | } 40 | 41 | return $media->filter($filter); 42 | } 43 | 44 | public function all(): LazyCollection 45 | { 46 | return $this->query()->cursor(); 47 | } 48 | 49 | public function allIds(): Collection 50 | { 51 | return $this->query()->pluck($this->model->getKeyName()); 52 | } 53 | 54 | public function getByModelType(string $modelType): LazyCollection 55 | { 56 | return $this->query()->where('model_type', $modelType)->cursor(); 57 | } 58 | 59 | public function getByIds(array $ids): LazyCollection 60 | { 61 | return $this->query()->whereIn($this->model->getKeyName(), $ids)->cursor(); 62 | } 63 | 64 | public function getByIdGreaterThan(int $startingFromId, bool $excludeStartingId = false, string $modelType = ''): LazyCollection 65 | { 66 | return $this->query() 67 | ->where($this->model->getKeyName(), $excludeStartingId ? '>' : '>=', $startingFromId) 68 | ->when($modelType !== '', fn (Builder $q) => $q->where('model_type', $modelType)) 69 | ->cursor(); 70 | } 71 | 72 | public function getByModelTypeAndCollectionName(string $modelType, string $collectionName): LazyCollection 73 | { 74 | return $this->query() 75 | ->where('model_type', $modelType) 76 | ->where('collection_name', $collectionName) 77 | ->cursor(); 78 | } 79 | 80 | public function getByCollectionName(string $collectionName): LazyCollection 81 | { 82 | return $this->query() 83 | ->where('collection_name', $collectionName) 84 | ->cursor(); 85 | } 86 | 87 | public function getOrphans(): LazyCollection 88 | { 89 | return $this->orphansQuery() 90 | ->cursor(); 91 | } 92 | 93 | public function getOrphansByCollectionName(string $collectionName): LazyCollection 94 | { 95 | return $this->orphansQuery() 96 | ->where('collection_name', $collectionName) 97 | ->cursor(); 98 | } 99 | 100 | protected function query(): Builder 101 | { 102 | return $this->model->newQuery(); 103 | } 104 | 105 | protected function orphansQuery(): Builder 106 | { 107 | return $this->query()->where(fn (Builder $query) => $query->whereDoesntHave( 108 | 'model', 109 | fn (Builder $q) => $q->hasMacro('withTrashed') ? $q->withTrashed() : $q, 110 | )); 111 | } 112 | 113 | protected function getDefaultFilterFunction(array $filters): Closure 114 | { 115 | return function (Media $media) use ($filters) { 116 | foreach ($filters as $property => $value) { 117 | if (! Arr::has($media->custom_properties, $property)) { 118 | return false; 119 | } 120 | 121 | if (Arr::get($media->custom_properties, $property) !== $value) { 122 | return false; 123 | } 124 | } 125 | 126 | return true; 127 | }; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Collections/MediaCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class MediaCollection extends Collection implements Htmlable 16 | { 17 | public ?string $collectionName = null; 18 | 19 | public ?string $formFieldName = null; 20 | 21 | /** 22 | * @return $this 23 | */ 24 | public function collectionName(string $collectionName): self 25 | { 26 | $this->collectionName = $collectionName; 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * @return $this 33 | */ 34 | public function formFieldName(string $formFieldName): self 35 | { 36 | $this->formFieldName = $formFieldName; 37 | 38 | return $this; 39 | } 40 | 41 | public function totalSizeInBytes(): int 42 | { 43 | return $this->sum('size'); 44 | } 45 | 46 | public function toHtml(): string 47 | { 48 | return e(json_encode(old($this->formFieldName ?? $this->collectionName) ?? $this->map(function (Media $media) { 49 | return [ 50 | 'name' => $media->name, 51 | 'file_name' => $media->file_name, 52 | 'uuid' => $media->uuid, 53 | 'preview_url' => $media->preview_url, 54 | 'original_url' => $media->original_url, 55 | 'order' => $media->order_column, 56 | 'custom_properties' => $media->custom_properties, 57 | 'extension' => $media->extension, 58 | 'size' => $media->size, 59 | ]; 60 | })->keyBy('uuid'))); 61 | } 62 | 63 | public function jsonSerialize(): array 64 | { 65 | if (config('media-library.use_default_collection_serialization')) { 66 | return parent::jsonSerialize(); 67 | } 68 | 69 | if (! ($this->formFieldName ?? $this->collectionName)) { 70 | return []; 71 | } 72 | 73 | return old($this->formFieldName ?? $this->collectionName) ?? $this->map(function (Media $media) { 74 | return [ 75 | 'name' => $media->name, 76 | 'file_name' => $media->file_name, 77 | 'uuid' => $media->uuid, 78 | 'preview_url' => $media->preview_url, 79 | 'original_url' => $media->original_url, 80 | 'order' => $media->order_column, 81 | 'custom_properties' => $media->custom_properties, 82 | 'extension' => $media->extension, 83 | 'size' => $media->size, 84 | ]; 85 | })->keyBy('uuid')->toArray(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Concerns/CustomMediaProperties.php: -------------------------------------------------------------------------------- 1 | setCustomProperty('custom_headers', $customHeaders); 13 | 14 | return $this; 15 | } 16 | 17 | public function getCustomHeaders(): array 18 | { 19 | return $this->getCustomProperty('custom_headers', []); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Concerns/HasUuid.php: -------------------------------------------------------------------------------- 1 | uuid)) { 15 | $model->uuid = (string) Str::uuid(); 16 | } 17 | }); 18 | } 19 | 20 | public static function findByUuid(string $uuid): ?Model 21 | { 22 | return static::where('uuid', $uuid)->first(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Concerns/IsSorted.php: -------------------------------------------------------------------------------- 1 | determineOrderColumnName(); 12 | 13 | $this->$orderColumnName = $this->getHighestOrderNumber() + 1; 14 | } 15 | 16 | public function getHighestOrderNumber(): int 17 | { 18 | return (int) static::where('model_type', $this->model_type) 19 | ->where('model_id', $this->model_id) 20 | ->max($this->determineOrderColumnName()); 21 | } 22 | 23 | public function scopeOrdered(Builder $query): Builder 24 | { 25 | return $query->orderBy($this->determineOrderColumnName()); 26 | } 27 | 28 | /* 29 | * This function reorders the records: the record with the first id in the array 30 | * will get the starting order (defaults to 1), the record with the second id 31 | * will get the starting order + 1, and so on. 32 | * 33 | * A starting order number can be optionally supplied. 34 | */ 35 | public static function setNewOrder(array $ids, int $startOrder = 1): void 36 | { 37 | foreach ($ids as $id) { 38 | $model = static::find($id); 39 | if (! $model) { 40 | continue; 41 | } 42 | 43 | $orderColumnName = $model->determineOrderColumnName(); 44 | 45 | $model->$orderColumnName = $startOrder++; 46 | 47 | $model->save(); 48 | } 49 | } 50 | 51 | protected function determineOrderColumnName(): string 52 | { 53 | return $this->sortable['order_column_name'] ?? 'order_column'; 54 | } 55 | 56 | public function shouldSortWhenCreating(): bool 57 | { 58 | return $this->sortable['sort_when_creating'] ?? true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Media.php: -------------------------------------------------------------------------------- 1 | 'array', 80 | 'custom_properties' => 'array', 81 | 'generated_conversions' => 'array', 82 | 'responsive_images' => 'array', 83 | ]; 84 | 85 | protected int $streamChunkSize = (1024 * 1024); // default to 1MB chunks. 86 | 87 | public function newCollection(array $models = []): MediaCollection 88 | { 89 | return new MediaCollection($models); 90 | } 91 | 92 | public function model(): MorphTo 93 | { 94 | return $this->morphTo(); 95 | } 96 | 97 | public function getFullUrl(string $conversionName = ''): string 98 | { 99 | return url($this->getUrl($conversionName)); 100 | } 101 | 102 | public function getUrl(string $conversionName = ''): string 103 | { 104 | $urlGenerator = UrlGeneratorFactory::createForMedia($this, $conversionName); 105 | 106 | return $urlGenerator->getUrl(); 107 | } 108 | 109 | public function getTemporaryUrl(DateTimeInterface $expiration, string $conversionName = '', array $options = []): string 110 | { 111 | $urlGenerator = $this->getUrlGenerator($conversionName); 112 | 113 | return $urlGenerator->getTemporaryUrl($expiration, $options); 114 | } 115 | 116 | public function getPath(string $conversionName = ''): string 117 | { 118 | $urlGenerator = $this->getUrlGenerator($conversionName); 119 | 120 | return $urlGenerator->getPath(); 121 | } 122 | 123 | public function getPathRelativeToRoot(string $conversionName = ''): string 124 | { 125 | return $this->getUrlGenerator($conversionName)->getPathRelativeToRoot(); 126 | } 127 | 128 | public function getUrlGenerator(string $conversionName): UrlGenerator 129 | { 130 | return UrlGeneratorFactory::createForMedia($this, $conversionName); 131 | } 132 | 133 | public function getAvailableUrl(array $conversionNames): string 134 | { 135 | foreach ($conversionNames as $conversionName) { 136 | if (! $this->hasGeneratedConversion($conversionName)) { 137 | continue; 138 | } 139 | 140 | return $this->getUrl($conversionName); 141 | } 142 | 143 | return $this->getUrl(); 144 | } 145 | 146 | public function getDownloadFilename(): string 147 | { 148 | return $this->file_name; 149 | } 150 | 151 | public function getAvailableFullUrl(array $conversionNames): string 152 | { 153 | foreach ($conversionNames as $conversionName) { 154 | if (! $this->hasGeneratedConversion($conversionName)) { 155 | continue; 156 | } 157 | 158 | return $this->getFullUrl($conversionName); 159 | } 160 | 161 | return $this->getFullUrl(); 162 | } 163 | 164 | public function getAvailablePath(array $conversionNames): string 165 | { 166 | foreach ($conversionNames as $conversionName) { 167 | if (! $this->hasGeneratedConversion($conversionName)) { 168 | continue; 169 | } 170 | 171 | return $this->getPath($conversionName); 172 | } 173 | 174 | return $this->getPath(); 175 | } 176 | 177 | protected function type(): Attribute 178 | { 179 | return Attribute::get( 180 | function () { 181 | $type = $this->getTypeFromExtension(); 182 | 183 | if ($type !== self::TYPE_OTHER) { 184 | return $type; 185 | } 186 | 187 | return $this->getTypeFromMime(); 188 | } 189 | ); 190 | } 191 | 192 | public function getTypeFromExtension(): string 193 | { 194 | $imageGenerator = ImageGeneratorFactory::forExtension($this->extension); 195 | 196 | return $imageGenerator 197 | ? $imageGenerator->getType() 198 | : static::TYPE_OTHER; 199 | } 200 | 201 | public function getTypeFromMime(): string 202 | { 203 | $imageGenerator = ImageGeneratorFactory::forMimeType($this->mime_type); 204 | 205 | return $imageGenerator 206 | ? $imageGenerator->getType() 207 | : static::TYPE_OTHER; 208 | } 209 | 210 | protected function extension(): Attribute 211 | { 212 | return Attribute::get(fn () => pathinfo($this->file_name, PATHINFO_EXTENSION)); 213 | } 214 | 215 | protected function humanReadableSize(): Attribute 216 | { 217 | return Attribute::get(fn () => File::getHumanReadableSize($this->size)); 218 | } 219 | 220 | public function getDiskDriverName(): string 221 | { 222 | return strtolower(config("filesystems.disks.{$this->disk}.driver")); 223 | } 224 | 225 | public function getConversionsDiskDriverName(): string 226 | { 227 | $diskName = $this->conversions_disk ?? $this->disk; 228 | 229 | return strtolower(config("filesystems.disks.{$diskName}.driver")); 230 | } 231 | 232 | public function hasCustomProperty(string $propertyName): bool 233 | { 234 | return Arr::has($this->custom_properties, $propertyName); 235 | } 236 | 237 | /** 238 | * Get the value of custom property with the given name. 239 | * 240 | * @param mixed $default 241 | */ 242 | public function getCustomProperty(string $propertyName, $default = null): mixed 243 | { 244 | return Arr::get($this->custom_properties, $propertyName, $default); 245 | } 246 | 247 | /** 248 | * @param mixed $value 249 | * @return $this 250 | */ 251 | public function setCustomProperty(string $name, $value): self 252 | { 253 | $customProperties = $this->custom_properties; 254 | 255 | Arr::set($customProperties, $name, $value); 256 | 257 | $this->custom_properties = $customProperties; 258 | 259 | return $this; 260 | } 261 | 262 | /** 263 | * @return $this 264 | */ 265 | public function forgetCustomProperty(string $name): self 266 | { 267 | $customProperties = $this->custom_properties; 268 | 269 | Arr::forget($customProperties, $name); 270 | 271 | $this->custom_properties = $customProperties; 272 | 273 | return $this; 274 | } 275 | 276 | public function getMediaConversionNames(): array 277 | { 278 | $conversions = ConversionCollection::createForMedia($this); 279 | 280 | return $conversions->map(fn (Conversion $conversion) => $conversion->getName())->toArray(); 281 | } 282 | 283 | public function getGeneratedConversions(): Collection 284 | { 285 | return collect($this->generated_conversions ?? []); 286 | } 287 | 288 | /** 289 | * @return $this 290 | */ 291 | public function markAsConversionGenerated(string $conversionName): self 292 | { 293 | $generatedConversions = $this->generated_conversions; 294 | 295 | Arr::set($generatedConversions, $conversionName, true); 296 | 297 | $this->generated_conversions = $generatedConversions; 298 | 299 | $this->saveOrTouch(); 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * @return $this 306 | */ 307 | public function markAsConversionNotGenerated(string $conversionName): self 308 | { 309 | $generatedConversions = $this->generated_conversions; 310 | 311 | Arr::set($generatedConversions, $conversionName, false); 312 | 313 | $this->generated_conversions = $generatedConversions; 314 | 315 | $this->saveOrTouch(); 316 | 317 | return $this; 318 | } 319 | 320 | public function hasGeneratedConversion(string $conversionName): bool 321 | { 322 | $generatedConversions = $this->generated_conversions; 323 | 324 | return Arr::get($generatedConversions, $conversionName, false); 325 | } 326 | 327 | /** 328 | * @return $this 329 | */ 330 | public function setStreamChunkSize(int $chunkSize): self 331 | { 332 | $this->streamChunkSize = $chunkSize; 333 | 334 | return $this; 335 | } 336 | 337 | public function toResponse($request): StreamedResponse 338 | { 339 | return $this->buildResponse($request, 'attachment'); 340 | } 341 | 342 | public function toInlineResponse($request): StreamedResponse 343 | { 344 | return $this->buildResponse($request, 'inline'); 345 | } 346 | 347 | private function buildResponse($request, string $contentDispositionType): StreamedResponse 348 | { 349 | $filename = str_replace('"', '\'', Str::ascii($this->getDownloadFilename())); 350 | 351 | $downloadHeaders = [ 352 | 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 353 | 'Content-Type' => $this->mime_type, 354 | 'Content-Length' => $this->size, 355 | 'Content-Disposition' => $contentDispositionType.'; filename="'.$filename.'"', 356 | 'Pragma' => 'public', 357 | ]; 358 | 359 | return response()->stream(function () { 360 | $stream = $this->stream(); 361 | 362 | while (! feof($stream)) { 363 | echo fread($stream, $this->streamChunkSize); 364 | flush(); 365 | } 366 | 367 | if (is_resource($stream)) { 368 | fclose($stream); 369 | } 370 | }, 200, $downloadHeaders); 371 | } 372 | 373 | public function getResponsiveImageUrls(string $conversionName = ''): array 374 | { 375 | return $this->responsiveImages($conversionName)->getUrls(); 376 | } 377 | 378 | public function hasResponsiveImages(string $conversionName = ''): bool 379 | { 380 | return count($this->getResponsiveImageUrls($conversionName)) > 0; 381 | } 382 | 383 | public function getSrcset(string $conversionName = ''): string 384 | { 385 | return $this->responsiveImages($conversionName)->getSrcset(); 386 | } 387 | 388 | protected function previewUrl(): Attribute 389 | { 390 | return Attribute::get( 391 | fn () => $this->hasGeneratedConversion('preview') ? $this->getUrl('preview') : '', 392 | ); 393 | } 394 | 395 | protected function originalUrl(): Attribute 396 | { 397 | return Attribute::get(fn () => $this->getUrl()); 398 | } 399 | 400 | /** @param string $collectionName */ 401 | public function move(HasMedia $model, $collectionName = 'default', string $diskName = '', string $fileName = ''): self 402 | { 403 | $newMedia = $this->copy($model, $collectionName, $diskName, $fileName); 404 | 405 | $this->forceDelete(); 406 | 407 | return $newMedia; 408 | } 409 | 410 | /** 411 | * @param null|Closure(FileAdder): FileAdder $fileAdderCallback 412 | */ 413 | public function copy( 414 | HasMedia $model, 415 | string $collectionName = 'default', 416 | string $diskName = '', 417 | string $fileName = '', 418 | ?Closure $fileAdderCallback = null 419 | ): self { 420 | $temporaryDirectory = TemporaryDirectory::create(); 421 | 422 | $temporaryFile = $temporaryDirectory->path('/').DIRECTORY_SEPARATOR.$this->file_name; 423 | 424 | /** @var Filesystem $filesystem */ 425 | $filesystem = app(Filesystem::class); 426 | 427 | $filesystem->copyFromMediaLibrary($this, $temporaryFile); 428 | 429 | $fileAdder = $model 430 | ->addMedia($temporaryFile) 431 | ->usingName($this->name) 432 | ->setOrder($this->order_column) 433 | ->withManipulations($this->manipulations) 434 | ->withCustomProperties($this->custom_properties); 435 | 436 | if ($fileName !== '') { 437 | $fileAdder->usingFileName($fileName); 438 | } 439 | 440 | if ($fileAdderCallback instanceof Closure) { 441 | $fileAdder = $fileAdderCallback($fileAdder); 442 | } 443 | 444 | $newMedia = $fileAdder->toMediaCollection($collectionName, $diskName); 445 | 446 | $temporaryDirectory->delete(); 447 | 448 | return $newMedia; 449 | } 450 | 451 | public function responsiveImages(string $conversionName = ''): RegisteredResponsiveImages 452 | { 453 | return new RegisteredResponsiveImages($this, $conversionName); 454 | } 455 | 456 | public function stream() 457 | { 458 | /** @var Filesystem $filesystem */ 459 | $filesystem = app(Filesystem::class); 460 | 461 | return $filesystem->getStream($this); 462 | } 463 | 464 | public function toHtml(): string 465 | { 466 | return $this->img()->toHtml(); 467 | } 468 | 469 | public function img(string $conversionName = '', $extraAttributes = []): HtmlableMedia 470 | { 471 | return (new HtmlableMedia($this)) 472 | ->conversion($conversionName) 473 | ->attributes($extraAttributes); 474 | } 475 | 476 | public function __invoke(...$arguments): HtmlableMedia 477 | { 478 | return $this->img(...$arguments); 479 | } 480 | 481 | public function temporaryUpload(): BelongsTo 482 | { 483 | MediaLibraryPro::ensureInstalled(); 484 | 485 | /** @var class-string $temporaryUploadModelClass */ 486 | $temporaryUploadModelClass = config('media-library.temporary_upload_model'); 487 | 488 | return $this->belongsTo($temporaryUploadModelClass); 489 | } 490 | 491 | public static function findWithTemporaryUploadInCurrentSession(array $uuids): EloquentCollection 492 | { 493 | MediaLibraryPro::ensureInstalled(); 494 | 495 | /** @var class-string $temporaryUploadModelClass */ 496 | $temporaryUploadModelClass = config('media-library.temporary_upload_model'); 497 | 498 | return static::query() 499 | ->whereIn('uuid', $uuids) 500 | ->whereHasMorph( 501 | 'model', 502 | [$temporaryUploadModelClass], 503 | fn (Builder $builder) => $builder->where('session_id', session()->getId()) 504 | ) 505 | ->get(); 506 | } 507 | 508 | public function mailAttachment(string $conversion = ''): Attachment 509 | { 510 | $attachment = Attachment::fromStorageDisk($this->disk, $this->getPathRelativeToRoot($conversion))->as($this->file_name); 511 | 512 | if ($this->mime_type) { 513 | $attachment->withMime($this->mime_type); 514 | } 515 | 516 | return $attachment; 517 | } 518 | 519 | public function toMailAttachment(): Attachment 520 | { 521 | return $this->mailAttachment(); 522 | } 523 | 524 | protected function saveOrTouch(): bool 525 | { 526 | if (! $this->exists || $this->isDirty()) { 527 | return $this->save(); 528 | } 529 | 530 | return $this->touch(); 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/MediaCollections/Models/Observers/MediaObserver.php: -------------------------------------------------------------------------------- 1 | shouldSortWhenCreating()) { 14 | if (is_null($media->order_column)) { 15 | $media->setHighestOrderNumber(); 16 | } 17 | } 18 | } 19 | 20 | public function updating(Media $media): void 21 | { 22 | /** @var Filesystem $filesystem */ 23 | $filesystem = app(Filesystem::class); 24 | 25 | if (config('media-library.moves_media_on_update')) { 26 | $filesystem->syncMediaPath($media); 27 | } 28 | 29 | if ($media->file_name !== $media->getOriginal('file_name')) { 30 | $filesystem->syncFileNames($media); 31 | } 32 | } 33 | 34 | public function updated(Media $media): void 35 | { 36 | if (is_null($media->getOriginal('model_id'))) { 37 | return; 38 | } 39 | 40 | $original = $media->getOriginal('manipulations'); 41 | 42 | if ($media->manipulations !== $original) { 43 | $eventDispatcher = Media::getEventDispatcher(); 44 | Media::unsetEventDispatcher(); 45 | 46 | /** @var FileManipulator $fileManipulator */ 47 | $fileManipulator = app(FileManipulator::class); 48 | 49 | $fileManipulator->createDerivedFiles($media); 50 | 51 | Media::setEventDispatcher($eventDispatcher); 52 | } 53 | } 54 | 55 | public function deleted(Media $media): void 56 | { 57 | if (method_exists($media, 'isForceDeleting') && ! $media->isForceDeleting()) { 58 | return; 59 | } 60 | 61 | /** @var Filesystem $filesystem */ 62 | $filesystem = app(Filesystem::class); 63 | 64 | $filesystem->removeAllFiles($media); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MediaLibraryServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-medialibrary') 22 | ->hasConfigFile('media-library') 23 | ->hasMigration('create_media_table') 24 | ->hasViews('media-library') 25 | ->hasCommands([ 26 | RegenerateCommand::class, 27 | ClearCommand::class, 28 | CleanCommand::class, 29 | ]); 30 | } 31 | 32 | public function packageBooted(): void 33 | { 34 | $mediaClass = config('media-library.media_model', Media::class); 35 | $mediaObserverClass = config('media-library.media_observer', MediaObserver::class); 36 | 37 | $mediaClass::observe(new $mediaObserverClass); 38 | } 39 | 40 | public function packageRegistered(): void 41 | { 42 | $this->app->bind(WidthCalculator::class, config('media-library.responsive_images.width_calculator')); 43 | $this->app->bind(TinyPlaceholderGenerator::class, config('media-library.responsive_images.tiny_placeholder_generator')); 44 | 45 | $this->app->scoped(MediaRepository::class, function () { 46 | $mediaClass = config('media-library.media_model'); 47 | 48 | return new MediaRepository(new $mediaClass); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ResponsiveImages/Events/ResponsiveImagesGeneratedEvent.php: -------------------------------------------------------------------------------- 1 | generateResponsiveImages($this->media); 26 | 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ResponsiveImages/RegisteredResponsiveImages.php: -------------------------------------------------------------------------------- 1 | generatedFor = $conversionName === '' 17 | ? 'media_library_original' 18 | : $conversionName; 19 | 20 | $this->files = collect($media->responsive_images[$this->generatedFor]['urls'] ?? []) 21 | ->map(fn (string $fileName) => new ResponsiveImage($fileName, $media)) 22 | ->filter(fn (ResponsiveImage $responsiveImage) => $responsiveImage->generatedFor() === $this->generatedFor); 23 | } 24 | 25 | public function getUrls(): array 26 | { 27 | return $this->files 28 | ->map(fn (ResponsiveImage $responsiveImage) => $responsiveImage->url()) 29 | ->values() 30 | ->toArray(); 31 | } 32 | 33 | public function getFilenames(): array 34 | { 35 | return $this->files->pluck('fileName')->toArray(); 36 | } 37 | 38 | public function getSrcset(): string 39 | { 40 | $filesSrcset = $this->files 41 | ->map(fn (ResponsiveImage $responsiveImage) => "{$responsiveImage->url()} {$responsiveImage->width()}w") 42 | ->implode(', '); 43 | 44 | $shouldAddPlaceholderSvg = config('media-library.responsive_images.use_tiny_placeholders') 45 | && $this->getPlaceholderSvg(); 46 | 47 | if ($shouldAddPlaceholderSvg) { 48 | $filesSrcset .= ', '.$this->getPlaceholderSvg().' 32w'; 49 | } 50 | 51 | return $filesSrcset; 52 | } 53 | 54 | public function getPlaceholderSvg(): ?string 55 | { 56 | return $this->media->responsive_images[$this->generatedFor]['base64svg'] ?? null; 57 | } 58 | 59 | public function delete(): void 60 | { 61 | $this->files->each->delete(); 62 | 63 | $responsiveImages = $this->media->responsive_images; 64 | 65 | unset($responsiveImages[$this->generatedFor]); 66 | 67 | $this->media->responsive_images = $responsiveImages; 68 | 69 | $this->media->save(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ResponsiveImages/ResponsiveImage.php: -------------------------------------------------------------------------------- 1 | responsive_images; 15 | 16 | $responsiveImages[$conversionName]['urls'][] = $fileName; 17 | 18 | $media->responsive_images = $responsiveImages; 19 | 20 | $media->save(); 21 | } 22 | 23 | public static function registerTinySvg(Media $media, string $base64Svg, string $conversionName): void 24 | { 25 | $responsiveImages = $media->responsive_images; 26 | 27 | $responsiveImages[$conversionName]['base64svg'] = $base64Svg; 28 | 29 | $media->responsive_images = $responsiveImages; 30 | 31 | $media->save(); 32 | } 33 | 34 | public function __construct(public string $fileName, public Media $media) {} 35 | 36 | public function url(): string 37 | { 38 | $conversionName = ''; 39 | 40 | if ($this->generatedFor() !== 'media_library_original') { 41 | $conversionName = $this->generatedFor(); 42 | } 43 | 44 | $urlGenerator = UrlGeneratorFactory::createForMedia($this->media, $conversionName); 45 | 46 | $url = $urlGenerator->getResponsiveImagesDirectoryUrl().rawurlencode($this->fileName); 47 | 48 | if (config('media-library.version_urls') === true) { 49 | $url = "{$url}?v={$this->media->updated_at->timestamp}"; 50 | } 51 | 52 | return $url; 53 | } 54 | 55 | public function generatedFor(): string 56 | { 57 | $propertyParts = $this->getPropertyParts(); 58 | 59 | array_pop($propertyParts); 60 | 61 | array_pop($propertyParts); 62 | 63 | return implode('_', $propertyParts); 64 | } 65 | 66 | public function width(): int 67 | { 68 | $propertyParts = $this->getPropertyParts(); 69 | 70 | array_pop($propertyParts); 71 | 72 | return (int) last($propertyParts); 73 | } 74 | 75 | public function height(): int 76 | { 77 | $propertyParts = $this->getPropertyParts(); 78 | 79 | return (int) last($propertyParts); 80 | } 81 | 82 | protected function getPropertyParts(): array 83 | { 84 | $propertyString = $this->stringBetween($this->fileName, '___', '.'); 85 | 86 | return explode('_', $propertyString); 87 | } 88 | 89 | protected function stringBetween(string $subject, string $startCharacter, string $endCharacter): string 90 | { 91 | $lastPos = strrpos($subject, $startCharacter); 92 | 93 | $between = substr($subject, $lastPos); 94 | 95 | $between = str_replace('___', '', $between); 96 | 97 | $between = strstr($between, $endCharacter, true); 98 | 99 | return $between; 100 | } 101 | 102 | /** 103 | * @return $this 104 | */ 105 | public function delete(): self 106 | { 107 | $pathGenerator = PathGeneratorFactory::create($this->media); 108 | 109 | $path = $pathGenerator->getPathForResponsiveImages($this->media); 110 | 111 | $fullPath = $path.$this->fileName; 112 | 113 | app(Filesystem::class)->removeFile($this->media, $fullPath); 114 | 115 | $responsiveImages = $this->media->responsive_images; 116 | 117 | unset($responsiveImages[$this->generatedFor()]); 118 | 119 | $this->media->responsive_images = $responsiveImages; 120 | 121 | $this->media->save(); 122 | 123 | return $this; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ResponsiveImages/ResponsiveImageGenerator.php: -------------------------------------------------------------------------------- 1 | fileNamer = app(config('media-library.file_namer')); 31 | } 32 | 33 | public function generateResponsiveImages(Media $media): void 34 | { 35 | $temporaryDirectory = TemporaryDirectory::create(); 36 | 37 | $baseImage = app(Filesystem::class)->copyFromMediaLibrary( 38 | $media, 39 | $temporaryDirectory->path(Str::random(16).'.'.$media->extension) 40 | ); 41 | 42 | $media = $this->cleanResponsiveImages($media); 43 | 44 | foreach ($this->widthCalculator->calculateWidthsFromFile($baseImage) as $width) { 45 | $this->generateResponsiveImage($media, $baseImage, 'media_library_original', $width, $temporaryDirectory); 46 | } 47 | 48 | event(new ResponsiveImagesGeneratedEvent($media)); 49 | 50 | $this->generateTinyJpg($media, $baseImage, 'media_library_original', $temporaryDirectory); 51 | 52 | $temporaryDirectory->delete(); 53 | } 54 | 55 | public function generateResponsiveImagesForConversion(Media $media, Conversion $conversion, string $baseImage): void 56 | { 57 | $temporaryDirectory = TemporaryDirectory::create(); 58 | 59 | $media = $this->cleanResponsiveImages($media, $conversion->getName()); 60 | 61 | $widthCalculator = $conversion->getWidthCalculator() ?? $this->widthCalculator; 62 | 63 | foreach ($widthCalculator->calculateWidthsFromFile($baseImage) as $width) { 64 | $this->generateResponsiveImage($media, $baseImage, $conversion->getName(), $width, $temporaryDirectory, $this->getConversionQuality($conversion)); 65 | } 66 | 67 | $this->generateTinyJpg($media, $baseImage, $conversion->getName(), $temporaryDirectory); 68 | 69 | $temporaryDirectory->delete(); 70 | } 71 | 72 | private function getConversionQuality(Conversion $conversion): int 73 | { 74 | return $conversion->getManipulations()->getFirstManipulationArgument('quality') ?: self::DEFAULT_CONVERSION_QUALITY; 75 | } 76 | 77 | public function generateResponsiveImage( 78 | Media $media, 79 | string $baseImage, 80 | string $conversionName, 81 | int $targetWidth, 82 | BaseTemporaryDirectory $temporaryDirectory, 83 | int $conversionQuality = self::DEFAULT_CONVERSION_QUALITY 84 | ): void { 85 | $extension = $this->fileNamer->extensionFromBaseImage($baseImage); 86 | $responsiveImagePath = $this->fileNamer->temporaryFileName($media, $extension); 87 | 88 | $tempDestination = $temporaryDirectory->path($responsiveImagePath); 89 | 90 | ImageFactory::load($baseImage) 91 | ->optimize() 92 | ->width($targetWidth) 93 | ->quality($conversionQuality) 94 | ->save($tempDestination); 95 | 96 | $responsiveImageHeight = ImageFactory::load($tempDestination)->getHeight(); 97 | 98 | // Users can customize the name like they want, but we expect the last part in a certain format 99 | $fileName = $this->addPropertiesToFileName( 100 | $responsiveImagePath, 101 | $conversionName, 102 | $targetWidth, 103 | $responsiveImageHeight, 104 | $extension 105 | ); 106 | 107 | $responsiveImagePath = $temporaryDirectory->path($fileName); 108 | 109 | rename($tempDestination, $responsiveImagePath); 110 | 111 | $this->filesystem->copyToMediaLibrary($responsiveImagePath, $media, 'responsiveImages'); 112 | 113 | ResponsiveImage::register($media, $fileName, $conversionName); 114 | } 115 | 116 | public function generateTinyJpg( 117 | Media $media, 118 | string $originalImagePath, 119 | string $conversionName, 120 | BaseTemporaryDirectory $temporaryDirectory 121 | ): void { 122 | if (! config('media-library.responsive_images.use_tiny_placeholders')) { 123 | return; 124 | } 125 | 126 | $tempDestination = $temporaryDirectory->path('tiny.jpg'); 127 | 128 | $this->tinyPlaceholderGenerator->generateTinyPlaceholder($originalImagePath, $tempDestination); 129 | 130 | $this->guardAgainstInvalidTinyPlaceHolder($tempDestination); 131 | 132 | $tinyImageDataBase64 = base64_encode(file_get_contents($tempDestination)); 133 | 134 | $tinyImageBase64 = 'data:image/jpeg;base64,'.$tinyImageDataBase64; 135 | 136 | $originalImage = ImageFactory::load($originalImagePath); 137 | 138 | $originalImageWidth = $originalImage->getWidth(); 139 | 140 | $originalImageHeight = $originalImage->getHeight(); 141 | 142 | $svg = view('media-library::placeholderSvg', compact( 143 | 'originalImageWidth', 144 | 'originalImageHeight', 145 | 'tinyImageBase64' 146 | )); 147 | 148 | $base64Svg = 'data:image/svg+xml;base64,'.base64_encode($svg); 149 | 150 | ResponsiveImage::registerTinySvg($media, $base64Svg, $conversionName); 151 | } 152 | 153 | protected function appendToFileName(string $filePath, string $suffix, ?string $extensionFilePath = null): string 154 | { 155 | $baseName = pathinfo($filePath, PATHINFO_FILENAME); 156 | 157 | $extension = pathinfo($extensionFilePath ?? $filePath, PATHINFO_EXTENSION); 158 | 159 | return "{$baseName}{$suffix}.{$extension}"; 160 | } 161 | 162 | protected function guardAgainstInvalidTinyPlaceHolder(string $tinyPlaceholderPath): void 163 | { 164 | if (! file_exists($tinyPlaceholderPath)) { 165 | throw InvalidTinyJpg::doesNotExist($tinyPlaceholderPath); 166 | } 167 | 168 | if (File::getMimeType($tinyPlaceholderPath) !== 'image/jpeg') { 169 | throw InvalidTinyJpg::hasWrongMimeType($tinyPlaceholderPath); 170 | } 171 | } 172 | 173 | protected function cleanResponsiveImages(Media $media, string $conversionName = 'media_library_original'): Media 174 | { 175 | $responsiveImages = $media->responsive_images; 176 | $responsiveImages[$conversionName]['urls'] = []; 177 | $media->responsive_images = $responsiveImages; 178 | 179 | $this->filesystem->removeResponsiveImages($media, $conversionName); 180 | 181 | return $media; 182 | } 183 | 184 | protected function addPropertiesToFileName(string $fileName, string $conversionName, int $width, int $height, string $extension): string 185 | { 186 | $fileName = pathinfo($fileName, PATHINFO_FILENAME); 187 | 188 | return "{$fileName}___{$conversionName}_{$width}_{$height}.{$extension}"; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ResponsiveImages/TinyPlaceholderGenerator/Blurred.php: -------------------------------------------------------------------------------- 1 | width(32)->blur(5)->save($tinyImageDestinationPath); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ResponsiveImages/TinyPlaceholderGenerator/TinyPlaceholderGenerator.php: -------------------------------------------------------------------------------- 1 | getWidth(); 15 | $height = $image->getHeight(); 16 | $fileSize = filesize($imagePath); 17 | 18 | return $this->calculateWidths($fileSize, $width, $height); 19 | } 20 | 21 | public function calculateWidths(int $fileSize, int $width, int $height): Collection 22 | { 23 | $targetWidths = collect(); 24 | 25 | $targetWidths->push($width); 26 | 27 | $ratio = $height / $width; 28 | $area = $height * $width; 29 | 30 | $predictedFileSize = $fileSize; 31 | $pixelPrice = $predictedFileSize / $area; 32 | 33 | while (true) { 34 | $predictedFileSize *= 0.7; 35 | 36 | $newWidth = (int) floor(sqrt(($predictedFileSize / $pixelPrice) / $ratio)); 37 | 38 | if ($this->finishedCalculating((int) $predictedFileSize, $newWidth)) { 39 | return $targetWidths; 40 | } 41 | 42 | $targetWidths->push($newWidth); 43 | } 44 | } 45 | 46 | protected function finishedCalculating(int $predictedFileSize, int $newWidth): bool 47 | { 48 | if ($newWidth < 20) { 49 | return true; 50 | } 51 | 52 | if ($predictedFileSize < (1024 * 10)) { 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ResponsiveImages/WidthCalculator/WidthCalculator.php: -------------------------------------------------------------------------------- 1 | guessMimeType($path); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Support/FileNamer/DefaultFileNamer.php: -------------------------------------------------------------------------------- 1 | getName()}"; 14 | } 15 | 16 | public function responsiveFileName(string $fileName): string 17 | { 18 | return pathinfo($fileName, PATHINFO_FILENAME); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Support/FileNamer/FileNamer.php: -------------------------------------------------------------------------------- 1 | responsiveFileName($media->file_name)}.{$extension}"; 26 | } 27 | 28 | public function extensionFromBaseImage(string $baseImage): string 29 | { 30 | return pathinfo($baseImage, PATHINFO_EXTENSION); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Support/FileRemover/DefaultFileRemover.php: -------------------------------------------------------------------------------- 1 | conversions_disk && $media->disk !== $media->conversions_disk) { 21 | $this->removeFromConversionsDirectory($media, $media->conversions_disk); 22 | $this->removeFromResponsiveImagesDirectory($media, $media->conversions_disk); 23 | $this->removeFromMediaDirectory($media, $media->conversions_disk); 24 | } 25 | 26 | $this->removeFromConversionsDirectory($media, $media->disk); 27 | $this->removeFromResponsiveImagesDirectory($media, $media->disk); 28 | $this->removeFromMediaDirectory($media, $media->disk); 29 | } 30 | 31 | public function removeFromMediaDirectory(Media $media, string $disk): void 32 | { 33 | $mediaDirectory = $this->mediaFileSystem->getMediaDirectory($media); 34 | 35 | collect([$mediaDirectory]) 36 | ->each(function (string $directory) use ($media, $disk) { 37 | try { 38 | $allFilePaths = $this->filesystem->disk($disk)->allFiles($directory); 39 | $imagePaths = array_filter( 40 | $allFilePaths, 41 | static fn (string $path) => Str::afterLast($path, '/') === $media->file_name 42 | ); 43 | foreach ($imagePaths as $imagePath) { 44 | $this->filesystem->disk($disk)->delete($imagePath); 45 | } 46 | 47 | if (! $this->filesystem->disk($disk)->allFiles($directory)) { 48 | $this->filesystem->disk($disk)->deleteDirectory($directory); 49 | } 50 | } catch (Exception $exception) { 51 | report($exception); 52 | } 53 | }); 54 | 55 | } 56 | 57 | public function removeFromConversionsDirectory(Media $media, string $disk): void 58 | { 59 | $conversionsDirectory = $this->mediaFileSystem->getMediaDirectory($media, 'conversions'); 60 | 61 | collect([$conversionsDirectory]) 62 | ->each(function (string $directory) use ($media, $disk) { 63 | try { 64 | $allFilePaths = $this->filesystem->disk($disk)->allFiles($directory); 65 | $conversions = $media->getMediaConversionNames() ?: []; 66 | $conversionsFilePaths = array_map( 67 | static fn (string $conversion) => $media->getPathRelativeToRoot($conversion), 68 | $conversions, 69 | ); 70 | $imagePaths = array_intersect($allFilePaths, $conversionsFilePaths); 71 | foreach ($imagePaths as $imagePath) { 72 | $this->filesystem->disk($disk)->delete($imagePath); 73 | } 74 | 75 | if (! $this->filesystem->disk($disk)->allFiles($directory)) { 76 | $this->filesystem->disk($disk)->deleteDirectory($directory); 77 | } 78 | } catch (Exception $exception) { 79 | report($exception); 80 | } 81 | }); 82 | } 83 | 84 | public function removeFromResponsiveImagesDirectory(Media $media, string $disk): void 85 | { 86 | $responsiveImagesDirectory = $this->mediaFileSystem->getMediaDirectory($media, 'responsiveImages'); 87 | $mediaRoot = PathGeneratorFactory::create($media)->getPathForResponsiveImages($media); 88 | /** @var FileNamer $fileNamer */ 89 | $fileNamer = app(config('media-library.file_namer')); 90 | $mediaFilename = $fileNamer->responsiveFileName($media->file_name); 91 | 92 | collect([$responsiveImagesDirectory]) 93 | ->unique() 94 | ->each(function (string $directory) use ($media, $disk, $mediaRoot, $mediaFilename) { 95 | try { 96 | $allFilePaths = $this->filesystem->disk($disk)->allFiles($directory); 97 | 98 | $conversions = $media->getMediaConversionNames() ?: []; 99 | $responsiveImagesFilePaths = collect($conversions) 100 | ->flatMap(static fn (string $conversion) => $media->responsiveImages($conversion)->getFilenames()) 101 | ->map(static fn (string $imagePath) => $mediaRoot.$imagePath) 102 | ->toArray(); 103 | 104 | $imagePaths = array_merge( 105 | array_intersect($allFilePaths, $responsiveImagesFilePaths), 106 | array_filter( 107 | $allFilePaths, 108 | static fn (string $path) => Str::startsWith($path, $mediaRoot.$mediaFilename.'___media_library_original_'), 109 | ), 110 | ); 111 | 112 | foreach ($imagePaths as $imagePath) { 113 | $this->filesystem->disk($disk)->delete($imagePath); 114 | } 115 | 116 | if (! $this->filesystem->disk($disk)->allFiles($directory)) { 117 | $this->filesystem->disk($disk)->deleteDirectory($directory); 118 | } 119 | } catch (Exception $exception) { 120 | report($exception); 121 | } 122 | }); 123 | } 124 | 125 | public function removeResponsiveImages(Media $media, string $conversionName): void 126 | { 127 | $responsiveImagesDirectory = $this->mediaFileSystem->getResponsiveImagesDirectory($media); 128 | 129 | $allFilePaths = $this->filesystem->disk($media->disk)->allFiles($responsiveImagesDirectory); 130 | 131 | $responsiveImagePaths = array_filter( 132 | $allFilePaths, 133 | fn (string $path) => Str::contains($path, $conversionName) 134 | ); 135 | 136 | $this->filesystem->disk($media->disk)->delete($responsiveImagePaths); 137 | } 138 | 139 | public function removeFile(string $path, string $disk): void 140 | { 141 | $this->filesystem->disk($disk)->delete($path); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Support/FileRemover/FileBaseFileRemover.php: -------------------------------------------------------------------------------- 1 | removeFile($this->mediaFileSystem->getMediaDirectory($media).$media->file_name, $media->disk); 12 | 13 | $this->removeConvertedImages($media); 14 | } 15 | 16 | public function removeConvertedImages(Media $media): void 17 | { 18 | collect($media->getMediaConversionNames())->each(function ($conversionName) use ($media) { 19 | $this->removeFile( 20 | path: $media->getPathRelativeToRoot($conversionName), 21 | disk: $media->conversions_disk 22 | ); 23 | 24 | $this->mediaFileSystem->removeResponsiveImages($media, $conversionName); 25 | }); 26 | } 27 | 28 | public function removeFile(string $path, string $disk): void 29 | { 30 | $this->filesystem->disk($disk)->delete($path); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Support/FileRemover/FileRemover.php: -------------------------------------------------------------------------------- 1 | loadFile($path); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Support/MediaLibraryPro.php: -------------------------------------------------------------------------------- 1 | mediaItems = collect(); 25 | 26 | $this->zipOptions = []; 27 | } 28 | 29 | /** 30 | * @return $this 31 | */ 32 | public function useZipOptions(callable $zipOptionsCallable): self 33 | { 34 | $zipOptionsCallable($this->zipOptions); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @return $this 41 | */ 42 | public function addMedia(...$mediaItems): self 43 | { 44 | collect($mediaItems) 45 | ->flatMap(function ($item) { 46 | if ($item instanceof Media) { 47 | return [$item]; 48 | } 49 | 50 | if ($item instanceof Collection) { 51 | return $item->reduce(function (array $carry, Media $media) { 52 | $carry[] = $media; 53 | 54 | return $carry; 55 | }, []); 56 | } 57 | 58 | return $item; 59 | }) 60 | ->each(fn (Media $media) => $this->mediaItems->push($media)); 61 | 62 | return $this; 63 | } 64 | 65 | public function getMediaItems(): Collection 66 | { 67 | return $this->mediaItems; 68 | } 69 | 70 | public function toResponse($request): StreamedResponse 71 | { 72 | $headers = [ 73 | 'Content-Disposition' => "attachment; filename=\"{$this->zipName}\"", 74 | 'Content-Type' => 'application/octet-stream', 75 | ]; 76 | 77 | return new StreamedResponse(fn () => $this->getZipStream(), 200, $headers); 78 | } 79 | 80 | public function getZipStream(): ZipStream 81 | { 82 | $this->zipOptions['outputName'] = $this->zipName; 83 | $zip = new ZipStream(...$this->zipOptions); 84 | 85 | $this->getZipStreamContents()->each(function (array $mediaInZip) use ($zip) { 86 | $stream = $mediaInZip['media']->stream(); 87 | 88 | $zip->addFileFromStream($mediaInZip['fileNameInZip'], $stream); 89 | 90 | if (is_resource($stream)) { 91 | fclose($stream); 92 | } 93 | }); 94 | 95 | $zip->finish(); 96 | 97 | return $zip; 98 | } 99 | 100 | protected function getZipStreamContents(): Collection 101 | { 102 | 103 | return $this->mediaItems->map(fn (Media $media, $mediaItemIndex) => [ 104 | 'fileNameInZip' => $this->getZipFileNamePrefix($this->mediaItems, $mediaItemIndex).$this->getFileNameWithSuffix($this->mediaItems, $mediaItemIndex), 105 | 'media' => $media, 106 | ]); 107 | } 108 | 109 | protected function getFileNameWithSuffix(Collection $mediaItems, int $currentIndex): string 110 | { 111 | $fileNameCount = 0; 112 | 113 | $fileName = $mediaItems[$currentIndex]->getDownloadFilename(); 114 | 115 | foreach ($mediaItems as $index => $media) { 116 | if ($index >= $currentIndex) { 117 | break; 118 | } 119 | 120 | if ($this->getZipFileNamePrefix($mediaItems, $index).$media->getDownloadFilename() === $this->getZipFileNamePrefix($mediaItems, $currentIndex).$fileName) { 121 | $fileNameCount++; 122 | } 123 | } 124 | 125 | if ($fileNameCount === 0) { 126 | return $fileName; 127 | } 128 | 129 | $extension = pathinfo($fileName, PATHINFO_EXTENSION); 130 | $fileNameWithoutExtension = pathinfo($fileName, PATHINFO_FILENAME); 131 | 132 | return "{$fileNameWithoutExtension} ({$fileNameCount}).{$extension}"; 133 | } 134 | 135 | protected function getZipFileNamePrefix(Collection $mediaItems, int $currentIndex): string 136 | { 137 | return $mediaItems[$currentIndex]->hasCustomProperty('zip_filename_prefix') ? $mediaItems[$currentIndex]->getCustomProperty('zip_filename_prefix') : ''; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Support/PathGenerator/DefaultPathGenerator.php: -------------------------------------------------------------------------------- 1 | getBasePath($media).'/'; 15 | } 16 | 17 | /* 18 | * Get the path for conversions of the given media, relative to the root storage path. 19 | */ 20 | public function getPathForConversions(Media $media): string 21 | { 22 | return $this->getBasePath($media).'/conversions/'; 23 | } 24 | 25 | /* 26 | * Get the path for responsive images of the given media, relative to the root storage path. 27 | */ 28 | public function getPathForResponsiveImages(Media $media): string 29 | { 30 | return $this->getBasePath($media).'/responsive-images/'; 31 | } 32 | 33 | /* 34 | * Get a unique base path for the given media. 35 | */ 36 | protected function getBasePath(Media $media): string 37 | { 38 | $prefix = config('media-library.prefix', ''); 39 | 40 | if ($prefix !== '') { 41 | return $prefix.'/'.$media->getKey(); 42 | } 43 | 44 | return $media->getKey(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Support/PathGenerator/PathGenerator.php: -------------------------------------------------------------------------------- 1 | $customPathGeneratorClass) { 25 | if (static::mediaBelongToModelClass($media, $modelClass)) { 26 | return $customPathGeneratorClass; 27 | } 28 | } 29 | 30 | return $defaultPathGeneratorClass; 31 | } 32 | 33 | protected static function mediaBelongToModelClass(Media $media, string $modelClass): bool 34 | { 35 | // model doesn't have morphMap, so morph type and class are equal 36 | if (is_a($media->model_type, $modelClass, true)) { 37 | return true; 38 | } 39 | // config is set via morphMap alias 40 | if ($media->model_type === $modelClass) { 41 | return true; 42 | } 43 | // config is set via morphMap class name 44 | if (is_a((string) Relation::getMorphedModel($media->model_type), $modelClass, true)) { 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | protected static function guardAgainstInvalidPathGenerator(string $pathGeneratorClass): void 52 | { 53 | if (! class_exists($pathGeneratorClass)) { 54 | throw InvalidPathGenerator::doesntExist($pathGeneratorClass); 55 | } 56 | 57 | if (! is_subclass_of($pathGeneratorClass, PathGenerator::class)) { 58 | throw InvalidPathGenerator::doesNotImplementPathGenerator($pathGeneratorClass); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Support/RemoteFile.php: -------------------------------------------------------------------------------- 1 | key; 12 | } 13 | 14 | public function getDisk(): string 15 | { 16 | return $this->disk; 17 | } 18 | 19 | public function getFilename(): string 20 | { 21 | return basename($this->key); 22 | } 23 | 24 | public function getName(): string 25 | { 26 | return pathinfo($this->getFilename(), PATHINFO_FILENAME); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/TemporaryDirectory.php: -------------------------------------------------------------------------------- 1 | media = $media; 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * @return $this 34 | */ 35 | public function setConversion(Conversion $conversion): UrlGenerator 36 | { 37 | $this->conversion = $conversion; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * @return $this 44 | */ 45 | public function setPathGenerator(PathGenerator $pathGenerator): UrlGenerator 46 | { 47 | $this->pathGenerator = $pathGenerator; 48 | 49 | return $this; 50 | } 51 | 52 | public function getPathRelativeToRoot(): string 53 | { 54 | if (is_null($this->conversion)) { 55 | return $this->pathGenerator->getPath($this->media).($this->media->file_name); 56 | } 57 | 58 | return $this->pathGenerator->getPathForConversions($this->media) 59 | .$this->conversion->getConversionFile($this->media); 60 | } 61 | 62 | protected function getDiskName(): string 63 | { 64 | return $this->conversion === null 65 | ? $this->media->disk 66 | : $this->media->conversions_disk; 67 | } 68 | 69 | protected function getDisk(): Filesystem 70 | { 71 | return Storage::disk($this->getDiskName()); 72 | } 73 | 74 | public function versionUrl(string $path = ''): string 75 | { 76 | if (! $this->config->get('media-library.version_urls')) { 77 | return $path; 78 | } 79 | 80 | return "{$path}?v={$this->media->updated_at->timestamp}"; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Support/UrlGenerator/DefaultUrlGenerator.php: -------------------------------------------------------------------------------- 1 | getDisk()->url($this->getPathRelativeToRoot()); 13 | 14 | return $this->versionUrl($url); 15 | } 16 | 17 | public function getTemporaryUrl(DateTimeInterface $expiration, array $options = []): string 18 | { 19 | return $this->getDisk()->temporaryUrl($this->getPathRelativeToRoot(), $expiration, $options); 20 | } 21 | 22 | public function getBaseMediaDirectoryUrl(): string 23 | { 24 | return $this->getDisk()->url('/'); 25 | } 26 | 27 | public function getPath(): string 28 | { 29 | return $this->getRootOfDisk().$this->getPathRelativeToRoot(); 30 | } 31 | 32 | public function getResponsiveImagesDirectoryUrl(): string 33 | { 34 | $path = $this->pathGenerator->getPathForResponsiveImages($this->media); 35 | 36 | return Str::finish($this->getDisk()->url($path), '/'); 37 | } 38 | 39 | protected function getRootOfDisk(): string 40 | { 41 | return $this->getDisk()->path('/'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Support/UrlGenerator/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | $options 24 | */ 25 | public function getTemporaryUrl(DateTimeInterface $expiration, array $options = []): string; 26 | 27 | public function getResponsiveImagesDirectoryUrl(): string; 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/UrlGenerator/UrlGeneratorFactory.php: -------------------------------------------------------------------------------- 1 | setMedia($media) 25 | ->setPathGenerator($pathGenerator); 26 | 27 | if ($conversionName !== '') { 28 | $conversion = ConversionCollection::createForMedia($media)->getByName($conversionName); 29 | 30 | $urlGenerator->setConversion($conversion); 31 | } 32 | 33 | return $urlGenerator; 34 | } 35 | 36 | public static function guardAgainstInvalidUrlGenerator(string $urlGeneratorClass): void 37 | { 38 | if (! class_exists($urlGeneratorClass)) { 39 | throw InvalidUrlGenerator::doesntExist($urlGeneratorClass); 40 | } 41 | 42 | if (! is_subclass_of($urlGeneratorClass, UrlGenerator::class)) { 43 | throw InvalidUrlGenerator::doesNotImplementUrlGenerator($urlGeneratorClass); 44 | } 45 | } 46 | } 47 | --------------------------------------------------------------------------------