Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to encode indexed color palette PNG format #1385

Merged
merged 11 commits into from
Aug 11, 2024
42 changes: 37 additions & 5 deletions src/Drivers/Gd/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,54 @@

namespace Intervention\Image\Drivers\Gd\Encoders;

use GdImage;
use Intervention\Image\Drivers\Gd\Cloner;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
use Intervention\Image\Exceptions\AnimationException;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;

class PngEncoder extends GenericPngEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
$gd = $image->core()->native();
$data = $this->buffered(function () use ($gd) {
imageinterlace($gd, $this->interlaced);
imagepng($gd, null, -1);
imageinterlace($gd, false);
$output = $this->prepareOutput($image);

// encode
$data = $this->buffered(function () use ($output) {
imageinterlace($output, $this->interlaced);
imagepng($output, null, -1);
});

return new EncodedImage($data, 'image/png');
}

/**
* Prepare given image instance for PNG format output according to encoder settings
*
* @param ImageInterface $image
* @throws RuntimeException
* @throws ColorException
* @throws AnimationException
* @return GdImage
*/
private function prepareOutput(ImageInterface $image): GdImage
{
if ($this->indexed) {
$output = clone $image;
$output->reduceColors(255);

return $output->core()->native();
}

return Cloner::clone($image->core()->native());
}
}
64 changes: 55 additions & 9 deletions src/Drivers/Imagick/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,74 @@
namespace Intervention\Image\Drivers\Imagick\Encoders;

use Imagick;
use ImagickException;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
use Intervention\Image\Exceptions\AnimationException;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;

class PngEncoder extends GenericPngEncoder implements SpecializedInterface
{
/**
* {@inheritdoc}
*
* @see EncoderInterface::encode()
*/
public function encode(ImageInterface $image): EncodedImage
{
$format = 'PNG';
$compression = Imagick::COMPRESSION_ZIP;
$output = $this->prepareOutput($image);

$imagick = $image->core()->native();
$imagick->setFormat($format);
$imagick->setImageFormat($format);
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$output->setCompression(Imagick::COMPRESSION_ZIP);
$output->setImageCompression(Imagick::COMPRESSION_ZIP);

if ($this->interlaced) {
$imagick->setInterlaceScheme(Imagick::INTERLACE_LINE);
$output->setInterlaceScheme(Imagick::INTERLACE_LINE);
}

return new EncodedImage($imagick->getImagesBlob(), 'image/png');
return new EncodedImage($output->getImagesBlob(), 'image/png');
}

/**
* Prepare given image instance for PNG format output according to encoder settings
*
* @param ImageInterface $image
* @throws AnimationException
* @throws RuntimeException
* @throws ColorException
* @throws ImagickException
* @return Imagick
*/
private function prepareOutput(ImageInterface $image): Imagick
{
$output = clone $image;

if ($this->indexed) {
// reduce colors
$output->reduceColors(256);

$output = $output->core()->native();

$output->setFormat('PNG');
$output->setImageFormat('PNG');

// $output->setImageBackgroundColor(new \ImagickPixel('#00ff00'));
// $output->setImageProperty();

// $output->setType(Imagick::IMGTYPE_PALETTEMATTE);
// $output->setOption('png:bit-depth', '8');
// $output->setOption('png:color-type', '4');

return $output;
}

// ensure to encode PNG image type 6 (true color alpha)
$output = clone $image->core()->native();
$output->setFormat('PNG32');
$output->setImageFormat('PNG32');

return $output;
}
}
2 changes: 1 addition & 1 deletion src/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class PngEncoder extends SpecializableEncoder
{
public function __construct(public bool $interlaced = false)
public function __construct(public bool $interlaced = false, public bool $indexed = false)
{
}
}
2 changes: 0 additions & 2 deletions src/Interfaces/ImageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,6 @@ public function pickColors(int $x, int $y): CollectionInterface;
* Return color that is mixed with transparent areas when converting to a format which
* does not support transparency.
*
* @deprecated Use configuration options of image manager instead
* @throws RuntimeException
* @return ColorInterface
*/
Expand All @@ -264,7 +263,6 @@ public function blendingColor(): ColorInterface;
* Set blending color will have no effect unless image is converted into a format
* which does not support transparency.
*
* @deprecated Use configuration options of image manager instead
* @param mixed $color
* @throws RuntimeException
* @return ImageInterface
Expand Down
2 changes: 1 addition & 1 deletion tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static function getTestResourceData($filename = 'test.jpg'): string
return file_get_contents(self::getTestResourcePath($filename));
}

public function getTestResourcePointer($filename = 'test.jpg')
public static function getTestResourcePointer($filename = 'test.jpg')
{
$pointer = fopen('php://temp', 'rw');
fputs($pointer, self::getTestResourceData($filename));
Expand Down
8 changes: 4 additions & 4 deletions tests/GdTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@

abstract class GdTestCase extends BaseTestCase
{
public function readTestImage($filename = 'test.jpg'): Image
public static function readTestImage($filename = 'test.jpg'): Image
{
return (new Driver())->specialize(new FilePathImageDecoder())->decode(
$this->getTestResourcePath($filename)
static::getTestResourcePath($filename)
);
}

public function createTestImage(int $width, int $height): Image
public static function createTestImage(int $width, int $height): Image
{
$gd = imagecreatetruecolor($width, $height);
imagefill($gd, 0, 0, imagecolorallocate($gd, 255, 0, 0));
Expand All @@ -32,7 +32,7 @@ public function createTestImage(int $width, int $height): Image
);
}

public function createTestAnimation(): Image
public static function createTestAnimation(): Image
{
$gd1 = imagecreatetruecolor(3, 2);
imagefill($gd1, 0, 0, imagecolorallocate($gd1, 255, 0, 0));
Expand Down
8 changes: 4 additions & 4 deletions tests/ImagickTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

abstract class ImagickTestCase extends BaseTestCase
{
public function readTestImage($filename = 'test.jpg'): Image
public static function readTestImage($filename = 'test.jpg'): Image
{
return (new Driver())->specialize(new FilePathImageDecoder())->decode(
$this->getTestResourcePath($filename)
static::getTestResourcePath($filename)
);
}

public function createTestImage(int $width, int $height): Image
public static function createTestImage(int $width, int $height): Image
{
$background = new ImagickPixel('rgb(255, 0, 0)');
$imagick = new Imagick();
Expand All @@ -36,7 +36,7 @@ public function createTestImage(int $width, int $height): Image
);
}

public function createTestAnimation(): Image
public static function createTestAnimation(): Image
{
$imagick = new Imagick();
$imagick->setFormat('gif');
Expand Down
27 changes: 0 additions & 27 deletions tests/Traits/CanDetectInterlacedPng.php

This file was deleted.

52 changes: 52 additions & 0 deletions tests/Traits/CanInspectPngFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Intervention\Image\Tests\Traits;

use Intervention\Image\Traits\CanBuildFilePointer;

trait CanInspectPngFormat
{
use CanBuildFilePointer;

/**
* Checks if the given image data is interlaced encoded PNG format
*
* @param string $imagedata
* @return bool
*/
private function isInterlacedPng(string $imagedata): bool
{
$f = $this->buildFilePointer($imagedata);
$contents = fread($f, 32);
fclose($f);

return ord($contents[28]) != 0;
}

/**
* Try to detect PNG color type from given binary data
*
* @param string $data
* @return string
*/
private function pngColorType(string $data): string
{
if (substr($data, 1, 3) !== 'PNG') {
return 'unkown';
}

$pos = strpos($data, 'IHDR');
$type = substr($data, $pos + 13, 1);

return match (unpack('C', $type)[1]) {
0 => 'grayscale',
2 => 'truecolor',
3 => 'indexed',
4 => 'grayscale-alpha',
6 => 'truecolor-alpha',
default => 'unknown',
};
}
}
61 changes: 59 additions & 2 deletions tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Intervention\Image\Encoders\PngEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Tests\GdTestCase;
use Intervention\Image\Tests\Traits\CanDetectInterlacedPng;
use Intervention\Image\Tests\Traits\CanInspectPngFormat;
use PHPUnit\Framework\Attributes\DataProvider;

#[RequiresPhpExtension('gd')]
#[CoversClass(\Intervention\Image\Encoders\PngEncoder::class)]
#[CoversClass(\Intervention\Image\Drivers\Gd\Encoders\PngEncoder::class)]
final class PngEncoderTest extends GdTestCase
{
use CanDetectInterlacedPng;
use CanInspectPngFormat;

public function testEncode(): void
{
Expand All @@ -34,4 +36,59 @@ public function testEncodeInterlaced(): void
$this->assertMediaType('image/png', (string) $result);
$this->assertTrue($this->isInterlacedPng((string) $result));
}

#[DataProvider('indexedDataProvider')]
public function testEncoderIndexed(ImageInterface $image, PngEncoder $encoder, string $result): void
{
$this->assertEquals(
$result,
$this->pngColorType((string) $encoder->encode($image)),
);
}

public static function indexedDataProvider(): array
{
return [
[
static::createTestImage(3, 2), // new
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::createTestImage(3, 2), // new
new PngEncoder(indexed: true),
'indexed',
],
[
static::readTestImage('circle.png'), // truecolor-alpha
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::readTestImage('circle.png'), // indexedcolor-alpha
new PngEncoder(indexed: true),
'indexed',
],
[
static::readTestImage('tile.png'), // indexed
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::readTestImage('tile.png'), // indexed
new PngEncoder(indexed: true),
'indexed',
],
[
static::readTestImage('test.jpg'), // jpeg
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::readTestImage('test.jpg'), // jpeg
new PngEncoder(indexed: true),
'indexed',
],
];
}
}
Loading