|
4 | 4 | * Description: Uses WebP as the default format for new JPEG image uploads if the server supports it. |
5 | 5 | * Experimental: No |
6 | 6 | * |
| 7 | + * @since 1.0.0 |
7 | 8 | * @package performance-lab |
8 | | - * @since 1.0.0 |
9 | 9 | */ |
10 | 10 |
|
11 | 11 | /** |
12 | | - * Filter the image editor default output format mapping. |
| 12 | + * Hook called by `wp_generate_attachment_metadata` to create the `sources` property for every image |
| 13 | + * size, the sources' property would create a new image size with all the mime types specified in |
| 14 | + * `webp_uploads_valid_image_mime_types`. If the original image is one of the mimes from |
| 15 | + * `webp_uploads_valid_image_mime_types` the image is just added to the `sources` property and not |
| 16 | + * created again. If the uploaded attachment is not a valid image this function does not alter the |
| 17 | + * metadata of the attachment, on the other hand a `sources` property is added. |
13 | 18 | * |
14 | | - * For uploaded JPEG images, map the default output format to WebP. |
| 19 | + * @since n.e.x.t |
15 | 20 | * |
16 | | - * @since 1.0.0 |
| 21 | + * @see wp_generate_attachment_metadata() |
| 22 | + * @see webp_uploads_valid_image_mime_types() |
17 | 23 | * |
18 | | - * @param string $output_format The image editor default output format mapping. |
19 | | - * @param string $filename Path to the image. |
20 | | - * @param string $mime_type The source image mime type. |
21 | | - * @return string The new output format mapping. |
| 24 | + * @param array $metadata An array with the metadata from this attachment. |
| 25 | + * @param int $attachment_id The ID of the attachment where the hook was dispatched. |
| 26 | + * @return array An array with the updated structure for the metadata before is stored in the database. |
22 | 27 | */ |
23 | | -function webp_uploads_filter_image_editor_output_format( $output_format, $filename, $mime_type ) { |
24 | | - // Only enable if the server supports WebP. |
25 | | - if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) { |
26 | | - return $output_format; |
| 28 | +function webp_uploads_create_sources_property( array $metadata, $attachment_id ) { |
| 29 | + // This should take place only on the JPEG image. |
| 30 | + $valid_mime_transforms = webp_uploads_get_supported_image_mime_transforms(); |
| 31 | + |
| 32 | + // Not a supported mime type to create the sources property. |
| 33 | + $mime_type = get_post_mime_type( $attachment_id ); |
| 34 | + if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) { |
| 35 | + return $metadata; |
| 36 | + } |
| 37 | + |
| 38 | + $file = get_attached_file( $attachment_id, true ); |
| 39 | + |
| 40 | + // File does not exist. |
| 41 | + if ( ! file_exists( $file ) ) { |
| 42 | + return $metadata; |
| 43 | + } |
| 44 | + |
| 45 | + $dirname = pathinfo( $file, PATHINFO_DIRNAME ); |
| 46 | + $image_sizes = array(); |
| 47 | + if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { |
| 48 | + $image_sizes = $metadata['sizes']; |
| 49 | + } |
| 50 | + |
| 51 | + foreach ( wp_get_registered_image_subsizes() as $size_name => $properties ) { |
| 52 | + // This image size does not exist on the defined sizes. |
| 53 | + if ( ! isset( $image_sizes[ $size_name ] ) || ! is_array( $image_sizes[ $size_name ] ) ) { |
| 54 | + continue; |
| 55 | + } |
| 56 | + |
| 57 | + $current_size = $image_sizes[ $size_name ]; |
| 58 | + $sources = array(); |
| 59 | + if ( isset( $current_size['sources'] ) && is_array( $current_size['sources'] ) ) { |
| 60 | + $sources = $current_size['sources']; |
| 61 | + } |
| 62 | + |
| 63 | + // Try to find the mime type of the image size. |
| 64 | + $current_mime = ''; |
| 65 | + if ( isset( $current_size['mime-type'] ) ) { |
| 66 | + $current_mime = $current_size['mime-type']; |
| 67 | + } elseif ( isset( $current_size['file'] ) ) { |
| 68 | + $current_mime = wp_check_filetype( $current_size['file'] )['type']; |
| 69 | + } |
| 70 | + |
| 71 | + if ( empty( $current_mime ) ) { |
| 72 | + continue; |
| 73 | + } |
| 74 | + |
| 75 | + $sources[ $current_mime ] = array( |
| 76 | + 'file' => isset( $current_size['file'] ) ? $current_size['file'] : '', |
| 77 | + 'filesize' => 0, |
| 78 | + ); |
| 79 | + |
| 80 | + // Set the filesize from the current mime image. |
| 81 | + $file_location = path_join( $dirname, $sources[ $current_mime ]['file'] ); |
| 82 | + if ( file_exists( $file_location ) ) { |
| 83 | + $sources[ $current_mime ]['filesize'] = filesize( $file_location ); |
| 84 | + } |
| 85 | + |
| 86 | + $formats = isset( $valid_mime_transforms[ $current_mime ] ) ? $valid_mime_transforms[ $current_mime ] : array(); |
| 87 | + |
| 88 | + foreach ( $formats as $mime ) { |
| 89 | + if ( empty( $sources[ $mime ] ) ) { |
| 90 | + $source = webp_uploads_generate_image_size( $attachment_id, $size_name, $mime ); |
| 91 | + if ( is_array( $source ) ) { |
| 92 | + $sources[ $mime ] = $source; |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + $current_size['sources'] = $sources; |
| 98 | + $metadata['sizes'][ $size_name ] = $current_size; |
| 99 | + } |
| 100 | + |
| 101 | + return $metadata; |
| 102 | +} |
| 103 | + |
| 104 | +add_filter( 'wp_generate_attachment_metadata', 'webp_uploads_create_sources_property', 10, 2 ); |
| 105 | + |
| 106 | +/** |
| 107 | + * Creates a new image based of the specified attachment with a defined mime type |
| 108 | + * this image would be stored in the same place as the provided size name inside the |
| 109 | + * metadata of the attachment. |
| 110 | + * |
| 111 | + * @since n.e.x.t |
| 112 | + * |
| 113 | + * @param int $attachment_id The ID of the attachment we are going to use as a reference to create the image. |
| 114 | + * @param string $size The size name that would be used to create this image, out of the registered subsizes. |
| 115 | + * @param string $mime A mime type we are looking to use to create this image. |
| 116 | + * |
| 117 | + * @return array|WP_Error |
| 118 | + */ |
| 119 | +function webp_uploads_generate_image_size( $attachment_id, $size, $mime ) { |
| 120 | + $sizes = wp_get_registered_image_subsizes(); |
| 121 | + $metadata = wp_get_attachment_metadata( $attachment_id ); |
| 122 | + |
| 123 | + if ( |
| 124 | + ! isset( $metadata['sizes'][ $size ], $sizes[ $size ] ) |
| 125 | + || ! is_array( $metadata['sizes'][ $size ] ) |
| 126 | + || ! is_array( $sizes[ $size ] ) |
| 127 | + ) { |
| 128 | + return new WP_Error( 'image_mime_type_invalid_metadata', __( 'The image does not have a valid metadata.', 'performance-lab' ) ); |
| 129 | + } |
| 130 | + |
| 131 | + // All subsizes are created out of the attached file. |
| 132 | + $file = get_attached_file( $attachment_id ); |
| 133 | + |
| 134 | + // File does not exist. |
| 135 | + if ( ! file_exists( $file ) ) { |
| 136 | + return new WP_Error( 'image_file_size_not_found', __( 'The provided size does not have a valid image file.', 'performance-lab' ) ); |
| 137 | + } |
| 138 | + |
| 139 | + // Create the subsizes out of the attached file. |
| 140 | + $editor = wp_get_image_editor( $file ); |
| 141 | + |
| 142 | + if ( is_wp_error( $editor ) ) { |
| 143 | + return $editor; |
| 144 | + } |
| 145 | + |
| 146 | + $allowed_mimes = array_flip( wp_get_mime_types() ); |
| 147 | + if ( ! isset( $allowed_mimes[ $mime ] ) || ! is_string( $allowed_mimes[ $mime ] ) ) { |
| 148 | + return new WP_Error( 'image_mime_type_invalid', __( 'The provided mime type is not allowed.', 'performance-lab' ) ); |
27 | 149 | } |
28 | 150 |
|
29 | | - // WebP lossless support is still limited on servers, so only apply to JPEGs. |
30 | | - if ( 'image/jpeg' !== $mime_type ) { |
31 | | - return $output_format; |
| 151 | + if ( ! wp_image_editor_supports( array( 'mime_type' => $mime ) ) ) { |
| 152 | + return new WP_Error( 'image_mime_type_not_supported', __( 'The provided mime type is not supported.', 'performance-lab' ) ); |
32 | 153 | } |
33 | 154 |
|
34 | | - // Skip conversion when creating the `-scaled` image (for large image uploads). |
35 | | - if ( preg_match( '/-scaled\..{3}.?$/', $filename ) ) { |
36 | | - return $output_format; |
| 155 | + $extension = explode( '|', $allowed_mimes[ $mime ] ); |
| 156 | + $extension = reset( $extension ); |
| 157 | + |
| 158 | + $width = null; |
| 159 | + $height = null; |
| 160 | + $crop = false; |
| 161 | + |
| 162 | + if ( isset( $metadata['sizes'][ $size ]['width'] ) ) { |
| 163 | + $width = $metadata['sizes'][ $size ]['width']; |
| 164 | + } elseif ( isset( $sizes[ $size ]['widht'] ) ) { |
| 165 | + $width = $sizes[ $size ]; |
| 166 | + } |
| 167 | + |
| 168 | + if ( isset( $metadata['sizes'][ $size ]['height'] ) ) { |
| 169 | + $height = $metadata['sizes'][ $size ]['height']; |
| 170 | + } elseif ( isset( $sizes[ $size ]['width'] ) ) { |
| 171 | + $height = $sizes[ $size ]; |
| 172 | + } |
| 173 | + |
| 174 | + if ( isset( $sizes[ $size ]['crop'] ) ) { |
| 175 | + $crop = (bool) $sizes[ $size ]['crop']; |
| 176 | + } |
| 177 | + |
| 178 | + $editor->resize( $width, $height, $crop ); |
| 179 | + $filename = $editor->generate_filename( null, null, $extension ); |
| 180 | + $filename = preg_replace( '/-(scaled|rotated|imagifyresized)/', '', $filename ); |
| 181 | + $image = $editor->save( $filename, $mime ); |
| 182 | + |
| 183 | + if ( is_wp_error( $image ) ) { |
| 184 | + return $image; |
37 | 185 | } |
38 | 186 |
|
39 | | - $output_format['image/jpeg'] = 'image/webp'; |
| 187 | + if ( empty( $image['file'] ) ) { |
| 188 | + return new WP_Error( 'image_file_not_present', __( 'The file key is not present on the image data', 'performance-lab' ) ); |
| 189 | + } |
| 190 | + |
| 191 | + return array( |
| 192 | + 'file' => $image['file'], |
| 193 | + 'filesize' => isset( $image['path'] ) ? filesize( $image['path'] ) : 0, |
| 194 | + ); |
| 195 | +} |
40 | 196 |
|
41 | | - return $output_format; |
| 197 | +/** |
| 198 | + * Returns an array with the list of valid mime types that a specific mime type can be converted into it, |
| 199 | + * for example an image/jpeg can be converted into an image/webp. |
| 200 | + * |
| 201 | + * @since n.e.x.t |
| 202 | + * |
| 203 | + * @return array<string, array<string>> An array of valid mime types, where the key is the mime type and the value is the extension type. |
| 204 | + */ |
| 205 | +function webp_uploads_get_supported_image_mime_transforms() { |
| 206 | + $image_mime_transforms = array( |
| 207 | + 'image/jpeg' => array( 'image/webp' ), |
| 208 | + 'image/webp' => array( 'image/jpeg' ), |
| 209 | + ); |
| 210 | + |
| 211 | + /** |
| 212 | + * Filter to allow the definition of a custom mime types, in which a defined mime type |
| 213 | + * can be transformed and provide a wide range of mime types. |
| 214 | + * |
| 215 | + * @since n.e.x.t |
| 216 | + * |
| 217 | + * @param array $image_mime_transforms A map with the valid mime transforms. |
| 218 | + */ |
| 219 | + return (array) apply_filters( 'webp_uploads_supported_image_mime_transforms', $image_mime_transforms ); |
| 220 | +} |
| 221 | + |
| 222 | +/** |
| 223 | + * Hook fired when an attachment is deleted, this hook is in charge of removing any |
| 224 | + * additional mime types created by this plugin besides the original image. Any source |
| 225 | + * with the same as the main image would not be removed by this hook due this file would |
| 226 | + * be removed by WordPress when the attachment is deleted, usually this happens after this |
| 227 | + * hook is executed. |
| 228 | + * |
| 229 | + * @since n.e.x.t |
| 230 | + * |
| 231 | + * @see wp_delete_attachment() |
| 232 | + * |
| 233 | + * @param int $attachment_id The ID of the attachment the sources are going to be deleted. |
| 234 | + */ |
| 235 | +function webp_uploads_remove_sources_files( $attachment_id ) { |
| 236 | + $metadata = wp_get_attachment_metadata( $attachment_id ); |
| 237 | + $file = get_attached_file( $attachment_id ); |
| 238 | + |
| 239 | + if ( |
| 240 | + ! isset( $metadata['sizes'] ) |
| 241 | + || empty( $file ) |
| 242 | + || ! is_array( $metadata['sizes'] ) |
| 243 | + ) { |
| 244 | + return; |
| 245 | + } |
| 246 | + |
| 247 | + $upload_path = wp_get_upload_dir(); |
| 248 | + if ( empty( $upload_path['basedir'] ) ) { |
| 249 | + return; |
| 250 | + } |
| 251 | + |
| 252 | + $intermediate_dir = path_join( $upload_path['basedir'], dirname( $file ) ); |
| 253 | + $basename = wp_basename( $file ); |
| 254 | + |
| 255 | + foreach ( $metadata['sizes'] as $size ) { |
| 256 | + if ( ! isset( $size['sources'] ) || ! is_array( $size['sources'] ) ) { |
| 257 | + continue; |
| 258 | + } |
| 259 | + |
| 260 | + $original_size_mime = empty( $size['mime-type'] ) ? '' : $size['mime-type']; |
| 261 | + |
| 262 | + foreach ( $size['sources'] as $mime => $properties ) { |
| 263 | + /** |
| 264 | + * When we face the same mime type as the original image, we ignore this file as this file |
| 265 | + * would be removed when the size is removed by WordPress itself. The meta information as well |
| 266 | + * would be deleted as soon as the image is removed. |
| 267 | + * |
| 268 | + * @see wp_delete_attachment |
| 269 | + */ |
| 270 | + if ( $original_size_mime === $mime ) { |
| 271 | + continue; |
| 272 | + } |
| 273 | + |
| 274 | + if ( ! is_array( $properties ) || empty( $properties['file'] ) ) { |
| 275 | + continue; |
| 276 | + } |
| 277 | + |
| 278 | + $intermediate_file = str_replace( $basename, $properties['file'], $file ); |
| 279 | + if ( ! empty( $intermediate_file ) ) { |
| 280 | + $intermediate_file = path_join( $upload_path['basedir'], $intermediate_file ); |
| 281 | + wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ); |
| 282 | + } |
| 283 | + } |
| 284 | + } |
42 | 285 | } |
43 | | -add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 ); |
44 | 286 |
|
| 287 | +add_action( 'delete_attachment', 'webp_uploads_remove_sources_files', 10, 1 ); |
0 commit comments