Skip to content

Commit

Permalink
Implement color limit for PNG & GIF encoders
Browse files Browse the repository at this point in the history
  • Loading branch information
olivervogel committed Oct 31, 2023
1 parent dc24ed9 commit b8d6023
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 17 deletions.
8 changes: 4 additions & 4 deletions src/Drivers/Abstract/AbstractImage.php
Expand Up @@ -70,17 +70,17 @@ public function toWebp(int $quality = 75): EncodedImage
);
}

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

Expand Down
19 changes: 15 additions & 4 deletions src/Drivers/Gd/Encoders/GifEncoder.php
Expand Up @@ -4,20 +4,29 @@

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');
Expand All @@ -32,8 +41,10 @@ protected function encodeAnimated(ImageInterface $image): EncodedImage
);

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');
Expand Down
13 changes: 11 additions & 2 deletions src/Drivers/Gd/Encoders/PngEncoder.php
Expand Up @@ -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');
Expand Down
59 changes: 59 additions & 0 deletions src/Drivers/Gd/Traits/CanReduceColors.php
@@ -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;
}
}
9 changes: 9 additions & 0 deletions src/Drivers/Imagick/Encoders/GifEncoder.php
Expand Up @@ -5,13 +5,21 @@
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;
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';
Expand All @@ -27,6 +35,7 @@ public function encode(ImageInterface $image): EncodedImage
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$imagick->optimizeImageLayers();
$this->maybeReduceColors($imagick, $this->color_limit);
$imagick = $imagick->deconstructImages();

return new EncodedImage($imagick->getImagesBlob(), 'image/gif');
Expand Down
9 changes: 9 additions & 0 deletions src/Drivers/Imagick/Encoders/PngEncoder.php
Expand Up @@ -4,12 +4,20 @@

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';
Expand All @@ -20,6 +28,7 @@ public function encode(ImageInterface $image): EncodedImage
$imagick->setImageFormat($format);
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$this->maybeReduceColors($imagick, $this->color_limit);

return new EncodedImage($imagick->getImagesBlob(), 'image/png');
}
Expand Down
38 changes: 38 additions & 0 deletions src/Drivers/Imagick/Traits/CanReduceColors.php
@@ -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;
}
}
4 changes: 2 additions & 2 deletions src/Interfaces/ImageInterface.php
Expand Up @@ -121,7 +121,7 @@ public function toWebp(int $quality = 75): EncodedImage;
*
* @return EncodedImage
*/
public function toGif(): EncodedImage;
public function toGif(int $color_limit = 0): EncodedImage;

/**
* Encode image to avif format
Expand All @@ -135,7 +135,7 @@ public function toAvif(): EncodedImage;
*
* @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
Expand Down
5 changes: 2 additions & 3 deletions tests/Drivers/Abstract/AbstractImageTest.php
Expand Up @@ -11,7 +11,6 @@
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;

Expand Down Expand Up @@ -120,7 +119,7 @@ public function testToGif(): void
$encoder->shouldReceive('encode')->with($img)->andReturn($encoded);

$img->shouldReceive('resolveDriverClass')
->with('Encoders\GifEncoder')
->with('Encoders\GifEncoder', 0)
->andReturn($encoder);

$result = $img->toGif();
Expand All @@ -136,7 +135,7 @@ public function testToPng(): void
$encoder->shouldReceive('encode')->with($img)->andReturn($encoded);

$img->shouldReceive('resolveDriverClass')
->with('Encoders\PngEncoder')
->with('Encoders\PngEncoder', 0)
->andReturn($encoder);

$result = $img->toPng();
Expand Down
14 changes: 14 additions & 0 deletions tests/Drivers/Gd/Encoders/GifEncoderTest.php
Expand Up @@ -7,6 +7,7 @@
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;

Expand All @@ -16,6 +17,8 @@
*/
class GifEncoderTest extends TestCase
{
use CanCreateGdTestImage;

protected function getTestImage(): Image
{
$gd1 = imagecreatetruecolor(30, 20);
Expand All @@ -41,4 +44,15 @@ public function testEncode(): void
$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));
}
}
16 changes: 15 additions & 1 deletion tests/Drivers/Gd/Encoders/PngEncoderTest.php
Expand Up @@ -7,6 +7,7 @@
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;

Expand All @@ -16,6 +17,8 @@
*/
class PngEncoderTest extends TestCase
{
use CanCreateGdTestImage;

protected function getTestImage(): Image
{
$frame = new Frame(imagecreatetruecolor(3, 2));
Expand All @@ -28,6 +31,17 @@ public function testEncode(): void
$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));
}
}

0 comments on commit b8d6023

Please sign in to comment.