Use first party image support

This commit is contained in:
DariusIII
2026-07-14 22:41:51 +02:00
parent cb4f4f1083
commit a37f3bd3c8
6 changed files with 214 additions and 65 deletions
+2
View File
@@ -218,6 +218,8 @@ TURNSTILE_ENABLED=false
ITEMS_PER_PAGE=50
ITEMS_PER_COVER_PAGE=25
# Image processing driver. Use gd when the Imagick PHP extension is unavailable.
IMAGE_DRIVER=imagick
MAX_PAGER_RESULTS=125000
ECHOCLI=true
RENAME_PAR2=false
+54 -39
View File
@@ -4,13 +4,12 @@ declare(strict_types=1);
namespace App\Services;
use Illuminate\Image\Image as LaravelImage;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\Exceptions\NotWritableException;
use Intervention\Image\ImageManager;
use Intervention\Image\Interfaces\ImageInterface;
use Spatie\LaravelImageOptimizer\Facades\ImageOptimizer;
use Throwable;
/**
* Resize/save/delete images to disk.
@@ -56,7 +55,7 @@ class ReleaseImageService
$this->vidSavePath = storage_path('covers/video/');
}
protected function fetchImage(string $imgLoc): bool|ImageInterface
protected function fetchImage(string $imgLoc): bool|LaravelImage
{
try {
// Create context with timeout settings for file_get_contents
@@ -107,9 +106,8 @@ class ReleaseImageService
return false;
}
$manager = new ImageManager(Driver::class);
$img = $manager->read($imageData);
} catch (\Exception $e) {
return Image::fromBytes($imageData);
} catch (Throwable $e) {
if ($e->getCode() === 404) {
cli()->notice('Data not available on server');
} elseif ($e->getCode() === 503) {
@@ -118,15 +116,9 @@ class ReleaseImageService
Log::debug('Exception fetching image from '.$imgLoc.': '.$e->getMessage());
cli()->notice('Unable to fetch image: '.$e->getMessage());
}
$img = false;
} catch (\Error $e) {
Log::debug('Error fetching image from '.$imgLoc.': '.$e->getMessage());
cli()->info($e->getMessage());
$img = false;
}
return $img;
return false;
}
/**
@@ -153,34 +145,38 @@ class ReleaseImageService
return 0;
}
// Check if we need to resize it.
if ($imgMaxWidth !== 0 && $imgMaxHeight !== 0) {
$width = $cover->width();
$height = $cover->height();
if ($width !== 0 || $height !== 0) {
$ratio = min($imgMaxHeight / $height, $imgMaxWidth / $width);
// New dimensions
$new_width = (int) ($ratio * $width);
$new_height = (int) ($ratio * $height);
if ($new_width < $width && $new_width > 10 && $new_height > 10) {
$cover->resize($new_width, $new_height);
$coverPath = $imgSavePath.$imgName.'.jpg';
if ($saveThumb) {
$cover->toJpeg(100)->save($imgSavePath.$imgName.'_thumb.jpg');
// Optimize the thumbnail.
ImageOptimizer::optimize($imgSavePath.$imgName.'_thumb.jpg');
try {
$shouldSaveThumb = false;
// Check if we need to resize it.
if ($imgMaxWidth !== 0 && $imgMaxHeight !== 0) {
$width = $cover->width();
$height = $cover->height();
if ($width !== 0 || $height !== 0) {
$ratio = min($imgMaxHeight / $height, $imgMaxWidth / $width);
// New dimensions
$new_width = (int) ($ratio * $width);
$new_height = (int) ($ratio * $height);
if ($new_width < $width && $new_width > 10 && $new_height > 10) {
$cover = $cover->resize($new_width, $new_height);
$shouldSaveThumb = $saveThumb;
}
}
}
}
// Store it on the hard drive.
$coverPath = $imgSavePath.$imgName.'.jpg';
try {
$cover->toJpeg(100)->save($coverPath);
// Optimize the image.
ImageOptimizer::optimize($coverPath);
} catch (NotWritableException $e) {
Log::debug('NotWritableException saving image to '.$coverPath.': '.$e->getMessage());
$jpeg = $cover->toJpeg()->quality(100)->toBytes();
if ($shouldSaveThumb && ! $this->writeOptimizedImage($imgSavePath.$imgName.'_thumb.jpg', $jpeg)) {
return 0;
}
if (! $this->writeOptimizedImage($coverPath, $jpeg)) {
return 0;
}
} catch (Throwable $e) {
Log::debug('Unable to process image '.$imgLoc.' for '.$coverPath.': '.$e->getMessage());
return 0;
}
@@ -194,6 +190,25 @@ class ReleaseImageService
return 1;
}
private function writeOptimizedImage(string $path, string $contents): bool
{
if (File::put($path, $contents) === false) {
Log::debug('Unable to write image to '.$path);
return false;
}
ImageOptimizer::optimize($path);
if (! File::isReadable($path)) {
Log::debug('Image was not readable after save: '.$path);
return false;
}
return true;
}
/**
* Delete images for the release.
*
+1 -1
View File
@@ -51,7 +51,7 @@
"dariusiii/tv-maze-php-api": "^2.0.0",
"elasticsearch/elasticsearch": "^8.19",
"guzzlehttp/guzzle": "^7.9",
"intervention/image": "^3.11",
"intervention/image": "^4.0",
"laravel/framework": "^13.0",
"laravel/horizon": "^5.45",
"laravel/pulse": "^1.7",
Generated
+20 -20
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "aaf81ade6c4cbf0e3f5d46ebd40b715f",
"content-hash": "8c7133089933c92e5fed429b9bc02822",
"packages": [
{
"name": "aharen/omdbapi",
@@ -1734,26 +1734,26 @@
},
{
"name": "intervention/gif",
"version": "4.2.4",
"version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/Intervention/gif.git",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c"
"reference": "bb395af960deffe64d70c976b4df9283f68e762d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c",
"reference": "c3598a16ebe7690cd55640c44144a9df383ea73c",
"url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
"reference": "bb395af960deffe64d70c976b4df9283f68e762d",
"shasum": ""
},
"require": {
"php": "^8.1"
"php": "^8.3"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
"squizlabs/php_codesniffer": "^4"
},
"type": "library",
"autoload": {
@@ -1772,7 +1772,7 @@
"homepage": "https://intervention.io/"
}
],
"description": "Native PHP GIF Encoder/Decoder",
"description": "PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"animation",
@@ -1782,7 +1782,7 @@
],
"support": {
"issues": "https://github.com/Intervention/gif/issues",
"source": "https://github.com/Intervention/gif/tree/4.2.4"
"source": "https://github.com/Intervention/gif/tree/5.0.1"
},
"funding": [
{
@@ -1798,31 +1798,31 @@
"type": "ko_fi"
}
],
"time": "2026-01-04T09:27:23+00:00"
"time": "2026-05-03T06:04:47+00:00"
},
{
"name": "intervention/image",
"version": "3.11.8",
"version": "4.2.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738"
"reference": "830907fc5397dfc2a51a4e90322d586989fc8364"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/cf04c8dd245697f701057c13d4bfe140d584e738",
"reference": "cf04c8dd245697f701057c13d4bfe140d584e738",
"url": "https://api.github.com/repos/Intervention/image/zipball/830907fc5397dfc2a51a4e90322d586989fc8364",
"reference": "830907fc5397dfc2a51a4e90322d586989fc8364",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"intervention/gif": "^4.2",
"php": "^8.1"
"intervention/gif": "^5",
"php": "^8.3"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^4"
},
@@ -1858,7 +1858,7 @@
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/3.11.8"
"source": "https://github.com/Intervention/image/tree/4.2.0"
},
"funding": [
{
@@ -1874,7 +1874,7 @@
"type": "ko_fi"
}
],
"time": "2026-05-01T08:20:10+00:00"
"time": "2026-07-09T13:07:14+00:00"
},
{
"name": "jean85/pretty-package-versions",
+5 -5
View File
@@ -4,17 +4,17 @@ return [
/*
|--------------------------------------------------------------------------
| Image Driver
| Default Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
| This option controls the default image processing driver that will be
| used when manipulating or converting images. This driver is always
| utilized unless another driver is explicitly specified instead.
|
| Supported: "gd", "imagick"
|
*/
'driver' => 'imagick',
'default' => env('IMAGE_DRIVER', 'imagick'),
];
+132
View File
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Services\ReleaseImageService;
use GdImage;
use Illuminate\Support\Facades\File;
use Spatie\LaravelImageOptimizer\Facades\ImageOptimizer;
use Tests\TestCase;
class ReleaseImageServiceTest extends TestCase
{
private string $temporaryDirectory;
protected function setUp(): void
{
parent::setUp();
config(['image.default' => 'gd']);
$this->temporaryDirectory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'release-image-'.uniqid('', true).DIRECTORY_SEPARATOR;
File::makeDirectory($this->temporaryDirectory, 0777, true);
}
protected function tearDown(): void
{
File::deleteDirectory($this->temporaryDirectory);
parent::tearDown();
}
public function test_it_converts_and_proportionally_resizes_a_local_image_to_jpeg(): void
{
$source = $this->createPng('source.png', 400, 200);
$destination = $this->temporaryDirectory.'cover.jpg';
ImageOptimizer::shouldReceive('optimize')->once()->with($destination);
$result = (new ReleaseImageService)->saveImage('cover', $source, $this->temporaryDirectory, 100, 100);
$this->assertSame(1, $result);
$this->assertSame('image/jpeg', File::mimeType($destination));
$this->assertImageDimensions($destination, 100, 50);
}
public function test_it_does_not_upscale_or_create_a_thumbnail_when_no_resize_occurs(): void
{
$source = $this->createPng('small.png', 40, 20);
$destination = $this->temporaryDirectory.'cover.jpg';
ImageOptimizer::shouldReceive('optimize')->once()->with($destination);
$result = (new ReleaseImageService)->saveImage('cover', $source, $this->temporaryDirectory, 250, 250, true);
$this->assertSame(1, $result);
$this->assertImageDimensions($destination, 40, 20);
$this->assertFileDoesNotExist($this->temporaryDirectory.'cover_thumb.jpg');
}
public function test_it_preserves_the_existing_small_dimension_resize_threshold(): void
{
$source = $this->createPng('short.png', 400, 20);
$destination = $this->temporaryDirectory.'cover.jpg';
ImageOptimizer::shouldReceive('optimize')->once()->with($destination);
$result = (new ReleaseImageService)->saveImage('cover', $source, $this->temporaryDirectory, 100, 100);
$this->assertSame(1, $result);
$this->assertImageDimensions($destination, 400, 20);
}
public function test_it_writes_and_optimizes_the_main_image_and_thumbnail(): void
{
$source = $this->createPng('source.png', 200, 100);
$destination = $this->temporaryDirectory.'cover.jpg';
$thumbnail = $this->temporaryDirectory.'cover_thumb.jpg';
ImageOptimizer::shouldReceive('optimize')->once()->with($thumbnail);
ImageOptimizer::shouldReceive('optimize')->once()->with($destination);
$result = (new ReleaseImageService)->saveImage('cover', $source, $this->temporaryDirectory, 100, 100, true);
$this->assertSame(1, $result);
$this->assertImageDimensions($destination, 100, 50);
$this->assertImageDimensions($thumbnail, 100, 50);
}
public function test_it_returns_zero_for_empty_missing_invalid_and_unwritable_inputs(): void
{
ImageOptimizer::shouldReceive('optimize')->never();
$service = new ReleaseImageService;
$this->assertSame(0, $service->saveImage('empty', '', $this->temporaryDirectory));
$this->assertSame(0, $service->saveImage('missing', $this->temporaryDirectory.'missing.png', $this->temporaryDirectory));
$invalid = $this->temporaryDirectory.'invalid.png';
File::put($invalid, 'not an image');
$this->assertSame(0, $service->saveImage('invalid', $invalid, $this->temporaryDirectory));
$source = $this->createPng('source.png', 100, 50);
$notDirectory = $this->temporaryDirectory.'not-a-directory';
File::put($notDirectory, 'file');
$this->assertSame(0, $service->saveImage('unwritable', $source, $notDirectory.DIRECTORY_SEPARATOR));
}
private function createPng(string $filename, int $width, int $height): string
{
$path = $this->temporaryDirectory.$filename;
$image = imagecreatetruecolor($width, $height);
$this->assertInstanceOf(GdImage::class, $image);
$color = imagecolorallocate($image, 50, 100, 150);
imagefill($image, 0, 0, $color);
imagepng($image, $path);
return $path;
}
private function assertImageDimensions(string $path, int $width, int $height): void
{
$dimensions = getimagesize($path);
$this->assertIsArray($dimensions);
$this->assertSame($width, $dimensions[0]);
$this->assertSame($height, $dimensions[1]);
}
}