From e003a9180a26f241fef05191454378dec3ba0ce9 Mon Sep 17 00:00:00 2001 From: Elvis Morales Date: Thu, 15 May 2025 21:39:14 -0700 Subject: [PATCH 1/3] Fix: Avoid image quality degradation caused by quantizeImage() on resized images WordPress 6.8 introduced a call to Imagick's quantizeImage() function, which reduces color depth during image resizing. This has been observed to degrade image quality, particularly for PNG files with gradients, soft transitions, or transparency. This change conditionally applies quantization only when max_colors is below 256. Props elvismdev. See Trac #63448. --- src/wp-includes/class-wp-image-editor-imagick.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 66085ac503e9c..8f36f48d71186 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -506,7 +506,14 @@ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIAN $current_colors = $this->image->getImageColors(); $max_colors = min( $max_colors, $current_colors ); } - $this->image->quantizeImage( $max_colors, $this->image->getColorspace(), 0, false, false ); + + /* + * Only apply color quantization under safe conditions + * such as if max_colors is set low to avoid image degradation. + */ + if ( $max_colors < 256 ) { + $this->image->quantizeImage( $max_colors, $this->image->getColorspace(), 0, false, false ); + } /** * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. From f6ed8007c1bc1444f8096b7edb27a776ba1a467b Mon Sep 17 00:00:00 2001 From: Elvis Morales Date: Fri, 16 May 2025 13:37:55 -0700 Subject: [PATCH 2/3] Refactor: Improve image quantization logic to avoid quality degradation on resized PNGs This update replaces the previous depth-based quantization check with a more accurate method: - Uses getImageColors() to count unique colors. - Uses getImageProperty('png:IHDR.color_type') to confirm the image is indexed (palette-based). Quantization is now only applied to true indexed PNGs with 256 colors or fewer, preventing degradation in full-color or gradient-rich images. Also retains grayscale and alpha channel logic for PNG optimization. See: https://core.trac.wordpress.org/ticket/63448 --- .../class-wp-image-editor-imagick.php | 65 ++++++++----------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 8f36f48d71186..702494c63152b 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -484,45 +484,36 @@ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIAN $this->image->setOption( 'png:compression-filter', '5' ); $this->image->setOption( 'png:compression-level', '9' ); $this->image->setOption( 'png:compression-strategy', '1' ); - // Check to see if a PNG is indexed, and find the pixel depth. - if ( is_callable( array( $this->image, 'getImageDepth' ) ) ) { - $indexed_pixel_depth = $this->image->getImageDepth(); - - // Indexed PNG files get some additional handling. - if ( 0 < $indexed_pixel_depth && 8 >= $indexed_pixel_depth ) { - // Check for an alpha channel. - if ( - is_callable( array( $this->image, 'getImageAlphaChannel' ) ) - && $this->image->getImageAlphaChannel() - ) { - $this->image->setOption( 'png:include-chunk', 'tRNS' ); - } else { - $this->image->setOption( 'png:exclude-chunk', 'all' ); - } - - // Reduce colors in the images to maximum needed, using the global colorspace. - $max_colors = pow( 2, $indexed_pixel_depth ); - if ( is_callable( array( $this->image, 'getImageColors' ) ) ) { - $current_colors = $this->image->getImageColors(); - $max_colors = min( $max_colors, $current_colors ); - } - - /* - * Only apply color quantization under safe conditions - * such as if max_colors is set low to avoid image degradation. - */ - if ( $max_colors < 256 ) { - $this->image->quantizeImage( $max_colors, $this->image->getColorspace(), 0, false, false ); - } - - /** - * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. - */ - if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() ) { - $this->image->setOption( 'png:format', 'png8' ); - } + + // Check for an alpha channel. + if ( + is_callable( array( $this->image, 'getImageAlphaChannel' ) ) + && $this->image->getImageAlphaChannel() + ) { + $this->image->setOption( 'png:include-chunk', 'tRNS' ); + } else { + $this->image->setOption( 'png:exclude-chunk', 'all' ); + } + + // Reduce colors in the image only if it's an indexed PNG with <= 256 colors. + if ( + is_callable( array( $this->image, 'getImageColors' ) ) && + is_callable( array( $this->image, 'getImageProperty' ) ) + ) { + $current_colors = $this->image->getImageColors(); + $color_type = $this->image->getImageProperty( 'png:IHDR.color_type' ); + + if ( $current_colors <= 256 && '3' === $color_type ) { + $this->image->quantizeImage( $current_colors, $this->image->getColorspace(), 0, false, false ); } } + + /** + * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. + */ + if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() ) { + $this->image->setOption( 'png:format', 'png8' ); + } } /* From 4be91f3eafcca225f3d5cc5584778c15fbebc3ec Mon Sep 17 00:00:00 2001 From: Elvis Morales Date: Fri, 16 May 2025 21:13:52 -0700 Subject: [PATCH 3/3] =?UTF-8?q?Fix:=20Apply=20PNG=20quantization=20only=20?= =?UTF-8?q?to=20true=20indexed=20images=20with=20=E2=89=A4=20max=20colors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refines the logic introduced in [59589] to prevent image quality degradation on resized PNGs. Quantization is now only applied when: - The image is indexed (color-type-orig == 3) - The actual color count (getImageColors) is ≤ the max allowed by bit depth This preserves the original optimization goals (palette size reduction) while preventing unintended degradation on full-color or gradient-rich images. Also maintains grayscale (png8) support and alpha channel chunk handling. Props @SirLouen @siliconforks @wildworks Fixes #63448 --- .../class-wp-image-editor-imagick.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 702494c63152b..51722fed18d30 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -485,32 +485,33 @@ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIAN $this->image->setOption( 'png:compression-level', '9' ); $this->image->setOption( 'png:compression-strategy', '1' ); - // Check for an alpha channel. + // Handle alpha chunk inclusion or exclusion. if ( - is_callable( array( $this->image, 'getImageAlphaChannel' ) ) - && $this->image->getImageAlphaChannel() + is_callable( array( $this->image, 'getImageAlphaChannel' ) ) && + $this->image->getImageAlphaChannel() ) { $this->image->setOption( 'png:include-chunk', 'tRNS' ); } else { $this->image->setOption( 'png:exclude-chunk', 'all' ); } - // Reduce colors in the image only if it's an indexed PNG with <= 256 colors. + // Only apply quantization to actual indexed PNGs. if ( is_callable( array( $this->image, 'getImageColors' ) ) && + is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'getImageProperty' ) ) ) { $current_colors = $this->image->getImageColors(); - $color_type = $this->image->getImageProperty( 'png:IHDR.color_type' ); + $bit_depth = $this->image->getImageDepth(); + $max_colors = pow( 2, $bit_depth ); + $color_type = $this->image->getImageProperty( 'png:IHDR.color-type-orig' ); - if ( $current_colors <= 256 && '3' === $color_type ) { + if ( $current_colors <= $max_colors && '3' === $color_type ) { $this->image->quantizeImage( $current_colors, $this->image->getColorspace(), 0, false, false ); } } - /** - * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. - */ + // If grayscale, ensure format is png8 to retain index palette. if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() ) { $this->image->setOption( 'png:format', 'png8' ); }