PluginProbe ʕ •ᴥ•ʔ
Modern Image Formats / 2.3.0
Modern Image Formats v2.3.0
2.7.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 2.0.0 2.0.1 2.0.2 2.1.0 2.2.0 2.3.0 2.4.0 2.5.0 2.5.1 2.6.0 2.6.1
webp-uploads / hooks.php
webp-uploads Last commit date
deprecated.php 2 years ago helper.php 1 year ago hooks.php 1 year ago image-edit.php 2 years ago load.php 1 year ago picture-element.php 1 year ago readme.txt 1 year ago rest-api.php 2 years ago settings.php 1 year ago uninstall.php 2 years ago
hooks.php
783 lines
1 <?php
2 /**
3 * Hook callbacks used for Modern Image Formats.
4 *
5 * @package webp-uploads
6 *
7 * @since 1.0.0
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 /**
15 * Hook called by `wp_generate_attachment_metadata` to create the `sources` property for every image
16 * size, the sources' property would create a new image size with all the mime types specified in
17 * `webp_uploads_get_upload_image_mime_transforms`. If the original image is one of the mimes from
18 * `webp_uploads_get_upload_image_mime_transforms` the image is just added to the `sources` property and not
19 * created again. If the uploaded attachment is not a supported mime by this function, the hook does not alter the
20 * metadata of the attachment. In addition to every single size the `sources` property is added at the
21 * top level of the image metadata to store the references for all the mime types for the `full` size image of the
22 * attachment.
23 *
24 * @since 1.0.0
25 *
26 * @see wp_generate_attachment_metadata()
27 * @see webp_uploads_get_upload_image_mime_transforms()
28 *
29 * @phpstan-param array{
30 * width: int,
31 * height: int,
32 * file: string,
33 * sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
34 * image_meta: array<string, mixed>,
35 * filesize: int
36 * } $metadata
37 *
38 * @param array<string, mixed> $metadata An array with the metadata from this attachment.
39 * @param int $attachment_id The ID of the attachment where the hook was dispatched.
40 *
41 * @return array{
42 * width: int,
43 * height: int,
44 * file: string,
45 * sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
46 * image_meta: array<string, mixed>,
47 * filesize: int,
48 * sources?: array<string, array{
49 * file: string,
50 * filesize: int
51 * }>
52 * } An array with the updated structure for the metadata before is stored in the database.
53 */
54 function webp_uploads_create_sources_property( array $metadata, int $attachment_id ): array {
55 $file = get_attached_file( $attachment_id, true );
56 // File does not exist.
57 if ( false === $file || ! file_exists( $file ) ) {
58 return $metadata;
59 }
60
61 $mime_type = webp_uploads_get_attachment_file_mime_type( $attachment_id, $file );
62 if ( '' === $mime_type ) {
63 return $metadata;
64 }
65
66 $valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
67
68 // Not a supported mime type to create the sources property.
69 if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
70 return $metadata;
71 }
72
73 // Make sure the top level `sources` key is a valid array.
74 if ( ! isset( $metadata['sources'] ) || ! is_array( $metadata['sources'] ) ) {
75 $metadata['sources'] = array();
76 }
77
78 if ( empty( $metadata['sources'][ $mime_type ] ) ) {
79 $metadata['sources'][ $mime_type ] = array(
80 'file' => wp_basename( $file ),
81 'filesize' => wp_filesize( $file ),
82 );
83 wp_update_attachment_metadata( $attachment_id, $metadata );
84 }
85
86 $original_size_data = array(
87 'width' => isset( $metadata['width'] ) ? (int) $metadata['width'] : 0,
88 'height' => isset( $metadata['height'] ) ? (int) $metadata['height'] : 0,
89 'crop' => false,
90 );
91
92 $original_directory = pathinfo( $file, PATHINFO_DIRNAME );
93 $filename = pathinfo( $file, PATHINFO_FILENAME );
94 $ext = pathinfo( $file, PATHINFO_EXTENSION );
95 $allowed_mimes = array_flip( wp_get_mime_types() );
96
97 // Create the sources for the full sized image.
98 foreach ( $valid_mime_transforms[ $mime_type ] as $targeted_mime ) {
99 // If this property exists no need to create the image again.
100 if ( ! empty( $metadata['sources'][ $targeted_mime ] ) ) {
101 continue;
102 }
103
104 // The targeted mime is not allowed in the current installation.
105 if ( empty( $allowed_mimes[ $targeted_mime ] ) ) {
106 continue;
107 }
108
109 $extension = explode( '|', $allowed_mimes[ $targeted_mime ] );
110 $destination = trailingslashit( $original_directory ) . "{$filename}-{$ext}.{$extension[0]}";
111 $image = webp_uploads_generate_additional_image_source( $attachment_id, 'full', $original_size_data, $targeted_mime, $destination );
112
113 if ( is_wp_error( $image ) ) {
114 continue;
115 }
116
117 if ( webp_uploads_should_discard_additional_image_file( $metadata, $image ) ) {
118 wp_delete_file_from_directory( $destination, $original_directory );
119 continue;
120 }
121
122 $metadata['sources'][ $targeted_mime ] = $image;
123 wp_update_attachment_metadata( $attachment_id, $metadata );
124 }
125
126 // If the original MIME type should not be generated/used, override the main image
127 // with the first MIME type image that actually should be generated. In that case,
128 // the original should be backed up.
129 if (
130 ! in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true ) &&
131 isset( $valid_mime_transforms[ $mime_type ][0] ) &&
132 isset( $allowed_mimes[ $mime_type ] ) &&
133 array_key_exists( 'file', $metadata ) &&
134 is_string( $metadata['file'] )
135 ) {
136 $valid_mime_type = $valid_mime_transforms[ $mime_type ][0];
137
138 // Only do the replacement if the attachment file is still set to the original MIME type one,
139 // and if there is a possible replacement source.
140 $file_data = wp_check_filetype( $metadata['file'], array( $allowed_mimes[ $mime_type ] => $mime_type ) );
141 if ( $file_data['type'] === $mime_type && isset( $metadata['sources'][ $valid_mime_type ] ) ) {
142 $saved_data = array(
143 'path' => trailingslashit( $original_directory ) . $metadata['sources'][ $valid_mime_type ]['file'],
144 'width' => $metadata['width'],
145 'height' => $metadata['height'],
146 );
147
148 $original_image = wp_get_original_image_path( $attachment_id );
149
150 // If WordPress already modified the original itself, keep the original and discard WordPress's generated version.
151 if ( ! empty( $metadata['original_image'] ) ) {
152 $uploadpath = wp_get_upload_dir();
153 $attached_file = get_attached_file( $attachment_id );
154 if ( false !== $attached_file ) {
155 wp_delete_file_from_directory( $attached_file, $uploadpath['basedir'] );
156 }
157 }
158
159 // Replace the attached file with the custom MIME type version.
160 if ( false !== $original_image ) {
161 $metadata = _wp_image_meta_replace_original( $saved_data, $original_image, $metadata, $attachment_id );
162 }
163
164 // Unset sources entry for the original MIME type, then save (to avoid inconsistent data
165 // in case of an error after this logic).
166 unset( $metadata['sources'][ $mime_type ] );
167 wp_update_attachment_metadata( $attachment_id, $metadata );
168 }
169 }
170
171 // Make sure we have some sizes to work with, otherwise avoid any work.
172 if ( empty( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ) {
173 return $metadata;
174 }
175
176 $sizes_with_mime_type_support = webp_uploads_get_image_sizes_additional_mime_type_support();
177
178 foreach ( $metadata['sizes'] as $size_name => $properties ) {
179 // Do nothing if this image size is not an array or is not allowed to have additional mime types.
180 if ( ! is_array( $properties ) || empty( $sizes_with_mime_type_support[ $size_name ] ) ) {
181 continue;
182 }
183
184 // Try to find the mime type of the image size.
185 $current_mime = '';
186 if ( isset( $properties['mime-type'] ) ) {
187 $current_mime = $properties['mime-type'];
188 } elseif ( isset( $properties['file'] ) ) {
189 $current_mime = wp_check_filetype( $properties['file'] )['type'];
190 }
191
192 // The mime type can't be determined.
193 if ( empty( $current_mime ) ) {
194 continue;
195 }
196
197 // Ensure a `sources` property exists on the existing size.
198 if ( empty( $properties['sources'] ) || ! is_array( $properties['sources'] ) ) {
199 $properties['sources'] = array();
200 }
201
202 if ( empty( $properties['sources'][ $current_mime ] ) && isset( $properties['file'] ) ) {
203 $properties['sources'][ $current_mime ] = array(
204 'file' => $properties['file'],
205 'filesize' => 0,
206 );
207 // Set the filesize from the current mime image.
208 $file_location = path_join( $original_directory, $properties['file'] );
209 if ( file_exists( $file_location ) ) {
210 $properties['sources'][ $current_mime ]['filesize'] = wp_filesize( $file_location );
211 }
212 $metadata['sizes'][ $size_name ] = $properties;
213 wp_update_attachment_metadata( $attachment_id, $metadata );
214 }
215
216 foreach ( $valid_mime_transforms[ $mime_type ] as $mime ) {
217 // If this property exists no need to create the image again.
218 if ( ! empty( $properties['sources'][ $mime ] ) ) {
219 continue;
220 }
221
222 $source = webp_uploads_generate_image_size( $attachment_id, $size_name, $mime );
223 if ( is_wp_error( $source ) ) {
224 continue;
225 }
226
227 if ( webp_uploads_should_discard_additional_image_file( $properties, $source ) ) {
228 $destination = path_join( $original_directory, $source['file'] );
229 wp_delete_file_from_directory( $destination, $original_directory );
230 continue;
231 }
232
233 $properties['sources'][ $mime ] = $source;
234 $metadata['sizes'][ $size_name ] = $properties;
235 wp_update_attachment_metadata( $attachment_id, $metadata );
236 }
237
238 $metadata['sizes'][ $size_name ] = $properties;
239 }
240
241 return $metadata;
242 }
243 add_filter( 'wp_generate_attachment_metadata', 'webp_uploads_create_sources_property', 10, 2 );
244
245 /**
246 * Filter on `wp_get_missing_image_subsizes` acting as an action for the logic of the plugin
247 * to determine if additional mime types still need to be created.
248 *
249 * This function only exists to work around a missing filter in WordPress core, to call the above
250 * `webp_uploads_create_sources_property()` function correctly.
251 *
252 * @since 1.0.0
253 *
254 * @see wp_get_missing_image_subsizes()
255 *
256 * @phpstan-param array{
257 * width: int,
258 * height: int,
259 * file: string,
260 * sizes: array<string, array{file: string, width: int, height: int, mime-type: string}>,
261 * image_meta: array<string, mixed>,
262 * filesize: int
263 * } $image_meta
264 *
265 * @param array|mixed $missing_sizes Associative array of arrays of image sub-sizes.
266 * @param array<string, mixed> $image_meta The metadata from the image.
267 * @param int $attachment_id The ID of the attachment.
268 * @return array<string, array{ width: int, height: int, crop: bool }> Associative array of arrays of image sub-sizes.
269 */
270 function webp_uploads_wp_get_missing_image_subsizes( $missing_sizes, array $image_meta, int $attachment_id ): array {
271 if ( ! is_array( $missing_sizes ) ) {
272 $missing_sizes = array();
273 }
274
275 // Only setup the trace array if we no longer have more sizes.
276 if ( ! empty( $missing_sizes ) ) {
277 return $missing_sizes;
278 }
279
280 /**
281 * The usage of `debug_backtrace` in this particular case is mainly to ensure the call to
282 * `wp_get_missing_image_subsizes()` originated from `wp_update_image_subsizes()`, since only then the
283 * additional image sizes should be generated. `wp_get_missing_image_subsizes()` could also be called
284 * from other places in which case the custom logic should not trigger. In an ideal world an action
285 * would exist in `wp_update_image_subsizes` that runs any time, but the current
286 * `wp_generate_attachment_metadata` filter is skipped when all core sub-sizes have been generated.
287 * An eventual core implementation will not require this workaround. The limit of 10 is used to allow
288 * for some flexibility. While by default the function would be on index 5, other custom code may
289 * cause the index to be slightly higher.
290 *
291 * @see wp_update_image_subsizes()
292 * @see wp_get_missing_image_subsizes()
293 */
294 // PHPCS ignore reason: Only the way to generate missing image subsize if all core sub-sizes have been generated.
295 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
296 $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
297
298 foreach ( $trace as $element ) {
299 if ( 'wp_update_image_subsizes' === $element['function'] ) {
300 webp_uploads_create_sources_property( $image_meta, $attachment_id );
301 break;
302 }
303 }
304
305 return array();
306 }
307 add_filter( 'wp_get_missing_image_subsizes', 'webp_uploads_wp_get_missing_image_subsizes', 10, 3 );
308
309 /**
310 * Filter the image editor default output format mapping to select the most appropriate
311 * output format depending on desired output formats and supported mime types by the image
312 * editor.
313 *
314 * @since 1.0.0
315 *
316 * @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.
317 * @param string|null $filename Path to the image.
318 * @param string|null $mime_type The source image mime type.
319 * @return array<string, string> The new output format mapping.
320 */
321 function webp_uploads_filter_image_editor_output_format( $output_format, ?string $filename, ?string $mime_type ): array {
322 if ( ! is_array( $output_format ) ) {
323 $output_format = array();
324 }
325
326 // Use the original mime type if this type is allowed.
327 $valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
328 if (
329 ! isset( $valid_mime_transforms[ $mime_type ] ) ||
330 in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true )
331 ) {
332 return $output_format;
333 }
334
335 // Find the first supported mime type by the image editor to use it as the default one.
336 foreach ( $valid_mime_transforms[ $mime_type ] as $target_mime ) {
337 if ( wp_image_editor_supports( array( 'mime_type' => $target_mime ) ) ) {
338 $output_format[ $mime_type ] = $target_mime;
339 break;
340 }
341 }
342
343 return $output_format;
344 }
345 add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
346
347 /**
348 * Hook fired when an attachment is deleted, this hook is in charge of removing any
349 * additional mime types created by this plugin besides the original image. Any source
350 * with the same as the main image would not be removed by this hook due this file would
351 * be removed by WordPress when the attachment is deleted, usually this happens after this
352 * hook is executed.
353 *
354 * @since 1.0.0
355 *
356 * @see wp_delete_attachment()
357 *
358 * @param int $attachment_id The ID of the attachment the sources are going to be deleted.
359 */
360 function webp_uploads_remove_sources_files( int $attachment_id ): void {
361 $file = get_attached_file( $attachment_id );
362
363 if ( empty( $file ) ) {
364 return;
365 }
366
367 $metadata = wp_get_attachment_metadata( $attachment_id );
368 // Make sure $sizes is always defined to allow the removal of original images after the first foreach loop.
369 $sizes = ! isset( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ? array() : $metadata['sizes'];
370
371 $upload_path = wp_get_upload_dir();
372 if ( empty( $upload_path['basedir'] ) ) {
373 return;
374 }
375
376 $intermediate_dir = path_join( $upload_path['basedir'], dirname( $file ) );
377 $basename = wp_basename( $file );
378
379 foreach ( $sizes as $size ) {
380 if ( ! isset( $size['sources'] ) || ! is_array( $size['sources'] ) ) {
381 continue;
382 }
383
384 $original_size_mime = empty( $size['mime-type'] ) ? '' : $size['mime-type'];
385
386 foreach ( $size['sources'] as $mime => $properties ) {
387 /**
388 * When we face the same mime type as the original image, we ignore this file as this file
389 * would be removed when the size is removed by WordPress itself. The meta information as well
390 * would be deleted as soon as the image is removed.
391 *
392 * @see wp_delete_attachment
393 */
394 if ( $original_size_mime === $mime ) {
395 continue;
396 }
397
398 if ( ! is_array( $properties ) || empty( $properties['file'] ) ) {
399 continue;
400 }
401
402 $intermediate_file = str_replace( $basename, $properties['file'], $file );
403 if ( '' === $intermediate_file ) {
404 continue;
405 }
406
407 $intermediate_file = path_join( $upload_path['basedir'], $intermediate_file );
408 if ( ! file_exists( $intermediate_file ) ) {
409 continue;
410 }
411
412 wp_delete_file_from_directory( $intermediate_file, $intermediate_dir );
413 }
414 }
415
416 if ( ! isset( $metadata['sources'] ) || ! is_array( $metadata['sources'] ) ) {
417 return;
418 }
419
420 $original_mime_from_post = get_post_mime_type( $attachment_id );
421 $original_mime_from_file = wp_check_filetype( $file )['type'];
422
423 // Delete full sizes mime types.
424 foreach ( $metadata['sources'] as $mime => $properties ) {
425 // Don't remove the image with the same mime type as the original image as this would be removed by WordPress.
426 if ( $mime === $original_mime_from_post || $mime === $original_mime_from_file ) {
427 continue;
428 }
429
430 if ( ! is_array( $properties ) || empty( $properties['file'] ) ) {
431 continue;
432 }
433
434 $full_size = str_replace( $basename, $properties['file'], $file );
435 if ( '' === $full_size ) {
436 continue;
437 }
438
439 $full_size_file = path_join( $upload_path['basedir'], $full_size );
440 if ( ! file_exists( $full_size_file ) ) {
441 continue;
442 }
443 wp_delete_file_from_directory( $full_size_file, $intermediate_dir );
444 }
445
446 $backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
447 $backup_sizes = is_array( $backup_sizes ) ? $backup_sizes : array();
448
449 foreach ( $backup_sizes as $backup_size ) {
450 if ( ! isset( $backup_size['sources'] ) || ! is_array( $backup_size['sources'] ) ) {
451 continue;
452 }
453
454 $original_backup_size_mime = empty( $backup_size['mime-type'] ) ? '' : $backup_size['mime-type'];
455
456 foreach ( $backup_size['sources'] as $backup_mime => $backup_properties ) {
457 /**
458 * When we face the same mime type as the original image, we ignore this file as this file
459 * would be removed when the size is removed by WordPress itself. The meta information as well
460 * would be deleted as soon as the image is removed.
461 *
462 * @see wp_delete_attachment
463 */
464 if ( $original_backup_size_mime === $backup_mime ) {
465 continue;
466 }
467
468 if ( ! is_array( $backup_properties ) || empty( $backup_properties['file'] ) ) {
469 continue;
470 }
471
472 $backup_intermediate_file = str_replace( $basename, $backup_properties['file'], $file );
473 if ( empty( $backup_intermediate_file ) ) {
474 continue;
475 }
476
477 $backup_intermediate_file = path_join( $upload_path['basedir'], $backup_intermediate_file );
478 if ( ! file_exists( $backup_intermediate_file ) ) {
479 continue;
480 }
481
482 wp_delete_file_from_directory( $backup_intermediate_file, $intermediate_dir );
483 }
484 }
485
486 $backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
487 $backup_sources = is_array( $backup_sources ) ? $backup_sources : array();
488
489 // Delete full sizes backup mime types.
490 foreach ( $backup_sources as $backup_mimes ) {
491
492 foreach ( $backup_mimes as $backup_mime_properties ) {
493 if ( ! is_array( $backup_mime_properties ) || empty( $backup_mime_properties['file'] ) ) {
494 continue;
495 }
496
497 $full_size = str_replace( $basename, $backup_mime_properties['file'], $file );
498 if ( empty( $full_size ) ) {
499 continue;
500 }
501
502 $full_size_file = path_join( $upload_path['basedir'], $full_size );
503 if ( ! file_exists( $full_size_file ) ) {
504 continue;
505 }
506 wp_delete_file_from_directory( $full_size_file, $intermediate_dir );
507 }
508 }
509 }
510 add_action( 'delete_attachment', 'webp_uploads_remove_sources_files', 10, 1 );
511
512 /**
513 * Filters `the_content` to update images so that they use the preferred MIME type where possible.
514 *
515 * By default, this is `image/webp`, if the current attachment contains the targeted MIME
516 * type. In the near future this will be filterable.
517 *
518 * Note that most of this function will not be needed for an eventual core implementation as it
519 * would rely on `wp_filter_content_tags()`.
520 *
521 * @since 1.0.0
522 *
523 * @see wp_filter_content_tags()
524 *
525 * @param string|mixed $content The content of the current post.
526 * @return string The content with the updated references to the images.
527 */
528 function webp_uploads_update_image_references( $content ): string {
529 if ( ! is_string( $content ) ) {
530 $content = '';
531 }
532
533 // Bail early if request is not for the frontend.
534 if ( ! webp_uploads_in_frontend_body() ) {
535 return $content;
536 }
537
538 // This content does not have any tag on it, move forward.
539 // TODO: Eventually this should use the HTML API to parse out the image tags and then update them.
540 if ( 0 === (int) preg_match_all( '/<(img)\s[^>]+>/', $content, $img_tags, PREG_SET_ORDER ) ) {
541 return $content;
542 }
543
544 $images = array();
545 foreach ( $img_tags as list( $img ) ) {
546 $processor = new WP_HTML_Tag_Processor( $img );
547 if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
548 // This condition won't ever be met since we're iterating over the IMG tags extracted with preg_match_all() above.
549 continue;
550 }
551
552 // Find the ID of each image by the class.
553 // TODO: It would be preferable to use the $processor->class_list() method but there seems to be some typing issues with PHPStan.
554 $class_name = $processor->get_attribute( 'class' );
555 if (
556 ! is_string( $class_name )
557 ||
558 1 !== preg_match( '/(?:^|\s)wp-image-([1-9]\d*)(?:\s|$)/i', $class_name, $matches )
559 ) {
560 continue;
561 }
562
563 // Make sure we use the last item on the list of matches.
564 $images[ $img ] = (int) $matches[1];
565 }
566
567 $attachment_ids = array_unique( array_filter( array_values( $images ) ) );
568 if ( count( $attachment_ids ) > 1 ) {
569 /**
570 * Warm the object cache with post and meta information for all found
571 * images to avoid making individual database calls.
572 */
573 _prime_post_caches( $attachment_ids, false, true );
574 }
575
576 foreach ( $images as $img => $attachment_id ) {
577 $content = str_replace( $img, webp_uploads_img_tag_update_mime_type( $img, 'the_content', $attachment_id ), $content );
578 }
579
580 return $content;
581 }
582
583 /**
584 * Finds all the urls with *.jpg and *.jpeg extension and updates with *.webp version for the provided image
585 * for the specified image sizes, the *.webp references are stored inside of each size.
586 *
587 * @since 1.0.0
588 *
589 * @param string $original_image An <img> tag where the urls would be updated.
590 * @param string $context The context where this is function is being used.
591 * @param int $attachment_id The ID of the attachment being modified.
592 * @return string The updated img tag.
593 */
594 function webp_uploads_img_tag_update_mime_type( string $original_image, string $context, int $attachment_id ): string {
595 $image = $original_image;
596 $metadata = wp_get_attachment_metadata( $attachment_id );
597
598 if ( empty( $metadata['file'] ) ) {
599 return $image;
600 }
601
602 $original_mime = get_post_mime_type( $attachment_id );
603 $target_mimes = webp_uploads_get_content_image_mimes( $attachment_id, $context );
604
605 foreach ( $target_mimes as $target_mime ) {
606 if ( $target_mime === $original_mime ) {
607 continue;
608 }
609
610 if ( ! isset( $metadata['sources'][ $target_mime ]['file'] ) ) {
611 continue;
612 }
613
614 /**
615 * Filter to replace additional image source file, by locating the original
616 * mime types of the file and return correct file path in the end.
617 *
618 * Altering the $image tag through this filter effectively short-circuits the default replacement logic using the preferred MIME type.
619 *
620 * @since 1.1.0
621 *
622 * @param string $image An <img> tag where the urls would be updated.
623 * @param int $attachment_id The ID of the attachment being modified.
624 * @param string $size The size name that would be used to create this image, out of the registered subsizes.
625 * @param string $target_mime The target mime in which the image should be created.
626 * @param string $context The context where this is function is being used.
627 */
628 $filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, 'full', $target_mime, $context );
629
630 // If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
631 if ( $filtered_image === $image ) {
632 $basename = wp_basename( $metadata['file'] );
633 $image = str_replace(
634 $basename,
635 $metadata['sources'][ $target_mime ]['file'],
636 $image
637 );
638 } else {
639 $image = $filtered_image;
640 }
641 }
642
643 if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
644 // Replace sub sizes for the image if present.
645 foreach ( $metadata['sizes'] as $size => $size_data ) {
646
647 if ( empty( $size_data['file'] ) ) {
648 continue;
649 }
650
651 foreach ( $target_mimes as $target_mime ) {
652 if ( $target_mime === $original_mime ) {
653 continue;
654 }
655
656 if ( ! isset( $size_data['sources'][ $target_mime ]['file'] ) ) {
657 continue;
658 }
659
660 if ( $size_data['file'] === $size_data['sources'][ $target_mime ]['file'] ) {
661 continue;
662 }
663
664 /** This filter is documented in plugins/webp-uploads/load.php */
665 $filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, $size, $target_mime, $context );
666
667 // If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
668 if ( $filtered_image === $image ) {
669 $image = str_replace(
670 $size_data['file'],
671 $size_data['sources'][ $target_mime ]['file'],
672 $image
673 );
674 } else {
675 $image = $filtered_image;
676 }
677 }
678 }
679 }
680
681 return $image;
682 }
683
684 /**
685 * Updates the references of the featured image to the a new image format if available, in the same way it
686 * occurs in the_content of a post.
687 *
688 * @since 1.0.0
689 *
690 * @param string $html The current HTML markup of the featured image.
691 * @param int $post_id The current post ID where the featured image is requested.
692 * @param int $attachment_id The ID of the attachment image.
693 * @return string The updated HTML markup.
694 */
695 function webp_uploads_update_featured_image( string $html, int $post_id, int $attachment_id ): string {
696 return webp_uploads_img_tag_update_mime_type( $html, 'post_thumbnail_html', $attachment_id );
697 }
698 add_filter( 'post_thumbnail_html', 'webp_uploads_update_featured_image', 10, 3 );
699
700 /**
701 * Returns an array of image size names that have secondary mime type output enabled. Core sizes and
702 * core theme sizes are enabled by default.
703 *
704 * Developers can control the generation of additional mime images for all sizes using the
705 * webp_uploads_image_sizes_with_additional_mime_type_support filter.
706 *
707 * @since 1.0.0
708 *
709 * @return array<string, bool> An array of image sizes that can have additional mime types.
710 */
711 function webp_uploads_get_image_sizes_additional_mime_type_support(): array {
712 $additional_sizes = wp_get_additional_image_sizes();
713 $allowed_sizes = array(
714 'thumbnail' => true,
715 'medium' => true,
716 'medium_large' => true,
717 'large' => true,
718 'post-thumbnail' => true,
719 );
720
721 foreach ( $additional_sizes as $size => $size_details ) {
722 $allowed_sizes[ $size ] = ! empty( $size_details['provide_additional_mime_types'] );
723 }
724
725 /**
726 * Filters whether additional mime types are allowed for image sizes.
727 *
728 * @since 1.0.0
729 *
730 * @param array<string, bool> $allowed_sizes A map of image size names and whether they are allowed to have additional mime types.
731 */
732 $allowed_sizes = (array) apply_filters( 'webp_uploads_image_sizes_with_additional_mime_type_support', $allowed_sizes );
733
734 return $allowed_sizes;
735 }
736
737 /**
738 * Updates the quality of WebP image sizes generated by WordPress to 82.
739 *
740 * @since 1.0.0
741 *
742 * @param int $quality Quality level between 1 (low) and 100 (high).
743 * @param string $mime_type Image mime type.
744 * @return int The updated quality for mime types.
745 */
746 function webp_uploads_modify_webp_quality( int $quality, string $mime_type ): int {
747 // For WebP images, always return 82 (other MIME types were already using 82 by default anyway).
748 if ( 'image/webp' === $mime_type ) {
749 return 82;
750 }
751
752 // Return default quality for non-WebP images in WP.
753 return $quality;
754 }
755 add_filter( 'wp_editor_set_quality', 'webp_uploads_modify_webp_quality', 10, 2 );
756
757 /**
758 * Displays the HTML generator tag for the Modern Image Formats plugin.
759 *
760 * See {@see 'wp_head'}.
761 *
762 * @since 1.0.0
763 */
764 function webp_uploads_render_generator(): void {
765 // Use the plugin slug as it is immutable.
766 echo '<meta name="generator" content="webp-uploads ' . esc_attr( WEBP_UPLOADS_VERSION ) . '">' . "\n";
767 }
768 add_action( 'wp_head', 'webp_uploads_render_generator' );
769
770 /**
771 * Initializes custom functionality for handling image uploads and content filters.
772 *
773 * @since 2.1.0
774 */
775 function webp_uploads_init(): void {
776 if ( webp_uploads_is_picture_element_enabled() ) {
777 add_filter( 'wp_content_img_tag', 'webp_uploads_wrap_image_in_picture', 10, 3 );
778 } else {
779 add_filter( 'the_content', 'webp_uploads_update_image_references', 13 ); // Run after wp_filter_content_tags.
780 }
781 }
782 add_action( 'init', 'webp_uploads_init' );
783