Skip to content

Commit 93718ab

Browse files
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. git-svn-id: https://develop.svn.wordpress.org/trunk@60667 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 710675d commit 93718ab

File tree

4 files changed

+108
-11
lines changed

4 files changed

+108
-11
lines changed

src/wp-includes/class-wp-image-editor-imagick.php

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,53 @@ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIAN
444444
}
445445

446446
try {
447+
/*
448+
* We need to perform some special handling for certain types of images:
449+
* 1. For PNG images, we need to specify compression settings and remove unneeded chunks.
450+
* 2. For indexed PNG images, the number of colors must not exceed 256.
451+
* 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved.
452+
* 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit),
453+
* we need to avoid saving the image using ImageMagick's 'png8' format,
454+
* because that supports only binary (1 bit) transparency.
455+
*
456+
* For #4 we want to check whether the image has a 1-bit alpha channel before resizing,
457+
* because resizing may cause the number of alpha values to multiply due to antialiasing.
458+
* (We're assuming that, if the original image had only a 1-bit alpha channel,
459+
* then a 1-bit alpha channel should be good enough for the resized images too.)
460+
* So we're going to perform all the necessary checks before resizing the image
461+
* and store the results in variables for later use.
462+
*/
463+
$is_png = false;
464+
$is_indexed_png = false;
465+
$is_indexed_png_with_alpha_channel = false;
466+
$is_indexed_png_with_true_alpha_transparency = false;
467+
468+
if ( 'image/png' === $this->mime_type ) {
469+
$is_png = true;
470+
471+
if (
472+
is_callable( array( $this->image, 'getImageProperty' ) )
473+
&& '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
474+
) {
475+
$is_indexed_png = true;
476+
477+
if (
478+
is_callable( array( $this->image, 'getImageAlphaChannel' ) )
479+
&& $this->image->getImageAlphaChannel()
480+
) {
481+
$is_indexed_png_with_alpha_channel = true;
482+
483+
if (
484+
is_callable( array( $this->image, 'getImageChannelDepth' ) )
485+
&& defined( 'Imagick::CHANNEL_ALPHA' )
486+
&& 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA )
487+
) {
488+
$is_indexed_png_with_true_alpha_transparency = true;
489+
}
490+
}
491+
}
492+
}
493+
447494
/*
448495
* To be more efficient, resample large images to 5x the destination size before resizing
449496
* whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
@@ -480,30 +527,42 @@ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIAN
480527
$this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
481528
}
482529

483-
if ( 'image/png' === $this->mime_type ) {
530+
if ( $is_png ) {
484531
$this->image->setOption( 'png:compression-filter', '5' );
485532
$this->image->setOption( 'png:compression-level', '9' );
486533
$this->image->setOption( 'png:compression-strategy', '1' );
487534

488535
// Indexed PNG files get some additional handling.
489536
// See #63448 for details.
490-
if (
491-
is_callable( array( $this->image, 'getImageProperty' ) )
492-
&& '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
493-
) {
537+
if ( $is_indexed_png ) {
494538

495539
// Check for an alpha channel.
496-
if (
497-
is_callable( array( $this->image, 'getImageAlphaChannel' ) )
498-
&& $this->image->getImageAlphaChannel()
499-
) {
540+
if ( $is_indexed_png_with_alpha_channel ) {
500541
$this->image->setOption( 'png:include-chunk', 'tRNS' );
501542
} else {
502543
$this->image->setOption( 'png:exclude-chunk', 'all' );
503544
}
504-
// Set the image format to Indexed PNG.
505-
$this->image->setOption( 'png:format', 'png8' );
506545

546+
$this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false );
547+
548+
/*
549+
* If the colorspace is 'gray', use the png8 format to ensure it stays indexed.
550+
* ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs,
551+
* even though grayscale PNGs usually have considerably larger file sizes.
552+
* But we can force ImageMagick to save the image as an indexed PNG instead,
553+
* by telling it to use png8 format.
554+
*
555+
* Note that we need to first call quantizeImage() before checking getImageColorspace(),
556+
* because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images
557+
* (and we have not found any other way to identify grayscale images).
558+
*
559+
* We need to avoid forcing indexed format for images with true alpha transparency,
560+
* because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG.
561+
*/
562+
if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) {
563+
// Set the image format to Indexed PNG.
564+
$this->image->setOption( 'png:format', 'png8' );
565+
}
507566
} else {
508567
$this->image->setOption( 'png:exclude-chunk', 'all' );
509568
}
28.7 KB
Loading
25.1 KB
Loading

tests/phpunit/tests/image/editorImagick.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,4 +873,42 @@ public static function data_png_color_type_after_resize() {
873873
),
874874
);
875875
}
876+
877+
/**
878+
* Tests that alpha transparency is preserved after resizing.
879+
*
880+
* @ticket 63448
881+
* @dataProvider data_alpha_transparency_is_preserved_after_resize
882+
*
883+
* @param string $file_path Path to the image file.
884+
*/
885+
public function test_alpha_transparency_is_preserved_after_resize( $file_path ) {
886+
887+
$temp_file = DIR_TESTDATA . '/images/test-temp.png';
888+
889+
$imagick_image_editor = new WP_Image_Editor_Imagick( $file_path );
890+
$imagick_image_editor->load();
891+
892+
$size = $imagick_image_editor->get_size();
893+
$imagick_image_editor->resize( $size['width'] * 0.5, $size['height'] * 0.5 );
894+
$imagick_image_editor->save( $temp_file );
895+
896+
$imagick = new Imagick( $temp_file );
897+
$alpha_channel_depth = $imagick->getImageChannelDepth( Imagick::CHANNEL_ALPHA );
898+
899+
unlink( $temp_file );
900+
901+
$this->assertGreaterThan( 1, $alpha_channel_depth, "Alpha transparency should be preserved after resize for {$file_path}." );
902+
}
903+
904+
public static function data_alpha_transparency_is_preserved_after_resize() {
905+
return array(
906+
'oval-or8' => array(
907+
DIR_TESTDATA . '/images/png-tests/oval-or8.png',
908+
),
909+
'oval-or8-grayscale-indexed' => array(
910+
DIR_TESTDATA . '/images/png-tests/oval-or8-grayscale-indexed.png',
911+
),
912+
);
913+
}
876914
}

0 commit comments

Comments
 (0)