Skip to content

feat: process New Block Editor media uploads with app Media settings - #25824

Open
dcalhoun wants to merge 14 commits into
trunkfrom
feat/process-gutenberg-kit-media-uploads
Open

feat: process New Block Editor media uploads with app Media settings#25824
dcalhoun wants to merge 14 commits into
trunkfrom
feat/process-gutenberg-kit-media-uploads

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

Ref CMM-1249. Ref CMM-2155.

Integrates GutenbergKit's native media upload pipeline (wordpress-mobile/GutenbergKit#357) so device media picked in the New Block Editor (NBE) is processed natively before upload, honoring the app's Media settings: Optimize Images, Max Image Upload Size, Image Quality, Max Video Upload Size, and Remove Location From Media. Previously these uploads went directly from the WebView to the REST API with no processing, bypassing the settings the legacy editor and My Site > Media already honor.

How it works

  • GBKMediaUploadProcessor implements GutenbergKit's new MediaUploadDelegate. When the editor uploads a device file, GutenbergKit's localhost upload server hands the staged file to processFile, which reuses MediaURLExporter with the same option mapping as MediaImportService:
    • Images are resized/recompressed per the Optimize Images settings, GPS EXIF is stripped per Remove Location, and non-web-safe formats (e.g. HEIC) are converted to JPEG, mirroring ItemProviderMediaExporter.
    • Videos are transcoded per the Max Video Upload Size preset, with the free-plan duration limit enforced (videoLimitExceeded surfaces WordPress's "requires a paid plan" message in the editor).
    • GIFs and non-media files pass through untouched (GutenbergKit forwards the original bytes verbatim); disallowed file extensions are rejected with the standard localized error.
    • When processing would be a no-op (optimization and location stripping disabled, web-safe format), the original file passes through without re-encoding.
  • Upload itself still uses GutenbergKit's default uploader, which relays the raw WordPress REST response to the editor, so attachment objects, sub-sizes, and error notices behave identically to a direct upload.
  • Wired unconditionally in PostGBKEditorViewController (covers posts, pages, and custom post types). If GutenbergKit's upload server cannot start, it degrades gracefully to the standard WebView upload path.

Notes

  • Uploaded attachments are not inserted into the local Core Data Media store, so My Site > Media shows them after the next sync. This matches the NBE's pre-existing behavior; syncing after upload is a possible follow-up.

Testing instructions

  1. Build and run the Jetpack scheme and sign in.
  2. Image resize: In App Settings > Media, enable Optimize Images with Max Image Upload Size 2000. Open a post in the NBE, add an Image block, and upload a photo larger than 2000px from the device. Verify the upload succeeds and the stored full-size image is at most 2000px (inspect the attachment in wp-admin or the block's URL).
  3. No-op passthrough: Disable Optimize Images and Remove Location From Media, upload the same photo, and verify it retains its original dimensions.
  4. Location stripping: Enable Remove Location From Media, upload a GPS-tagged photo, download the original from the site, and verify it has no GPS EXIF data.
  5. HEIC: Upload a HEIC image via the Files app and verify it arrives as JPEG.
  6. GIF: Upload an animated GIF and verify it still animates on the published post.
  7. Video: Set Max Video Upload Size to 480p, upload a video, and verify the transcoded resolution. On a free WordPress.com site, upload a video longer than 5 minutes and verify the "requires a paid plan" notice appears.
  8. Verify behavior on both a WordPress.com site and a self-hosted site with an application password.

@dcalhoun dcalhoun added Media Gutenberg Editing and display of Gutenberg blocks. [Type] Enhancement labels Jul 22, 2026
@dcalhoun dcalhoun added this to the 27.2 milestone Jul 22, 2026
@dcalhoun
dcalhoun force-pushed the feat/process-gutenberg-kit-media-uploads branch from 5eb00ce to f373395 Compare July 22, 2026 20:12
Comment thread WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift Outdated
@dcalhoun
dcalhoun marked this pull request as ready for review July 22, 2026 20:37
@dcalhoun
dcalhoun requested a review from jkmassel July 22, 2026 20:37
@wpmobilebot

wpmobilebot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number33787
VersionPR #25824
Bundle IDorg.wordpress.alpha
Commit435046a
Installation URL2jckuumdr1530
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number33787
VersionPR #25824
Bundle IDcom.jetpack.alpha
Commit435046a
Installation URL64jea119125b0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@dangermattic

dangermattic commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is assigned to the milestone 27.2. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

Gates the upcoming native media upload processing for the experimental
block editor, with a Debug-menu override for quick disabling.
Points GutenbergKit at the XCFramework snapshot for
wordpress-mobile/GutenbergKit#357, which adds the native media upload
server and MediaUploadDelegate. Swap to a tagged release before merge.
Implements GutenbergKit's MediaUploadDelegate so device media picked in
the experimental block editor is processed natively before upload,
honoring the app's Media settings: Optimize Images, Max Image Upload
Size, Image Quality, Max Video Upload Size, and Remove Location From
Media. Previously these uploads went directly from the WebView to the
REST API with no processing.

GBKMediaUploadProcessor mirrors the exporter option mapping used by
MediaImportService and reuses MediaURLExporter, so the editor now
matches the behavior of the legacy editor and My Site > Media. GIFs
and non-media files pass through untouched, and non-web-safe image
formats (e.g. HEIC) are converted to JPEG. Uploads still use
GutenbergKit's default uploader, which relays the raw WordPress
response to the editor.
Verifies resizing, GPS stripping, HEIC-to-JPEG conversion, GIF and
no-op passthrough, disallowed file extensions, and the video duration
limit using the existing media fixtures.
Adds the gbkMediaUploadOptimization flag to the Experimental Features
list and aligns display names with the "New Block Editor (NBE)"
naming.
Removes the gbkMediaUploadOptimization gate ahead of reverting the
flag. GutenbergKit degrades gracefully if the upload server cannot
start, so a dedicated kill switch isn't needed.
Moves the pin off the pr-build/357 snapshot now that
wordpress-mobile/GutenbergKit#357 has merged. Trunk also carries the
follow-up hardening in #561, which adds a defaulted handlesFile(ofType:
named:) to MediaUploadDelegate, so GBKMediaUploadProcessor conforms
unchanged.

No tagged release includes #357 yet — v0.19.0 predates it. Swap to a
tagged release before merge.
@dcalhoun
dcalhoun force-pushed the feat/process-gutenberg-kit-media-uploads branch from e65c714 to 435046a Compare August 17, 2026 12:36
@wpmobilebot

wpmobilebot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

dcalhoun and others added 5 commits August 17, 2026 09:08
SVG conforms to `UTType.image`, so `MediaURLExporter.expectedExport`
classifies it as `.image` and it reaches the exporter. ImageIO cannot
decode or encode SVG: `CGImageSourceCreateWithURL` returns a source with
zero images, and both `CGImageSourceCreateThumbnailAtIndex` and
`CGImageDestinationCreateWithURL` return nil. The export therefore fails
instead of producing a file.

Return the original file for SVG, as we already do for GIF, and drop
`.svg` from `webSafeImageTypes`. That set decides whether an image needs
converting to JPEG, so it should only hold raster formats ImageIO can
actually read and write; SVG's membership there implied it could reach
the exporter safely.

Only sites whose plan allows SVG can upload one, so this is not reachable
on plans where the picker greys the file out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MediaDirectory.temporary` is a computed property returning
`.temporary(id: UUID())`, so each export was written to a fresh
`tmp/<uuid>/Media/`. GutenbergKit's cleanup removes only the file at
`uploadURL`, never the two enclosing directories, so every processed
image or video left an empty directory pair behind for the lifetime of
the process.

Write every export to one directory identified by a fixed UUID.
`MediaFileManager.makeLocalMediaURL` increments filenames, so uploads
sharing a source name do not collide. The ID is stable across launches
so a directory orphaned by a crash is reused rather than accumulating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment claimed the guard skips processing "when it would be a
no-op", which is broader than what the condition tests. With image
optimization off, `imageSizeForUpload` returns `Int.max` (no downscale)
but `imageQualityForUpload` returns `.high`, so an image that falls
through is still re-encoded at 0.9 quality.

Describe what the guard actually checks and note that the quality
mapping matches `MediaImportService`. No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`makeExporter` is shared by the image and video paths, and it derived
`exportImageType` from the source URL's type alone. A video's UTI is not
in `webSafeImageTypes`, so every video export was configured to write
JPEG. `MediaURLExporter.exportVideo` ignores `imageOptions`, so this had
no effect, but it stated something untrue about the export.

Pass the classification `processFile` already computed and set
`exportImageType` only for an image. No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`exportsShareOneTemporaryDirectory` cleaned up each export inside the
loop body, so `defer` fired at the end of every iteration and deleted
the file before the next export ran. `incrementalFilename` only
increments while a file exists, so all three exports resolved to the
same path and the directory set was trivially of size one. The test
passed whether or not exports shared a directory.

Collect the output URLs and clean them up after the loop so all three
files coexist, and assert they are three distinct paths in one
directory. Verified by reverting dd8c5bb locally: the test now fails
with three directories instead of one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gutenberg Editing and display of Gutenberg blocks. Media [Type] Enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants