1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-30 09:10:21 +02:00

Merge branch 'feature/decoding-optimization' into develop

This commit is contained in:
Oliver Vogel
2024-01-27 18:43:12 +01:00
35 changed files with 879 additions and 151 deletions

View File

@@ -21,7 +21,7 @@
"require": {
"php": "^8.1",
"ext-mbstring": "*",
"intervention/gif": "^4"
"intervention/gif": "^4.0.1"
},
"require-dev": {
"phpunit/phpunit": "^9",

View File

@@ -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 = [];
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\AbstractDecoder as GenericAbstractDecoder;
use Intervention\Image\Exceptions\DecoderException;
abstract class AbstractDecoder extends GenericAbstractDecoder
{
/**
* Return media (mime) type of the file at given file path
*
* @param string $filepath
* @return string
*/
protected function getMediaTypeByFilePath(string $filepath): string
{
$info = getimagesize($filepath);
if (!is_array($info)) {
throw new DecoderException('Unable to decode input');
}
if (!array_key_exists('mime', $info)) {
throw new DecoderException('Unable to decode input');
}
return $info['mime'];
}
/**
* Return media (mime) type of the given image data
*
* @param string $data
* @return string
*/
protected function getMediaTypeByBinary(string $data): string
{
$info = getimagesizefromstring($data);
if (!is_array($info)) {
throw new DecoderException('Unable to decode input');
}
if (!array_key_exists('mime', $info)) {
throw new DecoderException('Unable to decode input');
}
return $info['mime'];
}
}

View File

@@ -4,35 +4,48 @@ declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\AbstractDecoder;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\DecoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Gif\Decoder as GifDecoder;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
use Intervention\Image\Origin;
use Intervention\Image\Modifiers\AlignRotationModifier;
class BinaryImageDecoder extends AbstractDecoder implements DecoderInterface
class BinaryImageDecoder extends GdImageDecoder implements DecoderInterface
{
use CanDecodeGif;
/**
* {@inheritdoc}
*
* @see DecoderInterface::decode()
*/
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_string($input)) {
throw new DecoderException('Unable to decode input');
}
if ($this->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;
}
}

View File

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

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders;
use GdImage;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ColorInterface;
class GdImageDecoder extends AbstractDecoder
{
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_object($input)) {
throw new DecoderException('Unable to decode input');
}
if (!($input instanceof GdImage)) {
throw new DecoderException('Unable to decode input');
}
if (!imageistruecolor($input)) {
imagepalettetotruecolor($input);
}
imagesavealpha($input, true);
// build image instance
return new Image(
new Driver(),
new Core([
new Frame($input)
])
);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Decoders\Traits;
use Intervention\Gif\Decoder as GifDecoder;
use Intervention\Gif\Splitter as GifSplitter;
use Intervention\Image\Drivers\Gd\Core;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Image;
use Intervention\Image\Interfaces\ImageInterface;
trait CanDecodeGif
{
/**
* Decode image from given GIF source which can be either a file path or binary data
*
* @param mixed $input
* @return ImageInterface
*/
protected function decodeGif(mixed $input): ImageInterface
{
$gif = GifDecoder::decode($input);
$splitter = GifSplitter::create($gif)->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;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverSpecialized;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
class AlignRotationModifier extends DriverSpecialized implements ModifierInterface
{
public function apply(ImageInterface $image): ImageInterface
{
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
};
}
}

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Imagick\Decoders;
use Imagick;
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\Modifiers\AlignRotationModifier;
class ImagickImageDecoder extends AbstractDecoder implements DecoderInterface
{
public function decode(mixed $input): ImageInterface|ColorInterface
{
if (!is_object($input)) {
throw new DecoderException('Unable to decode input');
}
if (!($input instanceof Imagick)) {
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 ($input->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;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Drivers\Imagick\Modifiers;
use Imagick;
use Intervention\Image\Drivers\DriverSpecialized;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
class AlignRotationModifier extends DriverSpecialized implements ModifierInterface
{
public function apply(ImageInterface $image): ImageInterface
{
switch ($image->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;
}
}

View File

@@ -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 . ').'),
};
}

View File

@@ -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}
*

View File

@@ -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
*

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Modifiers;
class AlignRotationModifier extends SpecializableModifier
{
}

View File

@@ -37,6 +37,19 @@ class Origin
return $this->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
*

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Analyzers;
use Intervention\Image\Analyzers\SpecializableAnalyzer;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Tests\TestCase;
use Mockery;
class SpecializableAnalyzerTest extends TestCase
{
public function testAnalyzer(): void
{
$analyzer = Mockery::mock(SpecializableAnalyzer::class)->makePartial();
$image = Mockery::mock(ImageInterface::class);
$image->shouldReceive('analyze')->andReturn('test');
$result = $analyzer->analyze($image);
$this->assertEquals('test', $result);
}
}

View File

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

View File

@@ -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

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Drivers\Gd;
use Intervention\Image\Colors\Rgb\Channels\Alpha;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Drivers\Gd\ColorProcessor;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Tests\TestCase;
class ColorProcessorTest extends TestCase
{
public function testColorToNative(): void
{
$processor = new ColorProcessor();
$result = $processor->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');
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Drivers\Gd\Decoders;
use Intervention\Image\Drivers\Gd\Decoders\AbstractDecoder;
use Intervention\Image\Tests\TestCase;
use Mockery;
class AbstractDecoderTest extends TestCase
{
public function testGetMediaTypeFromFilePath(): void
{
$decoder = Mockery::mock(AbstractDecoder::class)->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')));
}
}

View File

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

View File

@@ -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 = [];

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Drivers\Gd\Traits;
use Intervention\Image\Drivers\Gd\Decoders\Traits\CanDecodeGif;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Tests\TestCase;
use Mockery;
class CanDecodeGifTest extends TestCase
{
public function testDecodeGifFromBinaryAnimation(): void
{
$decoder = Mockery::mock(new class () {
use CanDecodeGif;
})->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());
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Drivers\Imagick;
use ImagickPixel;
use Intervention\Image\Colors\Rgb\Color;
use Intervention\Image\Colors\Rgb\Colorspace;
use Intervention\Image\Drivers\Imagick\ColorProcessor;
use Intervention\Image\Tests\TestCase;
class ColorProcessorTest extends TestCase
{
public function testColorToNative(): void
{
$processor = new ColorProcessor(new Colorspace());
$result = $processor->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)'));
}
}

View File

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

View File

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

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Encoders;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\SpecializableEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Tests\TestCase;
use Mockery;
class SpecializableEncoderTest extends TestCase
{
public function testConstructorDefault(): void
{
$encoder = new class () extends SpecializableEncoder
{
};
$this->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);
}
}

View File

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

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Intervention\Image\Tests\Modifiers;
use Intervention\Image\Modifiers\SpecializableModifier;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Tests\TestCase;
use Mockery;
class SpecializableModifierTest extends TestCase
{
public function testApply(): void
{
$modifier = Mockery::mock(SpecializableModifier::class)->makePartial();
$image = Mockery::mock(ImageInterface::class);
$image->shouldReceive('modify')->andReturn($image);
$result = $modifier->apply($image);
$this->assertInstanceOf(ImageInterface::class, $result);
}
}

View File

@@ -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());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B