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:
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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');
|
||||
|
@@ -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');
|
||||
|
59
src/Drivers/Gd/Traits/CanReduceColors.php
Normal file
59
src/Drivers/Gd/Traits/CanReduceColors.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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');
|
||||
|
@@ -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');
|
||||
}
|
||||
|
38
src/Drivers/Imagick/Traits/CanReduceColors.php
Normal file
38
src/Drivers/Imagick/Traits/CanReduceColors.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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
BIN
tests/images/gradient.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 948 B |
Reference in New Issue
Block a user