diff --git a/src/Collection.php b/src/Collection.php index 94b75185..5c218f22 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -185,10 +185,31 @@ class Collection implements CollectionInterface, IteratorAggregate, Countable return $this; } + /** + * {@inheritdoc} + * + * @see CollectionInterface::empty() + */ public function empty(): CollectionInterface { $this->items = []; return $this; } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::slice() + */ + public function slice(int $offset, ?int $length = null): CollectionInterface + { + if ($offset >= count($this->items)) { + throw new RuntimeException('Offset exceeds the maximum value.'); + } + + $this->items = array_slice($this->items, $offset, $length); + + return $this; + } } diff --git a/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php b/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php new file mode 100644 index 00000000..69ef8972 --- /dev/null +++ b/src/Drivers/Gd/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,20 @@ +core()->slice($this->offset, $this->length); + + return $image; + } +} diff --git a/src/Drivers/Imagick/Core.php b/src/Drivers/Imagick/Core.php index 4eabbf81..6840e8f0 100644 --- a/src/Drivers/Imagick/Core.php +++ b/src/Drivers/Imagick/Core.php @@ -7,6 +7,7 @@ use ImagickException; use Iterator; use Intervention\Image\Interfaces\CoreInterface; use Intervention\Image\Exceptions\AnimationException; +use Intervention\Image\Interfaces\CollectionInterface; use Intervention\Image\Interfaces\FrameInterface; class Core implements CoreInterface, Iterator @@ -17,6 +18,98 @@ class Core implements CoreInterface, Iterator { } + /** + * {@inheritdoc} + * + * @see CollectionInterface::has() + */ + public function has(int|string $key): bool + { + try { + $result = $this->imagick->setIteratorIndex($key); + } catch (ImagickException) { + return false; + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::push() + */ + public function push($item): CollectionInterface + { + return $this->add($item); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::get() + */ + public function get(int|string $key, $default = null): mixed + { + try { + $this->imagick->setIteratorIndex($key); + } catch (ImagickException) { + return $default; + } + + return new Frame($this->imagick->current()); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::getAtPosition() + */ + public function getAtPosition(int $key = 0, $default = null): mixed + { + return $this->get($key, $default); + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::empty() + */ + public function empty(): CollectionInterface + { + $this->imagick->clear(); + + return $this; + } + + /** + * {@inheritdoc} + * + * @see CollectionInterface::slice() + */ + public function slice(int $offset, ?int $length = null): CollectionInterface + { + $allowed_indexes = []; + $length = is_null($length) ? $this->count() : $length; + for ($i = $offset; $i < $offset + $length; $i++) { + $allowed_indexes[] = $i; + } + + $sliced = new Imagick(); + foreach ($this->imagick as $key => $native) { + if (in_array($key, $allowed_indexes)) { + $sliced->addImage($native->getImage()); + } + } + + $sliced = $sliced->coalesceImages(); + $sliced->setImageIterations($this->imagick->getImageIterations()); + + $this->imagick = $sliced; + + return $this; + } + public function add(FrameInterface $frame): CoreInterface { $imagick = $frame->native(); diff --git a/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php b/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php new file mode 100644 index 00000000..d72a7be4 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,20 @@ +core()->slice($this->offset, $this->length); + + return $image; + } +} diff --git a/src/Image.php b/src/Image.php index 9d406d76..cd978634 100644 --- a/src/Image.php +++ b/src/Image.php @@ -83,6 +83,7 @@ use Intervention\Image\Modifiers\RotateModifier; use Intervention\Image\Modifiers\ScaleDownModifier; use Intervention\Image\Modifiers\ScaleModifier; use Intervention\Image\Modifiers\SharpenModifier; +use Intervention\Image\Modifiers\SliceAnimationModifier; use Intervention\Image\Modifiers\TextModifier; use Intervention\Image\Typography\FontFactory; @@ -204,6 +205,11 @@ final class Image implements ImageInterface return $this->modify(new RemoveAnimationModifier($position)); } + public function sliceAnimation(int $offset = 0, ?int $length = null): ImageInterface + { + return $this->modify(new SliceAnimationModifier($offset, $length)); + } + /** * {@inheritdoc} * diff --git a/src/Interfaces/CollectionInterface.php b/src/Interfaces/CollectionInterface.php index e67b3b58..7a8d5c3c 100644 --- a/src/Interfaces/CollectionInterface.php +++ b/src/Interfaces/CollectionInterface.php @@ -67,4 +67,13 @@ interface CollectionInterface extends Traversable * @return CollectionInterface */ public function empty(): CollectionInterface; + + /** + * Extract items based on given values and discard the rest. + * + * @param int $offset + * @param null|int $length + * @return CollectionInterface + */ + public function slice(int $offset, ?int $length = 0): CollectionInterface; } diff --git a/src/Interfaces/CoreInterface.php b/src/Interfaces/CoreInterface.php index 14320cc2..772579fa 100644 --- a/src/Interfaces/CoreInterface.php +++ b/src/Interfaces/CoreInterface.php @@ -2,9 +2,7 @@ namespace Intervention\Image\Interfaces; -use Traversable; - -interface CoreInterface extends Traversable +interface CoreInterface extends CollectionInterface { /** * return driver's representation of the image core. @@ -60,5 +58,10 @@ interface CoreInterface extends Traversable */ public function setLoops(int $loops): CoreInterface; + /** + * Get first frame in core + * + * @return FrameInterface + */ public function first(): FrameInterface; } diff --git a/src/Interfaces/ImageInterface.php b/src/Interfaces/ImageInterface.php index 88986ff1..1384fc4f 100644 --- a/src/Interfaces/ImageInterface.php +++ b/src/Interfaces/ImageInterface.php @@ -112,6 +112,15 @@ interface ImageInterface extends IteratorAggregate, Countable */ public function removeAnimation(int|string $position = 0): ImageInterface; + /** + * Extract animation frames based on given values and discard the rest + * + * @param int $offset + * @param null|int $length + * @return ImageInterface + */ + public function sliceAnimation(int $offset = 0, ?int $length = null): ImageInterface; + /** * Return loop count of animated image * diff --git a/src/Modifiers/SliceAnimationModifier.php b/src/Modifiers/SliceAnimationModifier.php new file mode 100644 index 00000000..b69271fb --- /dev/null +++ b/src/Modifiers/SliceAnimationModifier.php @@ -0,0 +1,10 @@ +assertEquals(0, $collection->count()); $this->assertEquals(0, $result->count()); } + + public function testSlice(): void + { + $collection = new Collection(['a', 'b', 'c', 'd', 'e', 'f']); + $this->assertEquals(6, $collection->count()); + $result = $collection->slice(0, 3); + $this->assertEquals(['a', 'b', 'c'], $collection->toArray()); + $this->assertEquals(['a', 'b', 'c'], $result->toArray()); + $this->assertEquals('a', $result->get(0)); + $this->assertEquals('b', $result->get(1)); + $this->assertEquals('c', $result->get(2)); + + $result = $collection->slice(2, 1); + $this->assertEquals(['c'], $collection->toArray()); + $this->assertEquals(['c'], $result->toArray()); + $this->assertEquals('c', $result->get(0)); + } + + public function testSliceOutOfBounds(): void + { + $this->expectException(RuntimeException::class); + $collection = new Collection(['a', 'b', 'c']); + $collection->slice(6); + } } diff --git a/tests/Drivers/Imagick/CoreTest.php b/tests/Drivers/Imagick/CoreTest.php index 71859280..073db232 100644 --- a/tests/Drivers/Imagick/CoreTest.php +++ b/tests/Drivers/Imagick/CoreTest.php @@ -66,4 +66,77 @@ class CoreTest extends TestCase $this->assertEquals(12, $core->loops()); $this->assertInstanceOf(Core::class, $result); } + + public function testHas(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('red')); + $imagick->addImage(clone $imagick); + $core = new Core($imagick); + $this->assertTrue($core->has(0)); + $this->assertTrue($core->has(1)); + $this->assertFalse($core->has(2)); + } + + public function testPush(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('red')); + $imagick->addImage(clone $imagick); + $core = new Core($imagick); + $this->assertEquals(2, $core->count()); + + $im = new Imagick(); + $im->newImage(100, 100, new ImagickPixel('green')); + $result = $core->push(new Frame($im)); + $this->assertEquals(3, $core->count()); + $this->assertEquals(3, $result->count()); + } + + public function testGet(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('red')); + $imagick->addImage(clone $imagick); + $core = new Core($imagick); + $this->assertInstanceOf(Frame::class, $core->get(0)); + $this->assertInstanceOf(Frame::class, $core->get(1)); + $this->assertNull($core->get(2)); + $this->assertEquals('foo', $core->get(2, 'foo')); + } + + public function testEmpty(): void + { + $imagick = new Imagick(); + $imagick->newImage(100, 100, new ImagickPixel('red')); + $imagick->addImage(clone $imagick); + $core = new Core($imagick); + $this->assertEquals(2, $core->count()); + $result = $core->empty(); + $this->assertEquals(0, $core->count()); + $this->assertEquals(0, $result->count()); + } + + public function testSlice(): void + { + $imagick = new Imagick(); + + $im = new Imagick(); + $im->newImage(10, 10, new ImagickPixel('red')); + $imagick->addImage($im); + + $im = new Imagick(); + $im->newImage(10, 10, new ImagickPixel('green')); + $imagick->addImage($im); + + $im = new Imagick(); + $im->newImage(10, 10, new ImagickPixel('blue')); + $imagick->addImage($im); + + $core = new Core($imagick); + $this->assertEquals(3, $core->count()); + $result = $core->slice(1, 2); + $this->assertEquals(2, $core->count()); + $this->assertEquals(2, $result->count()); + } }