imagick icon indicating copy to clipboard operation
imagick copied to clipboard

Prevent automatic conversion of PNG images to grayscale

Open nosilver4u opened this issue 1 year ago • 5 comments

Related to #672

I ran into a quirk with one of the test images: https://ewwwio-downloads.b-cdn.net/png-tests/test8.png

With all the other test/sample images, so long as I preserve the tRNS chunk, and quantize them to 255 colors (or less), I get an image that is indexed (palette or palettematte).

But with test8.png, IM always saves it as grayscale+alpha, which vastly increases the file size. I took a look with identify -verbose test8.png:

Image:
  Filename: test8.png
  Permissions: rw-r--r--
  Format: PNG (Portable Network Graphics)
  Mime type: image/png
  Class: PseudoClass
  Geometry: 1405x816+0+0
  Units: Undefined
  Colorspace: sRGB
  Type: GrayscaleAlpha
  Base type: PaletteAlpha
  Endianness: Undefined
  Depth: 8-bit
  ...

Note that IM shows a Base type of PaletteAlpha, but a Type of GrayscaleAlpha, which is different from all the other sample images, which simply say Type: PaletteAlpha.

Even stranger, if I do $image->getImageType() prior to saving the file, it says imagick::IMGTYPE_PALETTEMATTE, but as soon as I do writeOutput(), it's GrayscaleAlpha--and not just "spoofing" like the original, but actual color type 4 with no palette.

I went through some of the ways to force it into color type 3 (indexed), and only 1 works on this image: $image->setOption( 'png:format', 'png8' );

Unfortunately, that isn't ideal for the other images, as it "re-indexes" them, and degrades the quality. Two possibilities come to mind:

  1. Disable the auto-grayscale conversion of IM, though I haven't found any way to do that. The colorspace:auto-grayscale option looked promising, but apparently does nothing with PNG images.
  2. Somehow detect when IM is going to auto-convert to grayscale, based on some attribute of the image, and only set the PNG8 format in that specific case.

I did find that the colorspace had been changed from SRGB to GRAY, but changing it back didn't seem to help (see code below).

Here's a code snippet to replicate:

<?php
$filename = 'test8.png';
$newname  = 'test8-1140x662.png';
$width = 1140;
$height = 662;

get_png_color_depth( $filename );

$image = new Imagick( $filename );

$image->resizeImage( $width, $height, Imagick::FILTER_TRIANGLE, 1 );

$image->setOption( 'png:compression-filter', '5' );
$image->setOption( 'png:compression-level', '9' );
$image->setOption( 'png:compression-strategy', '1' );
$image->setOption( 'png:include-chunk', 'tRNS' );

$current_colors = $image->getImageColors();
$max_colors     = min( $current_colors + 8, 255 );

$image->quantizeImage( $max_colors, $image->getColorspace(), 0, false, false );

if ( imagick::COLORSPACE_GRAY === $image->getImageColorspace() ) {
	echo "changing to SRGB\n";
	$image->setImageColorspace( imagick::COLORSPACE_SRGB );
}

$write_image_result = $image->writeImage( $newname );

if ( $write_image_result ) {
	get_png_color_depth( $newname );
}

function get_png_color_depth( $filename ) {
	if ( ! is_file( $filename ) ) {
		return;
	}

	$size = filesize( $filename );

	echo "size of $filename is $size\n";

	$file_handle = fopen( $filename, 'rb' );

	if ( ! $file_handle ) {
		return;
	}

	$png_header = fread( $file_handle, 4 );
	if ( chr( 0x89 ) . 'PNG' !== $png_header ) {
		return;
	}

	// Move forward 8 bytes.
	fread( $file_handle, 8 );
	$png_ihdr = fread( $file_handle, 4 );

	// Make sure we have an IHDR.
	if ( 'IHDR' !== $png_ihdr ) {
		return;
	}

	// Skip past the dimensions.
	$dimensions = fread( $file_handle, 8 );

	// Bit depth: 1 byte
	// Bit depth is a single-byte integer giving the number of bits per sample or
	// per palette index (not per pixel).
	//
	// Valid values are 1, 2, 4, 8, and 16, although not all values are allowed for all color types.
	$pixel_depth = ord( (string) fread( $file_handle, 1 ) );

	echo "pixel depth of $filename is $pixel_depth\n";

	// Color type is a single-byte integer that describes the interpretation of the image data.
	// Color type codes represent sums of the following values:
	// 1 (palette used), 2 (color used), and 4 (alpha channel used).
	// The valid color types are:
	// 0 => Grayscale
	// 2 => Truecolor
	// 3 => Indexed
	// 4 => Greyscale with alpha
	// 6 => Truecolour with alpha
	$color_type = ord( (string) fread( $file_handle, 1 ) );

	echo "color type of $filename is $color_type\n";

	fclose( $file_handle );
}

nosilver4u avatar May 30 '24 18:05 nosilver4u

And then I realized there's a potential answer right in front of me! What if I did this:

if ( Imagick::COLORSPACE_GRAY === $image->getImageColorspace() ) {
        // Forcing (previously indexed) grayscale image back to palette mode.
        $image->setOption( 'png:format', 'png8' );
}

Unless you can think of any other way to know that IM is about to save an image as grayscale?

nosilver4u avatar May 30 '24 20:05 nosilver4u

Apparently I put some words here a 10 years and two days ago:

https://stackoverflow.com/a/23924621/778719

Danack avatar May 31 '24 15:05 Danack

Not sure which method you were recommending, but as I mentioned originally, getImageType() returns imagick::IMGTYPE_PALETTEMATTE

However, the compareImages() trick does indeed work, though I guess the return value has changed since you wrote that :)

Thanks!

nosilver4u avatar May 31 '24 18:05 nosilver4u

So, I know this has been a while, but I just found a hole in my original solution... If the given (indexed) PNG image has a gradient, this pretty much destroys the gradient: $image->setOption( 'png:format', 'png8' );

Here's a sample image (same script as before):

Image

So back to my original question, is it possible to disable the auto-grayscale conversion of IM?

nosilver4u avatar May 27 '25 19:05 nosilver4u

I know that this is not really a bug but there are some close bugs so it fits better to my TODO list like so.

bukka avatar Aug 14 '25 09:08 bukka