mirror of
https://github.com/Intervention/image.git
synced 2025-08-19 12:11:26 +02:00
Trim Modifier (#1322)
Co-authored-by: Sibin Grasic <sibin.grasic@oblak.studio>
This commit is contained in:
82
src/Drivers/Gd/Modifiers/TrimModifier.php
Normal file
82
src/Drivers/Gd/Modifiers/TrimModifier.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Intervention\Image\Drivers\Gd\Modifiers;
|
||||
|
||||
use Intervention\Image\Exceptions\AnimationException;
|
||||
use Intervention\Image\Exceptions\NotSupportedException;
|
||||
use Intervention\Image\Exceptions\RuntimeException;
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
use Intervention\Image\Interfaces\SpecializedInterface;
|
||||
use Intervention\Image\Modifiers\TrimModifier as GenericTrimModifier;
|
||||
|
||||
class TrimModifier extends GenericTrimModifier implements SpecializedInterface
|
||||
{
|
||||
public function apply(ImageInterface $image): ImageInterface
|
||||
{
|
||||
if ($image->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);
|
||||
}
|
||||
}
|
26
src/Drivers/Imagick/Modifiers/TrimModifier.php
Normal file
26
src/Drivers/Imagick/Modifiers/TrimModifier.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Intervention\Image\Drivers\Imagick\Modifiers;
|
||||
|
||||
use Intervention\Image\Exceptions\NotSupportedException;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
use Intervention\Image\Interfaces\SpecializedInterface;
|
||||
use Intervention\Image\Modifiers\TrimModifier as GenericTrimModifier;
|
||||
|
||||
class TrimModifier extends GenericTrimModifier implements SpecializedInterface
|
||||
{
|
||||
public function apply(ImageInterface $image): ImageInterface
|
||||
{
|
||||
if ($image->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;
|
||||
}
|
||||
}
|
@@ -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}
|
||||
*
|
||||
|
@@ -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
|
||||
*
|
||||
|
14
src/Modifiers/TrimModifier.php
Normal file
14
src/Modifiers/TrimModifier.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Intervention\Image\Modifiers;
|
||||
|
||||
use Intervention\Image\Drivers\SpecializableModifier;
|
||||
|
||||
class TrimModifier extends SpecializableModifier
|
||||
{
|
||||
public function __construct(public int $tolerance = 0)
|
||||
{
|
||||
}
|
||||
}
|
55
tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php
Normal file
55
tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Intervention\Image\Tests\Unit\Drivers\Gd\Modifiers;
|
||||
|
||||
use Intervention\Image\Exceptions\NotSupportedException;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||
use Intervention\Image\Modifiers\TrimModifier;
|
||||
use Intervention\Image\Tests\GdTestCase;
|
||||
|
||||
#[RequiresPhpExtension('gd')]
|
||||
#[CoversClass(\Intervention\Image\Modifiers\TrimModifier::class)]
|
||||
#[CoversClass(\Intervention\Image\Drivers\Gd\Modifiers\TrimModifier::class)]
|
||||
final class TrimModifierTest extends GdTestCase
|
||||
{
|
||||
public function testTrim(): void
|
||||
{
|
||||
$image = $this->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());
|
||||
}
|
||||
}
|
55
tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php
Normal file
55
tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Intervention\Image\Tests\Unit\Drivers\Imagick\Modifiers;
|
||||
|
||||
use Intervention\Image\Exceptions\NotSupportedException;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||
use Intervention\Image\Modifiers\TrimModifier;
|
||||
use Intervention\Image\Tests\ImagickTestCase;
|
||||
|
||||
#[RequiresPhpExtension('imagick')]
|
||||
#[CoversClass(\Intervention\Image\Modifiers\TrimModifier::class)]
|
||||
#[CoversClass(\Intervention\Image\Drivers\Imagick\Modifiers\TrimModifier::class)]
|
||||
final class TrimModifierTest extends ImagickTestCase
|
||||
{
|
||||
public function testTrim(): void
|
||||
{
|
||||
$image = $this->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());
|
||||
}
|
||||
}
|
BIN
tests/resources/radial.png
Normal file
BIN
tests/resources/radial.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user