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