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