From f035f7d516b7fa32bd4f867c351dd69c0b4a65b8 Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Mon, 25 Mar 2024 19:46:16 +0100 Subject: [PATCH] Trim Modifier (#1322) Co-authored-by: Sibin Grasic --- src/Drivers/Gd/Modifiers/TrimModifier.php | 82 ++++++++++++++++++ .../Imagick/Modifiers/TrimModifier.php | 26 ++++++ src/Image.php | 11 +++ src/Interfaces/ImageInterface.php | 11 +++ src/Modifiers/TrimModifier.php | 14 +++ .../Drivers/Gd/Modifiers/TrimModifierTest.php | 55 ++++++++++++ .../Imagick/Modifiers/TrimModifierTest.php | 55 ++++++++++++ tests/resources/radial.png | Bin 0 -> 1254 bytes 8 files changed, 254 insertions(+) create mode 100644 src/Drivers/Gd/Modifiers/TrimModifier.php create mode 100644 src/Drivers/Imagick/Modifiers/TrimModifier.php create mode 100644 src/Modifiers/TrimModifier.php create mode 100644 tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php create mode 100644 tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php create mode 100644 tests/resources/radial.png diff --git a/src/Drivers/Gd/Modifiers/TrimModifier.php b/src/Drivers/Gd/Modifiers/TrimModifier.php new file mode 100644 index 00000000..7c011e0d --- /dev/null +++ b/src/Drivers/Gd/Modifiers/TrimModifier.php @@ -0,0 +1,82 @@ +isAnimated()) { + throw new NotSupportedException('Trim modifier cannot be applied to animated images.'); + } + + // apply tolerance with a min. value of .5 because the default tolerance of '0' should + // already trim away similar colors which is not the case with imagecropauto. + $trimmed = imagecropauto( + $image->core()->native(), + IMG_CROP_THRESHOLD, + max([.5, $this->tolerance / 10]), + $this->trimColor($image) + ); + + // if the tolerance is very high, it is possible that no image is left. + // imagick returns a 1x1 pixel image in this case. this does the same. + if ($trimmed === false) { + $trimmed = $this->driver()->createImage(1, 1)->core()->native(); + } + + $image->core()->setNative($trimmed); + + return $image; + } + + /** + * Create an average color from the colors of the four corner points of the given image + * + * @param ImageInterface $image + * @throws RuntimeException + * @throws AnimationException + * @return int + */ + private function trimColor(ImageInterface $image): int + { + // trim color base + $red = 0; + $green = 0; + $blue = 0; + + // corner coordinates + $size = $image->size(); + $cornerPoints = [ + new Point(0, 0), + new Point($size->width() - 1, 0), + new Point(0, $size->height() - 1), + new Point($size->width() - 1, $size->height() - 1), + ]; + + // create an average color to be used in trim operation + foreach ($cornerPoints as $pos) { + $cornerColor = imagecolorat($image->core()->native(), $pos->x(), $pos->y()); + $rgb = imagecolorsforindex($image->core()->native(), $cornerColor); + $red += round(round(($rgb['red'] / 51)) * 51); + $green += round(round(($rgb['green'] / 51)) * 51); + $blue += round(round(($rgb['blue'] / 51)) * 51); + } + + $red = (int) round($red / 4); + $green = (int) round($green / 4); + $blue = (int) round($blue / 4); + + return imagecolorallocate($image->core()->native(), $red, $green, $blue); + } +} diff --git a/src/Drivers/Imagick/Modifiers/TrimModifier.php b/src/Drivers/Imagick/Modifiers/TrimModifier.php new file mode 100644 index 00000000..88bc46a2 --- /dev/null +++ b/src/Drivers/Imagick/Modifiers/TrimModifier.php @@ -0,0 +1,26 @@ +isAnimated()) { + throw new NotSupportedException('Trim modifier cannot be applied to animated images.'); + } + + $imagick = $image->core()->native(); + $imagick->trimImage(($this->tolerance / 100 * $imagick->getQuantum()) / 1.5); + $imagick->setImagePage(0, 0, 0, 0); + + return $image; + } +} diff --git a/src/Image.php b/src/Image.php index 21fe3665..c336fc2b 100644 --- a/src/Image.php +++ b/src/Image.php @@ -88,6 +88,7 @@ use Intervention\Image\Modifiers\ScaleModifier; use Intervention\Image\Modifiers\SharpenModifier; use Intervention\Image\Modifiers\SliceAnimationModifier; use Intervention\Image\Modifiers\TextModifier; +use Intervention\Image\Modifiers\TrimModifier; use Intervention\Image\Typography\FontFactory; final class Image implements ImageInterface @@ -749,6 +750,16 @@ final class Image implements ImageInterface return $this->modify(new CropModifier($width, $height, $offset_x, $offset_y, $background, $position)); } + /** + * {@inheritdoc} + * + * @see ImageInterface::trim() + */ + public function trim(int $tolerance = 0): ImageInterface + { + return $this->modify(new TrimModifier($tolerance)); + } + /** * {@inheritdoc} * diff --git a/src/Interfaces/ImageInterface.php b/src/Interfaces/ImageInterface.php index 22be2360..48357881 100644 --- a/src/Interfaces/ImageInterface.php +++ b/src/Interfaces/ImageInterface.php @@ -6,6 +6,7 @@ namespace Intervention\Image\Interfaces; use Countable; use Intervention\Image\Encoders\AutoEncoder; +use Intervention\Image\Exceptions\AnimationException; use Intervention\Image\Exceptions\RuntimeException; use Intervention\Image\Origin; use IteratorAggregate; @@ -615,6 +616,16 @@ interface ImageInterface extends IteratorAggregate, Countable string $position = 'top-left' ): self; + /** + * Trim the image by removing border areas of similar color within a the given tolerance + * + * @param int $tolerance + * @throws RuntimeException + * @throws AnimationException + * @return ImageInterface + */ + public function trim(int $tolerance = 0): self; + /** * Place another image into the current image instance * diff --git a/src/Modifiers/TrimModifier.php b/src/Modifiers/TrimModifier.php new file mode 100644 index 00000000..89fa3992 --- /dev/null +++ b/src/Modifiers/TrimModifier.php @@ -0,0 +1,14 @@ +readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier()); + $this->assertEquals(28, $image->width()); + $this->assertEquals(28, $image->height()); + } + + public function testTrimGradient(): void + { + $image = $this->readTestImage('radial.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(50)); + $this->assertEquals(35, $image->width()); + $this->assertEquals(35, $image->height()); + } + + public function testTrimHighTolerance(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(1000000)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $this->assertColor(255, 255, 255, 0, $image->pickColor(0, 0)); + } + + public function testTrimAnimated(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(NotSupportedException::class); + $image->modify(new TrimModifier()); + } +} diff --git a/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php b/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php new file mode 100644 index 00000000..cd2bafdd --- /dev/null +++ b/tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php @@ -0,0 +1,55 @@ +readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier()); + $this->assertEquals(28, $image->width()); + $this->assertEquals(28, $image->height()); + } + + public function testTrimGradient(): void + { + $image = $this->readTestImage('radial.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(50)); + $this->assertEquals(29, $image->width()); + $this->assertEquals(29, $image->height()); + } + + public function testTrimHighTolerance(): void + { + $image = $this->readTestImage('trim.png'); + $this->assertEquals(50, $image->width()); + $this->assertEquals(50, $image->height()); + $image->modify(new TrimModifier(1000000)); + $this->assertEquals(1, $image->width()); + $this->assertEquals(1, $image->height()); + $this->assertColor(255, 255, 255, 0, $image->pickColor(0, 0)); + } + + public function testTrimAnimated(): void + { + $image = $this->readTestImage('animation.gif'); + $this->expectException(NotSupportedException::class); + $image->modify(new TrimModifier()); + } +} diff --git a/tests/resources/radial.png b/tests/resources/radial.png new file mode 100644 index 0000000000000000000000000000000000000000..788cbdfbe8af7a86e9873b7ba7f61e4f1ed0e8c6 GIT binary patch literal 1254 zcmWlZdo&%zTop`jErDl03Sc1uMa&v=@$Mbk+R)mHw{P+78(LZ*mE!elG&Z840rmB$t3!W3-n_x9SCB~X>=|lmP+bkN z7;-sYyuj0^c>EX@6(}#q{rk9g552v3`4Y9YsH#Fq2?`65mxtV3m&Ks z_`QaSWOZ=FaCqead3(u(d?`i#_w9)G+lKzKK;2zSbW$H;Xd5Z#pyKNRPQfyw+vHRO zVhAp=+cPj({BGnhC=8O=`WM#?E!UcMuqP?J@ck;}s0aH5W9@AXt^au_k4iMXwAba4 zM%Z3?^!!lo4iko^*Z7=L`ijCS13u$?Q2i4pwy3C)EcOq+NDdf0ouc9Md)f5%Qg(@c z+^r`AW29ayVts~_^^ShUL%QOmBCS~-%eA<1_0`Y8ic?LSia*&kNKZ|Ec@tTtSX`mq z$d6k}HD*@v=wYSya#3Q$F||J?FMm)!o#?&yr}S~@z_!bG@B^Rhd=$? zRMZ7dA(m6qyc-skN>y==*M6OtYkX2BJ4VVYS>;`Nj3OYXC8j#k*~Qh$Wd{x1^-csy z&z8q!aqc8GwF~(KpOk*ckJ0#B^c9``6T(9Glo$5RqW--Bx#x6wrMr0{t$kWLB`enr z&Bz){17EsZ)$~|ck8%PRtQ8t1k7jk(=gSR0L{n&P{UMImYY=cn; zq9lieRZ)XFbG8_eJJfGe&C?#oZ>)`>UQ|sdtHi5wSq(e2eN{73eZF1!sHJhV|DZ#r zvoV*fLbDSmtoS69nq)ZIo3bzPzwuA)3Vv8-%u_aJ{gEk4AKn;j26g`Uh4*woqA z$!zZy@shMzH2Q&+tl4E|4s{kQirk;dBy_XLnrRA$Un@$YZwRR?@8mu3*ybfS`iqy+ g6yQ+M@oY|6npu~tR^Q5)|0=@HW}kJwrRRnJ0o${>OaK4? literal 0 HcmV?d00001