Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add image service #616

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
32 changes: 13 additions & 19 deletions app/Jobs/UpdateUserAvatar.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@

use App\Models\User;
use App\Services\Avatar;
use App\Services\ImageOptimizer;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Drivers;
use Intervention\Image\ImageManager;
use Throwable;

final class UpdateUserAvatar implements ShouldQueue
Expand Down Expand Up @@ -60,16 +59,21 @@ public function handle(): void

Storage::disk('public')->put($avatar, $contents, 'public');

$this->resizer()->read($disk->path($avatar))
->coverDown(200, 200)
->save();

$this->user->update([
'avatar' => "$avatar",
$updated = $this->user->update([
'avatar' => $avatar,
'avatar_updated_at' => now(),
'is_uploaded_avatar' => $this->file !== null,
]);

if ($updated) {
ImageOptimizer::optimize(
path: $avatar,
width: 300,
height: 300,
isThumbnail: true
);
}

$this->ensureFileIsDeleted();
}

Expand All @@ -92,18 +96,8 @@ public function failed(?Throwable $exception): void
*/
private function ensureFileIsDeleted(): void
{
if ($this->file !== null) {
if (($this->file !== null) && File::exists($this->file)) {
File::delete($this->file);
}
}

/**
* Creates a new image resizer.
*/
private function resizer(): ImageManager
{
return new ImageManager(
new Drivers\Gd\Driver(),
);
}
}
39 changes: 7 additions & 32 deletions app/Livewire/Questions/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
use App\Models\User;
use App\Rules\MaxUploads;
use App\Rules\NoBlankCharacters;
use App\Services\ImageOptimizer;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\File;
use Illuminate\View\View;
use Imagick;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
Expand Down Expand Up @@ -267,9 +267,14 @@ public function uploadImages(): void

/** @var string $path */
$path = $image->store("images/{$today}", 'public');
$this->optimizeImage($path);

if ($path) {
ImageOptimizer::optimize(
path: $path,
width: 1000,
height: 1000
);

session()->push('images', $path);

$this->dispatch(
Expand All @@ -286,36 +291,6 @@ public function uploadImages(): void
$this->reset('images');
}

/**
* Optimize the images.
*/
public function optimizeImage(string $path): void
{
$imagePath = Storage::disk('public')->path($path);
$imagick = new Imagick($imagePath);

if ($imagick->getNumberImages() > 1) {
$imagick = $imagick->coalesceImages();

foreach ($imagick as $frame) {
$frame->resizeImage(1000, 1000, Imagick::FILTER_LANCZOS, 1, true);
$frame->stripImage();
$frame->setImageCompressionQuality(80);
}

$imagick = $imagick->deconstructImages();
$imagick->writeImages($imagePath, true);
} else {
$imagick->resizeImage(1000, 1000, Imagick::FILTER_LANCZOS, 1, true);
$imagick->stripImage();
$imagick->setImageCompressionQuality(80);
$imagick->writeImage($imagePath);
}

$imagick->clear();
$imagick->destroy();
}

/**
* Handle the image deletes.
*/
Expand Down
122 changes: 122 additions & 0 deletions app/Services/ImageOptimizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use Imagick;

final readonly class ImageOptimizer
{
/**
* The image path.
*/
private string $image;

/**
* The Imagick instance.
*/
private Imagick $imagick;

/**
* Create a new ImageOptimizer instance.
*/
public function __construct(
private string $path,
private int $width,
private int $height,
private int $quality,
private bool $isThumbnail,
private ?Imagick $instance = null,
) {
$this->image = Storage::disk('public')->path($this->path);
$this->imagick = $this->instance ?? new Imagick($this->image);
$this->optimizeImage();
}

/**
* Static factory method to optimize an image.
*/
public static function optimize(
string $path,
int $width,
int $height,
?int $quality = null,
bool $isThumbnail = false,
): void {
$quality ??= $isThumbnail ? 100 : 80;
new self($path, $width, $height, $quality, $isThumbnail);
}

/**
* Run the optimization process.
*/
private function optimizeImage(): void
{
if ($this->isThumbnail) {
$this->coverDown($this->width, $this->height);
}

$this->imagick->autoOrient();

if ($this->imagick->getNumberImages() > 1) {
$frames = $this->imagick->coalesceImages();

foreach ($frames as $frame) {
$this->resizeStripAndCompressImage($frame);
}

$imagick = $frames->deconstructImages();
$imagick->writeImages($this->image, true);
} else {
$this->resizeStripAndCompressImage($this->imagick);
$this->imagick->writeImage($this->image);
}

$this->imagick->clear();
$this->imagick->destroy();
}

/**
* Crop the image from the centre, while maintaining the desired aspect ratio.
*/
private function coverDown(int $width, int $height): void
{
$originalWidth = $this->imagick->getImageWidth();
$originalHeight = $this->imagick->getImageHeight();

$targetAspect = $width / $height;
$originalAspect = $originalWidth / $originalHeight;

if ($originalAspect > $targetAspect) {
$newHeight = $originalHeight;
$newWidth = (int) round($originalHeight * $targetAspect);
} else {
$newWidth = $originalWidth;
$newHeight = (int) round($originalWidth / $targetAspect);
}

$x = (int) round(($originalWidth - $newWidth) / 2);
$y = (int) round(($originalHeight - $newHeight) / 2);

$this->imagick->cropImage($newWidth, $newHeight, $x, $y);
$this->imagick->setImagePage($newWidth, $newHeight, 0, 0);
}

/**
* Resize, strip and compress the image.
*/
private function resizeStripAndCompressImage(Imagick $instance): void
{
$instance->resizeImage(
$this->width,
$this->height,
Imagick::FILTER_LANCZOS,
1,
true
);
$instance->stripImage();
$instance->setImageCompressionQuality($this->quality);
}
}
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"require": {
"php": "^8.3",
"filament/filament": "^3.2.113",
"intervention/image": "^3.8.0",
"laravel/fortify": "^1.21.1",
"laravel/framework": "^11.23.5",
"laravel/pennant": "^1.11.0",
Expand Down
Loading