From 71ca3141937479aa76dd99675808c619d277d7ef Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Thu, 21 Jul 2022 18:03:13 +0000 Subject: [PATCH] Media: enable generating multiple mime types for image uploads; specifically WebP versions for JPEG images by default. This changeset adds the capability for core media uploads to generate sub sized images in more than a single mime type. The output formats for each mime type can be controlled through a filter. WebP is used as an additional output format for JPEG images by default to improve front end performance. When generating additional mime types, only images which are smaller than the respective original are retained. By default, additional mime type images are only generated for the built-in core image sizes and any custom sizes that have opted in. Image meta is updated with a new 'sources' array containing file details for each mime type. Each image size in the 'sizes' array also gets a new 'sources' array that contains the image file details for each mime type. This change also increases image upload retries to accommodate additional image sizes. It also adds a `$mime_type` parameter to the `wp_get_missing_image_subsizes` function and filter. This change adds three new filters to enable full control of secondary mime image generation and output: * A new filter `wp_image_sizes_with_additional_mime_type_support` that filters the sizes that support secondary mime type output. Developers can use this to control the output of additional mime type sub-sized images on a per size basis. * A new filter `wp_upload_image_mime_transforms` that filters the output mime types for a given input mime type. Developers can use this to control generation of additional mime types for a given input mime type or even override the original mime type. * A new filter `wp_content_image_mimes` which controls image mime type output selection and order for frontend content. Developers can use this to control the mime type output preference order for content images. Content images inserted from the media library will use the available image versions based on the order from this filter. Thanks to the many contributors who helped develop, test and give feedback on this feature. A haiku to summarize: Upload a JPEG Images of all sizes Output as WebPs Props flixos90, MatthiasReinholz, studiolxv, markhowellsmead, eatingrules, pbiron, mukesh27, joegrainger, mehulkaklotar, tweetythierry, akshitsethi, peterwilsoncc, eugenemanuilov, mitogh, shetheliving, clarkeemily, codekraft, mikeschroder, clorith, kasparsd, spacedmonkey, trevorpfromsandee, jb510, scofennellgmailcom, seedsca, cagsmith, karinclimber, dainemawer, baxbridge, grapplerulrich, sobatkras, chynnabenton, tonylocalword, barneydavey, kwillmorth, garymatthews919, olliejones, imarkinteractive, jeffpaul, feastdesignco, webbeetle, masteradhoc. See #55443. Built from https://develop.svn.wordpress.org/trunk@53751 git-svn-id: http://core.svn.wordpress.org/trunk@53310 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/image.php | 573 +++++++++++++++++---- wp-includes/class-wp-image-editor.php | 65 ++- wp-includes/js/plupload/handlers.js | 2 +- wp-includes/js/plupload/handlers.min.js | 2 +- wp-includes/js/plupload/wp-plupload.js | 2 +- wp-includes/js/plupload/wp-plupload.min.js | 2 +- wp-includes/media.php | 116 +++++ wp-includes/post.php | 87 +++- wp-includes/version.php | 2 +- 9 files changed, 722 insertions(+), 129 deletions(-) diff --git a/wp-admin/includes/image.php b/wp-admin/includes/image.php index 1a9c9e2e9c..98689672b3 100644 --- a/wp-admin/includes/image.php +++ b/wp-admin/includes/image.php @@ -77,16 +77,23 @@ function wp_crop_image( $src, $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $s * Registered sub-sizes that are larger than the image are skipped. * * @since 5.3.0 + * @since 6.1.0 The $mime_type parameter was added. * - * @param int $attachment_id The image attachment post ID. + * @param int $attachment_id The image attachment post ID. + * @param string $mime_type Optional. The mime type to check for missing sizes. Default is the primary image mime. * @return array[] Associative array of arrays of image sub-size information for * missing image sizes, keyed by image size name. */ -function wp_get_missing_image_subsizes( $attachment_id ) { +function wp_get_missing_image_subsizes( $attachment_id, $mime_type = '' ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return array(); } + $primary_mime_type = get_post_mime_type( get_post( $attachment_id ) ); + if ( ! $mime_type ) { + $mime_type = $primary_mime_type; + } + $registered_sizes = wp_get_registered_image_subsizes(); $image_meta = wp_get_attachment_metadata( $attachment_id ); @@ -129,19 +136,38 @@ function wp_get_missing_image_subsizes( $attachment_id ) { * However we keep the old sub-sizes with the previous dimensions * as the image may have been used in an older post. */ - $missing_sizes = array_diff_key( $possible_sizes, $image_meta['sizes'] ); + $missing_sizes = array(); + foreach ( $possible_sizes as $size_name => $size_data ) { + if ( ! isset( $image_meta['sizes'][ $size_name ] ) ) { + $missing_sizes[ $size_name ] = $size_data; + continue; + } + + if ( ( isset( $size_data['mime-type'] ) && $size_data['mime-type'] === $mime_type ) || isset( $size_data['sources'][ $mime_type ] ) ) { + continue; + } + + $missing_sizes[ $size_name ] = $size_data; + } + + // Filter secondary mime types to those sizes that are enabled. + if ( $primary_mime_type !== $mime_type ) { + $missing_sizes = _wp_filter_image_sizes_additional_mime_type_support( $missing_sizes, $attachment_id ); + } /** * Filters the array of missing image sub-sizes for an uploaded image. * * @since 5.3.0 + * @since 6.1.0 The $mime_type filter parameter was added. * * @param array[] $missing_sizes Associative array of arrays of image sub-size information for * missing image sizes, keyed by image size name. * @param array $image_meta The image meta data. * @param int $attachment_id The image attachment post ID. + * @param string $mime_type The image mime type to get missing sizes for. */ - return apply_filters( 'wp_get_missing_image_subsizes', $missing_sizes, $image_meta, $attachment_id ); + return apply_filters( 'wp_get_missing_image_subsizes', $missing_sizes, $image_meta, $attachment_id, $mime_type ); } /** @@ -149,6 +175,7 @@ function wp_get_missing_image_subsizes( $attachment_id ) { * create them and update the image meta data. * * @since 5.3.0 + * @since 6.1.0 Now supports additional mime types, creating the additional sub-sizes and 'full' sized images. * * @param int $attachment_id The image attachment post ID. * @return array|WP_Error The updated image meta data array or WP_Error object @@ -167,14 +194,33 @@ function wp_update_image_subsizes( $attachment_id ) { return new WP_Error( 'invalid_attachment', __( 'The attached file cannot be found.' ) ); } } else { - $missing_sizes = wp_get_missing_image_subsizes( $attachment_id ); + // Get the primary and additional mime types to generate. + list( $primary_mime_type, $additional_mime_types ) = _wp_get_primary_and_additional_mime_types( $image_file, $attachment_id ); - if ( empty( $missing_sizes ) ) { - return $image_meta; + // Generate missing 'full' image files for additional mime types. + if ( ! empty( $additional_mime_types ) ) { + if ( isset( $image_meta['sources'] ) ) { + $missing_mime_types = array_diff( $additional_mime_types, array_keys( $image_meta['sources'] ) ); + } else { + $missing_mime_types = $additional_mime_types; + } + if ( ! empty( $missing_mime_types ) ) { + $image_meta = _wp_make_additional_mime_types( $missing_mime_types, $image_file, $image_meta, $attachment_id ); + } } - // This also updates the image meta. - $image_meta = _wp_make_subsizes( $missing_sizes, $image_file, $image_meta, $attachment_id ); + // Generate missing image sub-sizes for each mime type. + $all_mime_types = array_merge( array( $primary_mime_type ), $additional_mime_types ); + foreach ( $all_mime_types as $mime_type ) { + $missing_sizes = wp_get_missing_image_subsizes( $attachment_id, $mime_type ); + + if ( empty( $missing_sizes ) ) { + continue; + } + + // This also updates the image meta. + $image_meta = _wp_make_subsizes( $missing_sizes, $image_file, $image_meta, $attachment_id, $mime_type ); + } } /** This filter is documented in wp-admin/includes/image.php */ @@ -222,12 +268,13 @@ function _wp_image_meta_replace_original( $saved_data, $original_file, $image_me } /** - * Creates image sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata. + * Creates image mime variations and sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata. * * Intended for use after an image is uploaded. Saves/updates the image metadata after each * sub-size is created. If there was an error, it is added to the returned image metadata array. * * @since 5.3.0 + * @since 6.1.0 Generates sub-sizes in alternate mime types based on the `wp_image_mime_transforms` filter. * * @param string $file Full path to the image file. * @param int $attachment_id Attachment ID to process. @@ -248,6 +295,7 @@ function wp_create_image_subsizes( $file, $attachment_id ) { 'file' => _wp_relative_upload_path( $file ), 'filesize' => wp_filesize( $file ), 'sizes' => array(), + 'sources' => array(), ); // Fetch additional metadata from EXIF/IPTC. @@ -257,9 +305,112 @@ function wp_create_image_subsizes( $file, $attachment_id ) { $image_meta['image_meta'] = $exif_meta; } - // Do not scale (large) PNG images. May result in sub-sizes that have greater file size than the original. See #48736. - if ( 'image/png' !== $imagesize['mime'] ) { + // Get the primary and additional mime types to generate. + list( $primary_mime_type, $additional_mime_types ) = _wp_get_primary_and_additional_mime_types( $file, $attachment_id ); + list( $editor, $resized, $rotated ) = _wp_maybe_scale_and_rotate_image( $file, $attachment_id, $imagesize, $exif_meta, $primary_mime_type ); + if ( is_wp_error( $editor ) ) { + return $image_meta; + } + $suffix = _wp_get_image_suffix( $resized, $rotated ); + + // Save image only if either it was modified or if the primary mime type is different from the original. + if ( ! empty( $suffix ) || $primary_mime_type !== $imagesize['mime'] ) { + $saved = $editor->save( $editor->generate_filename( $suffix ) ); + + if ( ! is_wp_error( $saved ) ) { + $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id ); + + // If the image was rotated update the stored EXIF data. + if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) { + $image_meta['image_meta']['orientation'] = 1; + } + } else { + // TODO: Log errors. + } + } + + // Set 'sources' for the primary mime type. + $image_meta['sources'][ $primary_mime_type ] = _wp_get_sources_from_meta( $image_meta ); + + /* + * Initial save of the new metadata. + * At this point the file was uploaded and moved to the uploads directory + * but the image sub-sizes haven't been created yet and the `sizes` array is empty. + */ + wp_update_attachment_metadata( $attachment_id, $image_meta ); + + if ( ! empty( $additional_mime_types ) ) { + // Use the original file's exif_meta orientation information for secondary mime generation. + $saved_orientation = $image_meta['image_meta']['orientation']; + $image_meta['image_meta']['orientation'] = $exif_meta['orientation']; + $image_meta = _wp_make_additional_mime_types( $additional_mime_types, $file, $image_meta, $attachment_id ); + $image_meta['image_meta']['orientation'] = $saved_orientation; + + } + + $new_sizes = wp_get_registered_image_subsizes(); + + /** + * Filters the image sizes automatically generated when uploading an image. + * + * @since 2.9.0 + * @since 4.4.0 Added the `$image_meta` argument. + * @since 5.3.0 Added the `$attachment_id` argument. + * + * @param array $new_sizes Associative array of image sizes to be created. + * @param array $image_meta The image meta data: width, height, file, sizes, etc. + * @param int $attachment_id The attachment post ID for the image. + */ + $new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $image_meta, $attachment_id ); + + $image_meta = _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id, $primary_mime_type ); + + // Filter secondary mime types to those sizes that are enabled. + $new_sizes = _wp_filter_image_sizes_additional_mime_type_support( $new_sizes, $attachment_id ); + + foreach ( $additional_mime_types as $additional_mime_type ) { + $image_meta = _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id, $additional_mime_type ); + } + + return $image_meta; +} + +/** + * Returns a WP_Image_Editor instance where the image file has been scaled and rotated as necessary. + * + * @since 6.1.0 + * @access private + * + * @param string $file Full path to the image file. + * @param int $attachment_id Attachment ID. + * @param array $imagesize { + * Indexed array of the image width and height in pixels. + * + * @type int $0 The image width. + * @type int $1 The image height. + * } + * @param array|null $exif_meta EXIF metadata if extracted from the image file. + * @param string $mime_type Output mime type. + * @return array Array with three entries: The WP_Image_Editor instance, whether the image was resized, and whether the + * image was rotated (booleans). Each entry can alternatively be a WP_Error in case something went wrong. + */ +function _wp_maybe_scale_and_rotate_image( $file, $attachment_id, $imagesize, $exif_meta, $mime_type ) { + $resized = false; + $rotated = false; + + $editor = wp_get_image_editor( $file, array( 'mime_type' => $mime_type ) ); + if ( is_wp_error( $editor ) ) { + // This image cannot be edited. + return array( $editor, $resized, $rotated ); + } + + if ( ! empty( $mime_type ) ) { + $editor->set_output_mime_type( $mime_type ); + } + + // Do not scale (large) PNG images. May result in sub-sizes that have greater file size than the original. See #48736. + if ( 'image/png' !== $mime_type ) { /** * Filters the "BIG image" threshold value. * @@ -285,96 +436,65 @@ function wp_create_image_subsizes( $file, $attachment_id ) { // If the original image's dimensions are over the threshold, // scale the image and use it as the "full" size. - if ( $threshold && ( $image_meta['width'] > $threshold || $image_meta['height'] > $threshold ) ) { - $editor = wp_get_image_editor( $file ); - - if ( is_wp_error( $editor ) ) { - // This image cannot be edited. - return $image_meta; - } - + if ( $threshold && ( $imagesize[0] > $threshold || $imagesize[1] > $threshold ) ) { // Resize the image. $resized = $editor->resize( $threshold, $threshold ); - $rotated = null; // If there is EXIF data, rotate according to EXIF Orientation. if ( ! is_wp_error( $resized ) && is_array( $exif_meta ) ) { - $resized = $editor->maybe_exif_rotate(); - $rotated = $resized; - } - - if ( ! is_wp_error( $resized ) ) { - // Append "-scaled" to the image file name. It will look like "my_image-scaled.jpg". - // This doesn't affect the sub-sizes names as they are generated from the original image (for best quality). - $saved = $editor->save( $editor->generate_filename( 'scaled' ) ); - - if ( ! is_wp_error( $saved ) ) { - $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id ); - - // If the image was rotated update the stored EXIF data. - if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) { - $image_meta['image_meta']['orientation'] = 1; - } - } else { - // TODO: Log errors. - } - } else { - // TODO: Log errors. + $rotated = $editor->maybe_exif_rotate(); } } elseif ( ! empty( $exif_meta['orientation'] ) && 1 !== (int) $exif_meta['orientation'] ) { // Rotate the whole original image if there is EXIF data and "orientation" is not 1. - - $editor = wp_get_image_editor( $file ); - - if ( is_wp_error( $editor ) ) { - // This image cannot be edited. - return $image_meta; - } - - // Rotate the image. $rotated = $editor->maybe_exif_rotate(); - - if ( true === $rotated ) { - // Append `-rotated` to the image file name. - $saved = $editor->save( $editor->generate_filename( 'rotated' ) ); - - if ( ! is_wp_error( $saved ) ) { - $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id ); - - // Update the stored EXIF data. - if ( ! empty( $image_meta['image_meta']['orientation'] ) ) { - $image_meta['image_meta']['orientation'] = 1; - } - } else { - // TODO: Log errors. - } - } } } - /* - * Initial save of the new metadata. - * At this point the file was uploaded and moved to the uploads directory - * but the image sub-sizes haven't been created yet and the `sizes` array is empty. - */ - wp_update_attachment_metadata( $attachment_id, $image_meta ); + return array( $editor, $resized, $rotated ); +} - $new_sizes = wp_get_registered_image_subsizes(); +/** + * Gets the suffix to use for image files based on resizing and rotating. + * + * @since 6.1.0 + * @access private + * + * @param bool|WP_Error Whether the image was resized, or an error if resizing failed. + * @param bool|WP_Error Whether the image was rotated, or an error if rotating failed. + * @return string The suffix to use for the file name, or empty string if none. + */ +function _wp_get_image_suffix( $resized, $rotated ) { + if ( $resized && ! is_wp_error( $resized ) ) { + // Append "-scaled" to the image file name. It will look like "my_image-scaled.jpg". + // This doesn't affect the sub-sizes names as they are generated from the original image (for best quality). + return 'scaled'; + } - /** - * Filters the image sizes automatically generated when uploading an image. - * - * @since 2.9.0 - * @since 4.4.0 Added the `$image_meta` argument. - * @since 5.3.0 Added the `$attachment_id` argument. - * - * @param array $new_sizes Associative array of image sizes to be created. - * @param array $image_meta The image meta data: width, height, file, sizes, etc. - * @param int $attachment_id The attachment post ID for the image. - */ - $new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $image_meta, $attachment_id ); + if ( true === $rotated ) { + // Append `-rotated` to the image file name. + return 'rotated'; + } - return _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ); + if ( is_wp_error( $resized ) || is_wp_error( $rotated ) ) { + // TODO: Log errors. + } + return ''; +} + +/** + * Gets a sources array element from a meta. + * + * @since 6.1.0 + * @access private + * + * @param array $meta The meta to get the source from. + * @return array The source array element. + */ +function _wp_get_sources_from_meta( $meta ) { + return array( + 'file' => isset( $meta['file'] ) ? wp_basename( $meta['file'] ) : '', + 'filesize' => isset( $meta['filesize'] ) ? $meta['filesize'] : wp_filesize( $meta['path'] ), + ); } /** @@ -384,20 +504,26 @@ function wp_create_image_subsizes( $file, $attachment_id ) { * Errors are stored in the returned image metadata array. * * @since 5.3.0 + * @since 6.1.0 The $mime_type parameter was added. * @access private * - * @param array $new_sizes Array defining what sizes to create. - * @param string $file Full path to the image file. - * @param array $image_meta The attachment meta data array. - * @param int $attachment_id Attachment ID to process. + * @param array $new_sizes Array defining what sizes to create. + * @param string $file Full path to the image file. + * @param array $image_meta The attachment meta data array. + * @param int $attachment_id Attachment ID to process. + * @param string $mime_type Optional. The mime type to check for missing sizes. Default is the image mime of $file. * @return array The attachment meta data with updated `sizes` array. Includes an array of errors encountered while resizing. */ -function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { +function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id, $mime_type = '' ) { if ( empty( $image_meta ) || ! is_array( $image_meta ) ) { // Not an image attachment. return array(); } + if ( ! $mime_type ) { + $mime_type = wp_get_image_mime( $file ); + } + // Check if any of the new sizes already exist. if ( isset( $image_meta['sizes'] ) && is_array( $image_meta['sizes'] ) ) { foreach ( $image_meta['sizes'] as $size_name => $size_meta ) { @@ -407,7 +533,11 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { * To change the behavior, unset changed/mismatched sizes in the `sizes` array in image meta. */ if ( array_key_exists( $size_name, $new_sizes ) ) { - unset( $new_sizes[ $size_name ] ); + // Unset the size if it is either the required mime type already exists either as main mime type or + // within sources. + if ( $size_meta['mime-type'] === $mime_type || isset( $size_meta['sources'][ $mime_type ] ) ) { + unset( $new_sizes[ $size_name ] ); + } } } } else { @@ -433,13 +563,15 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { $new_sizes = array_filter( array_merge( $priority, $new_sizes ) ); - $editor = wp_get_image_editor( $file ); + $editor = wp_get_image_editor( $file, array( 'mime_type' => $mime_type ) ); if ( is_wp_error( $editor ) ) { // The image cannot be edited. return $image_meta; } + $editor->set_output_mime_type( $mime_type ); + // If stored EXIF data exists, rotate the source image before creating sub-sizes. if ( ! empty( $image_meta['image_meta'] ) ) { $rotated = $editor->maybe_exif_rotate(); @@ -457,7 +589,22 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { // TODO: Log errors. } else { // Save the size meta value. - $image_meta['sizes'][ $new_size_name ] = $new_size_meta; + if ( ! isset( $image_meta['sizes'][ $new_size_name ] ) ) { + $image_meta['sizes'][ $new_size_name ] = $new_size_meta; + } else { + // Remove any newly generated images that are larger than the primary mime type. + $new_size = isset( $new_size_meta['filesize'] ) ? $new_size_meta['filesize'] : 0; + $primary_size = isset( $image_meta['sizes'][ $new_size_name ]['filesize'] ) ? $image_meta['sizes'][ $new_size_name ]['filesize'] : 0; + + if ( $new_size && $primary_size && $new_size >= $primary_size ) { + wp_delete_file( dirname( $file ) . '/' . $new_size_meta['file'] ); + continue; + } + } + if ( ! isset( $image_meta['sizes'][ $new_size_name ]['sources'] ) ) { + $image_meta['sizes'][ $new_size_name ]['sources'] = array(); + } + $image_meta['sizes'][ $new_size_name ]['sources'][ $mime_type ] = _wp_get_sources_from_meta( $new_size_meta ); wp_update_attachment_metadata( $attachment_id, $image_meta ); } } @@ -466,7 +613,26 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { $created_sizes = $editor->multi_resize( $new_sizes ); if ( ! empty( $created_sizes ) ) { - $image_meta['sizes'] = array_merge( $image_meta['sizes'], $created_sizes ); + foreach ( $created_sizes as $created_size_name => $created_size_meta ) { + + // Primary mime type is set in 'sizes' array. + if ( ! isset( $image_meta['sizes'][ $created_size_name ] ) ) { + $image_meta['sizes'][ $created_size_name ] = $created_size_meta; + } else { + // Remove any newly generated images that are larger than the primary mime type. + $new_size = isset( $created_size_meta['filesize'] ) ? $created_size_meta['filesize'] : 0; + $primary_size = isset( $image_meta['sizes'][ $created_size_name ]['filesize'] ) ? $image_meta['sizes'][ $created_size_name ]['filesize'] : 0; + + if ( $new_size && $primary_size && $new_size >= $primary_size ) { + wp_delete_file( dirname( $file ) . '/' . $created_size_meta['file'] ); + continue; + } + } + if ( ! isset( $image_meta['sizes'][ $created_size_name ]['sources'] ) ) { + $image_meta['sizes'][ $created_size_name ]['sources'] = array(); + } + $image_meta['sizes'][ $created_size_name ]['sources'][ $mime_type ] = _wp_get_sources_from_meta( $new_size_meta ); + } wp_update_attachment_metadata( $attachment_id, $image_meta ); } } @@ -474,6 +640,125 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) { return $image_meta; } +/** + * Filters the list of image size objects that support secondary mime type output. + * + * @since 6.1.0 + * + * @param array $sizes Associative array of image sizes. + * @param int $attachment_id Attachment ID. + * @return array $sizes Filtered $sizes with only those that support secondary mime type output. + */ +function _wp_filter_image_sizes_additional_mime_type_support( $sizes, $attachment_id ) { + + // Include only the core sizes that do not rely on add_image_size(). Additional image sizes are opt-in. + $enabled_sizes = array( + 'thumbnail' => true, + 'medium' => true, + 'medium_large' => true, + 'large' => true, + 'post-thumbnail' => true, + ); + + /** + * Filter the sizes that support secondary mime type output. Developers can use this + * to control the output of additional mime type sub-sized images. + * + * @since 6.1.0 + * + * @param array $enabled_sizes Map of size names and whether they support secondary mime type output. + * @param int $attachment_id Attachment ID. + */ + $enabled_sizes = apply_filters( 'wp_image_sizes_with_additional_mime_type_support', $enabled_sizes, $attachment_id ); + + // Filter supported sizes to only include enabled sizes. + return array_intersect_key( $sizes, array_filter( $enabled_sizes ) ); +} + +/** + * Low-level function to create full-size images in additional mime types. + * + * Updates the image meta after each mime type image is created. + * + * @since 6.1.0 + * @access private + * + * @param array $new_mime_types Array defining what mime types to create. + * @param string $file Full path to the image file. + * @param array $image_meta The attachment meta data array. + * @param int $attachment_id Attachment ID to process. + * @return array The attachment meta data with updated `sizes` array. Includes an array of errors encountered while resizing. + */ +function _wp_make_additional_mime_types( $new_mime_types, $file, $image_meta, $attachment_id ) { + $imagesize = array( + $image_meta['width'], + $image_meta['height'], + ); + $exif_meta = isset( $image_meta['image_meta'] ) ? $image_meta['image_meta'] : null; + $original_file_size = isset( $image_meta['filesize'] ) ? $image_meta['filesize'] : wp_filesize( $file ); + + foreach ( $new_mime_types as $mime_type ) { + list( $editor, $resized, $rotated ) = _wp_maybe_scale_and_rotate_image( $file, $attachment_id, $imagesize, $exif_meta, $mime_type ); + if ( is_wp_error( $editor ) ) { + // The image cannot be edited. + continue; + } + + $suffix = _wp_get_image_suffix( $resized, $rotated ); + $extension = wp_get_default_extension_for_mime_type( $mime_type ); + + $saved = $editor->save( $editor->generate_filename( $suffix, null, $extension ) ); + + if ( is_wp_error( $saved ) ) { + // TODO: Log errors. + } else { + // If the saved image is larger than the original, discard it. + $filesize = isset( $saved['filesize'] ) ? $saved['filesize'] : wp_filesize( $saved['path'] ); + if ( $filesize && $original_file_size && $filesize > $original_file_size ) { + wp_delete_file( $saved['path'] ); + continue; + } + $image_meta['sources'][ $mime_type ] = _wp_get_sources_from_meta( $saved ); + wp_update_attachment_metadata( $attachment_id, $image_meta ); + } + } + + return $image_meta; +} + + +/** + * Check if an image belongs to an attachment. + * + * @since 6.1.0 + * @access private + * + * @param string $filename Full path to the image file. + * @param int $attachment_id Attachment ID to check. + * @return bool True if the image belongs to the attachment, false otherwise. + */ +function _wp_image_belongs_to_attachment( $filename, $attachment_id ) { + $meta_data = wp_get_attachment_metadata( $attachment_id ); + + if ( ! isset( $image_meta['sizes'] ) ) { + return false; + } + $sizes = $image_meta['sizes']; + foreach ( $sizes as $size ) { + if ( $size['file'] === $filename ) { + return true; + } + if ( isset( $size['sources'] ) && is_array( $size['sources'] ) ) { + foreach ( $size['sources'] as $source ) { + if ( $source['file'] === $filename ) { + return true; + } + } + } + } + return false; +} + /** * Generate attachment meta data and create image sub-sizes for images. * @@ -630,7 +915,7 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) { wp_update_attachment_metadata( $attachment_id, $metadata ); // Create sub-sizes saving the image meta after each. - $metadata = _wp_make_subsizes( $merged_sizes, $image_file, $metadata, $attachment_id ); + $metadata = _wp_make_subsizes( $merged_sizes, $image_file, $metadata, $attachment_id, '' ); } } } @@ -1157,3 +1442,97 @@ function _copy_image_file( $attachment_id ) { return $dst_file; } + +/** + * Returns an array with the list of valid mime types that a specific mime type should be converted into. + * For example an `image/jpeg` should be converted into an `image/jpeg` and `image/webp`. The first type + * is considered the primary output type for this image. + * + * Called for each uploaded image to determine the list of mime types that should be converted into. Then, + * called again for each image size as they are generated to check if the image should be converted into the mime type + * for that size. + * + * @since 6.1.0 + * + * @param int $attachment_id The attachment ID. + * @return array An array of valid mime types, where the key is the source file mime type and the list of mime types to + * generate. + */ +function wp_upload_image_mime_transforms( $attachment_id ) { + $default_image_mime_transforms = array( + 'image/jpeg' => array( 'image/jpeg', 'image/webp' ), + 'image/webp' => array( 'image/webp', 'image/jpeg' ), + ); + $image_mime_transforms = $default_image_mime_transforms; + + /** + * Filter the output mime types for a given input mime type and image size. + * + * @since 6.1.0 + * + * @param array $image_mime_transforms A map with the valid mime transforms where the key is the source file mime type + * and the value is one or more mime file types to generate. + * @param int $attachment_id The ID of the attachment where the hook was dispatched. + */ + $image_mime_transforms = apply_filters( 'wp_upload_image_mime_transforms', $image_mime_transforms, $attachment_id ); + + if ( ! is_array( $image_mime_transforms ) ) { + return $default_image_mime_transforms; + } + + return array_map( + function( $transforms_list ) { + return (array) $transforms_list; + }, + $image_mime_transforms + ); +} + +/** + * Extract the primary and additional mime output types for an image from the $image_mime_transforms. + * + * @since 6.1.0 + * @access private + * + * @param string $file Full path to the image file. + * @param int $attachment_id Attachment ID to process. + * @return array An array with two entries, the primary mime type and the list of additional mime types. + */ +function _wp_get_primary_and_additional_mime_types( $file, $attachment_id ) { + $image_mime_transforms = wp_upload_image_mime_transforms( $attachment_id ); + $original_mime_type = wp_get_image_mime( $file ); + $output_mime_types = isset( $image_mime_transforms[ $original_mime_type ] ) ? $image_mime_transforms[ $original_mime_type ] : array( $original_mime_type ); + + // Exclude any output mime types that the system doesn't support. + $output_mime_types = array_values( + array_filter( + $output_mime_types, + function( $mime_type ) { + return wp_image_editor_supports( + array( + 'mime_type' => $mime_type, + ) + ); + } + ) + ); + + // Handle an empty value for $output_mime_types: only output the original type. + if ( empty( $output_mime_types ) ) { + return array( $original_mime_type, array() ); + } + + // Use original mime type as primary mime type, or alternatively the first one. + $primary_mime_type_key = array_search( $original_mime_type, $output_mime_types, true ); + if ( false === $primary_mime_type_key ) { + $primary_mime_type_key = 0; + } + // Split output mime types into primary mime type and additional mime types. + $additional_mime_types = $output_mime_types; + list( $primary_mime_type ) = array_splice( $additional_mime_types, $primary_mime_type_key, 1 ); + + return array( + $primary_mime_type, + $additional_mime_types, + ); +} diff --git a/wp-includes/class-wp-image-editor.php b/wp-includes/class-wp-image-editor.php index caa3092d36..ccf43402a6 100644 --- a/wp-includes/class-wp-image-editor.php +++ b/wp-includes/class-wp-image-editor.php @@ -334,6 +334,11 @@ abstract class WP_Image_Editor { protected function get_output_format( $filename = null, $mime_type = null ) { $new_ext = null; + // If no mime type is passed but output mime type is set, use that. + if ( ! $mime_type && ! empty( $this->output_mime_type ) ) { + $mime_type = $this->output_mime_type; + } + // By default, assume specified type takes priority. if ( $mime_type ) { $new_ext = $this->get_extension( $mime_type ); @@ -425,18 +430,25 @@ abstract class WP_Image_Editor { } /** - * Builds an output filename based on current file, and adding proper suffix + * Builds an output filename based on current file, and adding proper suffix. * * @since 3.5.0 + * @since 6.1.0 Skips adding a suffix when set to an empty string. When the + * file extension being generated doesn't match the image file extension, + * add the extension to the suffix * - * @param string $suffix - * @param string $dest_path - * @param string $extension - * @return string filename + * @param string $suffix Optional. Suffix to add to the filename. The default null + * will result in a 'widthxheight' suffix. Passing + * an empty string will result in no suffix. + * @param string $dest_path Optional. The path to save the file to. The default null + * will use the image file path. + * @param string $extension Optional. The file extension to use. The default null + * will use the image file extension. + * @return string filename The generated file name. */ public function generate_filename( $suffix = null, $dest_path = null, $extension = null ) { // $suffix will be appended to the destination filename, just before the extension. - if ( ! $suffix ) { + if ( null === $suffix ) { $suffix = $this->get_suffix(); } @@ -457,7 +469,21 @@ abstract class WP_Image_Editor { } } - return trailingslashit( $dir ) . "{$name}-{$suffix}.{$new_ext}"; + if ( empty( $suffix ) ) { + $suffix = ''; + } else { + $suffix = "-{$suffix}"; + } + + // When the file extension being generated doesn't match the image file extension, + // add the extension to the suffix to ensure a unique file name. Prevents + // name conflicts when a single image type can have multiple extensions, + // eg. .jpg, .jpeg and .jpe are all valid JPEG extensions. + if ( ! empty( $extension ) && $extension !== $ext ) { + $suffix .= "-{$ext}"; + } + + return trailingslashit( $dir ) . "{$name}{$suffix}.{$new_ext}"; } /** @@ -637,5 +663,28 @@ abstract class WP_Image_Editor { return wp_get_default_extension_for_mime_type( $mime_type ); } -} + /** + * Set the editor output mime type, useful when outputting alternate mime types. + * + * Track that the mime type is set with the mime type set flag. + * + * @since 6.1.0 + * + * @param string $output_mime_type The mime type to set. + */ + public function set_output_mime_type( $output_mime_type ) { + $this->output_mime_type = $output_mime_type; + } + + /** + * Reset the mime type to the original file mime type. + * + * Reset the mime type set flag. + * + * @since 6.1.0 + */ + public function reset_output_mime_type() { + $this->output_mime_type = $this->mime_type; + } +} diff --git a/wp-includes/js/plupload/handlers.js b/wp-includes/js/plupload/handlers.js index fa602daf43..c7e3c253d3 100644 --- a/wp-includes/js/plupload/handlers.js +++ b/wp-includes/js/plupload/handlers.js @@ -486,7 +486,7 @@ jQuery( document ).ready( function( $ ) { times = tryAgainCount[ file.id ]; - if ( times && times > 4 ) { + if ( times && times > 8 ) { /* * The file may have been uploaded and attachment post created, * but post-processing and resizing failed... diff --git a/wp-includes/js/plupload/handlers.min.js b/wp-includes/js/plupload/handlers.min.js index 51c4032de4..a5ac72d1bc 100644 --- a/wp-includes/js/plupload/handlers.min.js +++ b/wp-includes/js/plupload/handlers.min.js @@ -1 +1 @@ -var uploader,uploader_init,topWin=window.dialogArguments||opener||parent||top;function fileQueued(e){jQuery(".media-blank").remove();var a=jQuery("#media-items").children(),r=post_id||0;1==a.length&&a.removeClass("open").find(".slidetoggle").slideUp(200),jQuery('
').attr("id","media-item-"+e.id).addClass("child-of-"+r).append('
0%
',jQuery('
').text(" "+e.name)).appendTo(jQuery("#media-items")),jQuery("#insert-gallery").prop("disabled",!0)}function uploadStart(){try{void 0!==topWin.tb_remove&&topWin.jQuery("#TB_overlay").unbind("click",topWin.tb_remove)}catch(e){}return!0}function uploadProgress(e,a){var r=jQuery("#media-item-"+a.id);jQuery(".bar",r).width(200*a.loaded/a.size),jQuery(".percent",r).html(a.percent+"%")}function fileUploading(e,a){var r=104857600;rr&&setTimeout(function(){a.status<3&&0===a.loaded&&(wpFileError(a,pluploadL10n.big_upload_failed.replace("%1$s",'').replace("%2$s","")),e.stop(),e.removeFile(a),e.start())},1e4)}function updateMediaForm(){var e=jQuery("#media-items").children();1==e.length?(e.addClass("open").find(".slidetoggle").show(),jQuery(".insert-gallery").hide()):1(\d+)<\/pre>$/,"$1"),/media-upload-error|error-div/.test(a))?r.html(a):(r.find(".percent").html(pluploadL10n.crunching),prepareMediaItem(e,a),updateMediaForm(),post_id&&r.hasClass("child-of-"+post_id)&&jQuery("#attachments-count").text(+jQuery("#attachments-count").text()+1))}function setResize(e){e?window.resize_width&&window.resize_height?uploader.settings.resize={enabled:!0,width:window.resize_width,height:window.resize_height,quality:100}:uploader.settings.multipart_params.image_resize=!0:delete uploader.settings.multipart_params.image_resize}function prepareMediaItem(e,a){var r="undefined"==typeof shortform?1:2,i=jQuery("#media-item-"+e.id);2==r&&2

'+e+"

")}function wpFileError(e,a){itemAjaxError(e.id,a)}function itemAjaxError(e,a){var r=jQuery("#media-item-"+e),i=r.find(".filename").text();r.data("last-err")!=e&&r.html('
'+pluploadL10n.dismiss+""+pluploadL10n.error_uploading.replace("%s",jQuery.trim(i))+" "+a+"
").data("last-err",e)}function deleteSuccess(e){var a;return"-1"==e?itemAjaxError(this.id,"You do not have permission. Has your session expired?"):"0"==e?itemAjaxError(this.id,"Could not be deleted. Has it been deleted already?"):(e=this.id,a=jQuery("#media-item-"+e),(e=jQuery("#type-of-"+e).val())&&jQuery("#"+e+"-counter").text(jQuery("#"+e+"-counter").text()-1),post_id&&a.hasClass("child-of-"+post_id)&&jQuery("#attachments-count").text(jQuery("#attachments-count").text()-1),1==jQuery("form.type-form #media-items").children().length&&0 '+pluploadL10n.deleted+" ").siblings("a.toggle").hide(),jQuery(".filename",a).append(jQuery("a.undo",a).removeClass("hidden")),void jQuery(".menu_order_input",a).hide())}function deleteError(){}function uploadComplete(){jQuery("#insert-gallery").prop("disabled",!1)}function switchUploader(e){e?(deleteUserSetting("uploader"),jQuery(".media-upload-form").removeClass("html-uploader"),"object"==typeof uploader&&uploader.refresh()):(setUserSetting("uploader","1"),jQuery(".media-upload-form").addClass("html-uploader"))}function uploadError(e,a,r,i){var t=104857600;switch(a){case plupload.FAILED:wpFileError(e,pluploadL10n.upload_failed);break;case plupload.FILE_EXTENSION_ERROR:wpFileExtensionError(i,e,pluploadL10n.invalid_filetype);break;case plupload.FILE_SIZE_ERROR:uploadSizeError(i,e);break;case plupload.IMAGE_FORMAT_ERROR:wpFileError(e,pluploadL10n.not_an_image);break;case plupload.IMAGE_MEMORY_ERROR:wpFileError(e,pluploadL10n.image_memory_exceeded);break;case plupload.IMAGE_DIMENSIONS_ERROR:wpFileError(e,pluploadL10n.image_dimensions_exceeded);break;case plupload.GENERIC_ERROR:wpQueueError(pluploadL10n.upload_failed);break;case plupload.IO_ERROR:tt?wpFileError(e,pluploadL10n.big_upload_failed.replace("%1$s",'').replace("%2$s","")):wpQueueError(pluploadL10n.io_error);break;case plupload.HTTP_ERROR:wpQueueError(pluploadL10n.http_error);break;case plupload.INIT_ERROR:jQuery(".media-upload-form").addClass("html-uploader");break;case plupload.SECURITY_ERROR:wpQueueError(pluploadL10n.security_error);break;default:wpFileError(e,pluploadL10n.default_error)}}function uploadSizeError(e,a){var r=pluploadL10n.file_exceeds_size_limit.replace("%s",a.name),r=jQuery("
").attr({id:"media-item-"+a.id,class:"media-item error"}).append(jQuery("

").text(r));jQuery("#media-items").append(r),e.removeFile(a)}function wpFileExtensionError(e,a,r){jQuery("#media-items").append('

'+r+"

"),e.removeFile(a)}function copyAttachmentUploadURLClipboard(){var i;new ClipboardJS(".copy-attachment-url").on("success",function(e){var a=jQuery(e.trigger),r=jQuery(".success",a.closest(".copy-to-clipboard-container"));e.clearSelection(),a.trigger("focus"),clearTimeout(i),r.removeClass("hidden"),i=setTimeout(function(){r.addClass("hidden")},3e3),wp.a11y.speak(pluploadL10n.file_url_copied)})}jQuery(document).ready(function(o){copyAttachmentUploadURLClipboard();var d,l={};o(".media-upload-form").bind("click.uploader",function(e){var a,r=o(e.target);r.is('input[type="radio"]')?(a=r.closest("tr")).hasClass("align")?setUserSetting("align",r.val()):a.hasClass("image-size")&&setUserSetting("imgsize",r.val()):r.is("button.button")?(a=(a=e.target.className||"").match(/url([^ '"]+)/))&&a[1]&&(setUserSetting("urlbutton",a[1]),r.siblings(".urlfield").val(r.data("link-url"))):r.is("a.dismiss")?r.parents(".media-item").fadeOut(200,function(){o(this).remove()}):r.is(".upload-flash-bypass a")||r.is("a.uploader-html")?(o("#media-items, p.submit, span.big-file-warning").css("display","none"),switchUploader(0),e.preventDefault()):r.is(".upload-html-bypass a")?(o("#media-items, p.submit, span.big-file-warning").css("display",""),switchUploader(1),e.preventDefault()):r.is("a.describe-toggle-on")?(r.parent().addClass("open"),r.siblings(".slidetoggle").fadeIn(250,function(){var e=o(window).scrollTop(),a=o(window).height(),r=o(this).offset().top,i=o(this).height();a&&r&&i&&(a=e+a)<(i=r+i)&&(i-a').attr("id","media-item-"+e.id).addClass("child-of-"+r).append('
0%
',jQuery('
').text(" "+e.name)).appendTo(jQuery("#media-items")),jQuery("#insert-gallery").prop("disabled",!0)}function uploadStart(){try{void 0!==topWin.tb_remove&&topWin.jQuery("#TB_overlay").unbind("click",topWin.tb_remove)}catch(e){}return!0}function uploadProgress(e,a){var r=jQuery("#media-item-"+a.id);jQuery(".bar",r).width(200*a.loaded/a.size),jQuery(".percent",r).html(a.percent+"%")}function fileUploading(e,a){var r=104857600;rr&&setTimeout(function(){a.status<3&&0===a.loaded&&(wpFileError(a,pluploadL10n.big_upload_failed.replace("%1$s",'').replace("%2$s","")),e.stop(),e.removeFile(a),e.start())},1e4)}function updateMediaForm(){var e=jQuery("#media-items").children();1==e.length?(e.addClass("open").find(".slidetoggle").show(),jQuery(".insert-gallery").hide()):1(\d+)<\/pre>$/,"$1"),/media-upload-error|error-div/.test(a))?r.html(a):(r.find(".percent").html(pluploadL10n.crunching),prepareMediaItem(e,a),updateMediaForm(),post_id&&r.hasClass("child-of-"+post_id)&&jQuery("#attachments-count").text(+jQuery("#attachments-count").text()+1))}function setResize(e){e?window.resize_width&&window.resize_height?uploader.settings.resize={enabled:!0,width:window.resize_width,height:window.resize_height,quality:100}:uploader.settings.multipart_params.image_resize=!0:delete uploader.settings.multipart_params.image_resize}function prepareMediaItem(e,a){var r="undefined"==typeof shortform?1:2,i=jQuery("#media-item-"+e.id);2==r&&2

'+e+"

")}function wpFileError(e,a){itemAjaxError(e.id,a)}function itemAjaxError(e,a){var r=jQuery("#media-item-"+e),i=r.find(".filename").text();r.data("last-err")!=e&&r.html('
'+pluploadL10n.dismiss+""+pluploadL10n.error_uploading.replace("%s",jQuery.trim(i))+" "+a+"
").data("last-err",e)}function deleteSuccess(e){var a;return"-1"==e?itemAjaxError(this.id,"You do not have permission. Has your session expired?"):"0"==e?itemAjaxError(this.id,"Could not be deleted. Has it been deleted already?"):(e=this.id,a=jQuery("#media-item-"+e),(e=jQuery("#type-of-"+e).val())&&jQuery("#"+e+"-counter").text(jQuery("#"+e+"-counter").text()-1),post_id&&a.hasClass("child-of-"+post_id)&&jQuery("#attachments-count").text(jQuery("#attachments-count").text()-1),1==jQuery("form.type-form #media-items").children().length&&0 '+pluploadL10n.deleted+" ").siblings("a.toggle").hide(),jQuery(".filename",a).append(jQuery("a.undo",a).removeClass("hidden")),void jQuery(".menu_order_input",a).hide())}function deleteError(){}function uploadComplete(){jQuery("#insert-gallery").prop("disabled",!1)}function switchUploader(e){e?(deleteUserSetting("uploader"),jQuery(".media-upload-form").removeClass("html-uploader"),"object"==typeof uploader&&uploader.refresh()):(setUserSetting("uploader","1"),jQuery(".media-upload-form").addClass("html-uploader"))}function uploadError(e,a,r,i){var t=104857600;switch(a){case plupload.FAILED:wpFileError(e,pluploadL10n.upload_failed);break;case plupload.FILE_EXTENSION_ERROR:wpFileExtensionError(i,e,pluploadL10n.invalid_filetype);break;case plupload.FILE_SIZE_ERROR:uploadSizeError(i,e);break;case plupload.IMAGE_FORMAT_ERROR:wpFileError(e,pluploadL10n.not_an_image);break;case plupload.IMAGE_MEMORY_ERROR:wpFileError(e,pluploadL10n.image_memory_exceeded);break;case plupload.IMAGE_DIMENSIONS_ERROR:wpFileError(e,pluploadL10n.image_dimensions_exceeded);break;case plupload.GENERIC_ERROR:wpQueueError(pluploadL10n.upload_failed);break;case plupload.IO_ERROR:tt?wpFileError(e,pluploadL10n.big_upload_failed.replace("%1$s",'').replace("%2$s","")):wpQueueError(pluploadL10n.io_error);break;case plupload.HTTP_ERROR:wpQueueError(pluploadL10n.http_error);break;case plupload.INIT_ERROR:jQuery(".media-upload-form").addClass("html-uploader");break;case plupload.SECURITY_ERROR:wpQueueError(pluploadL10n.security_error);break;default:wpFileError(e,pluploadL10n.default_error)}}function uploadSizeError(e,a){var r=pluploadL10n.file_exceeds_size_limit.replace("%s",a.name),r=jQuery("
").attr({id:"media-item-"+a.id,class:"media-item error"}).append(jQuery("

").text(r));jQuery("#media-items").append(r),e.removeFile(a)}function wpFileExtensionError(e,a,r){jQuery("#media-items").append('

'+r+"

"),e.removeFile(a)}function copyAttachmentUploadURLClipboard(){var i;new ClipboardJS(".copy-attachment-url").on("success",function(e){var a=jQuery(e.trigger),r=jQuery(".success",a.closest(".copy-to-clipboard-container"));e.clearSelection(),a.trigger("focus"),clearTimeout(i),r.removeClass("hidden"),i=setTimeout(function(){r.addClass("hidden")},3e3),wp.a11y.speak(pluploadL10n.file_url_copied)})}jQuery(document).ready(function(o){copyAttachmentUploadURLClipboard();var d,l={};o(".media-upload-form").bind("click.uploader",function(e){var a,r=o(e.target);r.is('input[type="radio"]')?(a=r.closest("tr")).hasClass("align")?setUserSetting("align",r.val()):a.hasClass("image-size")&&setUserSetting("imgsize",r.val()):r.is("button.button")?(a=(a=e.target.className||"").match(/url([^ '"]+)/))&&a[1]&&(setUserSetting("urlbutton",a[1]),r.siblings(".urlfield").val(r.data("link-url"))):r.is("a.dismiss")?r.parents(".media-item").fadeOut(200,function(){o(this).remove()}):r.is(".upload-flash-bypass a")||r.is("a.uploader-html")?(o("#media-items, p.submit, span.big-file-warning").css("display","none"),switchUploader(0),e.preventDefault()):r.is(".upload-html-bypass a")?(o("#media-items, p.submit, span.big-file-warning").css("display",""),switchUploader(1),e.preventDefault()):r.is("a.describe-toggle-on")?(r.parent().addClass("open"),r.siblings(".slidetoggle").fadeIn(250,function(){var e=o(window).scrollTop(),a=o(window).height(),r=o(this).offset().top,i=o(this).height();a&&r&&i&&(a=e+a)<(i=r+i)&&(i-a 4 ) { + if ( times && times > 8 ) { /* * The file may have been uploaded and attachment post created, * but post-processing and resizing failed... diff --git a/wp-includes/js/plupload/wp-plupload.min.js b/wp-includes/js/plupload/wp-plupload.min.js index 9985ba49d3..340fcd44f3 100644 --- a/wp-includes/js/plupload/wp-plupload.min.js +++ b/wp-includes/js/plupload/wp-plupload.min.js @@ -1 +1 @@ -window.wp=window.wp||{},function(e,l){var u;"undefined"!=typeof _wpPluploadSettings&&(l.extend(u=function(e){var n,t,i,p,d=this,r={container:"container",browser:"browse_button",dropzone:"drop_element"},s={};if(this.supports={upload:u.browser.supported},this.supported=this.supports.upload,this.supported){for(t in this.plupload=l.extend(!0,{multipart_params:{}},u.defaults),this.container=document.body,l.extend(!0,this,e),this)"function"==typeof this[t]&&(this[t]=l.proxy(this[t],this));for(t in r)this[t]&&(this[t]=l(this[t]).first(),this[t].length?(this[t].prop("id")||this[t].prop("id","__wp-uploader-id-"+u.uuid++),this.plupload[r[t]]=this[t].prop("id")):delete this[t]);(this.browser&&this.browser.length||this.dropzone&&this.dropzone.length)&&(this.uploader=new plupload.Uploader(this.plupload),delete this.plupload,this.param(this.params||{}),delete this.params,n=function(t,r,a){var e,o;if(r&&r.responseHeaders)if((o=r.responseHeaders.match(/x-wp-upload-attachment-id:\s*(\d+)/i))&&o[1]){if(o=o[1],(e=s[a.id])&&4').css({position:"fixed",top:"-1000px",left:"-1000px",height:0,width:0}).attr("id","wp-uploader-browser-"+this.uploader.id).appendTo("body")).append(this.browser))}this.uploader.refresh()}}),u.queue=new wp.media.model.Attachments([],{query:!1}),u.errors=new Backbone.Collection,e.Uploader=u)}(wp,jQuery); \ No newline at end of file +window.wp=window.wp||{},function(e,l){var u;"undefined"!=typeof _wpPluploadSettings&&(l.extend(u=function(e){var n,t,i,p,d=this,r={container:"container",browser:"browse_button",dropzone:"drop_element"},s={};if(this.supports={upload:u.browser.supported},this.supported=this.supports.upload,this.supported){for(t in this.plupload=l.extend(!0,{multipart_params:{}},u.defaults),this.container=document.body,l.extend(!0,this,e),this)"function"==typeof this[t]&&(this[t]=l.proxy(this[t],this));for(t in r)this[t]&&(this[t]=l(this[t]).first(),this[t].length?(this[t].prop("id")||this[t].prop("id","__wp-uploader-id-"+u.uuid++),this.plupload[r[t]]=this[t].prop("id")):delete this[t]);(this.browser&&this.browser.length||this.dropzone&&this.dropzone.length)&&(this.uploader=new plupload.Uploader(this.plupload),delete this.plupload,this.param(this.params||{}),delete this.params,n=function(t,r,a){var e,o;if(r&&r.responseHeaders)if((o=r.responseHeaders.match(/x-wp-upload-attachment-id:\s*(\d+)/i))&&o[1]){if(o=o[1],(e=s[a.id])&&8').css({position:"fixed",top:"-1000px",left:"-1000px",height:0,width:0}).attr("id","wp-uploader-browser-"+this.uploader.id).appendTo("body")).append(this.browser))}this.uploader.refresh()}}),u.queue=new wp.media.model.Attachments([],{query:!1}),u.errors=new Backbone.Collection,e.Uploader=u)}(wp,jQuery); \ No newline at end of file diff --git a/wp-includes/media.php b/wp-includes/media.php index aaf811d2d0..51c49f83d2 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1852,6 +1852,11 @@ function wp_filter_content_tags( $content, $context = null ) { $filtered_image = wp_img_tag_add_decoding_attr( $filtered_image, $context ); } + // Use alternate mime types when specified and available. + if ( $attachment_id > 0 && _wp_in_front_end_context() ) { + $filtered_image = wp_image_use_alternate_mime_types( $filtered_image, $context, $attachment_id ); + } + /** * Filters an img tag within the content for a given context. * @@ -1898,6 +1903,117 @@ function wp_filter_content_tags( $content, $context = null ) { return $content; } +/** + * Use alternate mime type images in the front end content output when available. + * + * @since 6.1.0 + * + * @param string $image The HTML `img` tag where the attribute should be added. + * @param string $context Additional context to pass to the filters. + * @param int $attachment_id The attachment ID. + * @return string Converted `img` tag with `loading` attribute added. + */ +function wp_image_use_alternate_mime_types( $image, $context, $attachment_id ) { + $metadata = wp_get_attachment_metadata( $attachment_id ); + if ( empty( $metadata['file'] ) ) { + return $image; + } + + // Only alter images with a `sources` attribute + if ( empty( $metadata['sources'] ) ) { + return $image; + }; + + $target_mimes = array( 'image/webp', 'image/jpeg' ); + + /** + * Filter the content image mime type output selection and order. + * + * When outputting images in the content, the first mime type available will be used. + * + * @since 6.1.0 + * + * @param array $target_mimes The image output mime type and order. Default is array( 'image/webp', 'image/jpeg' ). + * @param int $attachment_id The attachment ID. + * @param string $context Additional context to pass to the filters. + * @return array The filtered output mime type and order. Return an empty array to skip mime type substitution. + */ + $target_mimes = apply_filters( 'wp_content_image_mimes', $target_mimes, $attachment_id, $context ); + + if ( false === $target_mimes ) { + return $image; + } + + // Find the appropriate size for the provided URL in the first available mime type. + foreach ( $target_mimes as $target_mime ) { + // Handle full size image replacement. + if ( ! empty( $metadata['sources'][ $target_mime ]['file'] ) ) { + $src_filename = wp_basename( $metadata['file'] ); + + // This is the same MIME type as the original, so the entire $target_mime can be skipped. + // Since it is already the preferred MIME type, the entire loop can be cancelled. + if ( $metadata['sources'][ $target_mime ]['file'] === $src_filename ) { + break; + } + + $image = str_replace( $src_filename, $metadata['sources'][ $target_mime ]['file'], $image ); + + // The full size was replaced, so unset this entirely here so that in the next iteration it is no longer + // considered, simply for a small performance optimization. + unset( $metadata['sources'] ); + } + + // Go through each image size and replace with the first available mime type version. + foreach ( $metadata['sizes'] as $name => $size_data ) { + // Check if size has an original file. + if ( empty( $size_data['file'] ) ) { + continue; + } + + // Check if size has a source in the desired mime type. + if ( empty( $size_data['sources'][ $target_mime ]['file'] ) ) { + continue; + } + + $src_filename = wp_basename( $size_data['file'] ); + + // This is the same MIME type as the original, so the entire $target_mime can be skipped. + // Since it is already the preferred MIME type, the entire loop can be cancelled. + if ( $size_data['sources'][ $target_mime ]['file'] === $src_filename ) { + break 2; + } + + // Found a match, replace with the new filename. + $image = str_replace( $src_filename, $size_data['sources'][ $target_mime ]['file'], $image ); + + // This size was replaced, so unset this entirely here so that in the next iteration it is no longer + // considered, simply for a small performance optimization. + unset( $metadata['sizes'][ $name ] ); + } + } + return $image; +} + +/** + * Check if execution is currently in the front end content context, outside of . + * + * @since 6.1.0 + * @access private + * + * @return bool True if in the front end content context, false otherwise. + */ +function _wp_in_front_end_context() { + global $wp_query; + + // Check if this request is generally outside (or before) any frontend context. + if ( ! isset( $wp_query ) || defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) || is_feed() ) { + return false; + } + + // Check if we're anywhere before the 'wp_head' action has completed. + return did_action( 'template_redirect' ) && ! doing_action( 'wp_head' ); +} + /** * Adds `loading` attribute to an `img` HTML tag. * diff --git a/wp-includes/post.php b/wp-includes/post.php index 9689e008ea..c7dcc13b8d 100644 --- a/wp-includes/post.php +++ b/wp-includes/post.php @@ -6481,13 +6481,28 @@ function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) { $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); foreach ( $meta['sizes'] as $size => $sizeinfo ) { - $intermediate_file = str_replace( wp_basename( $file ), $sizeinfo['file'], $file ); - if ( ! empty( $intermediate_file ) ) { - $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file ); + // Check for alternate size mime types in the sizeinfo['sources'] array to delete. + if ( isset( $sizeinfo['sources'] ) && is_array( $sizeinfo['sources'] ) ) { + foreach ( $sizeinfo['sources'] as $mime => $properties ) { + $intermediate_file = str_replace( wp_basename( $file ), $properties['file'], $file ); + if ( ! empty( $intermediate_file ) ) { + $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file ); + if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { + $deleted = false; + } + } + } + } else { + // Otherwise, delete files from the sizeinfo data. + $intermediate_file = str_replace( wp_basename( $file ), $sizeinfo['file'], $file ); - if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { - $deleted = false; + if ( ! empty( $intermediate_file ) ) { + $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file ); + + if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { + $deleted = false; + } } } } @@ -6509,24 +6524,58 @@ function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) { } } - if ( is_array( $backup_sizes ) ) { - $del_dir = path_join( $uploadpath['basedir'], dirname( $meta['file'] ) ); - - foreach ( $backup_sizes as $size ) { - $del_file = path_join( dirname( $meta['file'] ), $size['file'] ); - - if ( ! empty( $del_file ) ) { - $del_file = path_join( $uploadpath['basedir'], $del_file ); - - if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) { - $deleted = false; - } + // Delete the full size images from 'sources' if available, or the root file. + if ( isset( $meta['sources'] ) && is_array( $meta['sources'] ) ) { + $sources = $meta['sources']; + $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); + foreach ( $sources as $mime => $properties ) { + if ( ! is_array( $properties ) || empty( $properties['file'] ) ) { + continue; } + $intermediate_file = str_replace( wp_basename( $file ), $properties['file'], $file ); + if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { + $deleted = false; + } + } + } else { + if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) { + $deleted = false; } } - if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) { - $deleted = false; + if ( is_array( $backup_sizes ) ) { + + $del_dir = path_join( $uploadpath['basedir'], dirname( $meta['file'] ) ); + // Delete the root (edited) file which was not deleted above. + if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) { + $deleted = false; + } + foreach ( $backup_sizes as $size ) { + // Delete files from 'sources' data if available, otherwise from 'sizes' data. + if ( isset( $meta['sources'] ) && is_array( $meta['sources'] ) ) { + // Delete any backup images stored in the 'sources' array. + if ( isset( $size['sources'] ) && is_array( $size['sources'] ) ) { + foreach ( $size['sources'] as $mime => $properties ) { + $del_file = path_join( dirname( $meta['file'] ), $properties['file'] ); + if ( ! empty( $del_file ) ) { + $del_file = path_join( $uploadpath['basedir'], $del_file ); + if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) { + $deleted = false; + } + } + } + } + } else { + $del_file = path_join( dirname( $meta['file'] ), $size['file'] ); + + if ( ! empty( $del_file ) ) { + $del_file = path_join( $uploadpath['basedir'], $del_file ); + if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) { + $deleted = false; + } + } + } + } } return $deleted; diff --git a/wp-includes/version.php b/wp-includes/version.php index ecaa82907c..2c4b6bfa1d 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.1-alpha-53750'; +$wp_version = '6.1-alpha-53751'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.