Files
provider_preview_imagemagick/tests/php/Unit/ProviderTest.php
T
2026-08-29 18:45:22 -04:00

225 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\ProviderPreviewImageMagick\Unit;
use Imagick;
use ImagickPixel;
use KTXC\Resource\ProviderManager;
use KTXF\Preview\PreviewGenerationException;
use KTXF\Preview\PreviewRequest;
use KTXF\Preview\PreviewSource;
use KTXF\Resource\BinaryResource;
use KTXF\Resource\Provider\ProviderInterface;
use KTXM\ProviderPreviewImageMagick\Module;
use KTXM\ProviderPreviewImageMagick\Provider;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ProviderTest extends TestCase
{
#[Test]
public function moduleRegistersThePreviewProvider(): void
{
$providers = $this->createMock(ProviderManager::class);
$providers->expects(self::once())->method('register')->with(
ProviderInterface::TYPE_PREVIEW,
'imagemagick',
Provider::class,
);
(new Module($providers))->boot();
}
public static function inputFormats(): iterable
{
yield 'JPEG' => ['JPEG', 'image/jpeg'];
yield 'PNG' => ['PNG', 'image/png'];
yield 'WebP' => ['WEBP', 'image/webp'];
yield 'GIF' => ['GIF', 'image/gif'];
}
#[Test]
#[DataProvider('inputFormats')]
public function generatesBoundedWebpFromSupportedInputs(string $format, string $mimeType): void
{
$provider = new Provider();
$result = $provider->generate(
$this->source($this->image($format, 800, 400), $mimeType),
$this->request(),
);
$output = $this->content($result->resource);
$image = new Imagick();
$image->readImageBlob($output);
self::assertSame('image/webp', $result->resource->mimeType());
self::assertSame(640, $result->width);
self::assertSame(320, $result->height);
self::assertSame('WEBP', $image->getImageFormat());
self::assertSame(640, $image->getImageWidth());
self::assertSame(320, $image->getImageHeight());
$image->clear();
}
#[Test]
public function doesNotUpscaleSmallImages(): void
{
$result = (new Provider())->generate(
$this->source($this->image('PNG', 100, 50), 'image/png'),
$this->request(),
);
self::assertSame(100, $result->width);
self::assertSame(50, $result->height);
}
public static function outputFormats(): iterable
{
yield 'JPEG' => ['image/jpeg', 'JPEG'];
yield 'PNG' => ['image/png', 'PNG'];
yield 'WebP' => ['image/webp', 'WEBP'];
}
#[Test]
#[DataProvider('outputFormats')]
public function encodesEveryAdvertisedOutputFormat(string $mimeType, string $format): void
{
$request = new PreviewRequest(640, 440, $mimeType, 80);
$result = (new Provider())->generate(
$this->source($this->image('PNG', 20, 20, 'rgba(255, 0, 0, 0.25)'), 'image/png'),
$request,
);
$image = new Imagick();
$image->readImageBlob($this->content($result->resource));
self::assertSame($mimeType, $result->resource->mimeType());
self::assertSame($format, $image->getImageFormat());
$image->clear();
}
#[Test]
public function preservesTransparencyInPngOutput(): void
{
$request = new PreviewRequest(640, 440, 'image/png', 80);
$result = (new Provider())->generate(
$this->source($this->image('PNG', 20, 20, 'rgba(255, 0, 0, 0.25)'), 'image/png'),
$request,
);
$image = new Imagick();
$image->readImageBlob($this->content($result->resource));
$color = $image->getImagePixelColor(0, 0)->getColor(1);
self::assertSame('image/png', $result->resource->mimeType());
self::assertLessThan(0.5, $color['a']);
$image->clear();
}
#[Test]
public function usesOnlyTheFirstAnimatedFrame(): void
{
$animation = new Imagick();
foreach (['red', 'blue'] as $color) {
$frame = new Imagick();
$frame->newImage(20, 20, new ImagickPixel($color));
$frame->setImageFormat('GIF');
$frame->setImageDelay(10);
$animation->addImage($frame);
$frame->clear();
}
$content = $animation->getImagesBlob();
$animation->clear();
$result = (new Provider())->generate($this->source($content, 'image/gif'), $this->request());
$output = new Imagick();
$output->readImageBlob($this->content($result->resource));
self::assertSame(1, $output->getNumberImages());
$color = $output->getImagePixelColor(0, 0)->getColor();
self::assertGreaterThan($color['b'], $color['r']);
$output->clear();
}
#[Test]
public function rejectsMalformedOrMismatchedImages(): void
{
$provider = new Provider();
try {
$provider->generate($this->source('not an image', 'image/png'), $this->request());
self::fail('Malformed input should fail');
} catch (PreviewGenerationException) {
self::addToAssertionCount(1);
}
$this->expectException(PreviewGenerationException::class);
$this->expectExceptionMessage('does not match');
$provider->generate(
$this->source($this->image('PNG', 20, 20), 'image/jpeg'),
$this->request(),
);
}
#[Test]
public function enforcesTheSourceSizeLimitWhileReading(): void
{
$content = $this->image('PNG', 20, 20);
$request = new PreviewRequest(
640,
440,
'image/webp',
80,
maxSourceSize: strlen($content) - 1,
);
$this->expectException(PreviewGenerationException::class);
$this->expectExceptionMessage('size limit');
(new Provider())->generate($this->source($content, 'image/png', size: null), $request);
}
private function request(): PreviewRequest
{
return new PreviewRequest(640, 440, 'image/webp', 80);
}
private function source(string $content, string $mimeType, ?int $size = null): PreviewSource
{
return new PreviewSource(
'document',
'image',
hash('sha256', $content),
$mimeType,
$size ?? strlen($content),
static fn(): BinaryResource => new BinaryResource(
'source.bin',
$mimeType,
(static function () use ($content): \Generator {
$middle = intdiv(strlen($content), 2);
yield substr($content, 0, $middle);
yield substr($content, $middle);
})(),
),
);
}
private function image(string $format, int $width, int $height, string $color = 'red'): string
{
$image = new Imagick();
$image->newImage($width, $height, new ImagickPixel($color));
$image->setImageFormat($format);
$content = $image->getImageBlob();
$image->clear();
return $content;
}
private function content(BinaryResource $resource): string
{
$content = '';
foreach ($resource->stream() as $chunk) {
$content .= $chunk;
}
return $content;
}
}