Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
82 changes: 80 additions & 2 deletions plugins/webp-uploads/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
*/
function webp_uploads_get_upload_image_mime_transforms(): array {

// Check the selected output format.
$output_format = webp_uploads_mime_type_supported( 'image/avif' ) ? webp_uploads_get_image_output_format() : 'webp';
// Check the selected output format. When the browser encodes the images, the server's own AVIF support is irrelevant.
$output_format = webp_uploads_get_image_output_format();
if ( 'avif' === $output_format && ! webp_uploads_mime_type_supported( 'image/avif' ) && ! webp_uploads_is_client_side_media_processing() ) {
$output_format = 'webp';
}

$default_transforms = array(
'image/jpeg' => array( 'image/' . $output_format ),
Expand Down Expand Up @@ -365,6 +368,81 @@ function webp_uploads_mime_type_supported( string $mime_type ): bool {
return true;
}

/**
* Checks whether the images for the current request are encoded by the browser rather than the server.
*
* Since WordPress 7.1, the block editor can process uploads client side: the browser uploads the original,
* generates the sub-sizes itself (transcoding them to the format reported via the `image_output_format`
* REST field), sideloads them, and finally asks the server to record the metadata. In that flow the
* server's image editor capabilities do not limit which modern format can be produced.
*
* @since n.e.x.t
*
* @return bool True while handling a REST request in which the browser supplies the encoded images, false otherwise.
*/
function webp_uploads_is_client_side_media_processing(): bool {
/**
* Filters whether the images for the current request are encoded client side.
*
* The plugin enables this for the REST API requests of the client side media processing flow, in which case
* the modern image format is selected without regard to what the server's image editor can encode.
*
* @since n.e.x.t
*
* @param bool $client_side Whether the current request's images are encoded client side. Default false.
*/
return (bool) apply_filters( 'webp_uploads_client_side_media_processing', false );
}

/**
* Checks whether WordPress offers client side media processing on this site.
*
* Client side media processing was introduced in WordPress 7.1 and requires a secure context. Whether a given
* browser is actually able to process images is a runtime check which happens in JavaScript.
*
* @since n.e.x.t
*
* @return bool True if client side media processing is enabled for the site, false otherwise.
*/
function webp_uploads_is_client_side_media_processing_enabled(): bool {
return function_exists( 'wp_is_client_side_media_processing_enabled' ) && wp_is_client_side_media_processing_enabled();
}

/**
* Checks whether a REST request is part of the client side media processing flow.
*
* This is the case for an attachment creation request in which the client opts out of server side sub-size
* generation, and for the sideload and finalize requests that supply and record the client generated images.
*
* @since n.e.x.t
*
* @param WP_REST_Request<array<string, mixed>> $request The REST request.
* @param mixed $handler The route handler used for the request.
* @return bool True if the browser supplies the encoded images for the request, false otherwise.
*/
function webp_uploads_is_client_side_media_processing_request( WP_REST_Request $request, $handler ): bool {
if (
! is_array( $handler ) ||
! isset( $handler['callback'] ) ||
! is_array( $handler['callback'] ) ||
! isset( $handler['callback'][0], $handler['callback'][1] ) ||
! $handler['callback'][0] instanceof WP_REST_Attachments_Controller ||
! is_string( $handler['callback'][1] )
) {
return false;
}

switch ( $handler['callback'][1] ) {
case 'create_item':
return false === $request->get_param( 'generate_sub_sizes' );
case 'sideload_item':
case 'finalize_item':
return true;
}

return false;
}

/**
* Checks if the server supports AVIF image format.
*
Expand Down
64 changes: 59 additions & 5 deletions plugins/webp-uploads/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,54 @@ function webp_uploads_wp_get_missing_image_subsizes( $missing_sizes, array $imag
}
add_filter( 'wp_get_missing_image_subsizes', 'webp_uploads_wp_get_missing_image_subsizes', 10, 3 );

/**
* Marks the start of a REST request in which the browser supplies the encoded images.
*
* @since n.e.x.t
*
* @see webp_uploads_is_client_side_media_processing()
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* @param mixed $handler Route handler used for the request.
* @param WP_REST_Request<array<string, mixed>>|mixed $request Request used to generate the response.
* @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed The unchanged result.
*/
function webp_uploads_start_client_side_media_processing_request( $response, $handler, $request ) {
if ( $request instanceof WP_REST_Request && webp_uploads_is_client_side_media_processing_request( $request, $handler ) ) {
add_filter( 'webp_uploads_client_side_media_processing', '__return_true' );
}
return $response;
}
add_filter( 'rest_request_before_callbacks', 'webp_uploads_start_client_side_media_processing_request', 10, 3 );

/**
* Marks the end of a REST request in which the browser supplies the encoded images.
*
* @since n.e.x.t
*
* @see webp_uploads_is_client_side_media_processing()
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed The unchanged result.
*/
function webp_uploads_end_client_side_media_processing_request( $response ) {
remove_filter( 'webp_uploads_client_side_media_processing', '__return_true' );
return $response;
}
add_filter( 'rest_request_after_callbacks', 'webp_uploads_end_client_side_media_processing_request' );

/**
* Filter the image editor default output format mapping to select the most appropriate
* output format depending on desired output formats and supported mime types by the image
* editor.
*
* When the browser encodes the images (see webp_uploads_is_client_side_media_processing()), the
* source mime type is mapped to the selected modern format regardless of what the server's image
* editor supports. This is also done when fallback output is enabled, since the fallback images in
* the original format are then generated by the server once the client side upload is finalized.
*
* @since 1.0.0
* @since n.e.x.t The server's image editor support is no longer consulted for client side media processing requests.
*
* @param array<string, string>|mixed $output_format An array of mime type mappings. Maps a source mime type to a new destination mime type. Default empty array.
* @param string|null $filename Path to the image.
Expand All @@ -342,12 +384,24 @@ function webp_uploads_filter_image_editor_output_format( $output_format, ?string
return $output_format;
}

// Use the original mime type if this type is allowed.
$valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
if (
! isset( $valid_mime_transforms[ $mime_type ] ) ||
in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true )
) {
if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
return $output_format;
}

if ( webp_uploads_is_client_side_media_processing() ) {
// The browser can encode any of the modern formats, so use the first one which differs from the source.
foreach ( $valid_mime_transforms[ $mime_type ] as $target_mime ) {
if ( $target_mime !== $mime_type && in_array( $target_mime, array( 'image/avif', 'image/webp' ), true ) ) {
$output_format[ $mime_type ] = $target_mime;
break;
}
}
return $output_format;
}

// Use the original mime type if this type is allowed.
if ( in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true ) ) {
return $output_format;
}

Expand Down
75 changes: 53 additions & 22 deletions plugins/webp-uploads/picture-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Potentially wrap an image tag in a picture element.
*
* @since 2.0.0
* @since n.e.x.t The fallback image is pointed at the original format when the image is in a modern format.
*
* @param string $image The image tag.
* @param string $context The context for the image tag.
Expand Down Expand Up @@ -158,27 +159,38 @@ function webp_uploads_wrap_image_in_picture( string $image, string $context, int
$sizes = $processor->get_attribute( 'sizes' );
$srcset = $processor->get_attribute( 'srcset' );

/**
* Gets the srcset for the image in a given mime type.
*
* @param string $image_mime_type The mime type.
* @return string|false The srcset, or false if none could be determined.
*/
$get_srcset_for_mime_type = static function ( string $image_mime_type ) use ( $attachment_id, $size_array, $image_meta, $mime_type_data ) {
// Filter core's wp_get_attachment_image_srcset to return the sources for the current mime type.
$filter = static function ( $sources ) use ( $mime_type_data, $image_mime_type ): array {
$filtered_sources = array();
foreach ( $sources as $source ) {
// Swap the URL for the current mime type.
if ( isset( $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ] ) ) {
$filename = $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ]['file'];
$filtered_sources[] = array(
'url' => dirname( $source['url'] ) . '/' . $filename,
'descriptor' => $source['descriptor'],
'value' => $source['value'],
);
}
}
return $filtered_sources;
};
add_filter( 'wp_calculate_image_srcset', $filter );
$image_srcset = wp_get_attachment_image_srcset( $attachment_id, $size_array, $image_meta );
remove_filter( 'wp_calculate_image_srcset', $filter );
return $image_srcset;
};

if ( null !== $srcset && null !== $sizes ) {
foreach ( $mime_types as $image_mime_type ) {
// Filter core's wp_get_attachment_image_srcset to return the sources for the current mime type.
$filter = static function ( $sources ) use ( $mime_type_data, $image_mime_type ): array {
$filtered_sources = array();
foreach ( $sources as $source ) {
// Swap the URL for the current mime type.
if ( isset( $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ] ) ) {
$filename = $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ]['file'];
$filtered_sources[] = array(
'url' => dirname( $source['url'] ) . '/' . $filename,
'descriptor' => $source['descriptor'],
'value' => $source['value'],
);
}
}
return $filtered_sources;
};
add_filter( 'wp_calculate_image_srcset', $filter );
$image_srcset = wp_get_attachment_image_srcset( $attachment_id, $size_array, $image_meta );
remove_filter( 'wp_calculate_image_srcset', $filter );
$image_srcset = $get_srcset_for_mime_type( $image_mime_type );
if ( is_string( $image_srcset ) ) {
$picture_sources .= sprintf(
'<source type="%s" srcset="%s"%s>',
Expand Down Expand Up @@ -208,11 +220,30 @@ function webp_uploads_wrap_image_in_picture( string $image, string $context, int
return $image;
}

// Tag the inner `<img>` so a later rewrite pass (for example `wp_content_img_tag`
// once this markup is embedded in post content) recognises it as already wrapped
// and skips it. See the idempotency guard above.
$marker = new WP_HTML_Tag_Processor( $image );
if ( $marker->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
/*
* The inner `<img>` is the fallback for browsers which support neither the modern format nor the picture
* element. When the image itself is already in a modern format, as is the case when the browser generated
* the sub-sizes (see webp_uploads_is_client_side_media_processing()), point it at the original format instead.
*/
$img_src = $marker->get_attribute( 'src' );
if ( is_string( $img_src ) && wp_check_filetype( $img_src )['type'] !== $original_file_mime_type ) {
$fallback_src = webp_uploads_get_mime_type_image( $attachment_id, $img_src, $original_file_mime_type );
if ( is_string( $fallback_src ) ) {
$marker->set_attribute( 'src', $fallback_src );
if ( null !== $srcset && null !== $sizes ) {
$fallback_srcset = $get_srcset_for_mime_type( $original_file_mime_type );
if ( is_string( $fallback_srcset ) ) {
$marker->set_attribute( 'srcset', $fallback_srcset );
}
}
}
}

// Tag the inner `<img>` so a later rewrite pass (for example `wp_content_img_tag`
// once this markup is embedded in post content) recognises it as already wrapped
// and skips it. See the idempotency guard above.
$marker->set_attribute( 'data-wp-picture-wrapped', true );
$image = $marker->get_updated_html();
}
Expand Down
Loading
Loading