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 for indexed color palette PNG output #1384

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Drivers/Gd/Decoders/NativeObjectDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,23 @@ public function decode(mixed $input): ImageInterface|ColorInterface

if (!imageistruecolor($input)) {
imagepalettetotruecolor($input);
$indexed = true;
}

imagesavealpha($input, true);

// build image instance
return new Image(
$image = new Image(
$this->driver(),
new Core([
new Frame($input)
])
);

// set origin
$image->origin()->setIndexed($indexed ?? false);

return $image;
}

/**
Expand Down
27 changes: 25 additions & 2 deletions src/Drivers/Gd/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,43 @@

use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
use Intervention\Image\Exceptions\RuntimeException;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;

class PngEncoder extends GenericPngEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$gd = $image->core()->native();
$gd = $this->maybeToPalette(clone $image)
->core()
->native();

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

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

/**
* Maybe turn given image color to indexed palette version according to encoder settings
*
* @param ImageInterface $image
* @throws RuntimeException
* @return ImageInterface
*/
private function maybeToPalette(ImageInterface $image): ImageInterface
{
if ($this->indexed === false) {
return $image;
}

if (is_null($this->indexed) && !$image->origin()->indexed()) {
return $image;
}

return $image->reduceColors(255);
}
}
39 changes: 38 additions & 1 deletion src/Drivers/Imagick/Decoders/NativeObjectDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public function decode(mixed $input): ImageInterface|ColorInterface
throw new DecoderException('Unable to decode input');
}

// get original indexed palette status for origin before any operation
$indexed = $this->hasIndexedColors($input);

// For some JPEG formats, the "coalesceImages()" call leads to an image
// completely filled with background color. The logic behind this is
// incomprehensible for me; could be an imagick bug.
Expand All @@ -50,9 +53,43 @@ public function decode(mixed $input): ImageInterface|ColorInterface
$image->modify(new AlignRotationModifier());
}

// set media type on origin
// set values on origin
$image->origin()->setMediaType($input->getImageMimeType());
$image->origin()->setIndexed($indexed);

return $image;
}

/**
* Determine if given imagick instance is a indexed palette color image
*
* @param Imagick $imagick
* @return bool
*/
private function hasIndexedColors(Imagick $imagick): bool
{
// Palette PNG files with alpha channel result incorrectly in "truecolor with alpha"
// in imagemagick 6 making it impossible to rely on Imagick::getImageType().
// This workaround looks at the the PNG data directly to decode the color type byte.

// detect imagick major version
$imagickVersion = dechex(Imagick::getVersion()['versionNumber']);
$imagickVersion = substr($imagickVersion, 0, 1);

// detect palette status manually in imagemagick 6
if (version_compare($imagickVersion, '6', '<=') && $imagick->getImageFormat() === 'PNG') {
$data = $imagick->getImageBlob();
$pos = strpos($data, 'IHDR');
$type = substr($data, $pos + 13, 1);
$type = unpack('C', $type)[1];

return $type === 3; // color type 3 is a PNG with indexed palette
}

// non-workaround version
return in_array(
$imagick->getImageType(),
[Imagick::IMGTYPE_PALETTE, Imagick::IMGTYPE_PALETTEMATTE],
);
}
}
4 changes: 3 additions & 1 deletion src/Drivers/Imagick/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ public function createImage(int $width, int $height): ImageInterface
$background = new ImagickPixel('rgba(255, 255, 255, 0)');

$imagick = new Imagick();
$imagick->newImage($width, $height, $background, 'png');
$imagick->newImage($width, $height, $background);
$imagick->setType(Imagick::IMGTYPE_UNDEFINED);
$imagick->setImageType(Imagick::IMGTYPE_UNDEFINED);
$imagick->setColorspace(Imagick::COLORSPACE_SRGB);
$imagick->setImageResolution(96, 96);
$imagick->setImageBackgroundColor($background);
$imagick->setFormat('PNG32');
$imagick->setImageFormat('PNG32');

return new Image($this, new Core($imagick));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/Imagick/Encoders/BmpEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class BmpEncoder extends GenericBmpEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$format = 'bmp';
$format = 'BMP';
$compression = Imagick::COMPRESSION_NO;

$imagick = $image->core()->native();
Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/Imagick/Encoders/GifEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class GifEncoder extends GenericGifEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$format = 'gif';
$format = 'GIF';
$compression = Imagick::COMPRESSION_LZW;

$imagick = $image->core()->native();
Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/Imagick/Encoders/Jpeg2000Encoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Jpeg2000Encoder extends GenericJpeg2000Encoder implements SpecializedInter
{
public function encode(ImageInterface $image): EncodedImageInterface
{
$format = 'jp2';
$format = 'JP2';
$compression = Imagick::COMPRESSION_JPEG;

$imagick = $image->core()->native();
Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/Imagick/Encoders/JpegEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class JpegEncoder extends GenericJpegEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$format = 'jpeg';
$format = 'JPEG';
$compression = Imagick::COMPRESSION_JPEG;
$blendingColor = $this->driver()->handleInput(
$this->driver()->config()->blendingColor
Expand Down
80 changes: 70 additions & 10 deletions src/Drivers/Imagick/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,88 @@
namespace Intervention\Image\Drivers\Imagick\Encoders;

use Imagick;
use ImagickException;
use Intervention\Image\EncodedImage;
use Intervention\Image\Encoders\PngEncoder as GenericPngEncoder;
use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\SpecializedInterface;
use Intervention\Image\Origin;

class PngEncoder extends GenericPngEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$format = 'png';
$compression = Imagick::COMPRESSION_ZIP;
$imagick = clone $image->core()->native();
$imagick->setCompression(Imagick::COMPRESSION_ZIP);
$imagick->setImageCompression(Imagick::COMPRESSION_ZIP);

$imagick = $image->core()->native();
$imagick->setFormat($format);
$imagick->setImageFormat($format);
$imagick->setCompression($compression);
$imagick->setImageCompression($compression);
$imagick = $this->setInterlaced($imagick);
$imagick = $this->setIndexed($imagick, $image->origin());

if ($this->interlaced) {
$imagick->setInterlaceScheme(Imagick::INTERLACE_LINE);
return new EncodedImage($imagick->getImagesBlob(), 'image/png');
}

/**
* Set interlace mode on given imagick object according to encoder settings
*
* @param Imagick $imagick
* @return Imagick
* @throws ImagickException
*/
private function setInterlaced(Imagick $imagick): Imagick
{
switch ($this->interlaced) {
case true:
$imagick->setInterlaceScheme(Imagick::INTERLACE_LINE);
break;

case false:
$imagick->setInterlaceScheme(Imagick::INTERLACE_NO);
break;
}

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

/**
* Set indexed color mode on given imagick object according to encoder settings
*
* @param Imagick $imagick
* @return Imagick
*/
private function setIndexed(Imagick $imagick, Origin $origin): Imagick
{
if ($this->indexed === true) {
$imagick->setFormat('PNG8');
$imagick->setImageFormat('PNG8');
$imagick->quantizeImage(
256,
$imagick->getImageColorspace(),
0,
false,
false
);
} elseif ($this->indexed === false) {
$imagick->setFormat('PNG32');
$imagick->setImageFormat('PNG32');
} elseif ($origin->indexed() === true) {
$imagick->setFormat('PNG8');
$imagick->setImageFormat('PNG8');
$imagick->quantizeImage(
256,
$imagick->getImageColorspace(),
0,
false,
false
);
} elseif ($origin->indexed() === false) {
$imagick->setFormat('PNG32');
$imagick->setImageFormat('PNG32');
} else {
$imagick->setFormat('PNG');
$imagick->setImageFormat('PNG');
}

return $imagick;
}
}
2 changes: 1 addition & 1 deletion src/Drivers/Imagick/Encoders/WebpEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class WebpEncoder extends GenericWebpEncoder implements SpecializedInterface
{
public function encode(ImageInterface $image): EncodedImage
{
$format = 'webp';
$format = 'WEBP';
$compression = Imagick::COMPRESSION_ZIP;

$imagick = $image->core()->native();
Expand Down
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 = null, public ?bool $indexed = null)
{
}
}
25 changes: 25 additions & 0 deletions src/Origin.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

class Origin
{
protected bool $indexed = false;

/**
* Create new origin instance
*
Expand Down Expand Up @@ -85,4 +87,27 @@ public function fileExtension(): ?string
{
return empty($this->filePath) ? null : pathinfo($this->filePath, PATHINFO_EXTENSION);
}

/**
* Get the marker that indicates whether the origin contains an indexed color palette
*
* @return bool
*/
public function indexed(): bool
{
return $this->indexed;
}

/**
* Set the marker that indicates whether the origin contains an indexed color palette
*
* @param bool $status
* @return Origin
*/
public function setIndexed(bool $status): self
{
$this->indexed = $status;

return $this;
}
}
25 changes: 21 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,24 @@ public function createTestImage(int $width, int $height): Image
);
}

public function createTestAnimation(): Image
public static function createTestImageTransparent(int $width, int $height): Image
{
$gd = imagecreatetruecolor($width, $height);
imagesavealpha($gd, true);
$background = imagecolorallocatealpha($gd, 255, 255, 255, 127);
imagealphablending($gd, false);
imagefill($gd, 0, 0, $background);
imagecolortransparent($gd, $background);

return new Image(
new Driver(),
new Core([
new Frame($gd)
])
);
}

public static function createTestAnimation(): Image
{
$gd1 = imagecreatetruecolor(3, 2);
imagefill($gd1, 0, 0, imagecolorallocate($gd1, 255, 0, 0));
Expand Down
Loading