Files
SkinbaseNova/tests/Unit/Images/SquareThumbnailServiceTest.php

152 lines
5.8 KiB
PHP

<?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);
});