Media editor: Let the crop instruction helper read the cropper by ramonjd · Pull Request #86147 · WordPress/gutenberg · GitHub
Skip to content

Media editor: Let the crop instruction helper read the cropper - #86147

Merged
ramonjd merged 1 commit into
trunkfrom
refactor/media-editor-instruction-helper-input
Nov 2, 2026
Merged

ramonjd merged 1 commit into
trunkfrom
refactor/media-editor-instruction-helper-input

Conversation

@ramonjd

@ramonjd ramonjd commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

What?

See #86145.

The helper that turns the cropper's edit into the instructions for the media REST endpoint (buildModifiers) now takes the cropper controller, instead of the edit state plus the image size as two arguments.

buildModifiers is a helper to build a Core-compatible modifiers array for the attachments REST controller from the cropper state.

Why?

This is a first small step of #86145, which makes the image cropper reusable by other editing flows.

With two arguments, every caller had to pull the image size out of the edit state itself and reach into its shape. If the helper reads both from the controller, callers pass one value and the shape of the edit state stays out of their code, which makes the helper easier to expose to other packages later.

How?

buildModifiers reads state and state.image from the controller it is given. The one caller, the media editor's save path, now passes session.cropper, and its own "no image loaded yet" check moved into the helper. The output is identical. Nothing is exported or changed in the hook, and there is no changelog entry because no consumer calls the helper.

A new test covers the case where no image has loaded yet.

Testing Instructions

  1. Run npm run test:unit -- packages/media-editor. The helper's parity test (6,000+ cases that compare its output with the export camera) and the rest of the package's tests should pass.
  2. In the media editor, open an image, rotate, flip and crop it, and save. The saved image should match the preview, as before. There is no user-facing change.
2026-11-02.13.57.41.mp4

Example usage by outside consumer

This is just FYI

import apiFetch from '@wordpress/api-fetch';
import { privateApis as mediaEditorPrivateApis } from '@wordpress/media-editor';
import { unlock } from './lock-unlock'; // the consumer's own private-API opt-in

// After PR 2 these come out of the private export.
const { Cropper, useCropperController, buildModifiers } = unlock( mediaEditorPrivateApis );

function SiteIconCropper( { attachmentId, src } ) {
        const cropper = useCropperController();

        const save = async () => {
                // One call: the edit and the image size are both read from the controller.
                const modifiers = buildModifiers( cropper );
                if ( ! modifiers.length ) {
                        return; // nothing changed, or no image loaded yet
                }
                await apiFetch( {
                        path: `/wp/v2/media/${ attachmentId }/edit`,
                        method: 'POST',
                        data: { src, modifiers },
                } );
        };

        return (
                <>
                        <div style={ { width: 600, height: 400 } }>
                                <Cropper src={ src } controller={ cropper } freeformCrop />
                        </div>
                        <button onClick={ () => cropper.setRotation( 90 ) }>Rotate</button>
                        <button onClick={ () => cropper.toggleFlip( 'horizontal' ) }>Flip</button>
                        <button onClick={ save }>Save</button>
                </>
        );
}

Use of AI

Claude helped plan and execute

`buildModifiers` took the cropper's edit state and the image size as two separate arguments, so every caller had to pull the size out of the state itself.

It now takes the cropper controller and reads both from it. Callers pass one value and no longer reach into the shape of the edit state. The output is unchanged and the existing parity test still passes.

Adds a test for the case where no image has loaded yet.
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: +11 B (0%)

Total Size: 8.58 MB

📦 View Changed
Filename Size Change
build/scripts/editor/index.min.js 559 kB +11 B (0%)

da5558f Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 3ea8069 trunk % Change
timeToFirstByte 46.35 ms +3.99% -2.91% 46.2 ms +11.9% -3.79% 0.32%
largestContentfulPaint 82 ms +12.2% -7.32% 80 ms +12.5% -2.5% 2.5%
lcpMinusTtfb 35 ms +20.43% -9.14% 33.7 ms +21.96% -6.23% 3.86%
wpBeforeTemplate 23.3 ms +4.46% -1.63% 23 ms +14.96% -1.09% 1.3%
wpTemplate 19 ms +7.21% -4.74% 19.04 ms +5.72% -4.67% -0.21%
wpTotal 42.57 ms +4.04% -2.58% 42.66 ms +11.06% -3.73% -0.21%
wpMemoryUsage 7.70 MB +0% -0% 7.67 MB +0% -0% 0.43%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 3ea8069 trunk % Change
timeToFirstByte 38.45 ms +3.51% -1.3% 34.85 ms +6.89% -2.15% 10.33%
largestContentfulPaint 90 ms +2.22% -2.22% 80 ms +2.5% -0% 12.5%
lcpMinusTtfb 50.85 ms +5.01% -1.57% 45.65 ms +0.77% -1.86% 11.39%
wpBeforeTemplate 20.85 ms +3.02% -1.06% 20.81 ms +5.19% -1.54% 0.19%
wpTemplate 14.56 ms +2.82% -3.37% 11.17 ms +4.12% -3.22% 30.35%
wpTotal 35.5 ms +3.49% -1.72% 31.99 ms +7.81% -2.09% 10.97%
wpMemoryUsage 6.39 MB +0% -0% 6.25 MB +0% -0% 2.32%
wpDbQueries 13 +0% -0% 9 +0% -0% 44.44%

media-processing

Metric 3ea8069 trunk % Change
mediaProcessingJpeg 411 ms +0.74% -0.9% 407.59 ms +0.68% -0.66% 0.84%
mediaProcessingAvif 6369.9 ms +0.41% -0.37% 6328.3 ms +0.14% -0.25% 0.66%
mediaProcessingJpegToAvif 4366.91 ms +0.33% -0.15% 4323.89 ms +0.08% -0.29% 0.99%

media-upload

Metric 3ea8069 trunk % Change
jpegUploadProcessing 1465.4 ms +33.57% -0.58% 1461.54 ms +0.55% -0.48% 0.26%
pngUploadProcessing 240.3 ms +2.25% -5.59% 235.43 ms +6.1% -4.29% 2.07%
largeJpegUploadProcessing 1459.34 ms +0.59% -0.8% 1451.45 ms +0.45% -0.21% 0.54%
multipleImageUploadProcessing 1604.69 ms +1.12% -1.27% 1600.76 ms +1.7% -0.69% 0.25%

post-editor

Metric 3ea8069 trunk % Change
serverResponse 262.11 ms +7.45% -5.83% 277.75 ms +1.23% -10.84% -5.63%
firstPaint 226.07 ms +22.41% -11.27% 292.33 ms +16.39% -18.61% -22.67%
domContentLoaded 579.91 ms +2.14% -1.9% 564.42 ms +6.78% -0.55% 2.74%
loaded 580.73 ms +2.13% -1.88% 565.2 ms +6.79% -0.54% 2.75%
firstContentfulPaint 343.51 ms +7.05% -2.64% 332.51 ms +2.32% -0.88% 3.31%
firstBlock 1465.15 ms +5.83% -1.9% 1466.17 ms +2.6% -2.88% -0.07%
type 13.02 ms +6.37% -9.6% 15.29 ms +13.15% -10.79% -14.85%
typeWithoutInspector 12.35 ms +7.21% -5.51% 13.26 ms +8.22% -7.32% -6.86%
typeWithTopToolbar 15.31 ms +7.71% -5.29% 17.69 ms +6.22% -3.5% -13.45%
typeContainer 5.95 ms +12.94% -4.54% 6.68 ms +10.18% -8.83% -10.93%
focus 70.88 ms +1.92% -2.1% 78.34 ms +6.52% -3.64% -9.52%
firstFocus 158.25 ms +0% -0% 177.43 ms +0% -0% -10.81%
selectAll 247.7 ms +0.45% -1.76% 267.04 ms +4.42% -2.18% -7.24%
undo 18.64 ms +1.77% -4.67% 24.05 ms +3.66% -10.31% -22.49%
listViewOpen 44.68 ms +5.64% -4.63% 50.64 ms +8.67% -2.29% -11.77%
inserterOpen 25.87 ms +3.25% -9.9% 28.52 ms +2% -11.99% -9.29%
inserterHover 7.66 ms +8.49% -5.87% 8.9 ms +5.17% -4.04% -13.93%
inserterSearch 6.89 ms +4.79% -5.66% 6.89 ms +11.18% -4.5% 0%
loadPatterns 407.74 ms +2.38% -3.56% 474.64 ms +3.09% -4.8% -14.09%
wpTotal 254.64 ms +7.47% -5.83% 270.43 ms +1.38% -10.91% -5.84%
wpMemoryUsage 14.73 MB +0% -0% 14.72 MB +0% -0% 0.1%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 3ea8069 trunk % Change
serverResponse 306.45 ms +3.73% -8.18% 314.04 ms +0.9% -9.44% -2.42%
firstPaint 279.99 ms +7.29% -3.14% 295.03 ms +4.08% -9.12% -5.1%
domContentLoaded 675.86 ms +5.48% -1.49% 675.26 ms +3.54% -0.16% 0.09%
loaded 676.55 ms +5.48% -1.5% 676.12 ms +3.53% -0.18% 0.06%
firstContentfulPaint 396.59 ms +8.14% -3.01% 390.93 ms +0.79% -1.51% 1.45%
firstBlock 1975.78 ms +7.91% -1.62% 1978.36 ms +1.72% -1.54% -0.13%
type 14.69 ms +8.78% -2.52% 15.28 ms +9.23% -12.96% -3.86%
navigate 89.14 ms +5.4% -4.77% 104.77 ms +0.13% -14.1% -14.92%
loadPatterns 920.38 ms +12.68% -12.44% 914.14 ms +11.72% -5.41% 0.68%
loadPages 1034.28 ms +1.35% -2.3% 1033.21 ms +3.44% -1.19% 0.1%
wpTotal 298.26 ms +3.71% -8.4% 305.8 ms +0.88% -9.71% -2.47%
wpMemoryUsage 12.77 MB +0% -0% 12.73 MB +0% -0% 0.27%
wpDbQueries 45 +0% -0% 45 +0% -0% 0%

da5558f Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Custom Completer: should insert option in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 1 failed attempt.

Undo restores the post selection when another synced entity is loaded (RTC WebSocket) in /test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

  Object {
    "attributeKey": "content",
    "blockIndex": 0,
    "content": "abcdef",
-   "endOffset": 3,
-   "offset": 3,
+   "endOffset": 6,
+   "offset": 6,
  }

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts:363:5
should insert mention in a table block in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 2 failed attempts.
Error: apiRequestContext.fetch: socket hang up
Call log:
  - → DELETE http://localhost:8889/wp-json/wp/v2/users/30?force=true&reassign=1
    - user-agent: Playwright/1.63.0 (x64; ubuntu 24.04) node/24.20 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br
    - X-WP-Nonce: dbb4462d1a
    - cookie: wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_23778236db82f19306f247e20a353a99=admin%7C1793760760%7C9JWYVs44brEXGNvtfwb4Z8Embx7WoAa1d4VL0O050tR%7Ca50e177e9b890c4052d087cc453ca32492f694fcfa9d18ab0b0a5319d1a0bab7; wp-settings-time-1=1793588617

    at RequestUtils.rest (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/rest.ts:111:39)
    at RequestUtils.deleteUser (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/users.ts:91:30)
    at map (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/users.ts:116:51)
    at RequestUtils.deleteAllUsers (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/users.ts:116:5)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js:73:3

should partially select with shift + click in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  -  1
+ Received  + 11

  Array [
    Object {
      "attributes": Object {
-       "content": "<strong>1</strong>|2",
+       "content": "<strong>1</strong>[",
+       "dropCap": false,
+     },
+     "innerBlocks": Array [],
+     "name": "core/paragraph",
+   },
+   Object {
+     "attributes": Object {
+       "content": "]2",
+       "dropCap": false,
      },
+     "innerBlocks": Array [],
      "name": "core/paragraph",
    },
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:1600:41

da5558f Run

@ramonjd
ramonjd requested a review from andrewserong November 2, 2026 02:59
@ramonjd ramonjd self-assigned this Nov 2, 2026
@ramonjd ramonjd added [Type] Code Quality Issues or PRs that relate to code quality [Feature] Media Anything that impacts the experience of managing media labels Nov 2, 2026
@ramonjd
ramonjd requested a balanced review from Copilot November 2, 2026 03:03
@ramonjd
ramonjd marked this pull request as ready for review November 2, 2026 03:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The review identifies no active issues requiring changes to the controller-based buildModifiers refactor.

No open issues 🎉

What changed in this PR

Refactors the media editor’s internal buildModifiers helper to read edit state and image dimensions from the cropper controller, preparing for reuse under #86145 without changing its output or exports.

Changes:

  • Replaces the helper’s two arguments with a controller argument.
  • Moves the missing-image guard into the helper and simplifies the save caller.
  • Updates parity tests and adds coverage for an unloaded image.
File Description
packages/​media-editor/​src/​components/​media-editor/​use-save-media-editor.ts Passes session.cropper directly to the helper.
packages/​media-editor/​src/​components/​media-editor-modal/​test/​build-modifiers.test.ts Updates helper calls and tests the missing-image case.
packages/​media-editor/​src/​components/​media-editor-modal/​build-modifiers.ts Reads state and natural image dimensions from the controller.

Review effort: Balanced

@andrewserong andrewserong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like a tidy change to me (and makes for a cleaner buildModifiers signature).

Smoke-tested a bunch of crops in the editor and all seemed to be identical between how they looked in the editor and how they appear post-crop.

Thanks for the example consumer usage, too!

LGTM :shipit:

@ramonjd
ramonjd merged commit 9def301 into trunk Nov 2, 2026
131 of 134 checks passed
@ramonjd
ramonjd deleted the refactor/media-editor-instruction-helper-input branch November 2, 2026 04:51
@github-actions github-actions Bot added this to the Gutenberg 24.4 milestone Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Media Anything that impacts the experience of managing media [Type] Code Quality Issues or PRs that relate to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants