Skip to content

Commit

Permalink
Trim Modifier (#1322)
Browse files Browse the repository at this point in the history
Co-authored-by: Sibin Grasic <sibin.grasic@oblak.studio>
  • Loading branch information
olivervogel and seebeen committed Mar 25, 2024
1 parent dffb2bb commit f035f7d
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/Drivers/Gd/Modifiers/TrimModifier.php
@@ -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 changes: 26 additions & 0 deletions src/Drivers/Imagick/Modifiers/TrimModifier.php
@@ -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;
}
}
11 changes: 11 additions & 0 deletions src/Image.php
Expand Up @@ -88,6 +88,7 @@
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
Expand Down Expand Up @@ -749,6 +750,16 @@ public function crop(
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}
*
Expand Down
11 changes: 11 additions & 0 deletions src/Interfaces/ImageInterface.php
Expand Up @@ -6,6 +6,7 @@

use Countable;
use Intervention\Image\Encoders\AutoEncoder;
use Intervention\Image\Exceptions\AnimationException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Origin;
use IteratorAggregate;
Expand Down Expand Up @@ -615,6 +616,16 @@ public function crop(
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
*
Expand Down
14 changes: 14 additions & 0 deletions src/Modifiers/TrimModifier.php
@@ -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 changes: 55 additions & 0 deletions tests/Unit/Drivers/Gd/Modifiers/TrimModifierTest.php
@@ -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 changes: 55 additions & 0 deletions tests/Unit/Drivers/Imagick/Modifiers/TrimModifierTest.php
@@ -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());
}
}
Binary file added tests/resources/radial.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f035f7d

Please sign in to comment.