From 6a4c704ac894447836385ed5daf0e2a04ee7dba7 Mon Sep 17 00:00:00 2001 From: arshidkv12 Date: Fri, 2 Oct 2026 15:49:51 +0600 Subject: [PATCH 1/3] Media: Use imageflip() in WP_Image_Editor_GD::flip(). Replaces the imagecopyresampled() implementation, which relies on negative source dimensions, with imageflip(). The old approach produces incorrect pixel data with PHP 8.6's bundled GD 2.4.0. See #66113. --- src/wp-includes/class-wp-image-editor-gd.php | 31 +++++++------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php index 34c0453ff693a..5bd845457f0e4 100644 --- a/src/wp-includes/class-wp-image-editor-gd.php +++ b/src/wp-includes/class-wp-image-editor-gd.php @@ -435,31 +435,22 @@ public function rotate( $angle ) { * * @param bool $horz Flip along Horizontal Axis. * @param bool $vert Flip along Vertical Axis. - * @return true|WP_Error + * @return bool|WP_Error */ public function flip( $horz, $vert ) { - $w = $this->size['width']; - $h = $this->size['height']; - $dst = wp_imagecreatetruecolor( $w, $h ); - - if ( is_gd_image( $dst ) ) { - $sx = $vert ? ( $w - 1 ) : 0; - $sy = $horz ? ( $h - 1 ) : 0; - $sw = $vert ? -$w : $w; - $sh = $horz ? -$h : $h; - - if ( imagecopyresampled( $dst, $this->image, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) { - if ( PHP_VERSION_ID < 80000 ) { // imagedestroy() has no effect as of PHP 8.0. - imagedestroy( $this->image ); - } - - $this->image = $dst; + if ( ! is_gd_image( $this->image ) ) { + return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file ); + } - return true; - } + if ( $horz && $vert ) { + return imageflip( $this->image, IMG_FLIP_BOTH ); + } elseif ( $horz ) { + return imageflip( $this->image, IMG_FLIP_VERTICAL ); + } elseif ( $vert ) { + return imageflip( $this->image, IMG_FLIP_HORIZONTAL ); } - return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file ); + return true; } /** From 8eec1a9f6849efed747fbe2463098f8a634413cd Mon Sep 17 00:00:00 2001 From: Ruman Ahmed Date: Fri, 2 Oct 2026 15:59:46 +0600 Subject: [PATCH 2/3] Media: Keep the `true|WP_Error` contract in `WP_Image_Editor_GD::flip()`. Converts a failed `imageflip()` call, or a missing GD image, into the existing `image_flip_error` WP_Error instead of returning `false`, matching `WP_Image_Editor::flip()` and the Imagick implementation. Adds tests confirming that every pixel moves to the expected position for each flip direction, and that a failure returns a WP_Error. Props ruman. See #66113. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-image-editor-gd.php | 20 ++-- tests/phpunit/tests/image/editorGd.php | 110 +++++++++++++++++++ 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php index 5bd845457f0e4..88ed9473e685a 100644 --- a/src/wp-includes/class-wp-image-editor-gd.php +++ b/src/wp-includes/class-wp-image-editor-gd.php @@ -435,22 +435,26 @@ public function rotate( $angle ) { * * @param bool $horz Flip along Horizontal Axis. * @param bool $vert Flip along Vertical Axis. - * @return bool|WP_Error + * @return true|WP_Error */ public function flip( $horz, $vert ) { - if ( ! is_gd_image( $this->image ) ) { - return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file ); + if ( ! $horz && ! $vert ) { + return true; } if ( $horz && $vert ) { - return imageflip( $this->image, IMG_FLIP_BOTH ); + $mode = IMG_FLIP_BOTH; } elseif ( $horz ) { - return imageflip( $this->image, IMG_FLIP_VERTICAL ); - } elseif ( $vert ) { - return imageflip( $this->image, IMG_FLIP_HORIZONTAL ); + $mode = IMG_FLIP_VERTICAL; + } else { + $mode = IMG_FLIP_HORIZONTAL; } - return true; + if ( is_gd_image( $this->image ) && imageflip( $this->image, $mode ) ) { + return true; + } + + return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file ); } /** diff --git a/tests/phpunit/tests/image/editorGd.php b/tests/phpunit/tests/image/editorGd.php index ac0e8268390c2..c08d3b33c8f21 100644 --- a/tests/phpunit/tests/image/editorGd.php +++ b/tests/phpunit/tests/image/editorGd.php @@ -586,6 +586,116 @@ public function test_flip() { $this->assertSame( $color_top_left, imagecolorat( $property->getValue( $gd_image_editor ), 0, 99 ) ); } + /** + * Tests that flipping moves every pixel to the expected position. + * + * @ticket 66113 + * + * @dataProvider data_flip + * + * @covers WP_Image_Editor_GD::flip + * + * @param bool $horz Whether to flip along the horizontal axis. + * @param bool $vert Whether to flip along the vertical axis. + */ + public function test_flip_moves_pixels( $horz, $vert ) { + $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' ); + $gd_image_editor->load(); + + $property = new ReflectionProperty( $gd_image_editor, 'image' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + + // A 3x2 image in which every pixel has a distinct color. + $width = 3; + $height = 2; + $image = imagecreatetruecolor( $width, $height ); + for ( $x = 0; $x < $width; $x++ ) { + for ( $y = 0; $y < $height; $y++ ) { + imagesetpixel( $image, $x, $y, ( $x + 1 ) * 0x100000 + ( $y + 1 ) * 0x10 ); + } + } + $original = imagecreatetruecolor( $width, $height ); + imagecopy( $original, $image, 0, 0, 0, 0, $width, $height ); + + $property->setValue( $gd_image_editor, $image ); + + $size_property = new ReflectionProperty( $gd_image_editor, 'size' ); + if ( PHP_VERSION_ID < 80100 ) { + $size_property->setAccessible( true ); + } + $size_property->setValue( + $gd_image_editor, + array( + 'width' => $width, + 'height' => $height, + ) + ); + + $this->assertTrue( $gd_image_editor->flip( $horz, $vert ), 'flip() should return true on success.' ); + + $flipped = $property->getValue( $gd_image_editor ); + for ( $x = 0; $x < $width; $x++ ) { + for ( $y = 0; $y < $height; $y++ ) { + $this->assertSame( + imagecolorat( $original, $x, $y ), + imagecolorat( $flipped, $vert ? $width - 1 - $x : $x, $horz ? $height - 1 - $y : $y ), + "Pixel ($x, $y) was not moved to the expected position." + ); + } + } + } + + /** + * Tests that flipping returns a WP_Error, not false, when the image cannot be flipped. + * + * @ticket 66113 + * + * @covers WP_Image_Editor_GD::flip + */ + public function test_flip_returns_wp_error_on_failure() { + $gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/gradient-square.jpg' ); + $gd_image_editor->load(); + + $property = new ReflectionProperty( $gd_image_editor, 'image' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $property->setValue( $gd_image_editor, false ); + + $result = $gd_image_editor->flip( true, false ); + + $this->assertWPError( $result, 'flip() should return a WP_Error on failure.' ); + $this->assertSame( 'image_flip_error', $result->get_error_code(), 'The error code should be image_flip_error.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_flip() { + return array( + 'horizontal axis' => array( + 'horz' => true, + 'vert' => false, + ), + 'vertical axis' => array( + 'horz' => false, + 'vert' => true, + ), + 'both axes' => array( + 'horz' => true, + 'vert' => true, + ), + 'neither axis' => array( + 'horz' => false, + 'vert' => false, + ), + ); + } + /** * Tests that an image created with WP_Image_Editor_GD preserves alpha with no resizing. * From 1b65126ac660c7f581f4d78430f41456c8a79433 Mon Sep 17 00:00:00 2001 From: Ruman Ahmed Date: Fri, 2 Oct 2026 17:10:55 +0600 Subject: [PATCH 3/3] Media: Use `imageflip()` in the deprecated `_flip_image_resource()`. `_flip_image_resource()` used the same `imagecopyresampled()` approach with negative source dimensions as `WP_Image_Editor_GD::flip()`, which produces a blank image with PHP 8.6's bundled GD 2.4.0. It now uses `imageflip()` as well, and returns the image unchanged when it is not a GD image or no axis is given. Props ruman. See #66113. Co-Authored-By: Claude Opus 5.5 --- src/wp-admin/includes/image-edit.php | 27 +++++++--------- tests/phpunit/tests/image/editorGd.php | 43 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index 2f6bc25740e2d..f6548cf6e5efb 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -577,25 +577,20 @@ function _rotate_image_resource( $img, $angle ) { function _flip_image_resource( $img, $horz, $vert ) { _deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::flip()' ); - $w = imagesx( $img ); - $h = imagesy( $img ); - $dst = wp_imagecreatetruecolor( $w, $h ); - - if ( is_gd_image( $dst ) ) { - $sx = $vert ? ( $w - 1 ) : 0; - $sy = $horz ? ( $h - 1 ) : 0; - $sw = $vert ? -$w : $w; - $sh = $horz ? -$h : $h; - - if ( imagecopyresampled( $dst, $img, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) { - if ( PHP_VERSION_ID < 80000 ) { // imagedestroy() has no effect as of PHP 8.0. - imagedestroy( $img ); - } + if ( ! is_gd_image( $img ) || ( ! $horz && ! $vert ) ) { + return $img; + } - $img = $dst; - } + if ( $horz && $vert ) { + $mode = IMG_FLIP_BOTH; + } elseif ( $horz ) { + $mode = IMG_FLIP_VERTICAL; + } else { + $mode = IMG_FLIP_HORIZONTAL; } + imageflip( $img, $mode ); + return $img; } diff --git a/tests/phpunit/tests/image/editorGd.php b/tests/phpunit/tests/image/editorGd.php index c08d3b33c8f21..c2b98bfb60d25 100644 --- a/tests/phpunit/tests/image/editorGd.php +++ b/tests/phpunit/tests/image/editorGd.php @@ -647,6 +647,49 @@ public function test_flip_moves_pixels( $horz, $vert ) { } } + /** + * Tests that the deprecated _flip_image_resource() moves every pixel to the expected position. + * + * @ticket 66113 + * + * @dataProvider data_flip + * + * @covers ::_flip_image_resource + * + * @expectedDeprecated _flip_image_resource + * + * @param bool $horz Whether to flip along the horizontal axis. + * @param bool $vert Whether to flip along the vertical axis. + */ + public function test_flip_image_resource_moves_pixels( $horz, $vert ) { + require_once ABSPATH . 'wp-admin/includes/image-edit.php'; + + // A 3x2 image in which every pixel has a distinct color. + $width = 3; + $height = 2; + $image = imagecreatetruecolor( $width, $height ); + for ( $x = 0; $x < $width; $x++ ) { + for ( $y = 0; $y < $height; $y++ ) { + imagesetpixel( $image, $x, $y, ( $x + 1 ) * 0x100000 + ( $y + 1 ) * 0x10 ); + } + } + $original = imagecreatetruecolor( $width, $height ); + imagecopy( $original, $image, 0, 0, 0, 0, $width, $height ); + + $flipped = _flip_image_resource( $image, $horz, $vert ); + + $this->assertTrue( is_gd_image( $flipped ), '_flip_image_resource() should return a GD image.' ); + for ( $x = 0; $x < $width; $x++ ) { + for ( $y = 0; $y < $height; $y++ ) { + $this->assertSame( + imagecolorat( $original, $x, $y ), + imagecolorat( $flipped, $vert ? $width - 1 - $x : $x, $horz ? $height - 1 - $y : $y ), + "Pixel ($x, $y) was not moved to the expected position." + ); + } + } + } + /** * Tests that flipping returns a WP_Error, not false, when the image cannot be flipped. *