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