Commit f8434f48a8 for wordpress.org

commit f8434f48a8a5ff4bbf17045cfb277685ddf357a3
Author: desrosj <desrosj@git.wordpress.org>
Date:   Tue Nov 10 01:35:40 2026 +0000

    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.

    Built from https://develop.svn.wordpress.org/trunk@65367


    git-svn-id: http://core.svn.wordpress.org/trunk@64506 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-admin/includes/image-edit.php b/wp-admin/includes/image-edit.php
index 2f6bc25740..90ccab2822 100644
--- a/wp-admin/includes/image-edit.php
+++ b/wp-admin/includes/image-edit.php
@@ -565,6 +565,7 @@ function _rotate_image_resource( $img, $angle ) {
  * Flips an image resource. Internal use only.
  *
  * @since 2.9.0
+ * @since 7.2.0 Switched to using imageflip() instead of imagecopyresampled() with negative dimensions.
  * @deprecated 3.5.0 Use WP_Image_Editor::flip()
  * @see WP_Image_Editor::flip()
  *
@@ -577,25 +578,43 @@ 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( $img ) ) {
+		return $img;
+	}

-	if ( is_gd_image( $dst ) ) {
-		$sx = $vert ? ( $w - 1 ) : 0;
-		$sy = $horz ? ( $h - 1 ) : 0;
-		$sw = $vert ? -$w : $w;
-		$sh = $horz ? -$h : $h;
+	// Keep returning a truecolor image, like the previous implementation did. imagewebp() rejects palette images.
+	if ( ! imageistruecolor( $img ) && ! imagepalettetotruecolor( $img ) ) {
+		return $img;
+	}

-		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 );
-			}
+	// Match the alpha settings of the previous implementation's copy, including when no axis is flipped.
+	imagealphablending( $img, false );
+	imagesavealpha( $img, true );

-			$img = $dst;
-		}
+	/*
+	 * imageflip() changes the image in place, so the image keeps its current resolution and interlace setting.
+	 * _rotate_image_resource() and _crop_image_resource() copy into a new image, which resets them to GD's defaults:
+	 * 96 DPI and no interlacing. Reset them here too, so a flip gives the same result as the other edits.
+	 *
+	 * @link https://core.trac.wordpress.org/ticket/66113
+	 */
+	imageresolution( $img, 96, 96 );
+	imageinterlace( $img, 0 );
+
+	if ( ! $horz && ! $vert ) {
+		return $img;
 	}

+	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/wp-includes/class-wp-image-editor-gd.php b/wp-includes/class-wp-image-editor-gd.php
index 34c0453ff6..10eed8178b 100644
--- a/wp-includes/class-wp-image-editor-gd.php
+++ b/wp-includes/class-wp-image-editor-gd.php
@@ -432,31 +432,59 @@ class WP_Image_Editor_GD extends WP_Image_Editor {
 	 * Flips current image.
 	 *
 	 * @since 3.5.0
+	 * @since 7.2.0 Switched to using imageflip() instead of imagecopyresampled() with negative dimensions.
 	 *
 	 * @param bool $horz Flip along Horizontal Axis.
 	 * @param bool $vert Flip along Vertical Axis.
 	 * @return true|WP_Error
 	 */
 	public function flip( $horz, $vert ) {
-		$w   = $this->size['width'];
-		$h   = $this->size['height'];
-		$dst = wp_imagecreatetruecolor( $w, $h );
+		if ( ! is_gd_image( $this->image ) ) {
+			return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
+		}

-		if ( is_gd_image( $dst ) ) {
-			$sx = $vert ? ( $w - 1 ) : 0;
-			$sy = $horz ? ( $h - 1 ) : 0;
-			$sw = $vert ? -$w : $w;
-			$sh = $horz ? -$h : $h;
+		/*
+		 * imageflip() works in place and keeps palette images as palette images.
+		 * The previous implementation always copied into a new truecolor image,
+		 * and saving or streaming as WebP relies on that, since imagewebp()
+		 * rejects palette images. Convert first to keep that behavior.
+		 */
+		if ( ! imageistruecolor( $this->image ) && ! imagepalettetotruecolor( $this->image ) ) {
+			return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
+		}

-			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 );
-				}
+		/*
+		 * imageflip() also keeps the current alpha settings, which rotate() leaves
+		 * with blending enabled. Reset them to what the previous implementation
+		 * always produced, including when no axis is flipped.
+		 */
+		imagealphablending( $this->image, false );
+		imagesavealpha( $this->image, true );

-				$this->image = $dst;
+		/*
+		 * imageflip() changes the image in place, so the image keeps its current resolution and interlace setting.
+		 * rotate(), crop() and resize() copy into a new image, which resets them to GD's defaults: 96 DPI and no
+		 * interlacing. Reset them here too, so a flip gives the same result as the other edits.
+		 *
+		 * @link https://core.trac.wordpress.org/ticket/66113
+		 */
+		imageresolution( $this->image, 96, 96 );
+		imageinterlace( $this->image, 0 );

-				return true;
-			}
+		if ( ! $horz && ! $vert ) {
+			return true;
+		}
+
+		if ( $horz && $vert ) {
+			$mode = IMG_FLIP_BOTH;
+		} elseif ( $horz ) {
+			$mode = IMG_FLIP_VERTICAL;
+		} else {
+			$mode = IMG_FLIP_HORIZONTAL;
+		}
+
+		if ( imageflip( $this->image, $mode ) ) {
+			return true;
 		}

 		return new WP_Error( 'image_flip_error', __( 'Image flip failed.' ), $this->file );
diff --git a/wp-includes/version.php b/wp-includes/version.php
index b5a35f3de3..09c0812203 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta4-65366';
+$wp_version = '7.2-beta4-65367';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.