Media REST API: Fix sideload and finalize for EXIF rotated images - #80295
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the Gutenberg Media REST API client-side upload flow so the original sideload/finalize path correctly handles EXIF quarter-turn rotations (where width/height are transposed), aligning behavior more closely with WordPress core’s “replace original” semantics and preventing rest_upload_dimension_mismatch 400s.
Changes:
- Treat
image_size=originalsideloads likescaled: swap the attachment’s main file and track the replaced file asoriginal_image. - In finalize, apply
originalsub-size metadata to the attachment (including resetting stored EXIF orientation to1to avoid re-rotation on refetch). - Relax
originaldimension validation to accept either exact dimensions or a width/height transpose.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/media/class-gutenberg-rest-attachments-controller.php | Updates sideload/finalize handling for original, resets orientation to avoid double-rotation, and allows transposed dimensions in validation. |
| phpunit/media/class-gutenberg-rest-attachments-controller-test.php | Renames/updates the existing original sideload test and adds regression/behavioral tests covering transposed dimensions and parity with server-side rotation behavior. |
| // Sideload the "original" (rotated) version. canola.jpg is 640x480, | ||
| // matching the stored dimensions, so validation passes. |
|
Flaky tests detected in 409b1e1. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/29472952588
|
|
Did a quick test, and it fixes the 2 birds with one stone! |
|
Sorry, I withdraw that. I was testing with the wrong image. No |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Thanks for the PR! I will test it soon. |
|
@andrewserong this tested well for me trying jpeg, heic, and avif rotated images (from packages/vips/src/test/fixtures). One small glitch I noticed with the avif - it shows faded out in the horizontal position while processing, then switches to the correct vertical alignment when completed: The other test images show the correct orientation immediately. |
Looking into this further. |
ps. its easy to reproduce by starting the upload while offline. |
Good catch! My best guess is that during the uploading preview, the browser is doing its best to render things as is (i.e. for jpegs, etc, it'll display with the rotation as it can, but this isn't supported for avif?) Separately, looks like the PHP unit tests are failing due to |
Merge the 'original' and 'scaled' finalize branches: both replace the attachment's main file, so both need the EXIF orientation reset that wp_create_image_subsizes() applies on its scale and rotate paths. Skip entries missing the file name so a malformed finalize payload cannot blank out the main file metadata. Fix the CI failures in the sideload tests: assert against the actual attached file basename instead of a hardcoded name, which breaks when earlier tests upload the same fixture and wp_unique_filename() adds a numeric suffix. Skip rotation-dependent tests when the image editor or exif extension cannot support them.
…ge-with-exif-rotation' into fix/400-error-when-uploading-image-with-exif-rotation # Conflicts: # phpunit/media/class-gutenberg-rest-attachments-controller-test.php
|
Pushed a few follow-ups after reviewing:
On the approach question: this is a faithful port of core's rotate path. On the AVIF preview glitch I mentioned above: expected browser behavior, not a bug in this PR. The rotated AVIF fixtures signal rotation only via EXIF; per the AVIF/MIAF spec renderers apply |
|
Fantastic, thanks so much @adamsilverstein, much appreciated! Your updates look good to me. Do you want to give this an approval and then I can merge it?
Super, thanks for that. I can give that a look shortly. |
|
Backport looks good and I've given it the ✅. Just updated a stale comment, but otherwise I think this one is good to go 🤞 |
ramonjd
left a comment
There was a problem hiding this comment.
LGTM - works as described, thanks folks!
|
Thanks for all the collaboration here! 🙇 |
…0295) * Media REST API: Fix sideload and finalize for EXIF rotated images * Consolidate the logic with scaled where we can * Try to fix tests * Apply orientation reset to scaled finalize and guard malformed payloads Merge the 'original' and 'scaled' finalize branches: both replace the attachment's main file, so both need the EXIF orientation reset that wp_create_image_subsizes() applies on its scale and rotate paths. Skip entries missing the file name so a malformed finalize payload cannot blank out the main file metadata. Fix the CI failures in the sideload tests: assert against the actual attached file basename instead of a hardcoded name, which breaks when earlier tests upload the same fixture and wp_unique_filename() adds a numeric suffix. Skip rotation-dependent tests when the image editor or exif extension cannot support them. * Add Core backport changelog entry for wordpress-develop PR 12550 * Update comment for accuracy --------- Co-authored-by: andrewserong <andrewserong@git.wordpress.org> Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org> Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
|
I just cherry-picked this PR to the wp/7.1 branch to get it included in the next release: b39c633 |
…0295) * Media REST API: Fix sideload and finalize for EXIF rotated images * Consolidate the logic with scaled where we can * Try to fix tests * Apply orientation reset to scaled finalize and guard malformed payloads Merge the 'original' and 'scaled' finalize branches: both replace the attachment's main file, so both need the EXIF orientation reset that wp_create_image_subsizes() applies on its scale and rotate paths. Skip entries missing the file name so a malformed finalize payload cannot blank out the main file metadata. Fix the CI failures in the sideload tests: assert against the actual attached file basename instead of a hardcoded name, which breaks when earlier tests upload the same fixture and wp_unique_filename() adds a numeric suffix. Skip rotation-dependent tests when the image editor or exif extension cannot support them. * Add Core backport changelog entry for wordpress-develop PR 12550 * Update comment for accuracy --------- Co-authored-by: andrewserong <andrewserong@git.wordpress.org> Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org> Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
|
I just cherry-picked this PR to the release/23.6 branch to get it included in the next release: 6f41b85 |
This updates the pinned commit hash of the Gutenberg repository from `e73c3c481db0650183f092af157f6e42efe9ee2d` to `4997026b75c922d8a6f77a03d72ed7cad04c7073`. A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@e73c3c4...4997026 - Notes: Replace blur-deselect bookkeeping with useFocusOutside (WordPress/gutenberg#80222) - Playlist: Update @SInCE tags to 7.1.0 (WordPress/gutenberg#80317) - fix playlist block Dimensions Design (WordPress/gutenberg#80312) - UI: Backport compat overlay fixes to WordPress 7.1 (WordPress/gutenberg#80322) - Editor: allow selecting which block styles to apply globally (WordPress/gutenberg#79839) - Global Styles: Reject non-string custom CSS in the REST controller (WordPress/gutenberg#80338) - Open inspector sidebar when toggling responsive editing (WordPress/gutenberg#80307) - Client Side Media: Honor image_strip_meta and image_max_bit_depth on the client upload path (WordPress/gutenberg#80218) - Hide block style variations when state is enabled in global styles (WordPress/gutenberg#80341) - Media REST API: Fix sideload and finalize for EXIF rotated images (WordPress/gutenberg#80295) - Fix upload snackbar stuck in uploading state on server-side uploads (WordPress/gutenberg#80345) - Try fixing responsive layout in Nav block (WordPress/gutenberg#80305) - Responsive styles: Use viewport dropdown to control states for in-editor global styles sidebar (WordPress/gutenberg#80339) - RichTextControl: Replace DOM focus tracking with a single React-tree focus boundary (WordPress/gutenberg#80324) - Notes: Finish WPDS treatment for mention chips (WordPress/gutenberg#80300) - Notes: Add placeholders to the RichText fields (WordPress/gutenberg#80296) - Fix upload hang when converting long animated GIFs: decode only the first frame for still outputs (WordPress/gutenberg#80260) (WordPress/gutenberg#80342) - Device preview dropdown: use active color for device icon when responsive styles are active (WordPress/gutenberg#80346) - Fix default aspect ratio for lazy loaded Featured image (WordPress/gutenberg#80386) - Vips/upload-media: consolidate optional params into options objects (WordPress/gutenberg#80330) - Autocompleters: Don't pre-encode mention search terms (WordPress/gutenberg#80377) - Animated GIF uploads: generate sub-sizes from the first frame, matching core (WordPress/gutenberg#80268) - Custom CSS: Fix cascade order against block style variations (WordPress/gutenberg#80340) - Rich Text: Restore the selection when focus returns to the editable (WordPress/gutenberg#80396) - Notes: Arm the mention kses allowance on REST note creation (WordPress/gutenberg#80221) - Fix upload snackbar double-counting a single HEIC upload in Safari (WordPress/gutenberg#80436) - ContentEditableControl: fix invalid label association with contenteditable div (WordPress/gutenberg#80441) - Editor: Disable canvas resizing while zoomed out (WordPress/gutenberg#80391) - Fix Color Picker Cursor Shaking Issue (WordPress/gutenberg#80205) (WordPress/gutenberg#80435) - Misc fixes for WordPress-Develop 7.0 merges (WordPress/gutenberg#80444) - Style Book: Restore live global styles updates on the styles route (WordPress/gutenberg#80459) - Worker threads: reject pending RPC calls on worker failure or termination (WordPress/gutenberg#79955) (WordPress/gutenberg#80421) - Media: Add timeout and size guardrails to client-side GIF to video conversion (WordPress/gutenberg#80420) - Post Content: Use the default block appender for empty content (WordPress/gutenberg#80026) - Block Supports: Handle nested array block gap values properly (WordPress/gutenberg#80464) - Editor: Restore fixed device preview height for mobile and tablet (WordPress/gutenberg#80466) - Block Editor: Guard against non-string spacing preset values (WordPress/gutenberg#80467) - Writing flow: fully select the ancestor when a text selection crosses a nesting boundary (WordPress/gutenberg#80462) - Block Editor: Reflect inherited Global Styles values in block inspector controls (WordPress/gutenberg#80481) - Autocomplete: Reference the suggestions list with `aria-controls` and `aria-haspopup` (WordPress/gutenberg#80403) (WordPress/gutenberg#80499) - Media: Remove the redundant __heicUploadSupport flag (WordPress/gutenberg#80486) - State control - avoid tertiary variant on toggle to match style of other dropdown toggles (WordPress/gutenberg#80505) - Icons: Store the sanitized SVG content when registering an icon (WordPress/gutenberg#80508) - Fix `useHomeEnd` on tabs in mac testing (WordPress/gutenberg#80374) - Playlist: Fix playback of tracks served without CORS headers (WordPress/gutenberg#80533) - Redirect editing events to extension handlers under editableRoot (WordPress/gutenberg#80287) - Writing flow: fully select the items when a selection extends down into a nested item (WordPress/gutenberg#80492) - Global Styles panels: fix wrong preset committed and shown when two color presets share a hex (WordPress/gutenberg#80497) - Replaces the `title` attributes used by revision inline diff annotations with `aria-describedby` (WordPress/gutenberg#80440) - Notes: Remove "Add note" from the inline styles dropdown (WordPress/gutenberg#80531) - Global Styles: Resolve per-level heading element styles in block inspector controls (WordPress/gutenberg#80495) - Notes: Render @ mentions as span chips and narrow the kses class allowance (WordPress/gutenberg#80528) - Revisions: Specify block level diff status via aria-label (WordPress/gutenberg#77779) - Backport from Core: improve icon name unit tests (WordPress/gutenberg#80552) - Device type preview: fix collapsing to content height (WordPress/gutenberg#80553) - Wrap notices in ThemeProvider with 0 corner radius (WordPress/gutenberg#80557) - Global Styles: Limit the inherited value treatment to the Gutenberg plugin (WordPress/gutenberg#80555) - Fix crashes when manipulating locked blocks (WordPress/gutenberg#80509) - Notes: align floating threads with their inline marker (WordPress/gutenberg#79877) Props wildworks. See #65529. git-svn-id: https://develop.svn.wordpress.org/trunk@62824 602fd350-edb4-49c9-b593-d223f7449a82
This updates the pinned commit hash of the Gutenberg repository from `e73c3c481db0650183f092af157f6e42efe9ee2d` to `4997026b75c922d8a6f77a03d72ed7cad04c7073`. A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@e73c3c4...4997026 - Notes: Replace blur-deselect bookkeeping with useFocusOutside (WordPress/gutenberg#80222) - Playlist: Update @SInCE tags to 7.1.0 (WordPress/gutenberg#80317) - fix playlist block Dimensions Design (WordPress/gutenberg#80312) - UI: Backport compat overlay fixes to WordPress 7.1 (WordPress/gutenberg#80322) - Editor: allow selecting which block styles to apply globally (WordPress/gutenberg#79839) - Global Styles: Reject non-string custom CSS in the REST controller (WordPress/gutenberg#80338) - Open inspector sidebar when toggling responsive editing (WordPress/gutenberg#80307) - Client Side Media: Honor image_strip_meta and image_max_bit_depth on the client upload path (WordPress/gutenberg#80218) - Hide block style variations when state is enabled in global styles (WordPress/gutenberg#80341) - Media REST API: Fix sideload and finalize for EXIF rotated images (WordPress/gutenberg#80295) - Fix upload snackbar stuck in uploading state on server-side uploads (WordPress/gutenberg#80345) - Try fixing responsive layout in Nav block (WordPress/gutenberg#80305) - Responsive styles: Use viewport dropdown to control states for in-editor global styles sidebar (WordPress/gutenberg#80339) - RichTextControl: Replace DOM focus tracking with a single React-tree focus boundary (WordPress/gutenberg#80324) - Notes: Finish WPDS treatment for mention chips (WordPress/gutenberg#80300) - Notes: Add placeholders to the RichText fields (WordPress/gutenberg#80296) - Fix upload hang when converting long animated GIFs: decode only the first frame for still outputs (WordPress/gutenberg#80260) (WordPress/gutenberg#80342) - Device preview dropdown: use active color for device icon when responsive styles are active (WordPress/gutenberg#80346) - Fix default aspect ratio for lazy loaded Featured image (WordPress/gutenberg#80386) - Vips/upload-media: consolidate optional params into options objects (WordPress/gutenberg#80330) - Autocompleters: Don't pre-encode mention search terms (WordPress/gutenberg#80377) - Animated GIF uploads: generate sub-sizes from the first frame, matching core (WordPress/gutenberg#80268) - Custom CSS: Fix cascade order against block style variations (WordPress/gutenberg#80340) - Rich Text: Restore the selection when focus returns to the editable (WordPress/gutenberg#80396) - Notes: Arm the mention kses allowance on REST note creation (WordPress/gutenberg#80221) - Fix upload snackbar double-counting a single HEIC upload in Safari (WordPress/gutenberg#80436) - ContentEditableControl: fix invalid label association with contenteditable div (WordPress/gutenberg#80441) - Editor: Disable canvas resizing while zoomed out (WordPress/gutenberg#80391) - Fix Color Picker Cursor Shaking Issue (WordPress/gutenberg#80205) (WordPress/gutenberg#80435) - Misc fixes for WordPress-Develop 7.0 merges (WordPress/gutenberg#80444) - Style Book: Restore live global styles updates on the styles route (WordPress/gutenberg#80459) - Worker threads: reject pending RPC calls on worker failure or termination (WordPress/gutenberg#79955) (WordPress/gutenberg#80421) - Media: Add timeout and size guardrails to client-side GIF to video conversion (WordPress/gutenberg#80420) - Post Content: Use the default block appender for empty content (WordPress/gutenberg#80026) - Block Supports: Handle nested array block gap values properly (WordPress/gutenberg#80464) - Editor: Restore fixed device preview height for mobile and tablet (WordPress/gutenberg#80466) - Block Editor: Guard against non-string spacing preset values (WordPress/gutenberg#80467) - Writing flow: fully select the ancestor when a text selection crosses a nesting boundary (WordPress/gutenberg#80462) - Block Editor: Reflect inherited Global Styles values in block inspector controls (WordPress/gutenberg#80481) - Autocomplete: Reference the suggestions list with `aria-controls` and `aria-haspopup` (WordPress/gutenberg#80403) (WordPress/gutenberg#80499) - Media: Remove the redundant __heicUploadSupport flag (WordPress/gutenberg#80486) - State control - avoid tertiary variant on toggle to match style of other dropdown toggles (WordPress/gutenberg#80505) - Icons: Store the sanitized SVG content when registering an icon (WordPress/gutenberg#80508) - Fix `useHomeEnd` on tabs in mac testing (WordPress/gutenberg#80374) - Playlist: Fix playback of tracks served without CORS headers (WordPress/gutenberg#80533) - Redirect editing events to extension handlers under editableRoot (WordPress/gutenberg#80287) - Writing flow: fully select the items when a selection extends down into a nested item (WordPress/gutenberg#80492) - Global Styles panels: fix wrong preset committed and shown when two color presets share a hex (WordPress/gutenberg#80497) - Replaces the `title` attributes used by revision inline diff annotations with `aria-describedby` (WordPress/gutenberg#80440) - Notes: Remove "Add note" from the inline styles dropdown (WordPress/gutenberg#80531) - Global Styles: Resolve per-level heading element styles in block inspector controls (WordPress/gutenberg#80495) - Notes: Render @ mentions as span chips and narrow the kses class allowance (WordPress/gutenberg#80528) - Revisions: Specify block level diff status via aria-label (WordPress/gutenberg#77779) - Backport from Core: improve icon name unit tests (WordPress/gutenberg#80552) - Device type preview: fix collapsing to content height (WordPress/gutenberg#80553) - Wrap notices in ThemeProvider with 0 corner radius (WordPress/gutenberg#80557) - Global Styles: Limit the inherited value treatment to the Gutenberg plugin (WordPress/gutenberg#80555) - Fix crashes when manipulating locked blocks (WordPress/gutenberg#80509) - Notes: align floating threads with their inline marker (WordPress/gutenberg#79877) Props wildworks. See #65529. Built from https://develop.svn.wordpress.org/trunk@62824 git-svn-id: http://core.svn.wordpress.org/trunk@62104 1a063a9b-81f0-0310-95a4-ce76da25c4cd


What?
Part of:
Update the logic for
'original'sized images in sideload and finalize to account for rotated images. To do so, essentially reuse the same logic that we have for thescaledsize.My thinking here is: when the client uploads a rotated image to upload as the original, it's very similar to the
scaledlogic, which is that we're effectively saying "please use this one, but point back to the original".Part of this is also at the finalize step, declaring that the image has been rotated and that the image rotation should be treated as
1(i.e. already handled).Note: I haven't put up a backport of this PR just yet as I'm still a little unsure if it's the right approach. If it looks okay, happy to do that, though!
Why?
Fixes a bug described in #77582 (comment)
Basically, with some EXIF rotated images (e.g. 6), we'd get a 400 error from sideload requests for the
originalsize because the dimensions did not match what the server expected.But, in the client-side media upload flow, we expect something like:
I'm a little hesitant about this PR because I don't fully understand the implications of re-using the logic like this. So I'll likely need to lean on @adamsilverstein for verification here!
How?
Testing Instructions
Download the raw version of this file: https://github.com/WordPress/gutenberg/blob/trunk/packages/vips/src/test/fixtures/exif-rotated-90cw.jpg
In
trunkthis fails with a 400 error. In this PR, it should successfully upload.With your network inspector open, select the block after upload, and look for the
mediaREST API request that returns the attachment response. Double-check the response and that the image sizes and other metadata look correct, includingoriginal_image.Screenshots or screencast
Before
After
Use of AI Tools
Claude Code and then Codex to verify the fix from Claude. This seemed to confirm that the fix needs to happen on the PHP side and not the JS side.