Make WordPress Core

Changeset 60667


Ignore:
Timestamp:
08/26/2025 08:55:24 PM (6 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.