Implement creator studio and upload updates
This commit is contained in:
152
tests/Unit/Images/SquareThumbnailServiceTest.php
Normal file
152
tests/Unit/Images/SquareThumbnailServiceTest.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Data\Images\CropBoxData;
|
||||
use App\Services\Images\Detectors\HeuristicSubjectDetector;
|
||||
use App\Services\Images\Detectors\NullSubjectDetector;
|
||||
use App\Services\Images\SquareThumbnailService;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->imageTestRoot = storage_path('framework/testing/square-thumbnails-' . Str::lower(Str::random(10)));
|
||||
File::deleteDirectory($this->imageTestRoot);
|
||||
File::ensureDirectoryExists($this->imageTestRoot);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
File::deleteDirectory($this->imageTestRoot);
|
||||
});
|
||||
|
||||
function squareThumbCreateImage(string $root, int $width, int $height, ?callable $drawer = null): string
|
||||
{
|
||||
$path = $root . DIRECTORY_SEPARATOR . Str::uuid()->toString() . '.png';
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
$background = imagecolorallocate($image, 22, 28, 36);
|
||||
imagefilledrectangle($image, 0, 0, $width, $height, $background);
|
||||
|
||||
if ($drawer !== null) {
|
||||
$drawer($image, $width, $height);
|
||||
}
|
||||
|
||||
imagepng($image, $path);
|
||||
imagedestroy($image);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
it('calculates a padded square crop and clamps it to image bounds', function () {
|
||||
$service = new SquareThumbnailService(new NullSubjectDetector());
|
||||
|
||||
$crop = $service->calculateCropBox(1200, 800, new CropBoxData(10, 40, 260, 180), [
|
||||
'padding_ratio' => 0.2,
|
||||
]);
|
||||
|
||||
expect($crop->width)->toBe($crop->height)
|
||||
->and($crop->x)->toBe(0)
|
||||
->and($crop->y)->toBeGreaterThanOrEqual(0)
|
||||
->and($crop->x + $crop->width)->toBeLessThanOrEqual(1200)
|
||||
->and($crop->y + $crop->height)->toBeLessThanOrEqual(800);
|
||||
});
|
||||
|
||||
it('falls back to a centered square crop when no focus box exists', function () {
|
||||
$service = new SquareThumbnailService(new NullSubjectDetector());
|
||||
|
||||
$crop = $service->calculateCropBox(900, 500, null);
|
||||
|
||||
expect($crop->width)->toBe(500)
|
||||
->and($crop->height)->toBe(500)
|
||||
->and($crop->x)->toBe(200)
|
||||
->and($crop->y)->toBe(0);
|
||||
});
|
||||
|
||||
it('generates a valid square thumbnail when smart detection is unavailable', function () {
|
||||
$source = squareThumbCreateImage($this->imageTestRoot, 640, 320, function ($image): void {
|
||||
$accent = imagecolorallocate($image, 240, 200, 80);
|
||||
imagefilledellipse($image, 320, 160, 180, 180, $accent);
|
||||
});
|
||||
$destination = $this->imageTestRoot . DIRECTORY_SEPARATOR . 'output.webp';
|
||||
|
||||
$service = new SquareThumbnailService(new NullSubjectDetector());
|
||||
$result = $service->generateFromPath($source, $destination, ['target_size' => 256]);
|
||||
$size = getimagesize($destination);
|
||||
|
||||
expect(File::exists($destination))->toBeTrue()
|
||||
->and($result->cropMode)->toBe('center')
|
||||
->and($size[0] ?? null)->toBe(256)
|
||||
->and($size[1] ?? null)->toBe(256);
|
||||
});
|
||||
|
||||
it('does not upscale tiny images when allow_upscale is disabled', function () {
|
||||
$source = squareThumbCreateImage($this->imageTestRoot, 60, 40, function ($image): void {
|
||||
$accent = imagecolorallocate($image, 220, 120, 120);
|
||||
imagefilledrectangle($image, 4, 4, 36, 32, $accent);
|
||||
});
|
||||
$destination = $this->imageTestRoot . DIRECTORY_SEPARATOR . 'tiny.webp';
|
||||
|
||||
$service = new SquareThumbnailService(new NullSubjectDetector());
|
||||
$result = $service->generateFromPath($source, $destination, [
|
||||
'target_size' => 256,
|
||||
'allow_upscale' => false,
|
||||
]);
|
||||
$size = getimagesize($destination);
|
||||
|
||||
expect($result->outputWidth)->toBe(40)
|
||||
->and($result->outputHeight)->toBe(40)
|
||||
->and($size[0] ?? null)->toBe(40)
|
||||
->and($size[1] ?? null)->toBe(40);
|
||||
});
|
||||
|
||||
it('can derive a saliency crop near the border', function () {
|
||||
$source = squareThumbCreateImage($this->imageTestRoot, 900, 600, function ($image): void {
|
||||
$subject = imagecolorallocate($image, 250, 240, 240);
|
||||
imagefilledellipse($image, 120, 240, 180, 220, $subject);
|
||||
});
|
||||
$detector = new HeuristicSubjectDetector();
|
||||
$size = getimagesize($source);
|
||||
|
||||
$result = $detector->detect($source, (int) $size[0], (int) $size[1]);
|
||||
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result?->strategy)->toBe('saliency')
|
||||
->and($result?->cropBox->x)->toBeLessThan(220);
|
||||
});
|
||||
|
||||
it('prefers a distinct subject over textured foliage', function () {
|
||||
$source = squareThumbCreateImage($this->imageTestRoot, 1200, 700, function ($image): void {
|
||||
$sky = imagecolorallocate($image, 165, 205, 255);
|
||||
$leaf = imagecolorallocate($image, 42, 118, 34);
|
||||
$leafDark = imagecolorallocate($image, 28, 88, 24);
|
||||
$branch = imagecolorallocate($image, 96, 72, 52);
|
||||
$fur = imagecolorallocate($image, 242, 236, 228);
|
||||
$ginger = imagecolorallocate($image, 214, 152, 102);
|
||||
$mouth = imagecolorallocate($image, 36, 20, 18);
|
||||
|
||||
imagefilledrectangle($image, 0, 0, 1200, 700, $sky);
|
||||
|
||||
for ($i = 0; $i < 28; $i++) {
|
||||
imageline($image, rand(0, 520), rand(0, 340), rand(80, 620), rand(80, 420), $branch);
|
||||
imagefilledellipse($image, rand(40, 560), rand(80, 620), rand(60, 120), rand(20, 60), $leaf);
|
||||
imagefilledellipse($image, rand(40, 560), rand(80, 620), rand(40, 90), rand(16, 44), $leafDark);
|
||||
}
|
||||
|
||||
imagefilledellipse($image, 890, 300, 420, 500, $fur);
|
||||
imagefilledpolygon($image, [760, 130, 840, 10, 865, 165], 3, $ginger);
|
||||
imagefilledpolygon($image, [930, 165, 1000, 10, 1070, 130], 3, $ginger);
|
||||
imagefilledellipse($image, 885, 355, 150, 220, $mouth);
|
||||
imagefilledellipse($image, 890, 520, 180, 130, $fur);
|
||||
});
|
||||
|
||||
$detector = new HeuristicSubjectDetector();
|
||||
$size = getimagesize($source);
|
||||
|
||||
$result = $detector->detect($source, (int) $size[0], (int) $size[1]);
|
||||
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result?->strategy)->toBe('saliency')
|
||||
->and($result?->cropBox->x)->toBeGreaterThan(180);
|
||||
});
|
||||
Reference in New Issue
Block a user