iamriajul/thumbhash-laravel
Laravel wrapper for ThumbHash, the compact image placeholder format. Generate tiny hashes from images and render blurred previews while full-size images load, helping reduce layout shift and improve perceived performance.
iamriajul/thumbhash-laravel wraps the ThumbHash algorithm — a tiny (typically ~25–35 byte) binary placeholder for an image — in a Laravel-friendly facade. You give it an image, you get a short string you can store in your database; later you turn that string back into a data URI you can drop straight into an <img src> or CSS background-image for blur-up loading.
Install and first encode/decode:
composer require iamriajul/thumbhash-laravel
use IamRiajul\ThumbHash\Facades\ThumbHash;
$hash = ThumbHash::encode(public_path('uploads/photo.jpg')); // short base64-ish string
$dataUri = ThumbHash::decode($hash); // "data:image/png;base64,..."
Where to look first:
intervention/image, so the Intervention major version (v2 vs v3) you already have installed determines which backend (GD vs Imagick) is used.config/thumbhash.php if published, for driver/size options.First real use case: add a thumbhash column to your images/posts table, generate the hash once at upload time, and render it as the placeholder while the full asset downloads. That single change is where 90% of the value is.
Schema::table('posts', function (Blueprint $table) {
$table->string('thumbhash', 100)->nullable()->after('image_path');
});
Generate at upload, store the hash — never on read. Encoding decodes the source file and resizes it, which is orders of magnitude more expensive than a decode. Do it in the same place you already process the upload (a UploadedImage action, a PhotoObserver, or a queued GenerateThumbHash job):
$post->update([
'image_path' => $path,
'thumbhash' => ThumbHash::encode(Storage::disk('public')->path($path)),
]);
Note the ->path() call: the encoder wants a filesystem path, not a Storage disk-relative key.
Blur-up placeholder in Blade. The decoded data URI is a small PNG (ThumbHash caps out around 32×32), so render it as the src and let CSS scale and blur it:
<div class="relative">
<img src="{{ ThumbHash::decode($post->thumbhash) }}"
class="absolute inset-0 w-full h-full object-cover blur-lg scale-110"
aria-hidden="true">
<img src="{{ Storage::url($post->image_path) }}"
class="relative w-full h-full object-cover"
loading="lazy" alt="{{ $post->title }}">
</div>
Average-colour fallback. For cards where you want a flat tint rather than a blurred image, keep a second column with the dominant colour and use the hash only on detail pages — the data URI is ~1–2 KB of HTML per image, which adds up on a 60-card grid.
API / Inertia responses. Decode server-side and ship the data URI as a normal string prop; there's no JS counterpart required. If you'd rather decode client-side to keep payloads small, store the raw hash and decode in the browser with the thumbhash npm package — the formats are interoperable.
Backfilling existing rows. Write an artisan command that chunks the table, skips rows with a non-null thumbhash, and dispatches a job per chunk. Guard it with ->whereNotNull('image_path')->whereNull('thumbhash') so re-runs are idempotent.
Testing. Encoding is deterministic for a given input image, so assert on the decoded output length or non-emptiness rather than the exact string, and commit one small fixture image to tests/Fixtures.
Upgrade to 1.0.3 if you're on PHP 8.1+. This release specifically fixes a float-conversion deprecation warning raised inside Intervention Image ("Implicit conversion from float … to int loses precision"). It's cosmetic — thumbnails still generate — but if you run with error_reporting(E_ALL) in tests or a strict deprecation-to-exception handler, it will fail your suite. Pin ^1.0.3.
Driver mismatch is the most common failure. The package inherits whatever intervention/image version your app already requires. If another dependency locks you to Intervention v2, the v3-only code paths won't behave as documented. Check composer show intervention/image before debugging anything else, and make sure the GD or Imagick PHP extension is actually installed — a missing extension surfaces as a confusing "Unable to read image" rather than an extension error.
Paths, not URLs. encode() needs a real filesystem path. Passing asset(...), Storage::url(...), or a disk-relative key will fail or, worse, silently encode nothing. Prefer Storage::disk($disk)->path($key) and handle the case where the disk is remote (S3) — you'll need to pull the file to a temp path first, which is another reason to generate hashes at upload time rather than lazily.
Non-image and corrupt files. encode() throws on unreadable input. Wrap backfill commands and user-upload hooks in try/catch and leave thumbhash null on failure; the front end should treat null as "render a solid colour" rather than blowing up a listing page.
Column sizing. A thumbnail hash is small, but base64 padding and future format tweaks mean you shouldn't use string(32). string(100) or text costs nothing and avoids a truncation bug that's only visible on some images.
Decode cost in loops. Decoding is fast in absolute terms but it's still PNG encoding per call. For pages rendering more than ~50 images, either cache the resulting data URI (Cache::remember("thumbhash:{$post->id}", 3600, fn () => ThumbHash::decode($post->thumbhash))) or store the decoded data URI alongside the hash at generation time.
Blur is on you. ThumbHash gives you a low-resolution image, not a blur. Without filter: blur() plus a slight scale() to hide the soft edges, upscaled placeholders look blocky and pixelated rather than like native blur-up.
Extension points are thin. There's no pluggable encoder strategy — if you need a different placeholder format (BlurHash, LQIP), keep it behind your own PlaceholderGenerator interface so you can swap the package out without touching views. The value of this package is the one-line Laravel binding, not deep configurability; treat it as a thin adapter and don't build architecture on top of it.
How can I help you explore Laravel packages today?