generated from Nodarx/template
feat: add ImageMagick preview provider
Implements the shared preview provider contract using ImageMagick to generate thumbnails for supported source types.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<?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('popover', 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('popover', 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(
|
||||
'popover',
|
||||
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('popover', 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user