1
0
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:
Oliver Vogel
2024-03-25 19:46:16 +01:00
committed by GitHub
parent dffb2bb4fe
commit f035f7d516
8 changed files with 254 additions and 0 deletions

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

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

View File

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

View File

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

View 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)
{
}
}

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB