Skip to content
47 changes: 33 additions & 14 deletions src/wp-admin/includes/image-edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
*
Expand All @@ -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 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same consideration noted for WP_Image_Editor_GD applies here as well:

Suggested change
imagesavealpha( $img, true );
imagesavealpha( $img, true );
/*
* 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, false );


$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;
}

Expand Down
58 changes: 43 additions & 15 deletions src/wp-includes/class-wp-image-editor-gd.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,31 +432,59 @@ public function rotate( $angle ) {
* 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 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there's one more change in how images are handled with GD.

Today in trunk on PHP 8.5, every flipped image is reset to GD's default DPI value of 96 and interlacing is turned off. This is because every GD operation produces a new image.

  • All new images in GD start with the default values.
  • Nothing in WP_Image_Editor_GD calls imageresolution() to carry the sources values over.
  • Resize and rotate operations also return a new image using the defaults.

In this PR the image now flips in place, which preserves the source file's values. While this is arguably better and more correct, the current behavior should be preserved after these changes until this can be properly considered and all operations can be updated to behave the same.

This should ensure that flip operations perform the same:

Suggested change
/*
* 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, false );

$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 );
Expand Down
Loading
Loading