diff --git a/src/Drivers/Gd/Encoders/PngEncoder.php b/src/Drivers/Gd/Encoders/PngEncoder.php index 6b20ea91..ebd2d50d 100644 --- a/src/Drivers/Gd/Encoders/PngEncoder.php +++ b/src/Drivers/Gd/Encoders/PngEncoder.php @@ -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()); + } } diff --git a/src/Drivers/Imagick/Encoders/PngEncoder.php b/src/Drivers/Imagick/Encoders/PngEncoder.php index 79ef2412..8b0fe7ab 100644 --- a/src/Drivers/Imagick/Encoders/PngEncoder.php +++ b/src/Drivers/Imagick/Encoders/PngEncoder.php @@ -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; } } diff --git a/src/Encoders/PngEncoder.php b/src/Encoders/PngEncoder.php index b6bb6cb5..7b2df853 100644 --- a/src/Encoders/PngEncoder.php +++ b/src/Encoders/PngEncoder.php @@ -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) { } } diff --git a/src/Interfaces/ImageInterface.php b/src/Interfaces/ImageInterface.php index 726c5207..efd9e221 100644 --- a/src/Interfaces/ImageInterface.php +++ b/src/Interfaces/ImageInterface.php @@ -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 */ @@ -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 diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 41caa2aa..9aa89c0c 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -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)); diff --git a/tests/GdTestCase.php b/tests/GdTestCase.php index 2cfa9527..ebe98a01 100644 --- a/tests/GdTestCase.php +++ b/tests/GdTestCase.php @@ -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)); @@ -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)); diff --git a/tests/ImagickTestCase.php b/tests/ImagickTestCase.php index cbd24dcc..bef7b674 100644 --- a/tests/ImagickTestCase.php +++ b/tests/ImagickTestCase.php @@ -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(); @@ -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'); diff --git a/tests/Traits/CanDetectInterlacedPng.php b/tests/Traits/CanDetectInterlacedPng.php deleted file mode 100644 index 19fffe46..00000000 --- a/tests/Traits/CanDetectInterlacedPng.php +++ /dev/null @@ -1,27 +0,0 @@ -buildFilePointer($imagedata); - $contents = fread($f, 32); - fclose($f); - - return ord($contents[28]) != 0; - } -} diff --git a/tests/Traits/CanInspectPngFormat.php b/tests/Traits/CanInspectPngFormat.php new file mode 100644 index 00000000..c09a4f7f --- /dev/null +++ b/tests/Traits/CanInspectPngFormat.php @@ -0,0 +1,52 @@ +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', + }; + } +} diff --git a/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php index 9c96591a..d9bf1da4 100644 --- a/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php +++ b/tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php @@ -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 { @@ -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', + ], + ]; + } } diff --git a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php index 503320b7..d4908fac 100644 --- a/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php +++ b/tests/Unit/Drivers/Imagick/Encoders/PngEncoderTest.php @@ -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\ImagickTestCase; -use Intervention\Image\Tests\Traits\CanDetectInterlacedPng; +use Intervention\Image\Tests\Traits\CanInspectPngFormat; +use PHPUnit\Framework\Attributes\DataProvider; #[RequiresPhpExtension('imagick')] #[CoversClass(\Intervention\Image\Encoders\PngEncoder::class)] #[CoversClass(\Intervention\Image\Drivers\Imagick\Encoders\PngEncoder::class)] final class PngEncoderTest extends ImagickTestCase { - use CanDetectInterlacedPng; + use CanInspectPngFormat; public function testEncode(): void { @@ -34,4 +36,65 @@ 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::createTestImage(3, 2)->fill('ccc'), // new grayscale + new PngEncoder(indexed: true), + 'grayscale', // result should be 'indexed' but there seems to be no way to force this with imagick + ], + [ + static::readTestImage('circle.png'), // truecolor-alpha + new PngEncoder(indexed: false), + 'truecolor-alpha', + ], + [ + static::readTestImage('circle.png'), // indexedcolor-alpha + new PngEncoder(indexed: true), + 'grayscale-alpha', // result should be 'indexed' but there seems to be no way to force this with imagick + ], + [ + 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', + ], + ]; + } }