Make WordPress Core

Changeset 65367


Ignore:
Timestamp:
11/10/2026 01:33:13 AM (22 hours ago)
Author:
desrosj
Message:

Media: Use imageflip() to flip images with GD.

WP_Image_Editor_GD::flip() and the deprecated _flip_image_resource() mirrored images by copying them with imagecopyresampled() and negative source dimensions. With the GD 2.4.0 bundled in PHP 8.6, this produces an all-black image. It affects flips in the image editor, and also JPEG uploads with an EXIF orientation of 2, 3 or 4, which WP_Image_Editor::maybe_exif_rotate() flips.

Both functions now flip the image in place with imageflip(), and the output stays the same as before. Palette images are still converted to truecolor, alpha blending is turned off and alpha saving on, and the resolution and interlacing are reset to GD's defaults of 96 DPI and off. This matches what rotate(), crop() and resize() produce. Whether every operation should keep the source image's resolution and interlacing will be decided in a follow-up ticket.

When no valid image is loaded, flip() now returns a WP_Error instead of throwing a TypeError.

Props sanjith2023, rumancodes, arshidkv12, dhruvang21, abhishekfdd, desrosj.
Fixes #66113.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/image-edit.php

    r63002 r65367  
    566566 *
    567567 * @since 2.9.0
     568 * @since 7.2.0 Switched to using imageflip() instead of imagecopyresampled() with negative dimensions.
    568569 * @deprecated 3.5.0 Use WP_Image_Editor::flip()
    569570 * @see WP_Image_Editor::flip()
    … …  
    578579        _deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::flip()' );
    579580
    580         $w   = imagesx( $img );
    581         $h   = imagesy( $img );
    582         $dst = wp_imagecreatetruecolor( $w, $h );
    583 
    584         if ( is_gd_image( $dst ) ) {
    585                 $sx = $vert ? ( $w - 1 ) : 0;
    586                 $sy = $horz ? ( $h - 1 ) : 0;
    587                 $sw = $vert ? -$w : $w;
    588                 $sh = $horz ? -$h : $h;
    589 
    590                 if ( imagecopyresampled( $dst, $img, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) {
    591                         if ( PHP_VERSION_ID < 80000 ) { // imagedestroy() has no effect as of PHP 8.0.
    592                                 imagedestroy( $img );
    593                         }
    594 
    595                         $img = $dst;
    596                 }
    597         }
     581        if ( ! is_gd_image( $img ) ) {
     582                return $img;
     583        }
     584
     585        // Keep returning a truecolor image, like the previous implementation did. imagewebp() rejects palette images.
     586        if ( ! imageistruecolor( $img ) && ! imagepalettetotruecolor( $img ) ) {
     587                return $img;
     588        }
     589
     590        // Match the alpha settings of the previous implementation's copy, including when no axis is flipped.
     591        imagealphablending( $img, false );
     592        imagesavealpha( $img, true );
     593
     594        /*
     595         * imageflip() changes the image in place, so the image keeps its current resolution and interlace setting.
     596         * _rotate_image_resource() and _crop_image_resource() copy into a new image, which resets them to GD's defaults:
     597         * 96 DPI and no interlacing. Reset them here too, so a flip gives the same result as the other edits.
     598         *
     599         * @link https://core.trac.wordpress.org/ticket/66113
     600         */
     601        imageresolution( $img, 96, 96 );
     602        imageinterlace( $img, 0 );
     603
     604        if ( ! $horz && ! $vert ) {
     605                return $img;
     606        }
     607
     608        if ( $horz && $vert ) {
     609                $mode = IMG_FLIP_BOTH;
     610        } elseif ( $horz ) {
     611                $mode = IMG_FLIP_VERTICAL;
     612        } else {
     613                $mode = IMG_FLIP_HORIZONTAL;
     614        }
     615
     616        imageflip( $img, $mode );
    598617
    599618        return $img;
  • trunk/src/wp-includes/class-wp-image-editor-gd.php

    r63501 r65367  
    433433         *
    434434         * @since 3.5.0
     435         * @since 7.2.0 Switched to using imageflip() instead of imagecopyresampled() with negative dimensions.
    435436         *
    436437         * @param bool $horz Flip along Horizontal Axis.
    … …  
    439440         */
    440441        public function flip( $horz, $vert ) {
    441                 $w   = $this->size['width'];
    442                 $h   = $this->size['height'];
    443                 $dst = wp_imagecreatetruecolor( $w, $h );
    444 
    445                 if ( is_gd_image( $dst ) ) {
    446                         $sx = $vert ? ( $w - 1 ) : 0;
    447                         $sy = $horz ? ( $h - 1 ) : 0;
    448                         $sw = $vert ? -$w : $w;
    449                         $sh = $horz ? -$h : $h;
    450 
    451                         if ( imagecopyresampled( $dst, $this->image, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) {
    452                                 if ( PHP_VERSION_ID < 80000 ) { // imagedestroy() has no effect as of PHP 8.0.
    453                                         imagedestroy( $this->image );
    454                                 }
    455 
    456                                 $this->image = $dst;
    457 
    458                                 return true;
    459                         }
     442                if ( ! is_gd_image( $this->image ) ) {
     443                        return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
     444                }
     445
     446                /*
     447                 * imageflip() works in place and keeps palette images as palette images.
     448                 * The previous implementation always copied into a new truecolor image,
     449                 * and saving or streaming as WebP relies on that, since imagewebp()
     450                 * rejects palette images. Convert first to keep that behavior.
     451                 */
     452                if ( ! imageistruecolor( $this->image ) && ! imagepalettetotruecolor( $this->image ) ) {
     453                        return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
     454                }
     455
     456                /*
     457                 * imageflip() also keeps the current alpha settings, which rotate() leaves
     458                 * with blending enabled. Reset them to what the previous implementation
     459                 * always produced, including when no axis is flipped.
     460                 */
     461                imagealphablending( $this->image, false );
     462                imagesavealpha( $this->image, true );
     463
     464                /*
     465                 * imageflip() changes the image in place, so the image keeps its current resolution and interlace setting.
     466                 * rotate(), crop() and resize() copy into a new image, which resets them to GD's defaults: 96 DPI and no
     467                 * interlacing. Reset them here too, so a flip gives the same result as the other edits.
     468                 *
     469                 * @link https://core.trac.wordpress.org/ticket/66113
     470                 */
     471                imageresolution( $this->image, 96, 96 );
     472                imageinterlace( $this->image, 0 );
     473
     474                if ( ! $horz && ! $vert ) {
     475                        return true;
     476                }
     477
     478                if ( $horz && $vert ) {
     479                        $mode = IMG_FLIP_BOTH;
     480                } elseif ( $horz ) {
     481                        $mode = IMG_FLIP_VERTICAL;
     482                } else {
     483                        $mode = IMG_FLIP_HORIZONTAL;
     484                }
     485
     486                if ( imageflip( $this->image, $mode ) ) {
     487                        return true;
    460488                }
    461489
  • trunk/tests/phpunit/tests/image/editorGd.php

    r65353 r65367  
    579579
    580580        /**
     581         * Tests that flip() moves every pixel to its mirrored position.
     582         *
     583         * A small non-square image with a distinct color per pixel is used so that
     584         * mixed up axes, off-by-one shifts and duplicated edges are all detected.
     585         *
     586         * @ticket 66113
     587         *
     588         * @dataProvider data_flip_axes
     589         *
     590         * @covers WP_Image_Editor_GD::flip
     591         *
     592         * @param bool $horz Whether to flip along the horizontal axis.
     593         * @param bool $vert Whether to flip along the vertical axis.
     594         */
     595        public function test_flip_moves_every_pixel( $horz, $vert ) {
     596                $w     = 3;
     597                $h     = 2;
     598                $color = static function ( $x, $y ) {
     599                        return ( ( $x * 40 + 10 ) << 16 ) | ( ( $y * 90 + 20 ) << 8 ) | 0x77;
     600                };
     601
     602                $image = imagecreatetruecolor( $w, $h );
     603                for ( $y = 0; $y < $h; $y++ ) {
     604                        for ( $x = 0; $x < $w; $x++ ) {
     605                                imagesetpixel( $image, $x, $y, $color( $x, $y ) );
     606                        }
     607                }
     608
     609                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' );
     610                $gd_image_editor->load();
     611
     612                $this->set_property_value( $gd_image_editor, 'image', $image );
     613
     614                // Keep the stored size in sync with the replaced image.
     615                $this->set_property_value(
     616                        $gd_image_editor,
     617                        'size',
     618                        array(
     619                                'width'  => $w,
     620                                'height' => $h,
     621                        )
     622                );
     623
     624                $this->assertTrue( $gd_image_editor->flip( $horz, $vert ) );
     625
     626                $flipped = $this->get_property_value( $gd_image_editor );
     627
     628                $this->assertSame( $w, imagesx( $flipped ), 'The width should not change.' );
     629                $this->assertSame( $h, imagesy( $flipped ), 'The height should not change.' );
     630
     631                for ( $y = 0; $y < $h; $y++ ) {
     632                        for ( $x = 0; $x < $w; $x++ ) {
     633                                $source_x = $vert ? $w - 1 - $x : $x;
     634                                $source_y = $horz ? $h - 1 - $y : $y;
     635
     636                                $this->assertSame(
     637                                        $color( $source_x, $source_y ),
     638                                        imagecolorat( $flipped, $x, $y ),
     639                                        "Pixel ($source_x,$source_y) did not move to ($x,$y)."
     640                                );
     641                        }
     642                }
     643        }
     644
     645        /**
     646         * Data provider with every combination of flip axes.
     647         *
     648         * @return array[]
     649         */
     650        public static function data_flip_axes() {
     651                return array(
     652                        'no flip'         => array( false, false ),
     653                        'vertical flip'   => array( true, false ),
     654                        'horizontal flip' => array( false, true ),
     655                        'both'            => array( true, true ),
     656                );
     657        }
     658
     659        /**
     660         * Tests that flip() returns a WP_Error when no valid GD image is loaded.
     661         *
     662         * @ticket 66113
     663         *
     664         * @dataProvider data_flip_axes
     665         *
     666         * @covers WP_Image_Editor_GD::flip
     667         *
     668         * @param bool $horz Whether to flip along the horizontal axis.
     669         * @param bool $vert Whether to flip along the vertical axis.
     670         */
     671        public function test_flip_returns_error_for_invalid_image( $horz, $vert ) {
     672                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' );
     673                $gd_image_editor->load();
     674
     675                $this->set_property_value( $gd_image_editor, 'image', false );
     676
     677                $result = $gd_image_editor->flip( $horz, $vert );
     678
     679                $this->assertWPError( $result, 'flip() should return a WP_Error when no valid image is loaded.' );
     680                $this->assertSame( 'image_flip_error', $result->get_error_code(), 'The error code should be image_flip_error.' );
     681        }
     682
     683        /**
     684         * Tests that maybe_exif_rotate() flips images whose EXIF orientation requires a flip.
     685         *
     686         * This is the most common way to reach flip(): uploading a mirrored or
     687         * upside-down JPEG. The image is only flipped, so its size does not change
     688         * and every pixel must end up at its mirrored position.
     689         *
     690         * @ticket 66113
     691         *
     692         * @dataProvider data_maybe_exif_rotate_flips_image
     693         *
     694         * @covers WP_Image_Editor::maybe_exif_rotate
     695         * @covers WP_Image_Editor_GD::flip
     696         *
     697         * @param int  $orientation EXIF orientation value.
     698         * @param bool $mirror_x    Whether the image should be mirrored left to right.
     699         * @param bool $mirror_y    Whether the image should be mirrored top to bottom.
     700         */
     701        public function test_maybe_exif_rotate_flips_image( $orientation, $mirror_x, $mirror_y ) {
     702                $file = DIR_TESTDATA . '/images/test-image-upside-down.jpg';
     703
     704                // The fixture has orientation 3. Override it to cover the other flip-only orientations.
     705                $filter = static function () use ( $orientation ) {
     706                        return $orientation;
     707                };
     708                add_filter( 'wp_image_maybe_exif_rotate', $filter );
     709
     710                $gd_image_editor = new WP_Image_Editor_GD( $file );
     711                $gd_image_editor->load();
     712
     713                $original = $this->get_property_value( $gd_image_editor );
     714                $w        = imagesx( $original );
     715                $h        = imagesy( $original );
     716
     717                // Snapshot a grid of pixels, since flip() modifies the image in place.
     718                $points = array();
     719                foreach ( array( 0, (int) ( $w / 3 ), (int) ( $w / 2 ), $w - 1 ) as $x ) {
     720                        foreach ( array( 0, (int) ( $h / 3 ), (int) ( $h / 2 ), $h - 1 ) as $y ) {
     721                                $points[] = array( $x, $y, imagecolorat( $original, $x, $y ) );
     722                        }
     723                }
     724
     725                $result = $gd_image_editor->maybe_exif_rotate();
     726
     727                remove_filter( 'wp_image_maybe_exif_rotate', $filter );
     728
     729                $this->assertTrue( $result, 'maybe_exif_rotate() should return true.' );
     730                $this->assertSame(
     731                        array(
     732                                'width'  => $w,
     733                                'height' => $h,
     734                        ),
     735                        $gd_image_editor->get_size(),
     736                        'Flipping should not change the size.'
     737                );
     738
     739                $flipped = $this->get_property_value( $gd_image_editor );
     740
     741                foreach ( $points as list( $x, $y, $color ) ) {
     742                        $target_x = $mirror_x ? $w - 1 - $x : $x;
     743                        $target_y = $mirror_y ? $h - 1 - $y : $y;
     744
     745                        $this->assertSame(
     746                                $color,
     747                                imagecolorat( $flipped, $target_x, $target_y ),
     748                                "Pixel ($x,$y) did not move to ($target_x,$target_y)."
     749                        );
     750                }
     751        }
     752
     753        /**
     754         * Data provider for test_maybe_exif_rotate_flips_image().
     755         *
     756         * @return array[]
     757         */
     758        public static function data_maybe_exif_rotate_flips_image() {
     759                return array(
     760                        'orientation 2: mirrored horizontally' => array( 2, true, false ),
     761                        'orientation 3: upside down'           => array( 3, true, true ),
     762                        'orientation 4: mirrored vertically'   => array( 4, false, true ),
     763                );
     764        }
     765
     766        /**
     767         * Tests that flip() converts palette images to truecolor, like the previous implementation.
     768         *
     769         * The previous implementation always copied into a new truecolor image.
     770         * Code that saves or streams the flipped image as WebP relies on that,
     771         * since imagewebp() rejects palette images.
     772         *
     773         * @ticket 66113
     774         *
     775         * @dataProvider data_flip_axes
     776         *
     777         * @covers WP_Image_Editor_GD::flip
     778         *
     779         * @param bool $horz Whether to flip along the horizontal axis.
     780         * @param bool $vert Whether to flip along the vertical axis.
     781         */
     782        public function test_flip_converts_palette_image_to_truecolor( $horz, $vert ) {
     783                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/test-image.gif' );
     784                $gd_image_editor->load();
     785
     786                $image = $this->get_property_value( $gd_image_editor );
     787                $w     = imagesx( $image );
     788                $h     = imagesy( $image );
     789
     790                $this->assertFalse( imageistruecolor( $image ), 'The fixture should be a palette image.' );
     791
     792                // Snapshot every pixel as a truecolor value, since flip() modifies the image in place.
     793                $colors = array();
     794                for ( $y = 0; $y < $h; $y++ ) {
     795                        for ( $x = 0; $x < $w; $x++ ) {
     796                                $rgba = imagecolorsforindex( $image, imagecolorat( $image, $x, $y ) );
     797
     798                                $colors[ $y ][ $x ] = ( $rgba['alpha'] << 24 ) | ( $rgba['red'] << 16 ) | ( $rgba['green'] << 8 ) | $rgba['blue'];
     799                        }
     800                }
     801
     802                $this->assertTrue( $gd_image_editor->flip( $horz, $vert ) );
     803
     804                $flipped = $this->get_property_value( $gd_image_editor );
     805
     806                $this->assertTrue( imageistruecolor( $flipped ), 'The flipped image should be a truecolor image.' );
     807
     808                for ( $y = 0; $y < $h; $y++ ) {
     809                        for ( $x = 0; $x < $w; $x++ ) {
     810                                $source_x = $vert ? $w - 1 - $x : $x;
     811                                $source_y = $horz ? $h - 1 - $y : $y;
     812
     813                                $this->assertSame(
     814                                        $colors[ $source_y ][ $source_x ],
     815                                        imagecolorat( $flipped, $x, $y ),
     816                                        "Pixel ($source_x,$source_y) did not move to ($x,$y)."
     817                                );
     818                        }
     819                }
     820        }
     821
     822        /**
     823         * Tests that a flipped palette image can be saved as WebP.
     824         *
     825         * imagewebp() rejects palette images with a warning and writes an empty file,
     826         * so this fails unless flip() returns a truecolor image.
     827         *
     828         * @ticket 66113
     829         *
     830         * @covers WP_Image_Editor_GD::flip
     831         * @covers WP_Image_Editor_GD::save
     832         */
     833        public function test_flipped_palette_image_can_be_saved_as_webp() {
     834                if ( ! WP_Image_Editor_GD::supports_mime_type( 'image/webp' ) ) {
     835                        $this->markTestSkipped( 'This GD build does not support WebP.' );
     836                }
     837
     838                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/test-image.gif' );
     839                $gd_image_editor->load();
     840
     841                $this->assertTrue( $gd_image_editor->flip( true, false ) );
     842
     843                $save_to_file = get_temp_dir() . wp_unique_filename( get_temp_dir(), 'flipped-palette.webp' );
     844                $saved        = $gd_image_editor->save( $save_to_file, 'image/webp' );
     845
     846                $this->assertNotWPError( $saved, 'Saving the flipped image as WebP should not fail.' );
     847
     848                $size     = wp_getimagesize( $saved['path'] );
     849                $filesize = filesize( $saved['path'] );
     850
     851                unlink( $saved['path'] );
     852
     853                $this->assertGreaterThan( 0, $filesize, 'The saved WebP file should not be empty.' );
     854                $this->assertIsArray( $size, 'The saved WebP file should be a valid image.' );
     855                $this->assertSame( 'image/webp', $size['mime'], 'The saved file should be a WebP image.' );
     856                $this->assertSame( array( 50, 50 ), array( $size[0], $size[1] ), 'The saved image size should not change.' );
     857        }
     858
     859        /**
     860         * Tests that flip() keeps the alpha channel of every pixel, in memory and after saving as PNG.
     861         *
     862         * Fully transparent pixels are left out on purpose: their hidden RGB value
     863         * is not visible and is not guaranteed to survive saving.
     864         *
     865         * @ticket 66113
     866         *
     867         * @dataProvider data_flip_axes
     868         *
     869         * @covers WP_Image_Editor_GD::flip
     870         *
     871         * @param bool $horz Whether to flip along the horizontal axis.
     872         * @param bool $vert Whether to flip along the vertical axis.
     873         */
     874        public function test_flip_keeps_alpha_channel( $horz, $vert ) {
     875                $w     = 3;
     876                $h     = 2;
     877                $color = static function ( $x, $y ) {
     878                        // Alpha values from 0 (opaque) to 125, all distinct.
     879                        $alpha = ( $y * 3 + $x ) * 25;
     880
     881                        return ( $alpha << 24 ) | ( ( $x * 40 + 10 ) << 16 ) | ( ( $y * 90 + 20 ) << 8 ) | 0x77;
     882                };
     883
     884                $image = imagecreatetruecolor( $w, $h );
     885                imagealphablending( $image, false );
     886                imagesavealpha( $image, true );
     887                for ( $y = 0; $y < $h; $y++ ) {
     888                        for ( $x = 0; $x < $w; $x++ ) {
     889                                imagesetpixel( $image, $x, $y, $color( $x, $y ) );
     890                        }
     891                }
     892
     893                $source_file = get_temp_dir() . wp_unique_filename( get_temp_dir(), 'flip-alpha.png' );
     894                imagepng( $image, $source_file );
     895
     896                $gd_image_editor = new WP_Image_Editor_GD( $source_file );
     897                $loaded          = $gd_image_editor->load();
     898
     899                unlink( $source_file );
     900
     901                $this->assertTrue( $loaded, 'The test image should load.' );
     902                $this->assertTrue( $gd_image_editor->flip( $horz, $vert ) );
     903
     904                $flipped = $this->get_property_value( $gd_image_editor );
     905
     906                $save_to_file = get_temp_dir() . wp_unique_filename( get_temp_dir(), 'flipped-alpha.png' );
     907                $saved        = $gd_image_editor->save( $save_to_file, 'image/png' );
     908
     909                $this->assertNotWPError( $saved, 'Saving the flipped image as PNG should not fail.' );
     910
     911                $reloaded = imagecreatefrompng( $saved['path'] );
     912                unlink( $saved['path'] );
     913
     914                for ( $y = 0; $y < $h; $y++ ) {
     915                        for ( $x = 0; $x < $w; $x++ ) {
     916                                $source_x = $vert ? $w - 1 - $x : $x;
     917                                $source_y = $horz ? $h - 1 - $y : $y;
     918                                $expected = $color( $source_x, $source_y );
     919
     920                                $this->assertSame(
     921                                        $expected,
     922                                        imagecolorat( $flipped, $x, $y ),
     923                                        "Pixel ($source_x,$source_y) did not move to ($x,$y) with its alpha in memory."
     924                                );
     925                                $this->assertSame(
     926                                        $expected,
     927                                        imagecolorat( $reloaded, $x, $y ),
     928                                        "Pixel ($source_x,$source_y) did not move to ($x,$y) with its alpha after saving."
     929                                );
     930                        }
     931                }
     932        }
     933
     934        /**
     935         * Tests that flipping along both axes gives the same image as rotating by 180 degrees.
     936         *
     937         * @ticket 66113
     938         *
     939         * @covers WP_Image_Editor_GD::flip
     940         */
     941        public function test_flip_both_axes_matches_rotate_180() {
     942                $file = DIR_TESTDATA . '/images/canola.jpg';
     943
     944                $flipped_editor = new WP_Image_Editor_GD( $file );
     945                $flipped_editor->load();
     946                $this->assertTrue( $flipped_editor->flip( true, true ) );
     947
     948                $rotated_editor = new WP_Image_Editor_GD( $file );
     949                $rotated_editor->load();
     950                $this->assertTrue( $rotated_editor->rotate( 180 ) );
     951
     952                $this->assertSame( $rotated_editor->get_size(), $flipped_editor->get_size(), 'The sizes should match.' );
     953
     954                $flipped = $this->get_property_value( $flipped_editor );
     955                $rotated = $this->get_property_value( $rotated_editor );
     956
     957                $w = imagesx( $flipped );
     958                $h = imagesy( $flipped );
     959
     960                // Count mismatches instead of asserting each pixel, since the image is large.
     961                $mismatches = 0;
     962                for ( $y = 0; $y < $h; $y++ ) {
     963                        for ( $x = 0; $x < $w; $x++ ) {
     964                                if ( imagecolorat( $flipped, $x, $y ) !== imagecolorat( $rotated, $x, $y ) ) {
     965                                        ++$mismatches;
     966                                }
     967                        }
     968                }
     969
     970                $this->assertSame( 0, $mismatches, 'Every pixel should match the image rotated by 180 degrees.' );
     971        }
     972
     973        /**
     974         * Tests that flip() disables alpha blending, including after rotate() enabled it.
     975         *
     976         * Alpha blending only affects drawing onto the image, so it is detected by
     977         * drawing a fully transparent pixel: with blending disabled, it replaces the
     978         * existing pixel instead of being blended into it.
     979         *
     980         * @ticket 66113
     981         *
     982         * @dataProvider data_flip_axes
     983         *
     984         * @covers WP_Image_Editor_GD::flip
     985         *
     986         * @param bool $horz Whether to flip along the horizontal axis.
     987         * @param bool $vert Whether to flip along the vertical axis.
     988         */
     989        public function test_flip_disables_alpha_blending_after_rotate( $horz, $vert ) {
     990                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' );
     991                $gd_image_editor->load();
     992
     993                $this->assertTrue( $gd_image_editor->rotate( 90 ) );
     994                $this->assertTrue( $gd_image_editor->flip( $horz, $vert ) );
     995
     996                $image = $this->get_property_value( $gd_image_editor );
     997
     998                imagesetpixel( $image, 0, 0, imagecolorallocatealpha( $image, 0, 0, 0, 127 ) );
     999
     1000                $this->assertSame( 127, ( imagecolorat( $image, 0, 0 ) >> 24 ) & 0x7F, 'Alpha blending should be disabled after flipping.' );
     1001        }
     1002
     1003        /**
     1004         * Tests that flip() resets the resolution and interlace setting to GD's defaults.
     1005         *
     1006         * The previous implementation always copied into a new image, which starts
     1007         * at 96 DPI with interlacing disabled, like rotate(), crop() and resize().
     1008         *
     1009         * @ticket 66113
     1010         *
     1011         * @dataProvider data_flip_axes
     1012         *
     1013         * @covers WP_Image_Editor_GD::flip
     1014         *
     1015         * @param bool $horz Whether to flip along the horizontal axis.
     1016         * @param bool $vert Whether to flip along the vertical axis.
     1017         */
     1018        public function test_flip_resets_resolution_and_interlace( $horz, $vert ) {
     1019                $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' );
     1020                $gd_image_editor->load();
     1021
     1022                $image = $this->get_property_value( $gd_image_editor );
     1023
     1024                imageresolution( $image, 300, 150 );
     1025                imageinterlace( $image, true );
     1026
     1027                $this->assertTrue( $gd_image_editor->flip( $horz, $vert ) );
     1028
     1029                $flipped = $this->get_property_value( $gd_image_editor );
     1030
     1031                $this->assertSame( array( 96, 96 ), imageresolution( $flipped ), 'The resolution should be reset to 96 DPI.' );
     1032                $this->assertFalse( (bool) imageinterlace( $flipped ), 'Interlacing should be disabled.' );
     1033        }
     1034
     1035        /**
    5811036         * Tests that an image created with WP_Image_Editor_GD preserves alpha with no resizing.
    5821037         *
  • trunk/tests/phpunit/tests/image/functions.php

    r65074 r65367  
    12041204                );
    12051205        }
     1206
     1207        /**
     1208         * Tests that _flip_image_resource() mirrors every pixel for each flip mode.
     1209         *
     1210         * A small non-square image with a distinct color per pixel is used so that
     1211         * mixed up axes, off-by-one shifts and duplicated edges are all detected.
     1212         *
     1213         * @ticket 66113
     1214         *
     1215         * @requires extension gd
     1216         *
     1217         * @expectedDeprecated _flip_image_resource
     1218         *
     1219         * @dataProvider data_flip_image_resource
     1220         *
     1221         * @covers ::_flip_image_resource
     1222         *
     1223         * @param bool $horz Whether to flip along the horizontal axis.
     1224         * @param bool $vert Whether to flip along the vertical axis.
     1225         */
     1226        public function test_flip_image_resource( $horz, $vert ) {
     1227                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     1228
     1229                $w     = 3;
     1230                $h     = 2;
     1231                $color = static function ( $x, $y ) {
     1232                        return ( ( $x * 40 + 10 ) << 16 ) | ( ( $y * 90 + 20 ) << 8 ) | 0x77;
     1233                };
     1234
     1235                $image = imagecreatetruecolor( $w, $h );
     1236                for ( $y = 0; $y < $h; $y++ ) {
     1237                        for ( $x = 0; $x < $w; $x++ ) {
     1238                                imagesetpixel( $image, $x, $y, $color( $x, $y ) );
     1239                        }
     1240                }
     1241
     1242                $flipped = _flip_image_resource( $image, $horz, $vert );
     1243
     1244                $this->assertTrue( is_gd_image( $flipped ), 'A GD image should be returned.' );
     1245                $this->assertSame( $w, imagesx( $flipped ), 'The width should not change.' );
     1246                $this->assertSame( $h, imagesy( $flipped ), 'The height should not change.' );
     1247
     1248                for ( $y = 0; $y < $h; $y++ ) {
     1249                        for ( $x = 0; $x < $w; $x++ ) {
     1250                                $source_x = $vert ? $w - 1 - $x : $x;
     1251                                $source_y = $horz ? $h - 1 - $y : $y;
     1252
     1253                                $this->assertSame(
     1254                                        $color( $source_x, $source_y ),
     1255                                        imagecolorat( $flipped, $x, $y ),
     1256                                        "Pixel ($source_x,$source_y) did not move to ($x,$y)."
     1257                                );
     1258                        }
     1259                }
     1260        }
     1261
     1262        /**
     1263         * Data provider for test_flip_image_resource().
     1264         *
     1265         * @return array[]
     1266         */
     1267        public static function data_flip_image_resource() {
     1268                return array(
     1269                        'no flip'         => array( false, false ),
     1270                        'vertical flip'   => array( true, false ),
     1271                        'horizontal flip' => array( false, true ),
     1272                        'both'            => array( true, true ),
     1273                );
     1274        }
     1275
     1276        /**
     1277         * Tests that _flip_image_resource() returns a truecolor image for a palette image.
     1278         *
     1279         * The previous implementation always copied into a new truecolor image.
     1280         * A small palette image with a distinct color per pixel is used, so every
     1281         * pixel can be checked at its mirrored position.
     1282         *
     1283         * @ticket 66113
     1284         *
     1285         * @requires extension gd
     1286         *
     1287         * @expectedDeprecated _flip_image_resource
     1288         *
     1289         * @dataProvider data_flip_image_resource
     1290         *
     1291         * @covers ::_flip_image_resource
     1292         *
     1293         * @param bool $horz Whether to flip along the horizontal axis.
     1294         * @param bool $vert Whether to flip along the vertical axis.
     1295         */
     1296        public function test_flip_image_resource_converts_palette_image_to_truecolor( $horz, $vert ) {
     1297                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     1298
     1299                $w     = 3;
     1300                $h     = 2;
     1301                $color = static function ( $x, $y ) {
     1302                        return ( ( $x * 40 + 10 ) << 16 ) | ( ( $y * 90 + 20 ) << 8 ) | 0x77;
     1303                };
     1304
     1305                $image = imagecreate( $w, $h );
     1306                for ( $y = 0; $y < $h; $y++ ) {
     1307                        for ( $x = 0; $x < $w; $x++ ) {
     1308                                $rgb = $color( $x, $y );
     1309                                imagesetpixel( $image, $x, $y, imagecolorallocate( $image, ( $rgb >> 16 ) & 0xFF, ( $rgb >> 8 ) & 0xFF, $rgb & 0xFF ) );
     1310                        }
     1311                }
     1312
     1313                $this->assertFalse( imageistruecolor( $image ), 'The test image should be a palette image.' );
     1314
     1315                $flipped = _flip_image_resource( $image, $horz, $vert );
     1316
     1317                $this->assertTrue( imageistruecolor( $flipped ), 'The flipped image should be a truecolor image.' );
     1318                $this->assertSame( $w, imagesx( $flipped ), 'The width should not change.' );
     1319                $this->assertSame( $h, imagesy( $flipped ), 'The height should not change.' );
     1320
     1321                for ( $y = 0; $y < $h; $y++ ) {
     1322                        for ( $x = 0; $x < $w; $x++ ) {
     1323                                $source_x = $vert ? $w - 1 - $x : $x;
     1324                                $source_y = $horz ? $h - 1 - $y : $y;
     1325
     1326                                $this->assertSame(
     1327                                        $color( $source_x, $source_y ),
     1328                                        imagecolorat( $flipped, $x, $y ),
     1329                                        "Pixel ($source_x,$source_y) did not move to ($x,$y)."
     1330                                );
     1331                        }
     1332                }
     1333        }
     1334
     1335        /**
     1336         * Tests that _flip_image_resource() returns an image that keeps its alpha channel when saved as PNG.
     1337         *
     1338         * The previous implementation always returned a new image with alpha saving
     1339         * enabled, even if the input had it disabled and no axis was flipped.
     1340         *
     1341         * @ticket 66113
     1342         *
     1343         * @requires extension gd
     1344         *
     1345         * @expectedDeprecated _flip_image_resource
     1346         *
     1347         * @dataProvider data_flip_image_resource
     1348         *
     1349         * @covers ::_flip_image_resource
     1350         *
     1351         * @param bool $horz Whether to flip along the horizontal axis.
     1352         * @param bool $vert Whether to flip along the vertical axis.
     1353         */
     1354        public function test_flip_image_resource_enables_alpha_saving( $horz, $vert ) {
     1355                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     1356
     1357                // A single half-transparent pixel, so its position does not change when flipped.
     1358                $image = imagecreatetruecolor( 1, 1 );
     1359                imagealphablending( $image, false );
     1360                imagesavealpha( $image, false );
     1361                imagesetpixel( $image, 0, 0, imagecolorallocatealpha( $image, 200, 100, 50, 64 ) );
     1362
     1363                $flipped = _flip_image_resource( $image, $horz, $vert );
     1364
     1365                $file = get_temp_dir() . wp_unique_filename( get_temp_dir(), 'flip-image-resource-alpha.png' );
     1366                imagepng( $flipped, $file );
     1367                $reloaded = imagecreatefrompng( $file );
     1368                unlink( $file );
     1369
     1370                $this->assertSame( 64, ( imagecolorat( $reloaded, 0, 0 ) >> 24 ) & 0x7F, 'The alpha channel should be kept when saving as PNG.' );
     1371        }
     1372
     1373        /**
     1374         * Tests that _flip_image_resource() returns an image with alpha blending disabled.
     1375         *
     1376         * The previous implementation always returned a new image with alpha blending
     1377         * disabled, even if the input had it enabled and no axis was flipped.
     1378         *
     1379         * Alpha blending only affects drawing onto the image, so it is detected by
     1380         * drawing a fully transparent pixel: with blending disabled, it replaces the
     1381         * existing pixel instead of being blended into it.
     1382         *
     1383         * @ticket 66113
     1384         *
     1385         * @requires extension gd
     1386         *
     1387         * @expectedDeprecated _flip_image_resource
     1388         *
     1389         * @dataProvider data_flip_image_resource
     1390         *
     1391         * @covers ::_flip_image_resource
     1392         *
     1393         * @param bool $horz Whether to flip along the horizontal axis.
     1394         * @param bool $vert Whether to flip along the vertical axis.
     1395         */
     1396        public function test_flip_image_resource_disables_alpha_blending( $horz, $vert ) {
     1397                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     1398
     1399                // New truecolor images have alpha blending enabled.
     1400                $image = imagecreatetruecolor( 3, 2 );
     1401
     1402                $flipped = _flip_image_resource( $image, $horz, $vert );
     1403
     1404                imagesetpixel( $flipped, 0, 0, imagecolorallocatealpha( $flipped, 0, 0, 0, 127 ) );
     1405
     1406                $this->assertSame( 127, ( imagecolorat( $flipped, 0, 0 ) >> 24 ) & 0x7F, 'Alpha blending should be disabled after flipping.' );
     1407        }
     1408
     1409        /**
     1410         * Tests that _flip_image_resource() resets the resolution and interlace setting to GD's defaults.
     1411         *
     1412         * The previous implementation always copied into a new image, which starts
     1413         * at 96 DPI with interlacing disabled, like _rotate_image_resource() and
     1414         * _crop_image_resource().
     1415         *
     1416         * @ticket 66113
     1417         *
     1418         * @requires extension gd
     1419         *
     1420         * @expectedDeprecated _flip_image_resource
     1421         *
     1422         * @dataProvider data_flip_image_resource
     1423         *
     1424         * @covers ::_flip_image_resource
     1425         *
     1426         * @param bool $horz Whether to flip along the horizontal axis.
     1427         * @param bool $vert Whether to flip along the vertical axis.
     1428         */
     1429        public function test_flip_image_resource_resets_resolution_and_interlace( $horz, $vert ) {
     1430                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     1431
     1432                $image = imagecreatetruecolor( 3, 2 );
     1433                imageresolution( $image, 300, 150 );
     1434                imageinterlace( $image, true );
     1435
     1436                $flipped = _flip_image_resource( $image, $horz, $vert );
     1437
     1438                $this->assertSame( array( 96, 96 ), imageresolution( $flipped ), 'The resolution should be reset to 96 DPI.' );
     1439                $this->assertFalse( (bool) imageinterlace( $flipped ), 'Interlacing should be disabled.' );
     1440        }
    12061441}
    12071442
Note: See TracChangeset for help on using the changeset viewer.