What's a BlurHash?
BlurHash is a technique for creating low fidelity, "blurred" representations of images, that are encoded into short strings, for use as placeholders; for example, while an image loads.
Click here for a quick demo.
Suppose we have the following image:
The BlurHashed equivalent looks like this:
The BlurHash itself is a short (approx 20-30 characters) string. In the case of the example above, it looks like this:
LNKB%R}]47jF7|xuzqwJ0xsorsS#
Due to its small size, it's entirely practical to include it in API response, or pass it to a view and use it in a web component:
<my-fancy-image
src="/path/to/photo.jpg"
alt="Just an example"
blurhash="LNKB%R}]47jF7|xuzqwJ0xsorsS#" />
This post isn't particularly concerned with how the BlurHash is used on the front-end; rather, how we can generate it server-side in order to make it available to the UI.
Generating BlurHashes
The BlurHash algorithm is open-source, and available in multiple languages; PHP is no exception and there's a package designed for use with Laravel.
It works with the Intervention Image package, and it's used like this:
use Intervention\Image\ImageManager;
use Bepsvpt\Blurhash\Facades\BlurHash;
$hash = BlurHash::encode((new ImageManager())->make(storage_path('path/to/photo.jpg')));
// LNKB%R}]47jF7|xuzqwJ0xsorsS#
This returns a string, like the example above.
You'd probably want to store this in the database at this point. However let's look at a fuller, more practical example.
Laravel Media Library
When dealing with any sort of media — including images — in a Laravel project I almost always reach for the Laravel Media Library package. It makes it easy to attach media to models, as well as providing support for responsive images and conversions (e.g. small vs large versions, thumbnails and so on).
Each media item is an Eloquent model that includes a path or URL to the file, with a corresponding entry in the media
database table; so that's the obvious place for the BlurHash to go.
Using listeners, the whole process of generating and storing the BlurHash value whenever an image gets added can be automated.
Adding BlurHash Support
First step is to create a migration to add the column:
Schema::table('media', function (Blueprint $table) {
$table->string('blurhash', 64)->nullable();
});
Next, create a job to generate and store the BlurHash:
<?php
use Bepsvpt\Blurhash\Facades\BlurHash;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Intervention\Image\ImageManager;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class BlurhashMedia implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var Media
*/
protected $media;
/**
* BlurhashMedia constructor.
* @param Media $media
*/
public function __construct(Media $media)
{
$this->media = $media;
}
/**
* Execute the job.
*/
public function handle(): void
{
$hash = BlurHash::encode((new ImageManager())->make($this->media->getUrl()));
$this->media->update(['blurhash' => $hash]);
}
}
Rather than listen to the created event on the Media model, the event we're interested in is MediaHasBeenAddedEvent
. This is because the media file might not be available when the model is created, such as when the file is being stored on a remote disk such as S3.
The relevant media model is available as a public property, so we can pass that to the job that we'll dispatch to (optionally) be run in the background.
First, though, we need to ensure that the file is an image. The media library package handles videos — a BlurHash would only make sense on their thumbnails — and things like documents and audio files.
The simplest way is to check the mime_type
attribute on the media model by checking against a whitelist:
/** @var \Spatie\MediaLibrary\MediaCollections\Models\Media $media **/
if(in_array($media->mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
// dispatch the job
}
Here's an example listener:
use Illuminate\Foundation\Bus\DispatchesJobs;
use Spatie\MediaLibrary\MediaCollections\Events\MediaHasBeenAddedEvent;
class MediaBlurhashListener
{
use DispatchesJobs;
/**
* Handle the event.
*/
public function handle(MediaHasBeenAddedEvent $event): void
{
if(in_array($event->media->mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
$this->dispatch(new BlurhashMedia($event->media));
}
}
}
Specify a listener in a service provider:
use Illuminate\Support\Facades\Event;
Event::listen([
MediaHasBeenAddedEvent::class,
], MediaBlurhashListener::class);
Now, new images will get their BlurHashes generated automatically.
This isn't, however, the most efficient way to do this since it calculates the hash on the full-size version of an image. Depending on the file size and resolution of your uploaded images, this may be resource-intensive or clog up your queues.
The alternative approach is to choose one of your configured conversions &mash; for example, a "small" version — and listen to the ConversionHasBeenCompletedEvent
event instead:
use Illuminate\Support\Facades\Event;
Event::listen([
MediaHasBeenAddedEvent::class,
], MediaBlurhashListener::class);
Then, in your listener, you'd check the name of the conversion, and fire the job if it matches the appropriate one.
Suppose you've defined the following conversions:
/**
* @param Media|null $media
*/
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('thumbnail')
->width(80)
->height(80)
->performOnCollections('photo');
$this
->addMediaConversion('small')
->width(320)
->height(240)
->performOnCollections('photo');
$this
->addMediaConversion('medium')
->width(640)
->height(480)
->performOnCollections('photo');
$this
->addMediaConversion('large')
->width(1280)
->height(960)
->performOnCollections('photo');
}
The small one looks like the best candidate. Whilst not the smallest, you'll notice that the thumbnail crops to a square; we're vanishingly unlikely to use a BlurHash for loading a thumbnail, so we need it to represent the appropriate aspect ratio.
So, in your listener:
class MediaBlurhashListener
{
use DispatchesJobs;
/**
* Handle the event.
*/
public function handle(ConversionHasBeenCompletedEvent $event): void
{
if(
$event->conversion->getName() === 'small' &&
in_array($event->media->mime_type, ['image/jpeg', 'image/png', 'image/gif'])
){
$this->dispatch(new BlurhashMedia($event->media));
}
}
}
Note that if you have differently named conversions for different models, this check might need to be a little more complex; e.g. small
for category photos, tiny
for product photos.
Size and Aspect Ratio
One more thing needs to be addressed; a characteristic rather than a downside of BlurHash, but a consideration nonetheless.
When decoding a BlurHash for rendering on the front-end, we need to specify a width and height so that it knows what sort of an area to fill.
For example, using the blurhash JavaScript library, you decode a BlurHash to paint onto a canvas like this:
import { decode } from "blurhash"
const pixels = decode('LNKB%R}]47jF7|xuzqwJ0xsorsS#', 1280, 980)
If you're using fixed-size images, then that's straightforward. In the example of the conversions above, you'll notice that all of the conversions bar the thumbnail have the same aspect ratio. In that case, all you need to do is use the aspect ratio when decoding the BlurHash; if you make it bigger than you need then all you need do is set the max-width
on the canvas to 100%.
For images with varying aspect ratios, you may need to include that along with the BlurHash in, say, an API response — that all just depends on how your application is set up, but hopefully I've provided enough theory to enable you to tweak it to suit.
The Media BlurHash Package
I've coded up all of the above functionality and published it as a Composer package for Laravel which you can find here. It has some additional configuration options, such as specifying a particular queue connection or name to run the job.
Wrapping Up
In this post I've introduced BlurHash as an approach to creating placeholders for images, focusing on the generation and storage in the back-end.
I've demonstrated an approach I use in Laravel, in conjunction with the Media Library package. In another post, I'll look at approaches to displaying them in the front-end.