You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Media: Use imageflip() in WP_Image_Editor_GD::flip(). - #13920
WP_Image_Editor_GD::flip() mirrored images by passing negative source dimensions to imagecopyresampled(). With PHP 8.6's bundled GD 2.4.0 that produces a black image, so Tests_Image_Editor_GD::test_flip fails (Failed asserting that 0 is identical to 8434407). It now calls imageflip(), which flips in place and does not depend on how GD handles negative sizes. _flip_image_resource(), the deprecated GD-resource version, gets the same change.
The switch keeps the previous output. The old code always copied into a new truecolor image with alphablending=false, savealpha=true, GD's default resolution of 96 DPI and interlacing off, and both functions now produce the same result. Without the truecolor conversion, a flipped GIF or 8-bit PNG saved or streamed as WebP becomes an empty file, because imagewebp() rejects palette images.
The most common way to hit this bug is uploading a JPEG whose EXIF orientation is 2, 3 or 4. WP_Image_Editor::maybe_exif_rotate() calls flip() for those, so on PHP 8.6 the uploaded image comes out black.
flip(): replaces wp_imagecreatetruecolor() + imagecopyresampled() with negative $sw/$sh with imageflip(). Returns WP_Error if $this->image is not a GD image. Converts palette images to truecolor, resets the alpha settings, and resets the resolution to 96 DPI with interlacing off before flipping, including for flip( false, false ).
src/wp-admin/includes/image-edit.php:578
_flip_image_resource(): the same changes. Returns $img unchanged if it is not a GD image.
tests/phpunit/tests/image/editorGd.php
New flip tests, listed below.
tests/phpunit/tests/image/functions.php
Pixel, palette, alpha-saving, and resolution and interlace tests for _flip_image_resource(), skipped when GD is not available.
New tests:
Test
Checks
test_flip_moves_every_pixel()
A 3×2 image with a different color per pixel. For all 4 flag combinations, every pixel moves to its mirrored position.
test_flip_returns_error_for_invalid_image()
flip() returns a WP_Error with the code image_flip_error when no valid image is loaded.
test_maybe_exif_rotate_flips_image()
EXIF orientations 2, 3 and 4 through maybe_exif_rotate(), using test-image-upside-down.jpg. The size is unchanged and sampled pixels are mirrored.
test_flip_converts_palette_image_to_truecolor()
A palette GIF becomes truecolor, and every pixel keeps its color at its mirrored position, for all 4 flag combinations.
test_flipped_palette_image_can_be_saved_as_webp()
A flipped palette GIF saved as WebP is a valid, non-empty WebP file.
test_flip_keeps_alpha_channel()
Pixels with different alpha values keep their alpha in memory and after saving as PNG.
test_flip_both_axes_matches_rotate_180()
flip( true, true ) matches rotate( 180 ) pixel for pixel on canola.jpg.
test_flip_disables_alpha_blending_after_rotate()
rotate() turns alpha blending on, and a later flip() turns it off again, as the old code did. Covers all 4 flag combinations, including no flip.
test_flip_resets_resolution_and_interlace()
An image set to 300×150 DPI with interlacing on comes back at 96×96 DPI with interlacing off, for all 4 flag combinations, including no flip.
A 3×2 palette image from imagecreate() with a different color per pixel becomes truecolor, and every pixel keeps its color at its mirrored position, for all 4 flag combinations.
test_flip_image_resource_enables_alpha_saving()
An image with alpha saving disabled keeps its alpha when the result is saved as PNG, for all 4 flag combinations, including no flip.
The same resolution and interlace check for _flip_image_resource().
Testing
Automated, with vendor/bin/phpunit --filter 'Tests_Image_Editor_GD|test_flip_image_resource'. To show that the tests catch the bug, I also ran them against trunk's old flip() and _flip_image_resource().
Code under test
PHP 8.3 / bundled GD 2.1
PHP 8.6.0RC4 / bundled GD 2.4.0
trunk (old implementation)
4 errors: the invalid-image test, where trunk throws a TypeError
23 failures + 4 errors
this PR
72 tests: 71 pass, 1 skipped (an existing trunk test that requires PHP 8.6)
72/72 pass
On PHP 8.3, trunk passes every pixel, palette, alpha, EXIF and rotate test, so the output does not change on GD versions that were already working. The resolution and interlace tests pass on trunk with both PHP versions, because the old code always returned a new image with GD's defaults; they fail on this PR without the reset. On PHP 8.6, trunk fails the pixel, EXIF, palette, alpha, rotate-180 and _flip_image_resource() tests.
PHPUnit on PHP 8.3 and PHP 8.6.0RC4: results above.
Full vendor/bin/phpunit --group image on PHP 8.3: 358 tests, no failures, 12 skipped.
vendor/bin/phpcs on the four changed files: clean.
vendor/bin/phpstan analyse on the two changed source files: no errors.
Manual: in Media > Library, edit canola.jpg with "Flip vertical", save, then "Flip horizontal", and save. Each saved file matches the original mirrored on the expected axes, pixel for pixel within JPEG noise, and is not black.
Manual: upload tests/phpunit/data/images/test-image-upside-down.jpg (EXIF orientation 3). It comes out upright as test-image-upside-down-rotated-1.jpg, not black.
Manual: in Edit Image, flip tests/phpunit/data/images/test-image.gif and save it with an image_editor_output_format filter that maps GIF to WebP. The saved file is a valid, flipped 50×50 WebP (458 bytes), not empty.
Manual tests ran in the local Docker environment on PHP 8.6.0RC4 with bundled GD 2.4.0. The Docker image has Imagick installed, so a local mu-plugin filtered wp_image_editors to use only WP_Image_Editor_GD.
Two existing trunk bugs showed up during the GIF-to-WebP test. This PR does not change either one:
The output-format filter has to be limited to the Edit Image save. On upload, the GIF is converted to WebP before any flip runs, and imagewebp() writes an empty file for the palette image (see Possible Follow-up).
After the save, the attachment points to test-image-e….gif, but the file on disk is test-image-e….webp, so the image does not display. wp_save_image() stores $new_path (src/wp-admin/includes/image-edit.php:1075-1077) rather than the path the editor actually saved to. This happens on trunk too, whenever the output format changes the extension.
To reproduce on PHP 8.6: LOCAL_PHP=8.6-fpm npm run env:start, then npm run test:php -- --filter 'Tests_Image_Editor_GD|test_flip_image_resource'.
Implementation Notes
Axis mapping:$horz flips along the horizontal axis, which swaps top and bottom, so it maps to IMG_FLIP_VERTICAL. $vert maps to IMG_FLIP_HORIZONTAL, and both map to IMG_FLIP_BOTH. This matches the old $sy/$sx logic and WP_Image_Editor_Imagick::flip(). The pixel tests fail if the axes are swapped.
Palette images become truecolor, as before.imageflip() alone would keep palette images as palette images. That breaks saving or streaming them as WebP, since imagewebp() warns Palette image not supported by webp, still returns true, and writes a 0-byte file. imagepalettetotruecolor() runs before the flip, and also when no axis is flipped, because the old code converted in every case.
Alpha settings match the old output. The old code always returned an image with alphablending=false and savealpha=true. imageflip() keeps the image's current settings, which are alphablending=true after rotate(). Both settings are now reset right after the truecolor conversion, before the no-axis return, so flip( false, false ) matches the old output too.
Resolution and interlace match the old output. A new GD image starts at 96 DPI with interlacing off, and nothing in WP_Image_Editor_GD copies the source's values over, so the old flip always reset them, as rotate(), crop() and resize() still do. imageflip() keeps the source's values, so both functions call imageresolution( 96, 96 ) and imageinterlace( 0 ), also before the no-axis return. 0 is passed instead of false because PHPStan checks against PHP 7.4, where the parameter is typed as int; it behaves the same on PHP 8. Keeping the source's values would arguably be more correct, but that should be done for all operations at once.
Validation comes first in flip(): The is_gd_image() check is placed before the "no axes" early return. Otherwise flip( false, false ) with no image loaded would return true, while every other path returns WP_Error.
Error handling:image_flip_error used to cover a failed wp_imagecreatetruecolor(), for example when memory runs out. imageflip() only returns false for an invalid mode, which this code cannot pass. The WP_Error is kept for a false return, and for a failed palette conversion.
_flip_image_resource() changes nothing on failure: It cannot return WP_Error, because its only core caller (image_edit_apply_changes(), src/wp-admin/includes/image-edit.php:759) assigns the result to $image and keeps using it. Returning the input unchanged matches the old failure behavior and the "(maybe) flipped" wording in its docblock.
No function_exists( 'imageflip' ) guard:imageflip() has been in PHP since 5.5, with no conditional compilation, and every libgd version PHP 7.4+ accepts includes it.
Memory: the flip no longer allocates a second full-size image. For a 6000×4000 photo that is about 96 MB less peak memory.
Remaining Behavior Differences
The flip happens in place. In WP_Image_Editor_GD, $this->image is protected and nothing outside the class holds the old object, so this has no effect there. _flip_image_resource( $img, … ) now also changes the caller's $img. Before, it was left untouched on PHP 8 and destroyed on PHP < 8. The function has been deprecated since 3.5.0, and its one core caller reassigns the return value.
Exact pixels for fully transparent areas. The old resampling changed the hidden RGB values of fully transparent pixels. imageflip() keeps them, so saved files can differ by a few bytes. Nothing visible changes.
The transparent color is kept. The old code always returned a new image, so imagecolortransparent() came back as -1. The in-place flip keeps the source's transparent color, including for flip( false, false ). Pixels in memory and saved PNG and WebP files are identical to the old output. The one visible difference is a truecolor image with a transparent color set, such as an RGB PNG with a tRNS chunk, that is flipped and then saved as GIF: pixels matching that color now come out transparent, where the old code left them opaque. Core only saves a PNG as GIF if an image_editor_output_format filter maps it that way. Palette GIFs and PNGs decode to the same pixels as before, because after the truecolor conversion their transparent color matches no pixel. Resetting it is not straightforward, since imagecolortransparent( $img, -1 ) does not clear it on truecolor images with PHP 8.3's bundled GD.
Possible Follow-up
Keeping palette images as palette images after a flip would make saved 8-bit PNGs smaller. On the png-tests fixtures, abhishekfdd measured the truecolor round trip making them 1.5× to 2.7× larger (Core-66113, comment 8). That is existing trunk behavior. Doing that safely needs a palette-to-truecolor conversion in the WebP and AVIF branches of WP_Image_Editor_GD::_save() and stream() instead. That change would also fix an existing bug: a palette PNG saved as WebP without any flip already produces an empty file on trunk. I left it out to keep this fix small this close to the 7.2 RC.
Follow-up ticket:Core-66584 tracks moving the repeated ReflectionProperty blocks in Tests_Image_Editor_GD into a helper method.
Suggested reading order:class-wp-image-editor-gd.php first, then image-edit.php (the same change), then the tests.
My WordPress.org account, @sanjith2023, is now linked to this GitHub account.
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Investigating the review feedback on this PR and Core-66113, implementing the palette, alpha, and resolution and interlace changes, writing the tests, running them on PHP 8.3 and PHP 8.6 against both trunk and this PR, and drafting this description. The automated test results above come from those runs. I ran the manual browser tests myself on PHP 8.6.0RC4. Claude Code checked the saved files on disk (pixel comparison and file headers).
Replaces the imagecopyresampled() call that used negative source dimensions with imageflip(). The flip is now done in place, so no second full-size image is allocated, and the result no longer depends on how the GD build handles negative dimensions.
`flip()` now returns a `WP_Error` for an invalid GD image, even when no flip is requested. `_flip_image_resource()` gets the same change and still returns its input unchanged when nothing can be flipped.
Adds tests that check every pixel position for each flip mode, and a test for the invalid-image error path.
Props ...
Fixes #66113.
The changes in this pull request can previewed and tested using a WordPress Playground instance.
WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.
Some things to be aware of
All changes will be lost when closing a tab with a Playground instance.
All changes will be lost when refreshing the page.
A fresh instance is created each time the link below is clicked.
Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.
Upgrade Develop Version Tests failed on all jobs at "Download WordPress" with `wp: command not found`, a transient wp-cli install failure unrelated to this change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When you have a chance, could you please link your WordPress.org user account with your GitHub one? You'll find instructions on the Make Core blog. This ensures you'll rightly receive attribution for you contributions in the release.
Since Trac is still the canonical source of truth for this repository (it's a mirror of the SVN repository), conversation often happens there. I added a comment with feedback on Core-66113, but I could not mention you by username because I'm not clear what that is.
Also, could you kindly add a disclaimer describing whether you used AI tools and how? Using AI is totally fine, but you must disclose these details so others can understand the surrounding context they were created with.
It looks like there are still several side effects that this switch will cause that are not described or accounted for in full. There are a few additional considerations I noted in Core-66113 that Copilot has also mentioned here.
These need to be thought through carefully, at a minimum should receive new tests, and possibly may require code to preserve the current behavior instead. Looking at the Testing checklist from the PR description, this is how Claude mapped each to the actual test changes:
#
Checklist item
Covered?
By what
1
phpunit --filter Tests_Image_Editor_GD passes
✅ In CI (not a test itself)
The PHP 8.6 CI job runs them, and the GD flip tests pass on 8.6.0RC2
2
test_flip_image_resource passes
✅ In CI (not a test itself)
Same CI run
3
Flip vertically in Edit Image and save → top/bottom swap
⚠️ Partly
test_flip_moves_every_pixel "vertical flip" case (true, false) checks the pixels from flip(). It doesn't cover the Edit Image path (image_edit_apply_changes()) or saving.
4
Flip horizontally → left/right swap
⚠️ Partly
Same test, "horizontal flip" case (false, true). Same gap.
5
Flip both → same as a 180° rotation
⚠️ Partly
The "both" case checks that every pixel is mirrored on both axes, which is mathematically a 180° rotation. Nothing compares the result with rotate( 180 ).
6
Upload a JPEG with EXIF orientation 2, 3 or 4 → upright
❌ No
No test calls maybe_exif_rotate() or uses an EXIF-oriented image. This is the upload path.
7
flip( false, false ) → true, image untouched
✅ Yes
The "no flip" case asserts true and checks every pixel is unchanged
8
flip() without a valid GD image → WP_Errorimage_flip_error
⚠️ Partly
test_flip_returns_error_for_invalid_image asserts a WP_Error, but never checks the error code is image_flip_error.
9
Flip a PNG with transparency → alpha kept
❌ No
The test images are opaque truecolor images from imagecreatetruecolor(). Nothing checks alpha.
10
phpcs clean on the changed files
✅ Yes, in CI (not a unit test)
"Coding standards / PHP checks" and "Check PHP compatibility" both pass
The switch to `imageflip()` changed two side effects of the previous
`imagecopyresampled()` implementation:
- Palette images stayed palette images. The old code always copied into a
new truecolor image, and saving or streaming the result as WebP relies on
that: `imagewebp()` rejects palette images and writes an empty file.
`WP_Image_Editor_GD::flip()` and `_flip_image_resource()` now convert
palette images to truecolor first, including when no axis is flipped.
- Alpha blending stayed enabled after `rotate()`. Reset it to the
`alphablending=false`, `savealpha=true` state the old code produced.
Tests:
- Set the editor size in the pixel tests so they run against the previous
implementation and fail on PHP 8.6 / GD 2.4.0.
- Cover `maybe_exif_rotate()` for EXIF orientations 2, 3 and 4.
- Check palette to truecolor conversion for both functions, saving a
flipped palette image as WebP, alpha on every pixel in memory and after
saving as PNG, flipping both axes against `rotate( 180 )`, alpha blending
after `rotate()`, and the `image_flip_error` code.
- Skip `test_flip_image_resource()` when GD is unavailable.
Results with `--filter 'Tests_Image_Editor_GD|test_flip_image_resource'`:
- PHP 8.3 / GD 2.1: 54/54 pass. The previous implementation passes every
palette, alpha and rotate test, so the output is unchanged.
- PHP 8.6.0RC4 / GD 2.4.0: 54/54 pass. The previous implementation has 20
failures and 4 errors.
See #66113.
Thanks @desrosj! I've addressed everything in 85eb6d2. The palette and alpha behavior now matches the old code, there are tests for each item from Core-66113, and the PR description has an AI disclosure and PHP 8.3 and 8.6 results. Details are in my reply on Trac: https://core.trac.wordpress.org/ticket/66113#comment:26. My WordPress.org username is @sanjith2023.
`WP_Image_Editor_GD::flip()` and `_flip_image_resource()` returned early
when neither axis was flipped, before resetting the alpha settings. The
previous implementation always returned an image with
`alphablending=false` and `savealpha=true`, so:
- A no-axis flip after `rotate()` left alpha blending enabled.
- `_flip_image_resource()` could return an image with alpha saving
disabled, dropping transparency in PNG output.
Set the alpha settings before the early return in both functions, and
cover the no-axis case in the alpha blending and PNG alpha tests.
See #66113.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Three new data providers are non-static despite using no instance state and should be declared public static to match the established test convention.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Declare data providers static
tests/phpunit/tests/image/editorGd.php:664
The three new data providers are non-static, unlike the existing providers in tests/phpunit/tests/image/editorGd.php:448,500 and tests/phpunit/tests/image/functions.php:56,1102. Declare them public static to follow the established convention; none uses instance state. Apply the same change at tests/phpunit/tests/image/editorGd.php:781 and tests/phpunit/tests/image/functions.php:1267.
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 newimage, which resets them to GD's defaults: 96DPIand no
* interlacing. Reset them here too, so a flip gives the same result as the other edits.
The reason will be displayed to describe this comment to others. Learn more.
Suggested change
*
* @requires function exif_read_data
The orientation here comes from the wp_image_maybe_exif_rotate filter, which is applied whether or not EXIF is available. So this could needlessly skip the test.
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to abstract this out into a private method as there are a ton of these. But this is fine for this PR because it follows the pattern throughout the rest of the class.
@sanjithsarkar could you open a new Trac ticket to follow up with this optimization later?
The reason will be displayed to describe this comment to others. Learn more.
In test_flip_image_resource_converts_palette_image_to_truecolor(), the top-left and bottom-left pixels are compared with their mirrored positions. However, all four corners of test-image.gif have the same color, an opaque near-white (rgb(254, 254, 254)).
Because of that, the two mirroring assertions pass even when the image is flipped along the wrong axis or not flipped at all. The truecolor check looks correct.
Could you build the image in the test instead, like the other pixel tests? A small palette image from imagecreate() with a different color in every pixel can't look the same after any flip, so every pixel can be checked at its mirrored position, the same way test_flip_converts_palette_image_to_truecolor() does.
…edback.
`imageflip()` changes the image in place, so a flipped image kept the
source's resolution and interlace setting. The previous implementation
copied into a new image, which resets them to GD's defaults (96 DPI, no
interlacing), like `rotate()`, `crop()` and `resize()` still do. Reset
them in `WP_Image_Editor_GD::flip()` and `_flip_image_resource()`,
including when no axis is flipped.
Tests:
- Add resolution and interlace tests for both functions.
- Build the palette image in the `_flip_image_resource()` palette test,
so every pixel is checked at its mirrored position.
- Rename the `data_flip_moves_every_pixel` provider to `data_flip_axes`.
- Add missing `@covers` annotations.
- Remove unneeded `@requires` annotations from the EXIF and WebP tests.
See #66113.
flip() and _flip_image_resource() now reset the resolution to 96 DPI and turn interlacing off, also for flip( false, false ), so they match the previous output. imageinterlace() gets 0 rather than false because PHPStan checks against PHP 7.4's int signature.
New tests: test_flip_resets_resolution_and_interlace() and test_flip_image_resource_resets_resolution_and_interlace().
Updated the @since wording, renamed the data provider to data_flip_axes, and removed the unneeded @requires lines.
test_flip_image_resource_converts_palette_image_to_truecolor() now builds a palette image with imagecreate() and a different color in every pixel, and checks each pixel at its mirrored position.
I opened Core-66584 for the reflection helper follow-up. The PR description is updated as well.
I ran the manual tests in the browser on PHP 8.6.0RC4 (bundled GD 2.4.0), with the GD editor forced: Edit Image flip vertical and horizontal, an EXIF orientation 3 upload, and a flipped GIF saved as WebP. All three work, and the results are in the updated description.
Two existing trunk issues showed up during the GIF-to-WebP test, and this PR doesn't change either one. They're also in the description:
A palette GIF converted to WebP on upload becomes an empty file, because imagewebp() rejects palette images.
After saving from Edit Image with an output format that changes the extension, the attachment points to .gif, but the file on disk is .webp. wp_save_image() stores $new_path rather than the path the editor actually saved to.
I can open separate tickets for these if that's useful.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WP_Image_Editor_GD::flip()mirrored images by passing negative source dimensions toimagecopyresampled(). With PHP 8.6's bundled GD 2.4.0 that produces a black image, soTests_Image_Editor_GD::test_flipfails (Failed asserting that 0 is identical to 8434407). It now callsimageflip(), which flips in place and does not depend on how GD handles negative sizes._flip_image_resource(), the deprecated GD-resource version, gets the same change.The switch keeps the previous output. The old code always copied into a new truecolor image with
alphablending=false,savealpha=true, GD's default resolution of 96 DPI and interlacing off, and both functions now produce the same result. Without the truecolor conversion, a flipped GIF or 8-bit PNG saved or streamed as WebP becomes an empty file, becauseimagewebp()rejects palette images.The most common way to hit this bug is uploading a JPEG whose EXIF orientation is 2, 3 or 4.
WP_Image_Editor::maybe_exif_rotate()callsflip()for those, so on PHP 8.6 the uploaded image comes out black.Trac ticket: https://core.trac.wordpress.org/ticket/66113
What Changed
src/wp-includes/class-wp-image-editor-gd.php:441flip(): replaceswp_imagecreatetruecolor()+imagecopyresampled()with negative$sw/$shwithimageflip(). ReturnsWP_Errorif$this->imageis not a GD image. Converts palette images to truecolor, resets the alpha settings, and resets the resolution to 96 DPI with interlacing off before flipping, including forflip( false, false ).src/wp-admin/includes/image-edit.php:578_flip_image_resource(): the same changes. Returns$imgunchanged if it is not a GD image.tests/phpunit/tests/image/editorGd.phptests/phpunit/tests/image/functions.php_flip_image_resource(), skipped when GD is not available.New tests:
test_flip_moves_every_pixel()test_flip_returns_error_for_invalid_image()flip()returns aWP_Errorwith the codeimage_flip_errorwhen no valid image is loaded.test_maybe_exif_rotate_flips_image()maybe_exif_rotate(), usingtest-image-upside-down.jpg. The size is unchanged and sampled pixels are mirrored.test_flip_converts_palette_image_to_truecolor()test_flipped_palette_image_can_be_saved_as_webp()test_flip_keeps_alpha_channel()test_flip_both_axes_matches_rotate_180()flip( true, true )matchesrotate( 180 )pixel for pixel oncanola.jpg.test_flip_disables_alpha_blending_after_rotate()rotate()turns alpha blending on, and a laterflip()turns it off again, as the old code did. Covers all 4 flag combinations, including no flip.test_flip_resets_resolution_and_interlace()test_flip_image_resource()_flip_image_resource().test_flip_image_resource_converts_palette_image_to_truecolor()imagecreate()with a different color per pixel becomes truecolor, and every pixel keeps its color at its mirrored position, for all 4 flag combinations.test_flip_image_resource_enables_alpha_saving()test_flip_image_resource_resets_resolution_and_interlace()_flip_image_resource().Testing
Automated, with
vendor/bin/phpunit --filter 'Tests_Image_Editor_GD|test_flip_image_resource'. To show that the tests catch the bug, I also ran them against trunk's oldflip()and_flip_image_resource().TypeErrorOn PHP 8.3, trunk passes every pixel, palette, alpha, EXIF and rotate test, so the output does not change on GD versions that were already working. The resolution and interlace tests pass on trunk with both PHP versions, because the old code always returned a new image with GD's defaults; they fail on this PR without the reset. On PHP 8.6, trunk fails the pixel, EXIF, palette, alpha, rotate-180 and
_flip_image_resource()tests.vendor/bin/phpunit --group imageon PHP 8.3: 358 tests, no failures, 12 skipped.vendor/bin/phpcson the four changed files: clean.vendor/bin/phpstan analyseon the two changed source files: no errors.canola.jpgwith "Flip vertical", save, then "Flip horizontal", and save. Each saved file matches the original mirrored on the expected axes, pixel for pixel within JPEG noise, and is not black.tests/phpunit/data/images/test-image-upside-down.jpg(EXIF orientation 3). It comes out upright astest-image-upside-down-rotated-1.jpg, not black.tests/phpunit/data/images/test-image.gifand save it with animage_editor_output_formatfilter that maps GIF to WebP. The saved file is a valid, flipped 50×50 WebP (458 bytes), not empty.Manual tests ran in the local Docker environment on PHP 8.6.0RC4 with bundled GD 2.4.0. The Docker image has Imagick installed, so a local mu-plugin filtered
wp_image_editorsto use onlyWP_Image_Editor_GD.Two existing trunk bugs showed up during the GIF-to-WebP test. This PR does not change either one:
imagewebp()writes an empty file for the palette image (see Possible Follow-up).test-image-e….gif, but the file on disk istest-image-e….webp, so the image does not display.wp_save_image()stores$new_path(src/wp-admin/includes/image-edit.php:1075-1077) rather than the path the editor actually saved to. This happens on trunk too, whenever the output format changes the extension.To reproduce on PHP 8.6:
LOCAL_PHP=8.6-fpm npm run env:start, thennpm run test:php -- --filter 'Tests_Image_Editor_GD|test_flip_image_resource'.Implementation Notes
$horzflips along the horizontal axis, which swaps top and bottom, so it maps toIMG_FLIP_VERTICAL.$vertmaps toIMG_FLIP_HORIZONTAL, and both map toIMG_FLIP_BOTH. This matches the old$sy/$sxlogic andWP_Image_Editor_Imagick::flip(). The pixel tests fail if the axes are swapped.imageflip()alone would keep palette images as palette images. That breaks saving or streaming them as WebP, sinceimagewebp()warnsPalette image not supported by webp, still returnstrue, and writes a 0-byte file.imagepalettetotruecolor()runs before the flip, and also when no axis is flipped, because the old code converted in every case.alphablending=falseandsavealpha=true.imageflip()keeps the image's current settings, which arealphablending=trueafterrotate(). Both settings are now reset right after the truecolor conversion, before the no-axis return, soflip( false, false )matches the old output too.WP_Image_Editor_GDcopies the source's values over, so the old flip always reset them, asrotate(),crop()andresize()still do.imageflip()keeps the source's values, so both functions callimageresolution( 96, 96 )andimageinterlace( 0 ), also before the no-axis return.0is passed instead offalsebecause PHPStan checks against PHP 7.4, where the parameter is typed asint; it behaves the same on PHP 8. Keeping the source's values would arguably be more correct, but that should be done for all operations at once.flip(): Theis_gd_image()check is placed before the "no axes" early return. Otherwiseflip( false, false )with no image loaded would returntrue, while every other path returnsWP_Error.image_flip_errorused to cover a failedwp_imagecreatetruecolor(), for example when memory runs out.imageflip()only returnsfalsefor an invalid mode, which this code cannot pass. TheWP_Erroris kept for afalsereturn, and for a failed palette conversion._flip_image_resource()changes nothing on failure: It cannot returnWP_Error, because its only core caller (image_edit_apply_changes(),src/wp-admin/includes/image-edit.php:759) assigns the result to$imageand keeps using it. Returning the input unchanged matches the old failure behavior and the "(maybe) flipped" wording in its docblock.function_exists( 'imageflip' )guard:imageflip()has been in PHP since 5.5, with no conditional compilation, and every libgd version PHP 7.4+ accepts includes it.Remaining Behavior Differences
WP_Image_Editor_GD,$this->imageis protected and nothing outside the class holds the old object, so this has no effect there._flip_image_resource( $img, … )now also changes the caller's$img. Before, it was left untouched on PHP 8 and destroyed on PHP < 8. The function has been deprecated since 3.5.0, and its one core caller reassigns the return value.imageflip()keeps them, so saved files can differ by a few bytes. Nothing visible changes.imagecolortransparent()came back as-1. The in-place flip keeps the source's transparent color, including forflip( false, false ). Pixels in memory and saved PNG and WebP files are identical to the old output. The one visible difference is a truecolor image with a transparent color set, such as an RGB PNG with atRNSchunk, that is flipped and then saved as GIF: pixels matching that color now come out transparent, where the old code left them opaque. Core only saves a PNG as GIF if animage_editor_output_formatfilter maps it that way. Palette GIFs and PNGs decode to the same pixels as before, because after the truecolor conversion their transparent color matches no pixel. Resetting it is not straightforward, sinceimagecolortransparent( $img, -1 )does not clear it on truecolor images with PHP 8.3's bundled GD.Possible Follow-up
Keeping palette images as palette images after a flip would make saved 8-bit PNGs smaller. On the
png-testsfixtures, abhishekfdd measured the truecolor round trip making them 1.5× to 2.7× larger (Core-66113, comment 8). That is existing trunk behavior. Doing that safely needs a palette-to-truecolor conversion in the WebP and AVIF branches ofWP_Image_Editor_GD::_save()andstream()instead. That change would also fix an existing bug: a palette PNG saved as WebP without any flip already produces an empty file on trunk. I left it out to keep this fix small this close to the 7.2 RC.Other Notes
imageflip()inWP_Image_Editor_GD::flip()and keep itstrue|WP_Errorcontract. #13925 were earlier attempts at the same fix and are now closed in favor of this PR. Their contributors are included in the props.ReflectionPropertyblocks inTests_Image_Editor_GDinto a helper method.class-wp-image-editor-gd.phpfirst, thenimage-edit.php(the same change), then the tests.Use of AI Tools