Adopt Laravel's Image facade for media processing, upgrade Intervention to v4 #110

Merged
jonny merged 1 commit from use_laravel_image_api into develop 2026-08-01 18:09:01 +02:00
9 changed files with 54 additions and 89 deletions

View file

@ -6,9 +6,9 @@ namespace App\Console\Commands;
use App\Models\Media; use App\Models\Media;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Image\ImageException;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ReprocessMediaImages extends Command class ReprocessMediaImages extends Command
{ {
@ -16,7 +16,7 @@ class ReprocessMediaImages extends Command
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling'; protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
public function handle(ImageManager $manager): void public function handle(): void
{ {
$media = Media::where('type', 'image') $media = Media::where('type', 'image')
->whereNotNull('image_widths') ->whereNotNull('image_widths')
@ -44,10 +44,10 @@ class ReprocessMediaImages extends Command
$this->info("Processing: {$path}"); $this->info("Processing: {$path}");
$image = Image::fromStorage($path, 'public');
try { try {
$file = Storage::disk('public')->get($path); $image->width();
$image = $manager->read($file); } catch (ImageException) {
} catch (DecoderException) {
$this->warn(' Could not decode image, skipping.'); $this->warn(' Could not decode image, skipping.');
continue; continue;
@ -57,11 +57,8 @@ class ReprocessMediaImages extends Command
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
$this->info(' Done.'); $this->info(' Done.');
} }

View file

@ -13,9 +13,10 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Image\ImageException;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
class MicropubMediaController extends Controller class MicropubMediaController extends Controller
@ -111,12 +112,9 @@ class MicropubMediaController extends Controller
$filename = Storage::disk('local')->putFile('media', $file); $filename = Storage::disk('local')->putFile('media', $file);
/** @var ImageManager $manager */
$manager = resolve(ImageManager::class);
try { try {
$image = $manager->read($request->file('file')); $width = Image::fromUpload($request->file('file'))->width();
$width = $image->width(); } catch (ImageException) {
} catch (Exception) {
// not an image // not an image
$width = null; $width = null;
} }

View file

@ -7,11 +7,11 @@ namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Image\ImageException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ProcessMedia implements ShouldQueue class ProcessMedia implements ShouldQueue
{ {
@ -30,15 +30,16 @@ class ProcessMedia implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*/ */
public function handle(ImageManager $manager): void public function handle(): void
{ {
// Load file // Load file
$file = Storage::disk('local')->get($this->filename); $file = Storage::disk('local')->get($this->filename);
// Open file // Open file
$image = Image::fromStorage($this->filename, 'local');
try { try {
$image = $manager->read($file); $width = $image->width();
} catch (DecoderException) { } catch (ImageException) {
// not an image; delete file and end job // not an image; delete file and end job
Storage::disk('local')->delete($this->filename); Storage::disk('local')->delete($this->filename);
@ -49,18 +50,15 @@ class ProcessMedia implements ShouldQueue
Storage::disk('public')->put($this->filename, $file); Storage::disk('public')->put($this->filename, $file);
// Create smaller versions if necessary // Create smaller versions if necessary
if ($image->width() > 1000) { if ($width > 1000) {
$filenameParts = explode('.', $this->filename); $filenameParts = explode('.', $this->filename);
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
// the following achieves this data flow // the following achieves this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
} }
// Now we can delete the locally saved image // Now we can delete the locally saved image

View file

@ -7,7 +7,6 @@ use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Key\InMemory;
@ -30,11 +29,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// configure Intervention/Image
$this->app->bind('Intervention\Image\ImageManager', function () {
return ImageManager::withDriver(config('image.driver'));
});
/** /**
* Paginate a standard Laravel Collection. * Paginate a standard Laravel Collection.
* *

View file

@ -15,7 +15,7 @@
"ext-sodium": "*", "ext-sodium": "*",
"cviebrock/eloquent-sluggable": "^13.0", "cviebrock/eloquent-sluggable": "^13.0",
"indieauth/client": "^1.1", "indieauth/client": "^1.1",
"intervention/image": "^3", "intervention/image": "^4.0",
"jonnybarnes/indieweb": "~0.2", "jonnybarnes/indieweb": "~0.2",
"jonnybarnes/webmentions-parser": "~0.5", "jonnybarnes/webmentions-parser": "~0.5",
"laravel/framework": "^13.0", "laravel/framework": "^13.0",

40
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "01ba04ab77c167a38ed826d7193ba5ef", "content-hash": "23983a4e6a8e79cb9636fe8f0e604eb7",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -1556,26 +1556,26 @@
}, },
{ {
"name": "intervention/gif", "name": "intervention/gif",
"version": "4.2.4", "version": "5.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/gif.git", "url": "https://github.com/Intervention/gif.git",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" "reference": "bb395af960deffe64d70c976b4df9283f68e762d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", "reference": "bb395af960deffe64d70c976b4df9283f68e762d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.1" "php": "^8.3"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8" "squizlabs/php_codesniffer": "^4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -1594,7 +1594,7 @@
"homepage": "https://intervention.io/" "homepage": "https://intervention.io/"
} }
], ],
"description": "Native PHP GIF Encoder/Decoder", "description": "PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif", "homepage": "https://github.com/intervention/gif",
"keywords": [ "keywords": [
"animation", "animation",
@ -1604,7 +1604,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/gif/issues", "issues": "https://github.com/Intervention/gif/issues",
"source": "https://github.com/Intervention/gif/tree/4.2.4" "source": "https://github.com/Intervention/gif/tree/5.0.1"
}, },
"funding": [ "funding": [
{ {
@ -1620,31 +1620,31 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2026-01-04T09:27:23+00:00" "time": "2026-05-03T06:04:47+00:00"
}, },
{ {
"name": "intervention/image", "name": "intervention/image",
"version": "3.11.8", "version": "4.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/image.git", "url": "https://github.com/Intervention/image.git",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738" "reference": "830907fc5397dfc2a51a4e90322d586989fc8364"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/cf04c8dd245697f701057c13d4bfe140d584e738", "url": "https://api.github.com/repos/Intervention/image/zipball/830907fc5397dfc2a51a4e90322d586989fc8364",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738", "reference": "830907fc5397dfc2a51a4e90322d586989fc8364",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-mbstring": "*", "ext-mbstring": "*",
"intervention/gif": "^4.2", "intervention/gif": "^5",
"php": "^8.1" "php": "^8.3"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^4" "squizlabs/php_codesniffer": "^4"
}, },
@ -1680,7 +1680,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/image/issues", "issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/3.11.8" "source": "https://github.com/Intervention/image/tree/4.2.0"
}, },
"funding": [ "funding": [
{ {
@ -1696,7 +1696,7 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2026-05-01T08:20:10+00:00" "time": "2026-07-09T13:07:14+00:00"
}, },
{ {
"name": "jonnybarnes/indieweb", "name": "jonnybarnes/indieweb",

View file

@ -1,5 +1,9 @@
<?php <?php
use Spatie\FlareClient\Sampling\RateSampler;
use Spatie\LaravelFlare\FlareConfig;
use Spatie\LaravelFlare\Senders\LaravelHttpSender;
return [ return [
/* /*
| |
@ -25,7 +29,7 @@ return [
| |
*/ */
'collects' => \Spatie\LaravelFlare\FlareConfig::defaultCollects( 'collects' => FlareConfig::defaultCollects(
ignore: [], ignore: [],
extra: [] extra: []
), ),
@ -74,7 +78,7 @@ return [
*/ */
'sender' => [ 'sender' => [
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class, 'class' => LaravelHttpSender::class,
'config' => [ 'config' => [
'timeout' => 10, 'timeout' => 10,
], ],
@ -165,7 +169,7 @@ return [
*/ */
'sampler' => [ 'sampler' => [
'class' => \Spatie\FlareClient\Sampling\RateSampler::class, 'class' => RateSampler::class,
'config' => [ 'config' => [
'rate' => env('FLARE_SAMPLER_RATE', 0.1), 'rate' => env('FLARE_SAMPLER_RATE', 0.1),
], ],

View file

@ -1,22 +0,0 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => Driver::class,
];

View file

@ -6,7 +6,6 @@ namespace Tests\Unit\Jobs;
use App\Jobs\ProcessMedia; use App\Jobs\ProcessMedia;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -15,10 +14,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function non_media_files_are_not_saved(): void public function non_media_files_are_not_saved(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/file.txt', 'This is not an image'); Storage::disk('local')->put('media/file.txt', 'This is not an image');
$job = new ProcessMedia('file.txt'); $job = new ProcessMedia('media/file.txt');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'file.txt'); $this->assertFileDoesNotExist(storage_path('app/media/').'file.txt');
} }
@ -26,10 +24,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function small_images_are_not_resized(): void public function small_images_are_not_resized(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png')); Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png'));
$job = new ProcessMedia('aaron.png'); $job = new ProcessMedia('media/aaron.png');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png'); $this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png');
@ -41,10 +38,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function large_images_have_smaller_images_created(): void public function large_images_have_smaller_images_created(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg'));
$job = new ProcessMedia('media/test-image.jpg'); $job = new ProcessMedia('media/test-image.jpg');
$job->handle($manager); $job->handle();
// These need to look in public disk // These need to look in public disk
Storage::disk('public')->assertExists('media/test-image.jpg'); Storage::disk('public')->assertExists('media/test-image.jpg');