Skip to content

Instantly share code, notes, and snippets.

@saeedvir
Created December 13, 2025 07:42
Show Gist options
  • Select an option

  • Save saeedvir/49b858ad58b4b19b4591898c8948300b to your computer and use it in GitHub Desktop.

Select an option

Save saeedvir/49b858ad58b4b19b4591898c8948300b to your computer and use it in GitHub Desktop.
ThumbBlur is a PHP helper class designed to generate lightweight blurred thumbnails of images using the GD library. It supports JPG, PNG, GIF, and WebP formats, and provides:
<?php
namespace App\Helpers;
/*
By:saeedvir (https://github.com/saeedvir)
Automatic hash-based caching to avoid regenerating the same thumbnail.
Aspect-ratio-preserving resizing for accurate thumbnails.
Flexible blur control, including multiple iterations for stronger effects.
Optional output format conversion (e.g., JPEG → WebP).
Base64 placeholder generation for inline LQIP use.
Dominant color extraction for placeholders or UI backgrounds.
Laravel integration with support for disks and storage paths.
*/
use Illuminate\Support\Facades\Storage;
use RuntimeException;
class ThumbBlur
{
protected string $source;
protected string $outputDir;
protected int $width;
protected int $height;
protected int $blur;
protected int $blurIterations;
protected string $fileType;
protected int $quality;
protected bool $keepAspectRatio;
protected ?array $dominantColorCache = null;
protected $image;
public function __construct(
string $source,
int $width = 32,
int $height = 32,
int $blur = 2,
int $blurIterations = 1,
?string $fileType = null,
int $quality = 75,
string $outputDir = 'thumbs/blur',
bool $keepAspectRatio = true
) {
if (! extension_loaded('gd')) {
throw new RuntimeException('GD extension is not available.');
}
$this->source = $source;
$this->width = max(1, $width);
$this->height = max(1, $height);
$this->blur = max(0, $blur);
$this->blurIterations = max(1, $blurIterations);
$this->quality = max(1, min(100, $quality));
$this->fileType = $fileType ?? strtolower(pathinfo($source, PATHINFO_EXTENSION));
$this->outputDir = trim($outputDir, '/');
$this->keepAspectRatio = $keepAspectRatio;
}
/* -----------------------------------------------------------------
| Public API
| -----------------------------------------------------------------
*/
public function generate(?string $disk = null): string
{
$this->loadImage();
$this->resize();
$this->applyBlur();
$filename = $this->getCacheFilename();
$relativePath = $this->outputDir . '/' . $filename;
if ($disk) {
Storage::disk($disk)->makeDirectory($this->outputDir);
if (! Storage::disk($disk)->exists($relativePath)) {
ob_start();
$this->outputImage();
Storage::disk($disk)->put($relativePath, ob_get_clean());
}
} else {
$absoluteDir = public_path($this->outputDir);
if (! is_dir($absoluteDir)) {
mkdir($absoluteDir, 0755, true);
}
$absolutePath = $absoluteDir . '/' . $filename;
if (! file_exists($absolutePath)) {
$this->outputImage($absolutePath);
}
}
return $relativePath;
}
public function toBase64(): string
{
$this->loadImage();
$this->resize();
$this->applyBlur();
ob_start();
$this->outputImage();
$data = ob_get_clean();
return 'data:image/' . $this->fileType . ';base64,' . base64_encode($data);
}
public function dominantColor(): array
{
if ($this->dominantColorCache) {
return $this->dominantColorCache;
}
$this->loadImage();
$thumb = imagecreatetruecolor(1, 1);
imagecopyresampled(
$thumb,
$this->image,
0,
0,
0,
0,
1,
1,
imagesx($this->image),
imagesy($this->image)
);
$rgb = imagecolorat($thumb, 0, 0);
unset($thumb);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
return $this->dominantColorCache = [
'r' => $r,
'g' => $g,
'b' => $b,
'hex' => sprintf('#%02x%02x%02x', $r, $g, $b),
];
}
public function setOutputType(string $type): self
{
$this->fileType = strtolower($type);
return $this;
}
public function setBlur(int $blur, int $iterations = 1): self
{
$this->blur = $blur;
$this->blurIterations = max(1, $iterations);
return $this;
}
/* -----------------------------------------------------------------
| Internal Helpers
| -----------------------------------------------------------------
*/
protected function getCacheFilename(): string
{
$hash = md5($this->source . $this->width . $this->height . $this->blur . $this->blurIterations . $this->fileType . $this->quality);
return $hash . '.' . $this->fileType;
}
protected function loadImage(): void
{
if ($this->image) {
return;
}
$path = is_file($this->source)
? $this->source
: Storage::path($this->source);
if (! is_readable($path)) {
throw new RuntimeException('Image source is not readable.');
}
$info = getimagesize($path);
if (! $info) {
throw new RuntimeException('Invalid image file.');
}
$mime = $info['mime'];
$this->image = match ($mime) {
'image/jpeg' => imagecreatefromjpeg($path),
'image/png' => imagecreatefrompng($path),
'image/gif' => imagecreatefromgif($path),
'image/webp' => $this->supportsWebp() ? imagecreatefromwebp($path) : throw new RuntimeException('GD WebP support is not enabled.'),
default => throw new RuntimeException('Unsupported image type.'),
};
if (! $this->image) {
throw new RuntimeException('Failed to load image. Format may be unsupported or corrupted.');
}
// Update fileType to match actual mime
$this->fileType = str_replace('image/', '', $mime);
}
protected function supportsWebp(): bool
{
return function_exists('imagecreatefromwebp') && (gd_info()['WebP Support'] ?? false);
}
protected function resize(): void
{
$srcW = imagesx($this->image);
$srcH = imagesy($this->image);
if ($this->keepAspectRatio) {
$ratio = min($this->width / $srcW, $this->height / $srcH);
$newW = (int) ($srcW * $ratio);
$newH = (int) ($srcH * $ratio);
} else {
$newW = $this->width;
$newH = $this->height;
}
$canvas = imagecreatetruecolor($newW, $newH);
imagecopyresampled($canvas, $this->image, 0, 0, 0, 0, $newW, $newH, $srcW, $srcH);
// Instead of imagedestroy
$this->image = $canvas;
}
protected function applyBlur(): void
{
for ($i = 0; $i < $this->blurIterations; ++$i) {
for ($j = 0; $j < $this->blur; ++$j) {
imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
}
}
}
protected function outputImage(?string $path = null): void
{
match ($this->fileType) {
'jpg', 'jpeg' => imagejpeg($this->image, $path, $this->quality),
'png' => imagepng($this->image, $path, (int) round(9 - ($this->quality / 11))),
'gif' => imagegif($this->image, $path),
'webp' => imagewebp($this->image, $path, $this->quality),
default => throw new RuntimeException('Unsupported image type.'),
};
}
public function __destruct()
{
if ($this->image) {
unset($this->image); // safe replacement
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment