1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-28 08:09:54 +02:00

Implement color limit for PNG & GIF encoders

This commit is contained in:
Oliver Vogel
2023-10-31 11:00:23 +01:00
parent dc24ed97f4
commit b8d6023bc4
14 changed files with 208 additions and 17 deletions

View File

@@ -70,17 +70,17 @@ abstract class AbstractImage implements ImageInterface
);
}
public function toGif(): EncodedImage
public function toGif(int $color_limit = 0): EncodedImage
{
return $this->encode(
$this->resolveDriverClass('Encoders\GifEncoder')
$this->resolveDriverClass('Encoders\GifEncoder', $color_limit)
);
}
public function toPng(): EncodedImage
public function toPng(int $color_limit = 0): EncodedImage
{
return $this->encode(
$this->resolveDriverClass('Encoders\PngEncoder')
$this->resolveDriverClass('Encoders\PngEncoder', $color_limit)
);
}

View File

@@ -4,20 +4,29 @@ namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Gif\Builder as GifBuilder;
use Intervention\Image\Drivers\Abstract\Encoders\AbstractEncoder;
use Intervention\Image\Drivers\Gd\Traits\CanReduceColors;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class GifEncoder extends AbstractEncoder implements EncoderInterface
{
use CanReduceColors;
public function __construct(protected int $color_limit = 0)
{
//
}
public function encode(ImageInterface $image): EncodedImage
{
if ($image->isAnimated()) {
return $this->encodeAnimated($image);
}
$data = $this->getBuffered(function () use ($image) {
imagegif($image->frame()->core());
$gd = $this->maybeReduceColors($image->frame()->core(), $this->color_limit);
$data = $this->getBuffered(function () use ($gd) {
imagegif($gd);
});
return new EncodedImage($data, 'image/gif');
@@ -32,8 +41,10 @@ class GifEncoder extends AbstractEncoder implements EncoderInterface
);
foreach ($image as $frame) {
$source = $this->encode($frame->toImage());
$builder->addFrame($source, $frame->delay());
$builder->addFrame(
$this->encode($frame->toImage()),
$frame->delay()
);
}
return new EncodedImage($builder->encode(), 'image/gif');

View File

@@ -3,16 +3,25 @@
namespace Intervention\Image\Drivers\Gd\Encoders;
use Intervention\Image\Drivers\Abstract\Encoders\AbstractEncoder;
use Intervention\Image\Drivers\Gd\Traits\CanReduceColors;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class PngEncoder extends AbstractEncoder implements EncoderInterface
{
use CanReduceColors;
public function __construct(protected int $color_limit = 0)
{
//
}
public function encode(ImageInterface $image): EncodedImage
{
$data = $this->getBuffered(function () use ($image) {
imagepng($image->frame()->core(), null, -1);
$gd = $this->maybeReduceColors($image->frame()->core(), $this->color_limit);
$data = $this->getBuffered(function () use ($gd) {
imagepng($gd, null, -1);
});
return new EncodedImage($data, 'image/png');

View File

@@ -0,0 +1,59 @@
<?php
namespace Intervention\Image\Drivers\Gd\Traits;
use GdImage;
trait CanReduceColors
{
/**
* Reduce colors in a given GdImage to the given limit. Reduction is only
* applied when the given limit is under the given threshold
*
* @param GdImage $gd
* @param int $limit
* @param int $threshold
* @return GdImage
*/
private function maybeReduceColors(GdImage $gd, int $limit, int $threshold = 256): GdImage
{
// no color limit: no reduction
if ($limit === 0) {
return $gd;
}
// limit is over threshold: no reduction
if ($limit > $threshold) {
return $gd;
}
// image size
$width = imagesx($gd);
$height = imagesy($gd);
// create empty gd
$reduced = imagecreatetruecolor($width, $height);
// create matte
$matte = imagecolorallocatealpha($reduced, 255, 255, 255, 127);
// fill with matte
imagefill($reduced, 0, 0, $matte);
imagealphablending($reduced, false);
// set transparency and get transparency index
imagecolortransparent($reduced, $matte);
// copy original image
imagecopy($reduced, $gd, 0, 0, 0, 0, $width, $height);
// reduce limit by one to include possible transparency in palette
$limit = imagecolortransparent($gd) === -1 ? $limit : $limit - 1;
// decrease colors
imagetruecolortopalette($reduced, true, $limit);
return $reduced;
}
}

View File

@@ -5,6 +5,7 @@ namespace Intervention\Image\Drivers\Imagick\Encoders;
use Imagick;
use Intervention\Image\Drivers\Abstract\Encoders\AbstractEncoder;
use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Drivers\Imagick\Traits\CanReduceColors;
use Intervention\Image\EncodedImage;
use Intervention\Image\Exceptions\EncoderException;
use Intervention\Image\Interfaces\EncoderInterface;
@@ -12,6 +13,13 @@ use Intervention\Image\Interfaces\ImageInterface;
class GifEncoder extends AbstractEncoder implements EncoderInterface
{
use CanReduceColors;
public function __construct(protected int $color_limit = 0)
{
//
}
public function encode(ImageInterface $image): EncodedImage
{
$format = 'gif';
@@ -27,6 +35,7 @@ class GifEncoder extends AbstractEncoder implements EncoderInterface
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$imagick->optimizeImageLayers();
$this->maybeReduceColors($imagick, $this->color_limit);
$imagick = $imagick->deconstructImages();
return new EncodedImage($imagick->getImagesBlob(), 'image/gif');

View File

@@ -4,12 +4,20 @@ namespace Intervention\Image\Drivers\Imagick\Encoders;
use Imagick;
use Intervention\Image\Drivers\Abstract\Encoders\AbstractEncoder;
use Intervention\Image\Drivers\Imagick\Traits\CanReduceColors;
use Intervention\Image\EncodedImage;
use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\ImageInterface;
class PngEncoder extends AbstractEncoder implements EncoderInterface
{
use CanReduceColors;
public function __construct(protected int $color_limit = 0)
{
//
}
public function encode(ImageInterface $image): EncodedImage
{
$format = 'png';
@@ -20,6 +28,7 @@ class PngEncoder extends AbstractEncoder implements EncoderInterface
$imagick->setImageFormat($format);
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$this->maybeReduceColors($imagick, $this->color_limit);
return new EncodedImage($imagick->getImagesBlob(), 'image/png');
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Intervention\Image\Drivers\Imagick\Traits;
use Imagick;
trait CanReduceColors
{
/**
* Returns a Imagick from a given image with reduced colors to a given limit.
* Reduction is only applied when the given limit is under the given threshold
*
* @param Imagick $imagick
* @param int $limit
* @param int $threshold
* @return Imagick
*/
private function maybeReduceColors(Imagick $imagick, int $limit, int $threshold = 256): Imagick
{
if ($limit === 0) {
return $imagick;
}
if ($limit > $threshold) {
return $imagick;
}
$imagick->quantizeImage(
$limit,
$imagick->getImageColorspace(),
0,
false,
false
);
return $imagick;
}
}

View File

@@ -121,7 +121,7 @@ interface ImageInterface extends Traversable, Countable
*
* @return EncodedImage
*/
public function toGif(): EncodedImage;
public function toGif(int $color_limit = 0): EncodedImage;
/**
* Encode image to avif format
@@ -135,7 +135,7 @@ interface ImageInterface extends Traversable, Countable
*
* @return EncodedImage
*/
public function toPng(): EncodedImage;
public function toPng(int $color_limit = 0): EncodedImage;
/**
* Return color of pixel at given position on given frame position

View File

@@ -11,7 +11,6 @@ use Intervention\Image\Interfaces\EncoderInterface;
use Intervention\Image\Interfaces\FrameInterface;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\ModifierInterface;
use Intervention\Image\Resolution;
use Intervention\Image\Tests\TestCase;
use Mockery;
@@ -120,7 +119,7 @@ class AbstractImageTest extends TestCase
$encoder->shouldReceive('encode')->with($img)->andReturn($encoded);
$img->shouldReceive('resolveDriverClass')
->with('Encoders\GifEncoder')
->with('Encoders\GifEncoder', 0)
->andReturn($encoder);
$result = $img->toGif();
@@ -136,7 +135,7 @@ class AbstractImageTest extends TestCase
$encoder->shouldReceive('encode')->with($img)->andReturn($encoded);
$img->shouldReceive('resolveDriverClass')
->with('Encoders\PngEncoder')
->with('Encoders\PngEncoder', 0)
->andReturn($encoder);
$result = $img->toPng();

View File

@@ -7,6 +7,7 @@ use Intervention\Image\Drivers\Gd\Encoders\GifEncoder;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Drivers\Gd\Image;
use Intervention\Image\Tests\TestCase;
use Intervention\Image\Tests\Traits\CanCreateGdTestImage;
use Intervention\MimeSniffer\MimeSniffer;
use Intervention\MimeSniffer\Types\ImageGif;
@@ -16,6 +17,8 @@ use Intervention\MimeSniffer\Types\ImageGif;
*/
class GifEncoderTest extends TestCase
{
use CanCreateGdTestImage;
protected function getTestImage(): Image
{
$gd1 = imagecreatetruecolor(30, 20);
@@ -41,4 +44,15 @@ class GifEncoderTest extends TestCase
$result = $encoder->encode($image);
$this->assertTrue(MimeSniffer::createFromString($result)->matches(new ImageGif()));
}
public function testEncodeReduced(): void
{
$image = $this->createTestImage('gradient.gif');
$gd = $image->frame()->core();
$this->assertEquals(15, imagecolorstotal($gd));
$encoder = new GifEncoder(2);
$result = $encoder->encode($image);
$gd = imagecreatefromstring((string) $result);
$this->assertEquals(2, imagecolorstotal($gd));
}
}

View File

@@ -7,6 +7,7 @@ use Intervention\Image\Drivers\Gd\Encoders\PngEncoder;
use Intervention\Image\Drivers\Gd\Frame;
use Intervention\Image\Drivers\Gd\Image;
use Intervention\Image\Tests\TestCase;
use Intervention\Image\Tests\Traits\CanCreateGdTestImage;
use Intervention\MimeSniffer\MimeSniffer;
use Intervention\MimeSniffer\Types\ImagePng;
@@ -16,6 +17,8 @@ use Intervention\MimeSniffer\Types\ImagePng;
*/
class PngEncoderTest extends TestCase
{
use CanCreateGdTestImage;
protected function getTestImage(): Image
{
$frame = new Frame(imagecreatetruecolor(3, 2));
@@ -28,6 +31,17 @@ class PngEncoderTest extends TestCase
$image = $this->getTestImage();
$encoder = new PngEncoder();
$result = $encoder->encode($image);
$this->assertTrue(MimeSniffer::createFromString($result)->matches(new ImagePng));
$this->assertTrue(MimeSniffer::createFromString($result)->matches(ImagePng::class));
}
public function testEncodeReduced(): void
{
$image = $this->createTestImage('tile.png');
$gd = $image->frame()->core();
$this->assertEquals(3, imagecolorstotal($gd));
$encoder = new PngEncoder(2);
$result = $encoder->encode($image);
$gd = imagecreatefromstring((string) $result);
$this->assertEquals(2, imagecolorstotal($gd));
}
}

View File

@@ -6,9 +6,9 @@ use Imagick;
use ImagickPixel;
use Intervention\Image\Collection;
use Intervention\Image\Drivers\Imagick\Encoders\GifEncoder;
use Intervention\Image\Drivers\Imagick\Frame;
use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Tests\TestCase;
use Intervention\Image\Tests\Traits\CanCreateImagickTestImage;
use Intervention\MimeSniffer\MimeSniffer;
use Intervention\MimeSniffer\Types\ImageGif;
@@ -18,6 +18,8 @@ use Intervention\MimeSniffer\Types\ImageGif;
*/
class GifEncoderTest extends TestCase
{
use CanCreateImagickTestImage;
protected function getTestImage(): Image
{
$imagick = new Imagick();
@@ -47,4 +49,16 @@ class GifEncoderTest extends TestCase
$result = $encoder->encode($image);
$this->assertTrue(MimeSniffer::createFromString($result)->matches(new ImageGif()));
}
public function testEncodeReduced(): void
{
$image = $this->createTestImage('gradient.gif');
$imagick = $image->frame()->core();
$this->assertEquals(15, $imagick->getImageColors());
$encoder = new GifEncoder(2);
$result = $encoder->encode($image);
$imagick = new Imagick();
$imagick->readImageBlob((string) $result);
$this->assertEquals(2, $imagick->getImageColors());
}
}

View File

@@ -8,6 +8,7 @@ use Imagick;
use ImagickPixel;
use Intervention\Image\Drivers\Imagick\Encoders\PngEncoder;
use Intervention\Image\Drivers\Imagick\Image;
use Intervention\Image\Tests\Traits\CanCreateImagickTestImage;
use Intervention\MimeSniffer\MimeSniffer;
use Intervention\MimeSniffer\Types\ImagePng;
use PHPUnit\Framework\TestCase;
@@ -17,6 +18,8 @@ use PHPUnit\Framework\TestCase;
*/
final class PngEncoderTest extends TestCase
{
use CanCreateImagickTestImage;
protected function getTestImage(): Image
{
$imagick = new Imagick();
@@ -32,4 +35,16 @@ final class PngEncoderTest extends TestCase
$result = $encoder->encode($image);
$this->assertTrue(MimeSniffer::createFromString((string) $result)->matches(new ImagePng()));
}
public function testEncodeReduced(): void
{
$image = $this->createTestImage('tile.png');
$imagick = $image->frame()->core();
$this->assertEquals(3, $imagick->getImageColors());
$encoder = new PngEncoder(2);
$result = $encoder->encode($image);
$imagick = new Imagick();
$imagick->readImageBlob((string) $result);
$this->assertEquals(2, $imagick->getImageColors());
}
}

BIN
tests/images/gradient.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B