From 08ef43f2954ba2b08a22b6ae3442d35021d30783 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 26 Aug 2025 20:57:34 +0000 Subject: [PATCH] Media: improve Imagick handling of indexed PNG images with transparency. Fix an issue where certain transparent PNG images experienced noticeable quality degradation when resized by Imagick. Follow up to [60246]. Props elvismdev, SirLouen, siliconforks, nosilver4u, iamshashank. Fixes #63448. Built from https://develop.svn.wordpress.org/trunk@60667 git-svn-id: http://core.svn.wordpress.org/trunk@60003 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/class-wp-image-editor-imagick.php | 81 ++++++++++++++++--- wp-includes/version.php | 2 +- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/wp-includes/class-wp-image-editor-imagick.php b/wp-includes/class-wp-image-editor-imagick.php index f57e6f281f..9acbf85d6c 100644 --- a/wp-includes/class-wp-image-editor-imagick.php +++ b/wp-includes/class-wp-image-editor-imagick.php @@ -444,6 +444,53 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { } try { + /* + * We need to perform some special handling for certain types of images: + * 1. For PNG images, we need to specify compression settings and remove unneeded chunks. + * 2. For indexed PNG images, the number of colors must not exceed 256. + * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved. + * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), + * we need to avoid saving the image using ImageMagick's 'png8' format, + * because that supports only binary (1 bit) transparency. + * + * For #4 we want to check whether the image has a 1-bit alpha channel before resizing, + * because resizing may cause the number of alpha values to multiply due to antialiasing. + * (We're assuming that, if the original image had only a 1-bit alpha channel, + * then a 1-bit alpha channel should be good enough for the resized images too.) + * So we're going to perform all the necessary checks before resizing the image + * and store the results in variables for later use. + */ + $is_png = false; + $is_indexed_png = false; + $is_indexed_png_with_alpha_channel = false; + $is_indexed_png_with_true_alpha_transparency = false; + + if ( 'image/png' === $this->mime_type ) { + $is_png = true; + + if ( + is_callable( array( $this->image, 'getImageProperty' ) ) + && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' ) + ) { + $is_indexed_png = true; + + if ( + is_callable( array( $this->image, 'getImageAlphaChannel' ) ) + && $this->image->getImageAlphaChannel() + ) { + $is_indexed_png_with_alpha_channel = true; + + if ( + is_callable( array( $this->image, 'getImageChannelDepth' ) ) + && defined( 'Imagick::CHANNEL_ALPHA' ) + && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA ) + ) { + $is_indexed_png_with_true_alpha_transparency = true; + } + } + } + } + /* * To be more efficient, resample large images to 5x the destination size before resizing * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111), @@ -480,30 +527,42 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { $this->image->setOption( 'jpeg:fancy-upsampling', 'off' ); } - if ( 'image/png' === $this->mime_type ) { + if ( $is_png ) { $this->image->setOption( 'png:compression-filter', '5' ); $this->image->setOption( 'png:compression-level', '9' ); $this->image->setOption( 'png:compression-strategy', '1' ); // Indexed PNG files get some additional handling. // See #63448 for details. - if ( - is_callable( array( $this->image, 'getImageProperty' ) ) - && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' ) - ) { + if ( $is_indexed_png ) { // Check for an alpha channel. - if ( - is_callable( array( $this->image, 'getImageAlphaChannel' ) ) - && $this->image->getImageAlphaChannel() - ) { + if ( $is_indexed_png_with_alpha_channel ) { $this->image->setOption( 'png:include-chunk', 'tRNS' ); } else { $this->image->setOption( 'png:exclude-chunk', 'all' ); } - // Set the image format to Indexed PNG. - $this->image->setOption( 'png:format', 'png8' ); + $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false ); + + /* + * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. + * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs, + * even though grayscale PNGs usually have considerably larger file sizes. + * But we can force ImageMagick to save the image as an indexed PNG instead, + * by telling it to use png8 format. + * + * Note that we need to first call quantizeImage() before checking getImageColorspace(), + * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images + * (and we have not found any other way to identify grayscale images). + * + * We need to avoid forcing indexed format for images with true alpha transparency, + * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG. + */ + if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) { + // Set the image format to Indexed PNG. + $this->image->setOption( 'png:format', 'png8' ); + } } else { $this->image->setOption( 'png:exclude-chunk', 'all' ); } diff --git a/wp-includes/version.php b/wp-includes/version.php index 324cf59953..0504de82ca 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.9-alpha-60666'; +$wp_version = '6.9-alpha-60667'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.