Skip to content

Commit

Permalink
Reintroduce indexed color palette marker on Origin::class
Browse files Browse the repository at this point in the history
  • Loading branch information
olivervogel committed Aug 1, 2024
1 parent 640127b commit 0874618
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
tests:
build: ./
working_dir: /project
command: bash -c "composer install && ./vendor/bin/phpunit --filter=PngEncoderTest"
command: bash -c "composer install && ./vendor/bin/phpunit"
volumes:
- ./:/project
coverage:
Expand Down
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],
);
}
}
56 changes: 32 additions & 24 deletions src/Drivers/Imagick/Encoders/PngEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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
{
Expand All @@ -20,7 +21,7 @@ public function encode(ImageInterface $image): EncodedImage
$imagick->setImageCompression(Imagick::COMPRESSION_ZIP);

$imagick = $this->setInterlaced($imagick);
$imagick = $this->setIndexed($imagick);
$imagick = $this->setIndexed($imagick, $image->origin());

return new EncodedImage($imagick->getImagesBlob(), 'image/png');
}
Expand Down Expand Up @@ -53,30 +54,37 @@ private function setInterlaced(Imagick $imagick): Imagick
* @param Imagick $imagick
* @return Imagick
*/
private function setIndexed(Imagick $imagick): Imagick
private function setIndexed(Imagick $imagick, Origin $origin): Imagick
{
switch ($this->indexed) {
case null:
$imagick->setFormat('PNG');
$imagick->setImageFormat('PNG');
break;

case true:
$imagick->setFormat('PNG');
$imagick->setImageFormat('PNG');
$imagick->quantizeImage(
256,
$imagick->getImageColorspace(),
0,
false,
false
);
break;

case false:
$imagick->setFormat('PNG');
$imagick->setImageFormat('PNG');
break;
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;
Expand Down
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;
}
}
92 changes: 92 additions & 0 deletions tests/Unit/Drivers/Gd/Encoders/PngEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
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\CanInspectPng;
use PHPUnit\Framework\Attributes\DataProvider;

#[RequiresPhpExtension('gd')]
#[CoversClass(\Intervention\Image\Encoders\PngEncoder::class)]
Expand All @@ -34,4 +36,94 @@ 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), // truecolor-alpha
new PngEncoder(),
'truecolor-alpha',
],
[
static::createTestImage(3, 2), // truecolor-alpha
new PngEncoder(indexed: true),
'indexed',
],
[
static::createTestImage(3, 2), // truecolor-alpha
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::createTestImageTransparent(3, 2), // truecolor-alpha
new PngEncoder(),
'truecolor-alpha',
],
[
static::createTestImageTransparent(3, 2), // truecolor-alpha
new PngEncoder(indexed: true),
'indexed',
],
[
static::createTestImageTransparent(3, 2), // truecolor-alpha
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::createTestImageTransparent(3, 2)->fill('fff'), // truecolor-alpha
new PngEncoder(),
'truecolor-alpha',
],
[
static::createTestImageTransparent(3, 2)->fill('fff'), // truecolor-alpha
new PngEncoder(indexed: true),
'indexed',
],
[
static::createTestImageTransparent(3, 2)->fill('fff'), // truecolor-alpha
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::readTestImage('tile.png'), // indexed
new PngEncoder(),
'indexed',
],
[
static::readTestImage('tile.png'), // indexed
new PngEncoder(indexed: true),
'indexed',
],
[
static::readTestImage('tile.png'), // indexed
new PngEncoder(indexed: false),
'truecolor-alpha',
],
[
static::readTestImage('test.jpg'), // foreign format
new PngEncoder(),
'truecolor-alpha',
],
[
static::readTestImage('test.jpg'), // foreign format
new PngEncoder(indexed: true),
'indexed',
],
[
static::readTestImage('test.jpg'), // foreign format
new PngEncoder(indexed: false),
'truecolor-alpha',
]
];
}
}
Loading

0 comments on commit 0874618

Please sign in to comment.