Image Optimization in Laravel Applications - Optimize User Images Easily

Updated:

I've recently been working on a client's Laravel application that lets users upload images, This is a dangerous task at the best of times, as you can't always rely on a user knowing they should optimize images before uploading them.

Luckily, doing image optimization and compression in Laravel is actually quite straight forward, thanks to a few optimization tools and packages that have a fluent api for you to use.

Laravel Validation, duh?

You could say: "well I'll just add some validation to ensure they don't upload an image more than 2mb in size".

And I say to that: "you're going to annoy a lot of your users."

Just think about the last time you took a great looking selfie, and planned to use that on every platform you use, so you go through them all, uploading directly from your phone, without even thinking about needing to optimizing the image at all.

Now, I don't know about Android, but I know for sure that iOS photos can be absolutely massive in size for no good reason. We're talking easily 8mb+.

So now your validation will annoy this user, because how can they use this great photo they just took on your website?

The Fix

For this, we'll be using 2 packages:

  • PHP Intervention Image
  • Spatie's Laravel Image Optimizer package

I'll walk you through how to configure these to get great results, and how you can integrate this into real world web application.

Setting Up The Image Optimization

Let's get started.

Installing The Packages

Install the 2 packages required by running composer require:

composer require intervention/image spatie/laravel-image-optimizer

Luckily we don't need to much configuration for Spatie's package, but we do need to add 2 lines of code for intervention image:

Inside app.php add the following line to your Providers array:

Intervention\Image\ImageServiceProvider::class,

And further down in the file, add the following line to the aliases array:  

'Image' => Intervention\Image\Facades\Image::class,

Configuring Your System

Spatie's package relies on a few binaries existing on the system, so we need to install a few extra packages (if you want to do this yourself, you can view the documentation here: https://github.com/spatie/laravel-image-optimizer)

If you're only worrying about png, jpeg, jpg and the like, you only need these 3 packages:

jpegoptim
optipng
pngquant

I personally run MacOS locally, so to run and test this code locally, I need to run the following command to install them:

brew install jpegoptim optipng pngquant

And for your server if you're running Ubunutu, you'll need to run this command:

sudo apt-get install jpegoptim optipng pngquant

There are a few other packages that you can install, like webp for optimizing webp images, but if you restrict the image type via validation, then you don't need to worry about this.

Lastly, you can play with the configuration for spatie's package by publishing the config:

php artisan vendor:publish --provider="Spatie\LaravelImageOptimizer\ImageOptimizerServiceProvider"

One thing you might want to change is the image quality under jpegoptim. Spatie defaults it to 85% but you may want it to be higher or lower.

Writing The Code

I've created a job to run this optimization, which just takes an image path. This makes it easily resuable and keeps the code in a single place.

You can create a new job by running the Laravel command:

php artisan make:job OptimizeImage

And fill it with this code:

<?php
 
namespace App\Jobs;
 
use Exception;
use Illuminate\Bus\Queueable;
use Intervention\Image\Facades\Image;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Spatie\LaravelImageOptimizer\Facades\ImageOptimizer;
 
class OptimzseImage implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
 
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(private string $imagePath)
{
}
 
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
try {
Image::make($this->imagePath)->resize(1000, 1000, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})->save();
 
ImageOptimizer::optimize($this->imagePath);
} catch (Exception $e) {
app('sentry')->captureException($e);
}
}
}

There are a few things to think about with this code, specifically this line:

Image::make($this->imagePath)->resize(1000, 1000, function ($constraint) {

The 1000, 1000 here resizes the image to a fixed size fitting that width & height, you can read more here: https://image.intervention.io/v2/api/resize

The reason why I have it set to 1000, 1000 is because any image uploaded on this platform will never be shown at a higher resolution, so there is no need to store it any higher and deal with the increased file size.

The other 2 functions aspectRatio() and upsize() simply constrain the aspect ratio, and prevent possible upsizing. This means if you were to resize a 1920x1080 image, the aspect ratio would be respected to keep the image looking good (as if you resize that to 1000x1000 you will have a very ugly cut off image).

Lastly we run spatie's image optimizer by calling:

ImageOptimizer::optimize($this->imagePath);

Running It

Now, let the user upload their image without the max size validation, and once uploaded, dispatch this job:

$path = Storage::disk('public')->path($uploadedFile);
 
OptimizeImage::dispatch($path);

And watch the magic happen!

That's it, short and sweet. Let me know how you go.