Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
4d2db5f
feat: add sideloading functionality and image resizing operations to …
adamsilverstein Jan 13, 2026
db2d0c0
Add getImageSizes and getImageSize selectors
adamsilverstein Jan 13, 2026
9bf2ee1
add imageSizes support to media upload settings and block editor sett…
adamsilverstein Jan 13, 2026
610f25f
add dependency for @wordpress/vips in upload-media
adamsilverstein Jan 13, 2026
aed40f2
add vips image processing utilities for format conversion, compressio…
adamsilverstein Jan 13, 2026
98c737c
Add the shopify/web-worker dependency
adamsilverstein Jan 13, 2026
f2014d3
export vips utility functions for image processing
adamsilverstein Jan 13, 2026
42077a5
Add thumbnail generation and sideload actions
adamsilverstein Jan 13, 2026
0903e04
linter fixes
adamsilverstein Jan 13, 2026
a5781a8
Ensure parentItem exists before using
adamsilverstein Jan 13, 2026
fa7e267
Add tests for sideloading and canceling image uploads
adamsilverstein Jan 13, 2026
740512c
rename imageSizes -> allImageSizes to avoid conflict
adamsilverstein Jan 13, 2026
4f59396
cleanup
adamsilverstein Jan 13, 2026
6caeab6
Simplify loading of all image sizes
adamsilverstein Jan 14, 2026
9f61d8f
update to new vips utils
adamsilverstein Jan 16, 2026
cd16ae1
update package lock
adamsilverstein Jan 16, 2026
bfda0f7
refactor vips tests to use updated mock functions
adamsilverstein Jan 16, 2026
37b776d
Add Error Handling in generateThumbnails and hasTransparency
adamsilverstein Jan 16, 2026
fc7cd3e
verify item exists before using
adamsilverstein Jan 16, 2026
6f7ca75
ensure item available
adamsilverstein Jan 16, 2026
382b02e
Improve docs and add abort signal
adamsilverstein Jan 16, 2026
bfbc620
remove async keyword, not needed (?)
adamsilverstein Jan 16, 2026
a1bb637
Add bigImageSizeThreshold setting to upload-media store
adamsilverstein Jan 16, 2026
e155638
Implement big image size threshold scaling
adamsilverstein Jan 16, 2026
989324f
Add E2E tests for big image size threshold
adamsilverstein Jan 16, 2026
75a94ec
Generate thumbnails from original image for better quality
adamsilverstein Jan 16, 2026
8dac9ad
Implement automatic image rotation matching server handling
adamsilverstein Jan 16, 2026
4d93fa8
Fix build and type issues for image rotation support
adamsilverstein Jan 17, 2026
9f70bdf
Fix CI failures for create subsized images PR
adamsilverstein Jan 23, 2026
4ed0b80
Exclude vips package from Storybook build
adamsilverstein Jan 23, 2026
b7db54b
Stub vips package for Storybook build
adamsilverstein Jan 23, 2026
d436829
Extend vips stub to cover wasm-vips and WASM files
adamsilverstein Jan 23, 2026
362560b
restore package lock from trunk
adamsilverstein Jan 30, 2026
563a024
update packages from trunk
adamsilverstein Jan 30, 2026
d3e939e
restore build file from trunk
adamsilverstein Jan 30, 2026
d308469
restore vips dependency
adamsilverstein Jan 31, 2026
8c132a6
restore vips dependency 2
adamsilverstein Jan 31, 2026
44f71b9
add vips dependency
adamsilverstein Jan 31, 2026
224c585
update readme
adamsilverstein Jan 31, 2026
b850e62
try: avoid extra preload
adamsilverstein Jan 31, 2026
f0c6cfa
Fix preload path to include image_sizes and image_size_threshold
adamsilverstein Feb 1, 2026
a37e246
fix phpcs
adamsilverstein Feb 2, 2026
6ac7a29
Fix WASM loading error by using module worker type
adamsilverstein Feb 2, 2026
65e5893
Fix wasm-vips loading in Blob URL Worker context
adamsilverstein Feb 3, 2026
4a2ad32
Remove HEIF support from wasm-vips due to trademark issues
adamsilverstein Feb 3, 2026
33416b3
Add media processing fields directly to template preload paths
adamsilverstein Feb 3, 2026
6bb8387
Fix CI failures: preload fields mismatch and Prettier formatting
adamsilverstein Feb 3, 2026
adb6e5c
clean stray comment
adamsilverstein Feb 3, 2026
795dfb7
remove unintended changes
adamsilverstein Feb 3, 2026
892e4c3
Fix memory leak in vips by ensuring operation cleanup
adamsilverstein Feb 4, 2026
1a0c8b6
Rename isUploadingByParentId to hasPendingItemsByParentId
adamsilverstein Feb 4, 2026
a253703
Fix test cleanup for mocked window.fetch
adamsilverstein Feb 4, 2026
f4fb34f
Clean up unused variable and update vips package reference
adamsilverstein Feb 4, 2026
d8af45f
Convert filesList to array before passing to addItems
adamsilverstein Feb 4, 2026
3ce45ac
Guard against duplicate upload-media store registration
adamsilverstein Feb 4, 2026
132f54c
restore loading vips prerelease directly
adamsilverstein Feb 4, 2026
2c6e0f7
Merge branch 'trunk' into 74355-create-subsized-images
adamsilverstein Feb 4, 2026
029fb95
add rotate image functionality
adamsilverstein Feb 4, 2026
ee789d5
update package lock
adamsilverstein Feb 4, 2026
d179aa4
correct vips load
adamsilverstein Feb 4, 2026
91a509c
Add Jest stub for vips worker module
adamsilverstein Feb 4, 2026
35def30
Fix vips test to mock correct module path
adamsilverstein Feb 4, 2026
4a4b9ca
Skip server side sub size generation
adamsilverstein Feb 6, 2026
31eab03
Add image processing concurrency constant
adamsilverstein Feb 6, 2026
5cc1bda
Add maxConcurrentImageProcessing to Settings type
adamsilverstein Feb 6, 2026
46f4640
Wire image processing concurrency into reducer
adamsilverstein Feb 6, 2026
3a43770
Add image processing concurrency selectors
adamsilverstein Feb 6, 2026
86ece0c
Gate image processing on concurrency limit
adamsilverstein Feb 6, 2026
f768f9a
Trigger queued image processing when slot opens
adamsilverstein Feb 6, 2026
ed2b000
Terminate VIPS worker when upload queue empties
adamsilverstein Feb 6, 2026
dfdf08c
Add tests for image processing selectors
adamsilverstein Feb 6, 2026
35475dd
Add missing terminateVipsWorker to stub and mock
adamsilverstein Feb 6, 2026
ccd49b3
Show spinner during sub-size image sideloading
adamsilverstein Feb 6, 2026
fceec95
Merge branch 'trunk' into 74355-create-subsized-images
adamsilverstein Feb 10, 2026
1cd5b9c
Merge branch 'trunk' into 74355-create-subsized-images
adamsilverstein Feb 10, 2026
e6b53da
Exclude generated worker-code.ts from ESLint
adamsilverstein Feb 11, 2026
d3fa5f3
Update package-lock.json for upload-media dep
adamsilverstein Feb 11, 2026
df87386
Lazy-load vips worker module on first use
adamsilverstein Feb 11, 2026
ba78a70
Add upload-media reference to block-library tsconfig
adamsilverstein Feb 11, 2026
db4bd7e
Unbundle vips from upload-media as a script module
adamsilverstein Feb 13, 2026
b252589
Merge remote-tracking branch 'origin/trunk' into 74355-create-subsize…
adamsilverstein Feb 13, 2026
f42a103
Make wasm-vips esbuild plugin generic via wpWorkers.resolve
adamsilverstein Feb 13, 2026
2c2c8db
Use ImageSizeCrop type for allImageSizes setting
adamsilverstein Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ node_modules
packages/block-serialization-spec-parser/parser.js
packages/icons/src/library/*.tsx
packages/react-native-editor/bundle
packages/vips/src/worker-code.ts
vendor
!.*.js
14 changes: 14 additions & 0 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,20 @@ function gutenberg_enqueue_latex_to_mathml_loader() {
wp_enqueue_script_module( '@wordpress/latex-to-mathml/loader' );
}

/**
* Enqueue the vips loader script module in the block editor.
*
* This registers @wordpress/vips/worker as a dynamic dependency in the import map,
* enabling on-demand loading of the ~3.8MB WASM-based image processing module
* when client-side media processing is triggered via @wordpress/upload-media.
*
* @see packages/vips/src/loader.ts
*/
add_action( 'enqueue_block_editor_assets', 'gutenberg_enqueue_vips_loader' );
function gutenberg_enqueue_vips_loader() {
wp_enqueue_script_module( '@wordpress/vips/loader' );
}

add_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_core_abilities' );
function gutenberg_enqueue_core_abilities() {
wp_enqueue_script_module( '@wordpress/core-abilities' );
Expand Down
31 changes: 31 additions & 0 deletions lib/compat/wordpress-7.0/preload.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,34 @@ static function ( $path ) {
return $paths;
}
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_9', 10, 2 );

/**
* Filters the block editor preload paths to include media processing fields.
*
* The packages/core-data/src/entities.js file requests additional fields
* (image_sizes, image_size_threshold) on the root endpoint that are not
* included in WordPress Core's default preload paths. This filter ensures
* the preloaded URL matches exactly what the JavaScript requests.
*
* @since 20.1.0
*
* @param array $paths REST API paths to preload.
* @return array Filtered preload paths.
*/
function gutenberg_block_editor_preload_paths_root_fields( $paths ) {
// Complete list of fields expected by packages/core-data/src/entities.js.
// This must match exactly for preloading to work (same fields, same order).
// @see packages/core-data/src/entities.js rootEntitiesConfig.__unstableBase
$root_fields = 'description,gmt_offset,home,image_sizes,image_size_threshold,image_output_formats,jpeg_interlaced,png_interlaced,gif_interlaced,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front';

foreach ( $paths as $key => $path ) {
if ( is_string( $path ) && str_starts_with( $path, '/?_fields=' ) ) {
// Replace with the complete fields list to ensure exact match.
$paths[ $key ] = '/?_fields=' . $root_fields;
break;
}
}

return $paths;
}
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_root_fields' );
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,31 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE
return $args;
}

/**
* Retrieves the attachment's schema, conforming to JSON Schema.
*
* Adds exif_orientation field to the schema.
*
* @return array Item schema data.
*/
public function get_item_schema() {
$schema = parent::get_item_schema();

$schema['properties']['exif_orientation'] = array(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we pass exit orientation back after uploading the original image, keeping the logic in one place (server side) for now. We can layer on client side support for image formats the server doesn't support in a later iteration.

'description' => __( 'EXIF orientation value from the original image. Values 1-8 follow the EXIF specification. A value other than 1 indicates the image needs rotation.', 'gutenberg' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
);

return $schema;
}

/**
* Prepares a single attachment output for response.
*
* Ensures 'missing_image_sizes' is set for PDFs and not just images.
* Adds 'exif_orientation' for images that need client-side rotation.
*
* @param WP_Post $item Attachment object.
* @param WP_REST_Request $request Request object.
Expand All @@ -92,10 +113,27 @@ public function prepare_item_for_response( $item, $request ): WP_REST_Response {

$data = $response->get_data();

// Handle missing image sizes for PDFs.

$fields = $this->get_fields_for_response( $request );

// Add EXIF orientation for images.
if ( rest_is_field_included( 'exif_orientation', $fields ) ) {
if ( wp_attachment_is_image( $item ) ) {
$metadata = wp_get_attachment_metadata( $item->ID, true );

// Get the EXIF orientation from the image metadata.
// This is stored by wp_read_image_metadata() during upload.
$orientation = 1; // Default: no rotation needed.
if (
is_array( $metadata ) &&
isset( $metadata['image_meta']['orientation'] )
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

core stores the orientation attribute when the image is uploaded

) {
$orientation = (int) $metadata['image_meta']['orientation'];
}

$data['exif_orientation'] = $orientation;
}
}

if (
rest_is_field_included( 'missing_image_sizes', $fields ) &&
empty( $data['missing_image_sizes'] )
Expand Down Expand Up @@ -158,7 +196,9 @@ public function create_item( $request ) {
if ( ! $request['generate_sub_sizes'] ) {
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );

// Disable server-side EXIF rotation so the client can handle it.
// This preserves the original orientation value in the metadata.
add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
}

if ( ! $request['convert_format'] ) {
Expand All @@ -169,6 +209,7 @@ public function create_item( $request ) {

remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );

return $response;
Expand Down
29 changes: 13 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@types/uuid": "8.3.1",
"@vitejs/plugin-react": "5.1.2",
"@wordpress/build": "file:./packages/wp-build",
"@wordpress/vips": "1.0.0-prerelease",
"ajv": "8.17.1",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function useMediaUploadSettings( settings = {} ) {
mediaSideload: settings.mediaSideload,
maxUploadFileSize: settings.maxUploadFileSize,
allowedMimeTypes: settings.allowedMimeTypes,
allImageSizes: settings.allImageSizes,
bigImageSizeThreshold: settings.bigImageSizeThreshold,
} ),
[ settings ]
);
Expand Down
1 change: 1 addition & 0 deletions packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"@wordpress/reusable-blocks": "file:../reusable-blocks",
"@wordpress/rich-text": "file:../rich-text",
"@wordpress/server-side-render": "file:../server-side-render",
"@wordpress/upload-media": "file:../upload-media",
"@wordpress/url": "file:../url",
"@wordpress/viewport": "file:../viewport",
"@wordpress/wordcount": "file:../wordcount",
Expand Down
15 changes: 14 additions & 1 deletion packages/block-library/src/image/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { image as icon, plugins as pluginsIcon } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { useResizeObserver } from '@wordpress/compose';
import { getProtocol, prependHTTPS } from '@wordpress/url';
import { store as uploadStore } from '@wordpress/upload-media';

/**
* Internal dependencies
Expand Down Expand Up @@ -344,6 +345,17 @@ export function ImageEdit( {

const isExternal = isExternalImage( id, url );
const src = isExternal ? url : undefined;

const isSideloading = useSelect(
( select ) => {
if ( ! window.__experimentalMediaProcessing || ! id ) {
return false;
}
return select( uploadStore ).isUploadingById( id );
},
[ id ]
);

const mediaPreview = !! url && (
<img
alt={ __( 'Edit image' ) }
Expand All @@ -357,7 +369,7 @@ export function ImageEdit( {
const shadowProps = getShadowClassesAndStyles( attributes );

const classes = clsx( className, {
'is-transient': !! temporaryURL,
'is-transient': !! temporaryURL || isSideloading,
'is-resized': !! width || !! height,
[ `size-${ sizeSlug }` ]: sizeSlug,
'has-custom-border':
Expand Down Expand Up @@ -448,6 +460,7 @@ export function ImageEdit( {
<figure { ...blockProps }>
<Image
temporaryURL={ temporaryURL }
isSideloading={ isSideloading }
attributes={ attributes }
setAttributes={ setAttributes }
isSingleSelected={ isSingleSelected }
Expand Down
7 changes: 5 additions & 2 deletions packages/block-library/src/image/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ function ContentOnlyControls( {

export default function Image( {
temporaryURL,
isSideloading,
attributes,
setAttributes,
isSingleSelected,
Expand Down Expand Up @@ -874,7 +875,9 @@ export default function Image( {
onSelectURL={ onSelectURL }
onError={ onUploadError }
onReset={ () => onSelectImage( undefined ) }
isUploading={ !! temporaryURL }
isUploading={
!! temporaryURL || isSideloading
}
emptyLabel={ __( 'Add image' ) }
/>
</ToolsPanelItem>
Expand Down Expand Up @@ -1065,7 +1068,7 @@ export default function Image( {
...shadowProps.style,
} }
/>
{ temporaryURL && <Spinner /> }
{ ( temporaryURL || isSideloading ) && <Spinner /> }
</>
);

Expand Down
1 change: 1 addition & 0 deletions packages/block-library/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
{ "path": "../keycodes" },
{ "path": "../primitives" },
{ "path": "../rich-text" },
{ "path": "../upload-media" },
{ "path": "../url" },
{ "path": "../wordcount" }
],
Expand Down
8 changes: 7 additions & 1 deletion packages/core-data/src/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ export const rootEntitiesConfig = [
baseURL: '/',
baseURLParams: {
// Please also change the preload path when changing this.
// @see lib/compat/wordpress-6.8/preload.php
// @see lib/compat/wordpress-7.0/preload.php
_fields: [
'description',
'gmt_offset',
'home',
'image_sizes',
'image_size_threshold',
'image_output_formats',
'jpeg_interlaced',
'png_interlaced',
'gif_interlaced',
'name',
'site_icon',
'site_icon_url',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const BLOCK_EDITOR_SETTINGS = [
'__experimentalDiscussionSettings',
'__experimentalFeatures',
'__experimentalGlobalStylesBaseStyles',
'allImageSizes',
'alignWide',
'blockInspectorTabs',
'maxUploadFileSize',
Expand Down Expand Up @@ -116,6 +117,8 @@ const {
function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
const isLargeViewport = useViewportMatch( 'medium' );
const {
allImageSizes,
bigImageSizeThreshold,
allowRightClickOverrides,
blockTypes,
focusMode,
Expand Down Expand Up @@ -154,6 +157,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
? getEntityRecord( 'root', 'site' )
: undefined;

// Fetch image sizes from REST API index for client-side media processing.
const baseData = getEntityRecord( 'root', '__unstableBase' );

function getSectionRootBlock() {
if ( renderingMode === 'template-locked' ) {
return getBlocksByName( 'core/post-content' )?.[ 0 ] ?? '';
Expand All @@ -168,6 +174,8 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
}

return {
allImageSizes: baseData?.image_sizes,
bigImageSizeThreshold: baseData?.image_size_threshold,
allowRightClickOverrides: get(
'core',
'allowRightClickOverrides'
Expand Down Expand Up @@ -328,6 +336,8 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
),
[ globalStylesDataKey ]: globalStylesData,
[ globalStylesLinksDataKey ]: globalStylesLinksData,
allImageSizes,
bigImageSizeThreshold,
allowedBlockTypes,
allowRightClickOverrides,
focusMode: focusMode && ! forceDisableFocusMode,
Expand Down Expand Up @@ -431,6 +441,8 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
editMediaEntity,
wrappedOnNavigateToEntityRecord,
deviceType,
allImageSizes,
bigImageSizeThreshold,
isNavigationOverlayContext,
] );
}
Expand Down
Loading
Loading