Make WordPress Core

Changeset 60667


Ignore:
Timestamp:
08/26/2025 08:55:24 PM (12 months ago)
Author:
adamsilverstein
Message:

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.

Location:
trunk
Files:
2 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-image-editor-imagick.php

    r60303 r60667  
    446446                try {
    447447                        /*
     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
     494                        /*
    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),
     
    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' );
     
    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' );
    506 
     545
     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' );
  • trunk/tests/phpunit/tests/image/editorImagick.php

    r60246 r60667  
    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}
Note: See TracChangeset for help on using the changeset viewer.