Prevent automatic conversion of PNG images to grayscale
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:
- 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.
- 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 );
}
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?
Apparently I put some words here a 10 years and two days ago:
https://stackoverflow.com/a/23924621/778719
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!
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):
So back to my original question, is it possible to disable the auto-grayscale conversion of IM?
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.