Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
30d839e
Add HEIC canvas fallback for client-side processing
adamsilverstein Mar 20, 2026
75bb708
Add backport changelog entry for HEIC canvas fallback
adamsilverstein Mar 20, 2026
d329759
Fix HEIC canvas fallback for Chrome on macOS
adamsilverstein Mar 20, 2026
60a9d10
Add HEIC decoding for Chrome on macOS via VideoDecoder
adamsilverstein Mar 21, 2026
63d75e1
Fix HEIC upload handling and parser issues
adamsilverstein Mar 21, 2026
b5f35cc
Remove redundant original HEIC sideload
adamsilverstein Mar 21, 2026
af72719
Skip HEIC URLs in editor and remove debug logging
adamsilverstein Mar 21, 2026
b326856
Add unit tests for HEIC container parser
adamsilverstein Mar 21, 2026
781b9f5
Add bounds check for HEIC grid descriptor
adamsilverstein Mar 21, 2026
ec42421
Add Safari HEIC support via canvas conversion
adamsilverstein Mar 23, 2026
17df2d7
Add unit tests for canvas utils, grid parsing, and utils
adamsilverstein Mar 24, 2026
cbf4dfb
Fix ImageDecoder resource leak and duplicate onBatchSuccess callback
adamsilverstein Mar 31, 2026
7721b4c
Ensure wp-admin/includes/image.php is loaded before calling wp_create…
adamsilverstein Apr 2, 2026
cecea00
Parse irot box from HEIC container and apply rotation in canvas conve…
adamsilverstein Apr 2, 2026
42db176
Convert HEIC to JPEG before upload instead of sideloading
adamsilverstein Apr 2, 2026
2ce8b26
Fix PHPCS: remove blank line after control structure
adamsilverstein Apr 3, 2026
2f41440
Preserve original HEIC file when uploading
adamsilverstein Apr 8, 2026
b987a0a
Merge branch 'trunk' into handle-heic-with-canvas
adamsilverstein Apr 13, 2026
657ff0d
Revert "Preserve original HEIC file when uploading"
adamsilverstein Apr 13, 2026
1549a7a
Preserve original HEIC via dedicated sideload field
adamsilverstein Apr 13, 2026
c3a6647
Remove orphaned JPEG when HEIC replaces original_image
adamsilverstein Apr 13, 2026
4cea4e5
Preserve HEIC under dedicated metadata key
adamsilverstein Apr 13, 2026
6671942
Remove orphan-JPEG cleanup from 'original' sideload branch
adamsilverstein Apr 13, 2026
6ff5a4c
Rename HEIC companion meta key to 'original'
adamsilverstein Apr 13, 2026
8c3ac6f
Merge branch 'trunk' into handle-heic-with-canvas
adamsilverstein Apr 20, 2026
84cdd06
Reconcile HEIC changes with trunk's concurrent sideload API
adamsilverstein Apr 20, 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
3 changes: 3 additions & 0 deletions backport-changelog/7.1/11323.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/11323

* https://github.com/WordPress/gutenberg/pull/76731
15 changes: 11 additions & 4 deletions lib/media/class-gutenberg-rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,21 @@ public function register_routes(): void {
'callback' => array( $this, 'sideload_item' ),
'permission_callback' => array( $this, 'sideload_item_permissions_check' ),
'args' => array(
'id' => array(
'id' => array(
'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ),
'type' => 'integer',
),
'image_size' => array(
'image_size' => array(
'description' => __( 'Image size.', 'gutenberg' ),
'type' => 'string',
'enum' => $valid_image_sizes,
'required' => true,
),
'generate_sub_sizes' => array(
'description' => __( 'Whether to generate image sub sizes from the sideloaded file.', 'gutenberg' ),
'type' => 'boolean',
'default' => false,
),
),
),
'allow_batch' => $this->allow_batch,
Expand Down Expand Up @@ -90,13 +95,15 @@ public function register_routes(): void {
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( false === $request['generate_sub_sizes'] ) {
$bypass_mime_check = false === $request['generate_sub_sizes'];

if ( $bypass_mime_check ) {
add_filter( 'wp_prevent_unsupported_mime_type_uploads', '__return_false' );
}

$result = parent::create_item_permissions_check( $request );

if ( false === $request['generate_sub_sizes'] ) {
if ( $bypass_mime_check ) {
remove_filter( 'wp_prevent_unsupported_mime_type_uploads', '__return_false' );
}

Expand Down
217 changes: 132 additions & 85 deletions lib/media/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,43 @@
/**
* Adds media-related functionality for client-side media processing.
*
* This file is structured in two tiers:
*
* 1. HEIC infrastructure — loaded whenever the feature filter is enabled.
* Browsers like Safari can decode HEIC via createImageBitmap() even
* without VIPS/SharedArrayBuffer, so HEIC MIME types, the custom REST
* controller, and REST field/index registrations are always needed.
*
* 2. Full VIPS/WASM processing — loaded only when the feature filter is
* enabled AND requires cross-origin isolation (DIP) at runtime.
*
* @package gutenberg
*/

if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
return;
}

/**
* Sets a global JS variable to indicate that client-side media processing is enabled.
*/
function gutenberg_set_client_side_media_processing_flag() {
if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
return;
}
wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
}
add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );
// ── Tier 1: HEIC infrastructure (always loaded) ─────────────────────

/**
* Returns a list of all available image sizes.
* Registers HEIC/HEIF as allowed upload MIME types.
*
* @return array Existing image sizes.
*/
function gutenberg_get_all_image_sizes(): array {
$sizes = wp_get_registered_image_subsizes();

foreach ( $sizes as $name => &$size ) {
$size['height'] = (int) $size['height'];
$size['width'] = (int) $size['width'];
$size['name'] = $name;
}
unset( $size );

return $sizes;
}

/**
* Returns the default output format mapping for the supported image formats.
* HEIC images can be decoded in the browser (via canvas/VideoDecoder).
* Registering these MIME types ensures the file picker's accept attribute
* includes them, preventing macOS from silently converting HEIC to JPEG
* on selection.
*
* @return array<string,string> Map of input formats to output formats.
* @param array $mimes Allowed MIME types (extension => type).
* @return array Modified MIME types.
*/
function gutenberg_get_default_image_output_formats() {
$input_formats = array(
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/avif',
'image/heic',
);

$output_formats = array();

foreach ( $input_formats as $mime_type ) {
/** This filter is documented in wp-includes/media.php */
$output_formats = apply_filters(
'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$output_formats,
'',
$mime_type
);
}

return $output_formats;
}

/**
* Filters the REST API root index data to add custom settings.
*
* @param WP_REST_Response $response Response data.
*/
function gutenberg_media_processing_filter_rest_index( WP_REST_Response $response ) {
/** This filter is documented in wp-admin/includes/images.php */
$image_size_threshold = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

$default_image_output_formats = gutenberg_get_default_image_output_formats();

/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$jpeg_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$png_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$gif_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

if ( current_user_can( 'upload_files' ) ) {
$response->data['image_sizes'] = gutenberg_get_all_image_sizes();
$response->data['image_size_threshold'] = $image_size_threshold;
$response->data['image_output_formats'] = (object) $default_image_output_formats;
$response->data['jpeg_interlaced'] = $jpeg_interlaced;
$response->data['png_interlaced'] = $png_interlaced;
$response->data['gif_interlaced'] = $gif_interlaced;
}

return $response;
function gutenberg_add_heic_upload_mimes( array $mimes ): array {
$mimes['heic'] = 'image/heic';
$mimes['heif'] = 'image/heif';
return $mimes;
}

add_filter( 'rest_index', 'gutenberg_media_processing_filter_rest_index' );

add_filter( 'upload_mimes', 'gutenberg_add_heic_upload_mimes' );

/**
* Overrides the REST controller for the attachment post type.
Expand All @@ -120,7 +59,6 @@ function gutenberg_filter_attachment_post_type_args( array $args, string $post_t

add_filter( 'register_post_type_args', 'gutenberg_filter_attachment_post_type_args', 10, 2 );


/**
* Registers additional REST fields for attachments.
*/
Expand Down Expand Up @@ -201,6 +139,115 @@ function gutenberg_rest_get_attachment_filesize( array $post ): ?int {
return null;
}

/**
* Returns a list of all available image sizes.
*
* @return array Existing image sizes.
*/
function gutenberg_get_all_image_sizes(): array {
$sizes = wp_get_registered_image_subsizes();

foreach ( $sizes as $name => &$size ) {
$size['height'] = (int) $size['height'];
$size['width'] = (int) $size['width'];
$size['name'] = $name;
}
unset( $size );

return $sizes;
}

/**
* Returns the default output format mapping for the supported image formats.
*
* @return array<string,string> Map of input formats to output formats.
*/
function gutenberg_get_default_image_output_formats() {
$input_formats = array(
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/avif',
'image/heic',
'image/heif',
);

$output_formats = array();

foreach ( $input_formats as $mime_type ) {
/** This filter is documented in wp-includes/media.php */
$output_formats = apply_filters(
'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$output_formats,
'',
$mime_type
);
}

return $output_formats;
}

/**
* Filters the REST API root index data to add custom settings.
*
* @param WP_REST_Response $response Response data.
*/
function gutenberg_media_processing_filter_rest_index( WP_REST_Response $response ) {
/** This filter is documented in wp-admin/includes/images.php */
$image_size_threshold = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

$default_image_output_formats = gutenberg_get_default_image_output_formats();

/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$jpeg_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$png_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$gif_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

if ( current_user_can( 'upload_files' ) ) {
$response->data['image_sizes'] = gutenberg_get_all_image_sizes();
$response->data['image_size_threshold'] = $image_size_threshold;
$response->data['image_output_formats'] = (object) $default_image_output_formats;
$response->data['jpeg_interlaced'] = $jpeg_interlaced;
$response->data['png_interlaced'] = $png_interlaced;
$response->data['gif_interlaced'] = $gif_interlaced;
}

return $response;
}

add_filter( 'rest_index', 'gutenberg_media_processing_filter_rest_index' );

/**
* Sets a global JS variable to indicate that HEIC canvas-based upload support is available.
*
* This flag is set whenever the media processing feature is enabled,
* regardless of whether the browser supports full VIPS-based processing.
* Browsers like Safari can use createImageBitmap() to decode HEIC images
* and convert them to JPEG for server-side sub-size generation.
*/
function gutenberg_set_heic_upload_support_flag() {
wp_add_inline_script( 'wp-block-editor', 'window.__heicUploadSupport = true', 'before' );
}
add_action( 'admin_init', 'gutenberg_set_heic_upload_support_flag' );

// ── Tier 2: Full client-side processing (VIPS/WASM) ─────────────────
// Everything below requires cross-origin isolation (Document-Isolation-Policy)
// and SharedArrayBuffer support, which is only available in Chromium 137+.

/**
* Sets a global JS variable to indicate that client-side media processing is enabled.
*/
function gutenberg_set_client_side_media_processing_flag() {
if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
return;
}
wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
}
add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );

/**
* Filters the list of rewrite rules formatted for output to an .htaccess file.
*
Expand Down
Loading
Loading