diff --git a/config/media-library.php b/config/media-library.php index fac25c683..b532b5c10 100644 --- a/config/media-library.php +++ b/config/media-library.php @@ -282,4 +282,6 @@ * disabled lazy loading globally in the service provider. */ 'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true), + + 'convert_gif_to_webp_using_gif2webp' => false, ]; diff --git a/src/Conversions/Actions/PerformConversionAction.php b/src/Conversions/Actions/PerformConversionAction.php index 596552fe3..53faf4702 100644 --- a/src/Conversions/Actions/PerformConversionAction.php +++ b/src/Conversions/Actions/PerformConversionAction.php @@ -2,6 +2,7 @@ namespace Spatie\MediaLibrary\Conversions\Actions; +use Illuminate\Support\Str; use Spatie\MediaLibrary\Conversions\Conversion; use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompletedEvent; use Spatie\MediaLibrary\Conversions\Events\ConversionWillStartEvent; @@ -19,20 +20,41 @@ public function execute( ): void { $imageGenerator = ImageGeneratorFactory::forMedia($media); - $copiedOriginalFile = $imageGenerator->convert($copiedOriginalFile, $conversion); + if ($conversion->shouldTouchFiles()) { + $copiedOriginalFile = $imageGenerator->convert($copiedOriginalFile, $conversion); + } if (! $copiedOriginalFile) { return; } + $shouldUseGif2WebpConverter = config('media-library.convert_gif_to_webp_using_gif2webp') + && pathinfo($copiedOriginalFile, PATHINFO_EXTENSION) === "gif" + && $conversion->getManipulations()->getManipulationArgument('format') == ["webp"]; + + if ($shouldUseGif2WebpConverter) { + $conversion->setUseGif2WebpAsConverter(); + } + event(new ConversionWillStartEvent($media, $conversion, $copiedOriginalFile)); - $manipulationResult = (new PerformManipulationsAction)->execute($media, $conversion, $copiedOriginalFile); + if ($conversion->shouldTouchFiles()) { + $manipulationResult = (new PerformManipulationsAction())->execute($media, $conversion, $copiedOriginalFile); + } else { + $manipulationResult = $copiedOriginalFile; + } $newFileName = $conversion->getConversionFile($media); $renamedFile = $this->renameInLocalDirectory($manipulationResult, $newFileName); + $manipulatedFile = $renamedFile; + if ($shouldUseGif2WebpConverter) { + $manipulatedFile = pathinfo($renamedFile, PATHINFO_DIRNAME) . '/' . (Str::random(32) . '.' . $media->extension); + copy($renamedFile, $manipulatedFile); + exec("gif2webp -lossy " . $renamedFile . " -o " . $renamedFile); + } + if ($conversion->shouldGenerateResponsiveImages()) { /** @var ResponsiveImageGenerator $responsiveImageGenerator */ $responsiveImageGenerator = app(ResponsiveImageGenerator::class); @@ -40,7 +62,8 @@ public function execute( $responsiveImageGenerator->generateResponsiveImagesForConversion( $media, $conversion, - $renamedFile + $manipulatedFile, + $shouldUseGif2WebpConverter ); } diff --git a/src/Conversions/Actions/PerformManipulationsAction.php b/src/Conversions/Actions/PerformManipulationsAction.php index ac263185b..67ff8f173 100644 --- a/src/Conversions/Actions/PerformManipulationsAction.php +++ b/src/Conversions/Actions/PerformManipulationsAction.php @@ -30,6 +30,15 @@ public function execute( $conversion->format($media->extension); } + if (Str::startsWith(File::mimeType($conversionTempFile), "video/") && $conversion->getManipulations()->getManipulationArgument('format') == ["webm"]) { + exec("ffmpeg -i " . $conversionTempFile . " -f webm -an " . $conversionTempFile . ".webm"); + if (File::exists($conversionTempFile . '.webm')) { + unlink($conversionTempFile); + rename($conversionTempFile . '.webm', $conversionTempFile); + } else { + throw new \RuntimeException("Converted webm file does not exist, check if ffmpeg is intalled!"); + } + } else { $image = Image::useImageDriver(config('media-library.image_driver')) ->loadFile($conversionTempFile) ->format('jpg'); @@ -40,6 +49,7 @@ public function execute( $image->save(); } catch (UnsupportedImageFormat) { + } } return $conversionTempFile; diff --git a/src/Conversions/Conversion.php b/src/Conversions/Conversion.php index 3f95d2abc..a3d9e2ec5 100644 --- a/src/Conversions/Conversion.php +++ b/src/Conversions/Conversion.php @@ -30,14 +30,18 @@ class Conversion protected ?WidthCalculator $widthCalculator = null; + protected bool $withoutTouchingFiles = false; + protected ?string $loadingAttributeValue; protected int $pdfPageNumber = 1; + protected bool $useGif2Webp = false; + public function __construct( protected string $name, ) { - $optimizerChain = OptimizerChainFactory::create(config('media-library.image_optimizers')); + $optimizerChain = OptimizerChainFactory::create(config('media-library.image_optimizers'))->setTimeout(config('media-library.image_optimizer_timeout') ?? 60); $this->manipulations = new Manipulations; $this->manipulations->optimize($optimizerChain)->format('jpg'); @@ -92,6 +96,17 @@ public function shouldKeepOriginalImageFormat(): bool return $this->keepOriginalImageFormat; } + public function setUseGif2WebpAsConverter(): void + { + $this->useGif2Webp = true; + $this->removeManipulation('format'); + } + + public function shouldUseGif2WebpAsConverter(): bool + { + return $this->useGif2Webp; + } + public function getManipulations(): Manipulations { return $this->manipulations; @@ -205,6 +220,17 @@ public function getWidthCalculator(): ?WidthCalculator return $this->widthCalculator; } + public function withoutTouchingFiles(): self + { + $this->withoutTouchingFiles = true; + + return $this; + } + + public function shouldTouchFiles(): bool { + return !$this->withoutTouchingFiles; + } + public function shouldGenerateResponsiveImages(): bool { return $this->generateResponsiveImages; @@ -223,6 +249,10 @@ public function getResultExtension(string $originalFileExtension = ''): string } } + if ($this->shouldUseGif2WebpAsConverter()) { + return "webp"; + } + if ($manipulationArgument = Arr::get($this->manipulations->getManipulationArgument('format'), 0)) { return $manipulationArgument; } diff --git a/src/Conversions/ImageGenerators/Video.php b/src/Conversions/ImageGenerators/Video.php index 47d7c76c2..f0a553741 100644 --- a/src/Conversions/ImageGenerators/Video.php +++ b/src/Conversions/ImageGenerators/Video.php @@ -12,6 +12,10 @@ class Video extends ImageGenerator { public function convert(string $file, ?Conversion $conversion = null): ?string { + if ($conversion->getManipulations()->getManipulationArgument('format') == ["webm"]) { + return $file; + } + $ffmpeg = FFMpeg::create([ 'ffmpeg.binaries' => config('media-library.ffmpeg_path'), 'ffprobe.binaries' => config('media-library.ffprobe_path'), diff --git a/src/ResponsiveImages/ResponsiveImageGenerator.php b/src/ResponsiveImages/ResponsiveImageGenerator.php index 0f39cc448..5f2d2849f 100644 --- a/src/ResponsiveImages/ResponsiveImageGenerator.php +++ b/src/ResponsiveImages/ResponsiveImageGenerator.php @@ -52,7 +52,7 @@ public function generateResponsiveImages(Media $media): void $temporaryDirectory->delete(); } - public function generateResponsiveImagesForConversion(Media $media, Conversion $conversion, string $baseImage): void + public function generateResponsiveImagesForConversion(Media $media, Conversion $conversion, string $baseImage, bool $shouldUseGif2WebpConverter): void { $temporaryDirectory = TemporaryDirectory::create(); @@ -61,7 +61,7 @@ public function generateResponsiveImagesForConversion(Media $media, Conversion $ $widthCalculator = $conversion->getWidthCalculator() ?? $this->widthCalculator; foreach ($widthCalculator->calculateWidthsFromFile($baseImage) as $width) { - $this->generateResponsiveImage($media, $baseImage, $conversion->getName(), $width, $temporaryDirectory, $this->getConversionQuality($conversion)); + $this->generateResponsiveImage($media, $baseImage, $conversion->getName(), $width, $temporaryDirectory, $this->getConversionQuality($conversion), $shouldUseGif2WebpConverter, $conversion->shouldTouchFiles()); } $this->generateTinyJpg($media, $baseImage, $conversion->getName(), $temporaryDirectory); @@ -80,18 +80,24 @@ public function generateResponsiveImage( string $conversionName, int $targetWidth, BaseTemporaryDirectory $temporaryDirectory, - int $conversionQuality = self::DEFAULT_CONVERSION_QUALITY + int $conversionQuality = self::DEFAULT_CONVERSION_QUALITY, + bool $shouldUseGif2WebpConverter = false, + bool $shouldTouchFiles = true ): void { $extension = $this->fileNamer->extensionFromBaseImage($baseImage); $responsiveImagePath = $this->fileNamer->temporaryFileName($media, $extension); $tempDestination = $temporaryDirectory->path($responsiveImagePath); - ImageFactory::load($baseImage) - ->optimize() - ->width($targetWidth) - ->quality($conversionQuality) - ->save($tempDestination); + if ($shouldTouchFiles) { + ImageFactory::load($baseImage) + ->optimize() + ->width($targetWidth) + ->quality($conversionQuality) + ->save($tempDestination); + } else { + copy($baseImage, $tempDestination); + } $responsiveImageHeight = ImageFactory::load($tempDestination)->getHeight(); @@ -101,13 +107,17 @@ public function generateResponsiveImage( $conversionName, $targetWidth, $responsiveImageHeight, - $extension + $shouldUseGif2WebpConverter ? "webp" : $extension ); $responsiveImagePath = $temporaryDirectory->path($fileName); rename($tempDestination, $responsiveImagePath); + if ($shouldUseGif2WebpConverter) { + exec("gif2webp -lossy " . $responsiveImagePath . " -o " . $responsiveImagePath); + } + $this->filesystem->copyToMediaLibrary($responsiveImagePath, $media, 'responsiveImages'); ResponsiveImage::register($media, $fileName, $conversionName); diff --git a/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php b/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php index 3aa46e601..72ad6935b 100644 --- a/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php +++ b/tests/ResponsiveImages/ResponsiveImageGeneratorTest.php @@ -1,8 +1,11 @@ not()->toHaveKey('base64svg'); }); +it('can generate responsive animated images', function (string $driver) { + config()->set('media-library.image_driver', $driver); + config()->set('media-library.responsive_images.use_tiny_placeholders', false); + config()->set('media-library.convert_gif_to_webp_using_gif2webp', true); + + $testModel = new class() extends TestModelWithoutMediaConversions + { + public function registerMediaCollections(): void + { + $this->addMediaCollection('images') + ->registerMediaConversions(function (Media $media) { + $this + ->addMediaConversion('webp') + ->withResponsiveImages() + ->greyscale() + ->format('webp'); + }); + } + }; + + $model = $testModel::create(['name' => 'testmodel']); + $model->addMedia($this->getTestGif()) + ->toMediaCollection('images'); + + $imagick = Image::useImageDriver(config('media-library.image_driver'))->loadFile($this->getTempDirectory("media/1/{$this->fileName}.gif"))->image(); + + expect(count($imagick))->toBe(10); + + $imagick = Image::useImageDriver(config('media-library.image_driver'))->loadFile($this->getTempDirectory("media/1/responsive-images/{$this->fileName}___webp_267_267.webp"))->image(); + + expect(count($imagick))->toBe(10); +})->with(['imagick']); + it('will not delete responsive images of images with similar names saved on the same directory', function () { config(['media-library.path_generator' => TestCustomPathGenerator::class]); diff --git a/tests/TestCase.php b/tests/TestCase.php index aaac1169a..c15a282e9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -268,6 +268,11 @@ public function getTestMp4(): string return $this->getTestFilesDirectory('test.mp4'); } + public function getTestGif(): string + { + return $this->getTestFilesDirectory('test.gif'); + } + public function getTestImageWithoutExtension(): string { return $this->getTestFilesDirectory('image'); diff --git a/tests/TestSupport/testfiles/test.gif b/tests/TestSupport/testfiles/test.gif new file mode 100644 index 000000000..9a180fe86 Binary files /dev/null and b/tests/TestSupport/testfiles/test.gif differ