Imagick / imagick

šŸŒˆ The Imagick PHP extension šŸŒˆ
http://pecl.php.net/imagick
Other
548 stars 139 forks source link

Attempting to save image with PNG8 prefix throws fatal error #672

Closed nosilver4u closed 5 months ago

nosilver4u commented 5 months ago

Hi all, We've been attempting to improve the resizing of PNG images in WordPress and have run into a bit of a roadblock. But I'm not sure if we're missing something, or if this is a bug.

In short, we're specifically looking at indexed PNG images with a bit depth of 8 (or lower). When an image is scaled down, more colors get added, and often the scaled image ends up larger than the original. To fix this, I've used the quantizeImage() method to constrain the total number of colors, so that it is closer to the original and can be saved with a palette in indexed mode (color type 3). You can see what I've done so far here: https://github.com/WordPress/wordpress-develop/commit/40eb2d533f6068f5a4b6da25a6d29c85514bfe43

Unfortunately, even though the number of colors has been reduced to be below 256, the image doesn't get saved as indexed. I tried this: $image->setOption( 'png:color-type', 3 ); and nothing changes. I also tried $image->setImageType( imagick::IMGTYPE_PALETTE ) or $image->setImageType( imagick::IMGTYPE_PALETTEMATTE ) and that doesn't seem to help either.

At https://stackoverflow.com/questions/10348743/how-to-convert-png32-to-png8-via-imagick-in-php it is suggested to use $image->writeImage( "png8:$filename" ); and apparently at some point that worked, but I'm getting fatal errors when attempting that.

PHP Fatal error: Uncaught ImagickException: Cannot write PNG8 or color-type 3; colormap is NULL '/sites/test.exactlywww.com/files/wp-content/uploads/2024/05/cat-desk8-10-300x290.png' @ error/png.c/WriteOnePNGImage/9666

Likewise, $image->setOption( 'png:format', 'png8' ) also throws the same error, which I think might work better for our purposes--if it worked at all.

So, is there anything else we can do to try and get Imagick to save PNG images as indexed? Is this a bug or how is it that it used to work, and no longer does?

Thanks for your time and any help you can offer!

Edit: I'm primarily testing with ImageMagick 6.9.11-60 Q16 x86_64 2021-01-25 https://imagemagick.org Imagick version 3.7.0

and also with ImageMagick 7.1.1-19 Q16-HDRI x86_64 21601 https://imagemagick.org Imagick version 3.7.0

nosilver4u commented 5 months ago

Oh, and here are a couple images I've used for testing: https://ewwwio-downloads.b-cdn.net/png-tests/deskcat8.png https://ewwwio-downloads.b-cdn.net/png-tests/test8.png https://ewwwio-downloads.b-cdn.net/png-tests/Palette_icon-or8.png https://ewwwio-downloads.b-cdn.net/png-tests/rabbit-time-paletted-or8.png

Danack commented 5 months ago

Please can you give me a standalone piece of code that shows the problem? i.e. some code that can be run from the command line.

Although I might be able to reproduce your problem from your description, it might take me quite a while, or the problem might not show itself with different code running on my system.

nosilver4u commented 5 months ago

I was thinking of that last night, but ran out of time, so I'll work up a simple script to replicate it.

nosilver4u commented 5 months ago

Ok, I wrote a script locally, where I have the latest ImageMagick (7.1.1-32 Q16-HDRI x86_64 22207), and Imagick 3.7.0. palette-problem.php.zip

It's kind of long as I put in all the custom code for reading pixel depth and color type, as that doesn't seem to work great in Imagick directly. But if you'd rather copy it than use the zip, here you go:

<?php
$filename = 'deskcat8.png';
$newname  = 'deskcat8-test.png';

get_png_color_depth( $filename );

$image = new Imagick( $filename );

$current_colors = $image->getImageColors();
echo "started with $current_colors colors\n";

$image->resizeImage( 1140, 1102, 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:exclude-chunk', 'all' );

if ( $image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
        echo "hollder\n";
        $image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
}

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

$new_colors = $image->getImageColors();
echo "ended up with $new_colors colors\n";

// Here are all the things I've tried:
// $image->setOption( 'png:format', 'png8' );
// $image->setImageType( imagick::IMGTYPE_PALETTEMATTE );
// $image->setOption( 'png:color-type', 3 );

if ( 8 < $image->getImageDepth() ) {
        echo "deeeep\n";
        $image->setImageDepth( 8 );
}

// Or if we try to prefix the filename directly in writeImage():
// $write_image_result = $image->writeImage( 'PNG8:' . $newname );
$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 );
}
Danack commented 5 months ago

Thanks.

While I attempt to investigate what is going wrong, if you need a fix, then just calling ImageMagick through the command line with something like:

 convert deskcat8.png -resize 1140x1102 PNG8:im_output_png8.png

Does seem to give the desired result.

Danack commented 5 months ago

So. Try commenting out the line:

$image->setOption( 'png:exclude-chunk', 'all' );

and comment in the lines:

// Here are all the things I've tried:
$image->setOption( 'png:format', 'png8' );
$image->setImageType( imagick::IMGTYPE_PALETTEMATTE );
$image->setOption( 'png:color-type', 3 );

Does your code now behave as you wish?

If it does, then maybe use $image->stripImage rather than excluding chunks. I think that should prevent any 'extra' chunks from being saved, though that is just a guess rather than knowledge.

nosilver4u commented 5 months ago

Aha, you nailed it with the exclude-chunk option!

From reading the docs on that define, there is this useful bit:

If the ancillary PNG tRNS chunk is excluded and the image has transparency, the PNG colortype is forced to be 4 or 6 (GRAY_ALPHA or RGBA).

Thank you so much!

nosilver4u commented 5 months ago

So 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.

Any ideas?

Danack commented 5 months ago

Please can you submit it as a new issue. If nothing else, keeping issues closed gives me a marginal sense of accomplishment.

nosilver4u commented 5 months ago

will do!