diff --git a/composer.json b/composer.json index edaf37a5..3aa68173 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "require": { "php": "^8.1", "ext-mbstring": "*", - "intervention/gif": "^4" + "intervention/gif": "^4.0.1" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/src/Drivers/AbstractDecoder.php b/src/Drivers/AbstractDecoder.php index 41e4ee07..9550de96 100644 --- a/src/Drivers/AbstractDecoder.php +++ b/src/Drivers/AbstractDecoder.php @@ -68,21 +68,29 @@ abstract class AbstractDecoder extends DriverSpecialized implements DecoderInter } /** - * Extract and return EXIF data from given image data string + * Extract and return EXIF data from given input which can be binary image + * data or a file path. * - * @param string $image_data + * @param string $path_or_data * @return CollectionInterface */ - protected function extractExifData(string $image_data): CollectionInterface + protected function extractExifData(string $path_or_data): CollectionInterface { if (!function_exists('exif_read_data')) { return new Collection(); } try { - $pointer = $this->buildFilePointer($image_data); - $data = @exif_read_data($pointer, null, true); - fclose($pointer); + $source = match (true) { + (strlen($path_or_data) <= PHP_MAXPATHLEN && is_file($path_or_data)) => $path_or_data, // path + default => $this->buildFilePointer($path_or_data), // data + }; + + // extract exif data + $data = @exif_read_data($source, null, true); + if (is_resource($source)) { + fclose($source); + } } catch (Exception) { $data = []; } diff --git a/src/Drivers/Gd/Decoders/AbstractDecoder.php b/src/Drivers/Gd/Decoders/AbstractDecoder.php new file mode 100644 index 00000000..d75996b2 --- /dev/null +++ b/src/Drivers/Gd/Decoders/AbstractDecoder.php @@ -0,0 +1,53 @@ +isGifFormat($input)) { - return $this->decodeGif($input); // decode (animated) gif - } + $image = match ($this->isGifFormat($input)) { + true => $this->decodeGif($input), + default => $this->decodeBinary($input), + }; - return $this->decodeString($input); + return $image; } - private function decodeString(string $input): ImageInterface + /** + * Decode image from given binary data + * + * @param string $input + * @return ImageInterface + * @throws DecoderException + */ + private function decodeBinary(string $input): ImageInterface { $gd = @imagecreatefromstring($input); @@ -40,63 +53,23 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface throw new DecoderException('Unable to decode input'); } - if (!imageistruecolor($gd)) { - imagepalettetotruecolor($gd); - } - imagesavealpha($gd, true); + // create image instance + $image = parent::decode($gd); - // build image instance - $image = new Image( - new Driver(), - new Core([ - new Frame($gd) - ]), - $this->extractExifData($input) - ); + // extract & set exif data + $image->setExif($this->extractExifData($input)); - if ($info = getimagesizefromstring($input)) { - $image->setOrigin( - new Origin($info['mime']) + try { + // set mediaType on origin + $image->origin()->setMediaType( + $this->getMediaTypeByBinary($input) ); + } catch (DecoderException) { } - // fix image orientation - return match ($image->exif('IFD0.Orientation')) { - 2 => $image->flop(), - 3 => $image->rotate(180), - 4 => $image->rotate(180)->flop(), - 5 => $image->rotate(270)->flop(), - 6 => $image->rotate(270), - 7 => $image->rotate(90)->flop(), - 8 => $image->rotate(90), - default => $image - }; - } + // adjust image orientation + $image->modify(new AlignRotationModifier()); - private function decodeGif(string $input): ImageInterface - { - $gif = GifDecoder::decode($input); - - if (!$gif->isAnimated()) { - return $this->decodeString($input); - } - - $splitter = GifSplitter::create($gif)->split(); - $delays = $splitter->getDelays(); - - // build core - $core = new Core(); - $core->setLoops($gif->getMainApplicationExtension()?->getLoops()); - foreach ($splitter->coalesceToResources() as $key => $data) { - $core->push( - (new Frame($data))->setDelay($delays[$key] / 100) - ); - } - - $image = new Image(new Driver(), $core); - - return $image->setOrigin( - new Origin('image/gif') - ); + return $image; } } diff --git a/src/Drivers/Gd/Decoders/FilePathImageDecoder.php b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php index be0cbff0..c88d636e 100644 --- a/src/Drivers/Gd/Decoders/FilePathImageDecoder.php +++ b/src/Drivers/Gd/Decoders/FilePathImageDecoder.php @@ -5,13 +5,17 @@ declare(strict_types=1); namespace Intervention\Image\Drivers\Gd\Decoders; use Exception; +use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif; use Intervention\Image\Exceptions\DecoderException; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\DecoderInterface; use Intervention\Image\Interfaces\ImageInterface; +use Intervention\Image\Modifiers\AlignRotationModifier; -class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterface +class FilePathImageDecoder extends GdImageDecoder implements DecoderInterface { + use CanDecodeGif; + public function decode(mixed $input): ImageInterface|ColorInterface { if (!is_string($input)) { @@ -30,11 +34,39 @@ class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterfac throw new DecoderException('Unable to decode input'); } - // decode image - $image = parent::decode(file_get_contents($input)); + // detect media (mime) type + $mediaType = $this->getMediaTypeByFilePath($input); - // set file path on origin + $image = match ($mediaType) { + // gif files might be animated and therefore cannot + // be handled by the standard GD decoder. + 'image/gif' => $this->decodeGif($input), + default => parent::decode(match ($mediaType) { + 'image/jpeg', 'image/jpg', 'image/pjpeg' => imagecreatefromjpeg($input), + 'image/webp', 'image/x-webp' => imagecreatefromwebp($input), + 'image/png', 'image/x-png' => imagecreatefrompng($input), + 'image/avif', 'image/x-avif' => imagecreatefromavif($input), + 'image/bmp', + 'image/ms-bmp', + 'image/x-bitmap', + 'image/x-bmp', + 'image/x-ms-bmp', + 'image/x-win-bitmap', + 'image/x-windows-bmp', + 'image/x-xbitmap' => imagecreatefrombmp($input), + default => throw new DecoderException('Unable to decode input'), + }), + }; + + // set file path & mediaType on origin $image->origin()->setFilePath($input); + $image->origin()->setMediaType($mediaType); + + // extract exif + $image->setExif($this->extractExifData($input)); + + // adjust image orientation + $image->modify(new AlignRotationModifier()); return $image; } diff --git a/src/Drivers/Gd/Decoders/GdImageDecoder.php b/src/Drivers/Gd/Decoders/GdImageDecoder.php new file mode 100644 index 00000000..d48b4659 --- /dev/null +++ b/src/Drivers/Gd/Decoders/GdImageDecoder.php @@ -0,0 +1,42 @@ +split(); + $delays = $splitter->getDelays(); + + // build core + $core = new Core(); + + // set loops on core + if ($loops = $gif->getMainApplicationExtension()?->getLoops()) { + $core->setLoops($loops); + } + + // add GDImage instances to core + foreach ($splitter->coalesceToResources() as $key => $native) { + $core->push( + (new Frame($native))->setDelay($delays[$key] / 100) + ); + } + + // create image + $image = new Image(new Driver(), $core); + + // set media type + $image->origin()->setMediaType('image/gif'); + + return $image; + } +} diff --git a/src/Drivers/Gd/Modifiers/AlignRotationModifier.php b/src/Drivers/Gd/Modifiers/AlignRotationModifier.php new file mode 100644 index 00000000..19702f60 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/AlignRotationModifier.php @@ -0,0 +1,26 @@ +exif('IFD0.Orientation')) { + 2 => $image->flop(), + 3 => $image->rotate(180), + 4 => $image->rotate(180)->flop(), + 5 => $image->rotate(270)->flop(), + 6 => $image->rotate(270), + 7 => $image->rotate(90)->flop(), + 8 => $image->rotate(90), + default => $image + }; + } +} diff --git a/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php b/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php index 07679e5d..cd99ad43 100644 --- a/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php +++ b/src/Drivers/Imagick/Decoders/BinaryImageDecoder.php @@ -6,17 +6,12 @@ namespace Intervention\Image\Drivers\Imagick\Decoders; use Imagick; use ImagickException; -use Intervention\Image\Drivers\AbstractDecoder; -use Intervention\Image\Drivers\Imagick\Core; -use Intervention\Image\Drivers\Imagick\Driver; use Intervention\Image\Exceptions\DecoderException; -use Intervention\Image\Image; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\DecoderInterface; use Intervention\Image\Interfaces\ImageInterface; -use Intervention\Image\Origin; -class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface +class BinaryImageDecoder extends ImagickImageDecoder implements DecoderInterface { public function decode(mixed $input): ImageInterface|ColorInterface { @@ -31,59 +26,11 @@ class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface throw new DecoderException('Unable to decode input'); } - // For some JPEG formats, the "coalesceImages()" call leads to an image - // completely filled with background color. The logic behind this is - // incomprehensible for me; could be an imagick bug. - if ($imagick->getImageFormat() != 'JPEG') { - $imagick = $imagick->coalesceImages(); - } + // decode image + $image = parent::decode($imagick); - // fix image orientation - switch ($imagick->getImageOrientation()) { - case Imagick::ORIENTATION_TOPRIGHT: // 2 - $imagick->flopImage(); - break; - - case Imagick::ORIENTATION_BOTTOMRIGHT: // 3 - $imagick->rotateimage("#000", 180); - break; - - case Imagick::ORIENTATION_BOTTOMLEFT: // 4 - $imagick->rotateimage("#000", 180); - $imagick->flopImage(); - break; - - case Imagick::ORIENTATION_LEFTTOP: // 5 - $imagick->rotateimage("#000", -270); - $imagick->flopImage(); - break; - - case Imagick::ORIENTATION_RIGHTTOP: // 6 - $imagick->rotateimage("#000", -270); - break; - - case Imagick::ORIENTATION_RIGHTBOTTOM: // 7 - $imagick->rotateimage("#000", -90); - $imagick->flopImage(); - break; - - case Imagick::ORIENTATION_LEFTBOTTOM: // 8 - $imagick->rotateimage("#000", -90); - break; - } - - // set new orientation in image - $imagick->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); - - $image = new Image( - new Driver(), - new Core($imagick), - $this->extractExifData($input) - ); - - $image->setOrigin(new Origin( - $imagick->getImageMimeType() - )); + // extract exif data + $image->setExif($this->extractExifData($input)); return $image; } diff --git a/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php b/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php index 687a2375..7d3d2274 100644 --- a/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php +++ b/src/Drivers/Imagick/Decoders/FilePathImageDecoder.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace Intervention\Image\Drivers\Imagick\Decoders; use Exception; +use Imagick; +use ImagickException; use Intervention\Image\Exceptions\DecoderException; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\DecoderInterface; use Intervention\Image\Interfaces\ImageInterface; -class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterface +class FilePathImageDecoder extends ImagickImageDecoder implements DecoderInterface { public function decode(mixed $input): ImageInterface|ColorInterface { @@ -30,12 +32,22 @@ class FilePathImageDecoder extends BinaryImageDecoder implements DecoderInterfac throw new DecoderException('Unable to decode input'); } + try { + $imagick = new Imagick(); + $imagick->readImage($input); + } catch (ImagickException) { + throw new DecoderException('Unable to decode input'); + } + // decode image - $image = parent::decode(file_get_contents($input)); + $image = parent::decode($imagick); // set file path on origin $image->origin()->setFilePath($input); + // extract exif data + $image->setExif($this->extractExifData($input)); + return $image; } } diff --git a/src/Drivers/Imagick/Decoders/ImagickImageDecoder.php b/src/Drivers/Imagick/Decoders/ImagickImageDecoder.php new file mode 100644 index 00000000..954832f6 --- /dev/null +++ b/src/Drivers/Imagick/Decoders/ImagickImageDecoder.php @@ -0,0 +1,50 @@ +getImageFormat() != 'JPEG') { + $input = $input->coalesceImages(); + } + + $image = new Image( + new Driver(), + new Core($input) + ); + + // adjust image rotatation + $image->modify(new AlignRotationModifier()); + + // set media type on origin + $image->origin()->setMediaType($input->getImageMimeType()); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Modifiers/AlignRotationModifier.php b/src/Drivers/Imagick/Modifiers/AlignRotationModifier.php new file mode 100644 index 00000000..f0f1a5bc --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/AlignRotationModifier.php @@ -0,0 +1,54 @@ +core()->native()->getImageOrientation()) { + case Imagick::ORIENTATION_TOPRIGHT: // 2 + $image->core()->native()->flopImage(); + break; + + case Imagick::ORIENTATION_BOTTOMRIGHT: // 3 + $image->core()->native()->rotateimage("#000", 180); + break; + + case Imagick::ORIENTATION_BOTTOMLEFT: // 4 + $image->core()->native()->rotateimage("#000", 180); + $image->core()->native()->flopImage(); + break; + + case Imagick::ORIENTATION_LEFTTOP: // 5 + $image->core()->native()->rotateimage("#000", -270); + $image->core()->native()->flopImage(); + break; + + case Imagick::ORIENTATION_RIGHTTOP: // 6 + $image->core()->native()->rotateimage("#000", -270); + break; + + case Imagick::ORIENTATION_RIGHTBOTTOM: // 7 + $image->core()->native()->rotateimage("#000", -90); + $image->core()->native()->flopImage(); + break; + + case Imagick::ORIENTATION_LEFTBOTTOM: // 8 + $image->core()->native()->rotateimage("#000", -90); + break; + } + + // set new orientation in image + $image->core()->native()->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); + + return $image; + } +} diff --git a/src/Encoders/MediaTypeEncoder.php b/src/Encoders/MediaTypeEncoder.php index 8072c74e..591c637a 100644 --- a/src/Encoders/MediaTypeEncoder.php +++ b/src/Encoders/MediaTypeEncoder.php @@ -40,15 +40,30 @@ class MediaTypeEncoder extends SpecializableEncoder implements EncoderInterface protected function encoderByMediaType(string $type): EncoderInterface { return match (strtolower($type)) { - 'image/webp' => new WebpEncoder(quality: $this->quality), - 'image/avif' => new AvifEncoder(quality: $this->quality), - 'image/jpeg' => new JpegEncoder(quality: $this->quality), - 'image/bmp' => new BmpEncoder(), + 'image/webp', + 'image/x-webp' => new WebpEncoder(quality: $this->quality), + 'image/avif', + 'image/x-avif' => new AvifEncoder(quality: $this->quality), + 'image/jpeg', + 'image/jpg', + 'image/pjpeg' => new JpegEncoder(quality: $this->quality), + 'image/bmp', + 'image/ms-bmp', + 'image/x-bitmap', + 'image/x-bmp', + 'image/x-ms-bmp', + 'image/x-win-bitmap', + 'image/x-windows-bmp', + 'image/x-xbitmap' => new BmpEncoder(), 'image/gif' => new GifEncoder(), - 'image/png' => new PngEncoder(), + 'image/png', + 'image/x-png' => new PngEncoder(), 'image/tiff' => new TiffEncoder(quality: $this->quality), - 'image/jp2', 'image/jpx', 'image/jpm' => new Jpeg2000Encoder(quality: $this->quality), - 'image/heic', 'image/heif', => new HeicEncoder(quality: $this->quality), + 'image/jp2', + 'image/jpx', + 'image/jpm' => new Jpeg2000Encoder(quality: $this->quality), + 'image/heic', + 'image/heif', => new HeicEncoder(quality: $this->quality), default => throw new EncoderException('No encoder found for media type (' . $type . ').'), }; } diff --git a/src/Image.php b/src/Image.php index 75103904..60968ac5 100644 --- a/src/Image.php +++ b/src/Image.php @@ -249,6 +249,18 @@ final class Image implements ImageInterface return is_null($query) ? $this->exif : $this->exif->get($query); } + /** + * {@inheritdoc} + * + * @see ImgageInterface::setExif() + */ + public function setExif(CollectionInterface $exif): ImageInterface + { + $this->exif = $exif; + + return $this; + } + /** * {@inheritdoc} * diff --git a/src/Interfaces/ImageInterface.php b/src/Interfaces/ImageInterface.php index 4694e524..41633a5e 100644 --- a/src/Interfaces/ImageInterface.php +++ b/src/Interfaces/ImageInterface.php @@ -144,6 +144,14 @@ interface ImageInterface extends IteratorAggregate, Countable */ public function exif(?string $query = null): mixed; + /** + * Set exif data for the image object + * + * @param CollectionInterface $exif + * @return ImageInterface + */ + public function setExif(CollectionInterface $exif): ImageInterface; + /** * Return image resolution/density * diff --git a/src/Modifiers/AlignRotationModifier.php b/src/Modifiers/AlignRotationModifier.php new file mode 100644 index 00000000..41d400d0 --- /dev/null +++ b/src/Modifiers/AlignRotationModifier.php @@ -0,0 +1,9 @@ +mediaType(); } + /** + * Set media type of current instance + * + * @param string $type + * @return Origin + */ + public function setMediaType(string $type): self + { + $this->mediaType = $type; + + return $this; + } + /** * Return file path of origin * diff --git a/tests/Analyzers/SpecializableAnalyzerTest.php b/tests/Analyzers/SpecializableAnalyzerTest.php new file mode 100644 index 00000000..42ad2224 --- /dev/null +++ b/tests/Analyzers/SpecializableAnalyzerTest.php @@ -0,0 +1,23 @@ +makePartial(); + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('analyze')->andReturn('test'); + + $result = $analyzer->analyze($image); + $this->assertEquals('test', $result); + } +} diff --git a/tests/Colors/Cmyk/Decoders/StringColorDecoderTest.php b/tests/Colors/Cmyk/Decoders/StringColorDecoderTest.php index f73093c8..2dd30ed8 100644 --- a/tests/Colors/Cmyk/Decoders/StringColorDecoderTest.php +++ b/tests/Colors/Cmyk/Decoders/StringColorDecoderTest.php @@ -6,6 +6,7 @@ namespace Intervention\Image\Tests\Colors\Cmyk\Decoders; use Intervention\Image\Colors\Cmyk\Color; use Intervention\Image\Colors\Cmyk\Decoders\StringColorDecoder; +use Intervention\Image\Exceptions\DecoderException; use Intervention\Image\Tests\TestCase; /** @@ -29,9 +30,15 @@ class StringColorDecoderTest extends TestCase $this->assertInstanceOf(Color::class, $result); $this->assertEquals([0, 100, 100, 0], $result->toArray()); - $result = $decoder->decode('cmyk(0%, 100%, 100%, 0%)'); $this->assertInstanceOf(Color::class, $result); $this->assertEquals([0, 100, 100, 0], $result->toArray()); } + + public function testDecodeInvalid(): void + { + $decoder = new StringColorDecoder(); + $this->expectException(DecoderException::class); + $decoder->decode(null); + } } diff --git a/tests/Drivers/AbstractDecoderTest.php b/tests/Drivers/AbstractDecoderTest.php index 7dfb505f..c5ce37a6 100644 --- a/tests/Drivers/AbstractDecoderTest.php +++ b/tests/Drivers/AbstractDecoderTest.php @@ -7,6 +7,7 @@ namespace Intervention\Image\Tests\Drivers; use Exception; use Intervention\Image\Drivers\AbstractDecoder; use Intervention\Image\Exceptions\DecoderException; +use Intervention\Image\Interfaces\CollectionInterface; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Tests\TestCase; @@ -51,6 +52,29 @@ class AbstractDecoderTest extends TestCase $decoder->handle('test input'); } + public function testIsGifFormat(): void + { + $decoder = Mockery::mock(AbstractDecoder::class)->makePartial(); + $this->assertFalse($decoder->isGifFormat($this->getTestImageData('exif.jpg'))); + $this->assertTrue($decoder->isGifFormat($this->getTestImageData('red.gif'))); + } + + public function testExtractExifDataFromBinary(): void + { + $decoder = Mockery::mock(AbstractDecoder::class)->makePartial(); + $result = $decoder->extractExifData($this->getTestImageData('exif.jpg')); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals('Oliver Vogel', $result->get('IFD0.Artist')); + } + + public function testExtractExifDataFromPath(): void + { + $decoder = Mockery::mock(AbstractDecoder::class)->makePartial(); + $result = $decoder->extractExifData($this->getTestImagePath('exif.jpg')); + $this->assertInstanceOf(CollectionInterface::class, $result); + $this->assertEquals('Oliver Vogel', $result->get('IFD0.Artist')); + } + public function testParseDataUri(): void { $decoder = new class () extends AbstractDecoder diff --git a/tests/Drivers/Gd/ColorProcessorTest.php b/tests/Drivers/Gd/ColorProcessorTest.php new file mode 100644 index 00000000..c33b71e2 --- /dev/null +++ b/tests/Drivers/Gd/ColorProcessorTest.php @@ -0,0 +1,42 @@ +colorToNative(new Color(255, 55, 0, 255)); + $this->assertEquals(16725760, $result); + } + + public function testNativeToColor(): void + { + $processor = new ColorProcessor(); + $result = $processor->nativeToColor(16725760); + $this->assertInstanceOf(Color::class, $result); + $this->assertEquals(255, $result->channel(Red::class)->value()); + $this->assertEquals(55, $result->channel(Green::class)->value()); + $this->assertEquals(0, $result->channel(Blue::class)->value()); + $this->assertEquals(255, $result->channel(Alpha::class)->value()); + } + + public function testNativeToColorInvalid(): void + { + $processor = new ColorProcessor(); + $this->expectException(ColorException::class); + $processor->nativeToColor('test'); + } +} diff --git a/tests/Drivers/Gd/Decoders/AbstractDecoderTest.php b/tests/Drivers/Gd/Decoders/AbstractDecoderTest.php new file mode 100644 index 00000000..a77c3dd3 --- /dev/null +++ b/tests/Drivers/Gd/Decoders/AbstractDecoderTest.php @@ -0,0 +1,24 @@ +makePartial(); + $this->assertEquals('image/jpeg', $decoder->getMediaTypeByFilePath($this->getTestImagePath('test.jpg'))); + } + + public function testGetMediaTypeFromFileBinary(): void + { + $decoder = Mockery::mock(AbstractDecoder::class)->makePartial(); + $this->assertEquals('image/jpeg', $decoder->getMediaTypeByBinary($this->getTestImageData('test.jpg'))); + } +} diff --git a/tests/Drivers/Gd/DriverTest.php b/tests/Drivers/Gd/DriverTest.php index ab3af1e2..ff6fb3f0 100644 --- a/tests/Drivers/Gd/DriverTest.php +++ b/tests/Drivers/Gd/DriverTest.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Gd; +use Intervention\Image\Colors\Rgb\Colorspace; +use Intervention\Image\Colors\Rgb\Decoders\HexColorDecoder; use Intervention\Image\Drivers\Gd\Driver; +use Intervention\Image\Interfaces\ColorInterface; +use Intervention\Image\Interfaces\ColorProcessorInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Tests\TestCase; @@ -43,4 +47,38 @@ class DriverTest extends TestCase $this->assertEquals(5, $image->loops()); $this->assertEquals(2, $image->count()); } + + public function testHandleInputImage(): void + { + $result = $this->driver->handleInput($this->getTestImagePath('test.jpg')); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testHandleInputColor(): void + { + $result = $this->driver->handleInput('ffffff'); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testHandleInputObjects(): void + { + $result = $this->driver->handleInput('ffffff', [ + new HexColorDecoder() + ]); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testHandleInputClassnames(): void + { + $result = $this->driver->handleInput('ffffff', [ + HexColorDecoder::class + ]); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testColorProcessor(): void + { + $result = $this->driver->colorProcessor(new Colorspace()); + $this->assertInstanceOf(ColorProcessorInterface::class, $result); + } } diff --git a/tests/Drivers/Gd/Modifiers/QuantizeColorsModifierTest.php b/tests/Drivers/Gd/Modifiers/QuantizeColorsModifierTest.php index 8d0435ec..8343b40d 100644 --- a/tests/Drivers/Gd/Modifiers/QuantizeColorsModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/QuantizeColorsModifierTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Gd\Modifiers; +use Intervention\Image\Exceptions\InputException; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Modifiers\QuantizeColorsModifier; use Intervention\Image\Tests\TestCase; @@ -26,6 +27,21 @@ class QuantizeColorsModifierTest extends TestCase $this->assertColorCount(4, $image); } + public function testNoColorReduction(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->assertColorCount(15, $image); + $image->modify(new QuantizeColorsModifier(150)); + $this->assertColorCount(15, $image); + } + + public function testInvalidColorInput(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->expectException(InputException::class); + $image->modify(new QuantizeColorsModifier(0)); + } + private function assertColorCount(int $count, ImageInterface $image): void { $colors = []; diff --git a/tests/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php b/tests/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php index f46950da..ea190c6a 100644 --- a/tests/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php +++ b/tests/Drivers/Gd/Modifiers/RemoveAnimationModifierTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Gd\Modifiers; +use Intervention\Image\Exceptions\InputException; use Intervention\Image\Modifiers\RemoveAnimationModifier; use Intervention\Image\Tests\TestCase; use Intervention\Image\Tests\Traits\CanCreateGdTestImage; @@ -34,4 +35,11 @@ class RemoveAnimationModifierTest extends TestCase $this->assertEquals(1, count($image)); $this->assertEquals(1, count($result)); } + + public function testApplyInvalid(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(InputException::class); + $image->modify(new RemoveAnimationModifier('test')); + } } diff --git a/tests/Drivers/Gd/Traits/CanDecodeGifTest.php b/tests/Drivers/Gd/Traits/CanDecodeGifTest.php new file mode 100644 index 00000000..44d2299a --- /dev/null +++ b/tests/Drivers/Gd/Traits/CanDecodeGifTest.php @@ -0,0 +1,57 @@ +makePartial(); + + $result = $decoder->decodeGif($this->getTestImageData('animation.gif')); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('image/gif', $result->origin()->mediaType()); + } + + public function testDecodeGifFromBinaryStatic(): void + { + $decoder = Mockery::mock(new class () { + use CanDecodeGif; + })->makePartial(); + + $result = $decoder->decodeGif($this->getTestImageData('red.gif')); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('image/gif', $result->origin()->mediaType()); + } + + public function testDecodeGifFromPathAnimation(): void + { + $decoder = Mockery::mock(new class () { + use CanDecodeGif; + })->makePartial(); + + $result = $decoder->decodeGif($this->getTestImagePath('animation.gif')); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('image/gif', $result->origin()->mediaType()); + } + + public function testDecodeGifFromPathStatic(): void + { + $decoder = Mockery::mock(new class () { + use CanDecodeGif; + })->makePartial(); + + $result = $decoder->decodeGif($this->getTestImagePath('red.gif')); + $this->assertInstanceOf(ImageInterface::class, $result); + $this->assertEquals('image/gif', $result->origin()->mediaType()); + } +} diff --git a/tests/Drivers/Imagick/ColorProcessorTest.php b/tests/Drivers/Imagick/ColorProcessorTest.php new file mode 100644 index 00000000..5515b581 --- /dev/null +++ b/tests/Drivers/Imagick/ColorProcessorTest.php @@ -0,0 +1,27 @@ +colorToNative(new Color(255, 55, 0, 255)); + $this->assertInstanceOf(ImagickPixel::class, $result); + } + + public function testNativeToColor(): void + { + $processor = new ColorProcessor(new Colorspace()); + $result = $processor->nativeToColor(new ImagickPixel('rgb(255, 55, 0)')); + } +} diff --git a/tests/Drivers/Imagick/DriverTest.php b/tests/Drivers/Imagick/DriverTest.php index 36b51a40..b497a4f0 100644 --- a/tests/Drivers/Imagick/DriverTest.php +++ b/tests/Drivers/Imagick/DriverTest.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Imagick; +use Intervention\Image\Colors\Rgb\Colorspace; +use Intervention\Image\Colors\Rgb\Decoders\HexColorDecoder; use Intervention\Image\Drivers\Imagick\Driver; +use Intervention\Image\Interfaces\ColorInterface; +use Intervention\Image\Interfaces\ColorProcessorInterface; use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Tests\TestCase; @@ -43,4 +47,38 @@ class DriverTest extends TestCase $this->assertEquals(5, $image->loops()); $this->assertEquals(2, $image->count()); } + + public function testHandleInputImage(): void + { + $result = $this->driver->handleInput($this->getTestImagePath('test.jpg')); + $this->assertInstanceOf(ImageInterface::class, $result); + } + + public function testHandleInputColor(): void + { + $result = $this->driver->handleInput('ffffff'); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testHandleInputObjects(): void + { + $result = $this->driver->handleInput('ffffff', [ + new HexColorDecoder() + ]); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testHandleInputClassnames(): void + { + $result = $this->driver->handleInput('ffffff', [ + HexColorDecoder::class + ]); + $this->assertInstanceOf(ColorInterface::class, $result); + } + + public function testColorProcessor(): void + { + $result = $this->driver->colorProcessor(new Colorspace()); + $this->assertInstanceOf(ColorProcessorInterface::class, $result); + } } diff --git a/tests/Drivers/Imagick/Modifiers/QuantizeColorsModifierTest.php b/tests/Drivers/Imagick/Modifiers/QuantizeColorsModifierTest.php index cf0e860b..29bc62ef 100644 --- a/tests/Drivers/Imagick/Modifiers/QuantizeColorsModifierTest.php +++ b/tests/Drivers/Imagick/Modifiers/QuantizeColorsModifierTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Imagick\Modifiers; +use Intervention\Image\Exceptions\InputException; use Intervention\Image\Modifiers\QuantizeColorsModifier; use Intervention\Image\Tests\TestCase; use Intervention\Image\Tests\Traits\CanCreateImagickTestImage; @@ -24,4 +25,19 @@ class QuantizeColorsModifierTest extends TestCase $image->modify(new QuantizeColorsModifier(4)); $this->assertEquals(4, $image->core()->native()->getImageColors()); } + + public function testNoColorReduction(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->assertEquals(15, $image->core()->native()->getImageColors()); + $image->modify(new QuantizeColorsModifier(150)); + $this->assertEquals(15, $image->core()->native()->getImageColors()); + } + + public function testInvalidColorInput(): void + { + $image = $this->readTestImage('gradient.bmp'); + $this->expectException(InputException::class); + $image->modify(new QuantizeColorsModifier(0)); + } } diff --git a/tests/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php b/tests/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php index 9ca4c126..c12fb96e 100644 --- a/tests/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php +++ b/tests/Drivers/Imagick/Modifiers/RemoveAnimationModifierTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Intervention\Image\Tests\Drivers\Imagick\Modifiers; +use Intervention\Image\Exceptions\InputException; use Intervention\Image\Modifiers\RemoveAnimationModifier; use Intervention\Image\Tests\TestCase; use Intervention\Image\Tests\Traits\CanCreateImagickTestImage; @@ -34,4 +35,11 @@ class RemoveAnimationModifierTest extends TestCase $this->assertEquals(1, count($image)); $this->assertEquals(1, count($result)); } + + public function testApplyInvalid(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(InputException::class); + $image->modify(new RemoveAnimationModifier('test')); + } } diff --git a/tests/Encoders/SpecializableEncoderTest.php b/tests/Encoders/SpecializableEncoderTest.php new file mode 100644 index 00000000..a6eb7c91 --- /dev/null +++ b/tests/Encoders/SpecializableEncoderTest.php @@ -0,0 +1,52 @@ +assertEquals(75, $encoder->quality); + } + + public function testConstructorList(): void + { + $encoder = new class (1) extends SpecializableEncoder + { + }; + + $this->assertEquals(1, $encoder->quality); + } + + public function testConstructorNamed(): void + { + $encoder = new class (quality: 1) extends SpecializableEncoder + { + }; + + $this->assertEquals(1, $encoder->quality); + } + + public function testEncode(): void + { + $encoder = Mockery::mock(SpecializableEncoder::class)->makePartial(); + $image = Mockery::mock(ImageInterface::class); + $encoded = Mockery::mock(EncodedImage::class); + $image->shouldReceive('encode')->andReturn($encoded); + + $result = $encoder->encode($image); + $this->assertInstanceOf(EncodedImage::class, $result); + } +} diff --git a/tests/ImageManagerTest.php b/tests/ImageManagerTest.php index 5a60b843..1a1d2680 100644 --- a/tests/ImageManagerTest.php +++ b/tests/ImageManagerTest.php @@ -109,6 +109,14 @@ class ImageManagerTest extends TestCase $this->assertInstanceOf(ImageInterface::class, $image); } + /** @requires extension gd */ + public function testReadGdWithRotationAdjustment(): void + { + $manager = new ImageManager(GdDriver::class); + $image = $manager->read(__DIR__ . '/images/orientation.jpg'); + $this->assertColor(255, 255, 255, 255, $image->pickColor(0, 24)); + } + /** @requires extension imagick */ public function testCreateImagick() { @@ -174,4 +182,12 @@ class ImageManagerTest extends TestCase $image = $manager->read(__DIR__ . '/images/red.gif', [new BinaryImageDecoder(), new FilePathImageDecoder()]); $this->assertInstanceOf(ImageInterface::class, $image); } + + /** @requires extension imagick */ + public function testReadImagickWithRotationAdjustment(): void + { + $manager = new ImageManager(ImagickDriver::class); + $image = $manager->read(__DIR__ . '/images/orientation.jpg'); + $this->assertColor(255, 255, 255, 255, $image->pickColor(0, 24)); + } } diff --git a/tests/Modifiers/SpecializableModifierTest.php b/tests/Modifiers/SpecializableModifierTest.php new file mode 100644 index 00000000..075b44fc --- /dev/null +++ b/tests/Modifiers/SpecializableModifierTest.php @@ -0,0 +1,23 @@ +makePartial(); + $image = Mockery::mock(ImageInterface::class); + $image->shouldReceive('modify')->andReturn($image); + + $result = $modifier->apply($image); + $this->assertInstanceOf(ImageInterface::class, $result); + } +} diff --git a/tests/OriginTest.php b/tests/OriginTest.php index 0ee86e1d..88ec9673 100644 --- a/tests/OriginTest.php +++ b/tests/OriginTest.php @@ -8,16 +8,6 @@ use Intervention\Image\Origin; class OriginTest extends TestCase { - public function testMediaType(): void - { - $origin = new Origin(); - $this->assertEquals('application/octet-stream', $origin->mediaType()); - - $origin = new Origin('image/gif'); - $this->assertEquals('image/gif', $origin->mediaType()); - $this->assertEquals('image/gif', $origin->mimetype()); - } - public function testFilePath(): void { $origin = new Origin('image/jpeg', __DIR__ . '/tests/images/example.jpg'); @@ -32,4 +22,17 @@ class OriginTest extends TestCase $origin = new Origin('image/jpeg'); $this->assertEquals('', $origin->fileExtension()); } + + public function testSetGetMediaType(): void + { + $origin = new Origin(); + $this->assertEquals('application/octet-stream', $origin->mediaType()); + + $origin = new Origin('image/gif'); + $this->assertEquals('image/gif', $origin->mediaType()); + $this->assertEquals('image/gif', $origin->mimetype()); + $result = $origin->setMediaType('image/jpeg'); + $this->assertEquals('image/jpeg', $origin->mediaType()); + $this->assertEquals('image/jpeg', $result->mediaType()); + } } diff --git a/tests/images/orientation.jpg b/tests/images/orientation.jpg new file mode 100644 index 00000000..49c16de2 Binary files /dev/null and b/tests/images/orientation.jpg differ