PluginProbe ʕ •ᴥ•ʔ
Modern Image Formats / 2.7.0
Modern Image Formats v2.7.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 14 hours ago helper.php 14 hours ago hooks.php 14 hours ago image-edit.php 14 hours ago load.php 14 hours ago picture-element.php 14 hours ago readme.txt 14 hours ago rest-api.php 14 hours ago settings.php 14 hours ago uninstall.php 14 hours ago
hooks.php
1018 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 declare( strict_types = 1 );
11
12 // @codeCoverageIgnoreStart
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16 // @codeCoverageIgnoreEnd
17
18 /**
19 * Hook called by `wp_generate_attachment_metadata` to create the `sources` property for every image
20 * size, the sources' property would create a new image size with all the mime types specified in
21 * `webp_uploads_get_upload_image_mime_transforms`. If the original image is one of the mimes from
22 * `webp_uploads_get_upload_image_mime_transforms` the image is just added to the `sources` property and not
23 * created again. If the uploaded attachment is not a supported mime by this function, the hook does not alter the
24 * metadata of the attachment. In addition to every single size the `sources` property is added at the
25 * top level of the image metadata to store the references for all the mime types for the `full` size image of the
26 * attachment.
27 *
28 * @since 1.0.0
29 *
30 * @see wp_generate_attachment_metadata()
31 * @see webp_uploads_get_upload_image_mime_transforms()
32 *
33 * @phpstan-param array{
34 * width: int,
35 * height: int,
36 * file: string,
37 * sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string }>,
38 * image_meta: array<string, mixed>,
39 * filesize: int
40 * } $metadata
41 *
42 * @param array<string, mixed> $metadata An array with the metadata from this attachment.
43 * @param int $attachment_id The ID of the attachment where the hook was dispatched.
44 *
45 * @return array{
46 * width: int,
47 * height: int,
48 * file: string,
49 * sizes: array<string, array{ file: string, width: int, height: int, 'mime-type': string, sources?: array<string, array{ file: string, filesize: int }> }>,
50 * image_meta: array<string, mixed>,
51 * filesize: int,
52 * sources?: array<string, array{
53 * file: string,
54 * filesize: int
55 * }>,
56 * ...
57 * } An array with the updated structure for the metadata before is stored in the database.
58 */
59 function webp_uploads_create_sources_property( array $metadata, int $attachment_id ): array {
60 $file = get_attached_file( $attachment_id, true );
61 // File does not exist.
62 if ( false === $file || ! file_exists( $file ) ) {
63 return $metadata;
64 }
65
66 $mime_type = webp_uploads_get_attachment_file_mime_type( $attachment_id, $file );
67 if ( '' === $mime_type ) {
68 return $metadata;
69 }
70
71 $valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
72
73 // Not a supported mime type to create the sources property.
74 if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
75 return $metadata;
76 }
77
78 // Make sure the top level `sources` key is a valid array.
79 if ( ! isset( $metadata['sources'] ) || ! is_array( $metadata['sources'] ) ) {
80 $metadata['sources'] = array();
81 }
82
83 if ( ! isset( $metadata['sources'][ $mime_type ]['file'] ) ) {
84 $metadata['sources'][ $mime_type ] = array(
85 'file' => wp_basename( $file ),
86 'filesize' => wp_filesize( $file ),
87 );
88 wp_update_attachment_metadata( $attachment_id, $metadata );
89 }
90
91 $original_size_data = array(
92 'width' => isset( $metadata['width'] ) ? (int) $metadata['width'] : 0,
93 'height' => isset( $metadata['height'] ) ? (int) $metadata['height'] : 0,
94 'crop' => false,
95 );
96
97 $original_directory = pathinfo( $file, PATHINFO_DIRNAME );
98 $filename = pathinfo( $file, PATHINFO_FILENAME );
99 $ext = pathinfo( $file, PATHINFO_EXTENSION );
100 $allowed_mimes = array_flip( wp_get_mime_types() );
101
102 // Create the sources for the full sized image.
103 foreach ( $valid_mime_transforms[ $mime_type ] as $targeted_mime ) {
104 // If this property exists no need to create the image again.
105 if ( isset( $metadata['sources'][ $targeted_mime ]['file'] ) ) {
106 continue;
107 }
108
109 // The targeted mime is not allowed in the current installation.
110 if ( ! isset( $allowed_mimes[ $targeted_mime ] ) ) {
111 continue;
112 }
113
114 $extension = explode( '|', $allowed_mimes[ $targeted_mime ] );
115 $destination = trailingslashit( $original_directory ) . "{$filename}-{$ext}.{$extension[0]}";
116 $image = webp_uploads_generate_additional_image_source( $attachment_id, 'full', $original_size_data, $targeted_mime, $destination );
117
118 if ( is_wp_error( $image ) ) {
119 continue;
120 }
121
122 if ( webp_uploads_should_discard_additional_image_file( $metadata, $image ) ) {
123 wp_delete_file_from_directory( $destination, $original_directory );
124 continue;
125 }
126
127 $metadata['sources'][ $targeted_mime ] = $image;
128 wp_update_attachment_metadata( $attachment_id, $metadata );
129 }
130
131 // If the original MIME type should not be generated/used, override the main image
132 // with the first MIME type image that actually should be generated. In that case,
133 // the original should be backed up.
134 if (
135 ! in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true ) &&
136 isset( $valid_mime_transforms[ $mime_type ][0] ) &&
137 isset( $allowed_mimes[ $mime_type ] ) &&
138 array_key_exists( 'file', $metadata ) &&
139 is_string( $metadata['file'] )
140 ) {
141 $valid_mime_type = $valid_mime_transforms[ $mime_type ][0];
142
143 // Only do the replacement if the attachment file is still set to the original MIME type one,
144 // and if there is a possible replacement source.
145 $file_data = wp_check_filetype( $metadata['file'], array( $allowed_mimes[ $mime_type ] => $mime_type ) );
146 if ( $file_data['type'] === $mime_type && isset( $metadata['sources'][ $valid_mime_type ] ) ) {
147 $saved_data = array(
148 'path' => trailingslashit( $original_directory ) . $metadata['sources'][ $valid_mime_type ]['file'],
149 'width' => $metadata['width'],
150 'height' => $metadata['height'],
151 );
152
153 $original_image = wp_get_original_image_path( $attachment_id );
154
155 // If WordPress already modified the original itself, keep the original and discard WordPress's generated version.
156 if ( isset( $metadata['original_image'] ) && is_string( $metadata['original_image'] ) && '' !== $metadata['original_image'] ) {
157 $uploadpath = wp_get_upload_dir();
158 $attached_file = get_attached_file( $attachment_id );
159 if ( false !== $attached_file ) {
160 wp_delete_file_from_directory( $attached_file, $uploadpath['basedir'] );
161 }
162 }
163
164 // Replace the attached file with the custom MIME type version.
165 if ( false !== $original_image ) {
166 // @phpstan-ignore no.private.function
167 $metadata = _wp_image_meta_replace_original( $saved_data, $original_image, $metadata, $attachment_id );
168 }
169
170 // Unset sources entry for the original MIME type, then save (to avoid inconsistent data
171 // in case of an error after this logic).
172 unset( $metadata['sources'][ $mime_type ] );
173 wp_update_attachment_metadata( $attachment_id, $metadata );
174 }
175 }
176
177 // Make sure we have some sizes to work with, otherwise avoid any work.
178 if (
179 ! isset( $metadata['sizes'] ) ||
180 ! is_array( $metadata['sizes'] ) ||
181 0 === count( $metadata['sizes'] )
182 ) {
183 return $metadata;
184 }
185
186 $sizes_with_mime_type_support = webp_uploads_get_image_sizes_additional_mime_type_support();
187
188 foreach ( $metadata['sizes'] as $size_name => $properties ) {
189 // Do nothing if this image size is not an array or is not allowed to have additional mime types.
190 if (
191 ! is_array( $properties ) ||
192 ! isset( $sizes_with_mime_type_support[ $size_name ] ) ||
193 false === $sizes_with_mime_type_support[ $size_name ]
194 ) {
195 continue;
196 }
197
198 // Try to find the mime type of the image size.
199 $current_mime = '';
200 if ( isset( $properties['mime-type'] ) ) {
201 $current_mime = $properties['mime-type'];
202 } elseif ( isset( $properties['file'] ) ) {
203 $current_mime = wp_check_filetype( $properties['file'] )['type'];
204 }
205
206 // The mime type can't be determined.
207 if ( ! is_string( $current_mime ) || '' === $current_mime ) {
208 continue;
209 }
210
211 // Ensure a `sources` property exists on the existing size.
212 if ( ! isset( $properties['sources'] ) || ! is_array( $properties['sources'] ) ) {
213 $properties['sources'] = array();
214 }
215
216 if ( ! isset( $properties['sources'][ $current_mime ]['file'] ) && isset( $properties['file'] ) ) {
217 $properties['sources'][ $current_mime ] = array(
218 'file' => $properties['file'],
219 'filesize' => 0,
220 );
221 // Set the filesize from the current mime image.
222 $file_location = path_join( $original_directory, $properties['file'] );
223 if ( file_exists( $file_location ) ) {
224 $properties['sources'][ $current_mime ]['filesize'] = wp_filesize( $file_location );
225 }
226 $metadata['sizes'][ $size_name ] = $properties;
227 wp_update_attachment_metadata( $attachment_id, $metadata );
228 }
229
230 foreach ( $valid_mime_transforms[ $mime_type ] as $mime ) {
231 // If this property exists no need to create the image again.
232 if ( isset( $properties['sources'][ $mime ]['file'] ) ) {
233 continue;
234 }
235
236 $source = webp_uploads_generate_image_size( $attachment_id, $size_name, $mime );
237 if ( is_wp_error( $source ) ) {
238 continue;
239 }
240
241 if ( webp_uploads_should_discard_additional_image_file( $properties, $source ) ) {
242 $destination = path_join( $original_directory, $source['file'] );
243 wp_delete_file_from_directory( $destination, $original_directory );
244 continue;
245 }
246
247 $properties['sources'][ $mime ] = $source;
248 $metadata['sizes'][ $size_name ] = $properties;
249 wp_update_attachment_metadata( $attachment_id, $metadata );
250 }
251
252 $metadata['sizes'][ $size_name ] = $properties;
253 }
254
255 return $metadata;
256 }
257 add_filter( 'wp_generate_attachment_metadata', 'webp_uploads_create_sources_property', 10, 2 );
258
259 /**
260 * Filter on `wp_get_missing_image_subsizes` acting as an action for the logic of the plugin
261 * to determine if additional mime types still need to be created.
262 *
263 * This function only exists to work around a missing filter in WordPress core, to call the above
264 * `webp_uploads_create_sources_property()` function correctly.
265 *
266 * @since 1.0.0
267 *
268 * @see wp_get_missing_image_subsizes()
269 *
270 * @phpstan-param array{
271 * width: int,
272 * height: int,
273 * file: string,
274 * sizes: array<string, array{file: string, width: int, height: int, mime-type: string}>,
275 * image_meta: array<string, mixed>,
276 * filesize: int
277 * } $image_meta
278 *
279 * @param array|mixed $missing_sizes Associative array of arrays of image sub-sizes.
280 * @param array<string, mixed> $image_meta The metadata from the image.
281 * @param int $attachment_id The ID of the attachment.
282 * @return array<string, array{ width: int, height: int, crop: bool }> Associative array of arrays of image sub-sizes.
283 */
284 function webp_uploads_wp_get_missing_image_subsizes( $missing_sizes, array $image_meta, int $attachment_id ): array {
285 if ( ! is_array( $missing_sizes ) ) {
286 $missing_sizes = array();
287 }
288
289 // Only setup the trace array if we no longer have more sizes.
290 if ( count( $missing_sizes ) > 0 ) {
291 return $missing_sizes;
292 }
293
294 /**
295 * The usage of `debug_backtrace` in this particular case is mainly to ensure the call to
296 * `wp_get_missing_image_subsizes()` originated from `wp_update_image_subsizes()`, since only then the
297 * additional image sizes should be generated. `wp_get_missing_image_subsizes()` could also be called
298 * from other places in which case the custom logic should not trigger. In an ideal world an action
299 * would exist in `wp_update_image_subsizes` that runs any time, but the current
300 * `wp_generate_attachment_metadata` filter is skipped when all core sub-sizes have been generated.
301 * An eventual core implementation will not require this workaround. The limit of 10 is used to allow
302 * for some flexibility. While by default the function would be on index 5, other custom code may
303 * cause the index to be slightly higher.
304 *
305 * @see wp_update_image_subsizes()
306 * @see wp_get_missing_image_subsizes()
307 */
308 // PHPCS ignore reason: Only the way to generate missing image subsize if all core sub-sizes have been generated.
309 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
310 $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
311
312 foreach ( $trace as $element ) {
313 if ( 'wp_update_image_subsizes' === $element['function'] ) {
314 webp_uploads_create_sources_property( $image_meta, $attachment_id );
315 break;
316 }
317 }
318
319 return array();
320 }
321 add_filter( 'wp_get_missing_image_subsizes', 'webp_uploads_wp_get_missing_image_subsizes', 10, 3 );
322
323 /**
324 * Filter the image editor default output format mapping to select the most appropriate
325 * output format depending on desired output formats and supported mime types by the image
326 * editor.
327 *
328 * @since 1.0.0
329 *
330 * @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.
331 * @param string|null $filename Path to the image.
332 * @param string|null $mime_type The source image mime type.
333 * @return array<string, string> The new output format mapping.
334 */
335 function webp_uploads_filter_image_editor_output_format( $output_format, ?string $filename, ?string $mime_type ): array {
336 if ( ! is_array( $output_format ) ) {
337 $output_format = array();
338 }
339
340 // Without a known source mime type there is nothing to map.
341 if ( null === $mime_type ) {
342 return $output_format;
343 }
344
345 // Use the original mime type if this type is allowed.
346 $valid_mime_transforms = webp_uploads_get_upload_image_mime_transforms();
347 if (
348 ! isset( $valid_mime_transforms[ $mime_type ] ) ||
349 in_array( $mime_type, $valid_mime_transforms[ $mime_type ], true )
350 ) {
351 return $output_format;
352 }
353
354 // Find the first supported mime type by the image editor to use it as the default one.
355 foreach ( $valid_mime_transforms[ $mime_type ] as $target_mime ) {
356 if ( wp_image_editor_supports( array( 'mime_type' => $target_mime ) ) ) {
357 $output_format[ $mime_type ] = $target_mime;
358 break;
359 }
360 }
361
362 return $output_format;
363 }
364 add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
365
366 /**
367 * Hook fired when an attachment is deleted, this hook is in charge of removing any
368 * additional mime types created by this plugin besides the original image. Any source
369 * with the same as the main image would not be removed by this hook due this file would
370 * be removed by WordPress when the attachment is deleted, usually this happens after this
371 * hook is executed.
372 *
373 * @since 1.0.0
374 *
375 * @see wp_delete_attachment()
376 *
377 * @param int $attachment_id The ID of the attachment the sources are going to be deleted.
378 */
379 function webp_uploads_remove_sources_files( int $attachment_id ): void {
380 $file = get_attached_file( $attachment_id );
381
382 if ( false === (bool) $file ) {
383 return;
384 }
385
386 $metadata = wp_get_attachment_metadata( $attachment_id );
387 // Make sure $sizes is always defined to allow the removal of original images after the first foreach loop.
388 $sizes = ! isset( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ? array() : $metadata['sizes'];
389
390 $upload_path = wp_get_upload_dir();
391 if (
392 ! isset( $upload_path['basedir'] ) ||
393 ! is_string( $upload_path['basedir'] ) ||
394 '' === $upload_path['basedir']
395 ) {
396 return;
397 }
398
399 $intermediate_dir = path_join( $upload_path['basedir'], dirname( $file ) );
400 $basename = wp_basename( $file );
401
402 foreach ( $sizes as $size ) {
403 if ( ! isset( $size['sources'] ) || ! is_array( $size['sources'] ) ) {
404 continue;
405 }
406
407 $original_size_mime = isset( $size['mime-type'] ) && is_string( $size['mime-type'] ) ? $size['mime-type'] : '';
408
409 foreach ( $size['sources'] as $mime => $properties ) {
410 /**
411 * When we face the same mime type as the original image, we ignore this file as this file
412 * would be removed when the size is removed by WordPress itself. The meta information as well
413 * would be deleted as soon as the image is removed.
414 *
415 * @see wp_delete_attachment
416 */
417 if ( $original_size_mime === $mime ) {
418 continue;
419 }
420
421 if (
422 ! isset( $properties['file'] ) ||
423 ! is_string( $properties['file'] ) ||
424 '' === $properties['file']
425 ) {
426 continue;
427 }
428
429 $intermediate_file = str_replace( $basename, $properties['file'], $file );
430 if ( '' === $intermediate_file ) {
431 continue;
432 }
433
434 $intermediate_file = path_join( $upload_path['basedir'], $intermediate_file );
435 if ( ! file_exists( $intermediate_file ) ) {
436 continue;
437 }
438
439 wp_delete_file_from_directory( $intermediate_file, $intermediate_dir );
440 }
441 }
442
443 if ( ! isset( $metadata['sources'] ) || ! is_array( $metadata['sources'] ) ) {
444 return;
445 }
446
447 $original_mime_from_post = get_post_mime_type( $attachment_id );
448 $original_mime_from_file = wp_check_filetype( $file )['type'];
449
450 // Delete full sizes mime types.
451 foreach ( $metadata['sources'] as $mime => $properties ) {
452 // Don't remove the image with the same mime type as the original image as this would be removed by WordPress.
453 if ( $mime === $original_mime_from_post || $mime === $original_mime_from_file ) {
454 continue;
455 }
456
457 if (
458 ! isset( $properties['file'] ) ||
459 ! is_string( $properties['file'] ) ||
460 '' === $properties['file']
461 ) {
462 continue;
463 }
464
465 $full_size = str_replace( $basename, $properties['file'], $file );
466 if ( '' === $full_size ) {
467 continue;
468 }
469
470 $full_size_file = path_join( $upload_path['basedir'], $full_size );
471 if ( ! file_exists( $full_size_file ) ) {
472 continue;
473 }
474 wp_delete_file_from_directory( $full_size_file, $intermediate_dir );
475 }
476
477 $backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
478 $backup_sizes = is_array( $backup_sizes ) ? $backup_sizes : array();
479
480 foreach ( $backup_sizes as $backup_size ) {
481 if ( ! isset( $backup_size['sources'] ) || ! is_array( $backup_size['sources'] ) ) {
482 continue;
483 }
484
485 $original_backup_size_mime = isset( $backup_size['mime-type'] ) && is_string( $backup_size['mime-type'] ) ? $backup_size['mime-type'] : '';
486
487 foreach ( $backup_size['sources'] as $backup_mime => $backup_properties ) {
488 /**
489 * When we face the same mime type as the original image, we ignore this file as this file
490 * would be removed when the size is removed by WordPress itself. The meta information as well
491 * would be deleted as soon as the image is removed.
492 *
493 * @see wp_delete_attachment
494 */
495 if ( $original_backup_size_mime === $backup_mime ) {
496 continue;
497 }
498
499 if (
500 ! isset( $backup_properties['file'] ) ||
501 ! is_string( $backup_properties['file'] ) ||
502 '' === $backup_properties['file']
503 ) {
504 continue;
505 }
506
507 $backup_intermediate_file = str_replace( $basename, $backup_properties['file'], $file );
508 if ( '' === $backup_intermediate_file ) {
509 continue;
510 }
511
512 $backup_intermediate_file = path_join( $upload_path['basedir'], $backup_intermediate_file );
513 if ( ! file_exists( $backup_intermediate_file ) ) {
514 continue;
515 }
516
517 wp_delete_file_from_directory( $backup_intermediate_file, $intermediate_dir );
518 }
519 }
520
521 $backup_sources = get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true );
522 $backup_sources = is_array( $backup_sources ) ? $backup_sources : array();
523
524 // Delete full sizes backup mime types.
525 foreach ( $backup_sources as $backup_mimes ) {
526
527 foreach ( $backup_mimes as $backup_mime_properties ) {
528 if (
529 ! isset( $backup_mime_properties['file'] ) ||
530 ! is_string( $backup_mime_properties['file'] ) ||
531 '' === $backup_mime_properties['file']
532 ) {
533 continue;
534 }
535
536 $full_size = str_replace( $basename, $backup_mime_properties['file'], $file );
537 if ( '' === $full_size ) {
538 continue;
539 }
540
541 $full_size_file = path_join( $upload_path['basedir'], $full_size );
542 if ( ! file_exists( $full_size_file ) ) {
543 continue;
544 }
545 wp_delete_file_from_directory( $full_size_file, $intermediate_dir );
546 }
547 }
548 }
549 add_action( 'delete_attachment', 'webp_uploads_remove_sources_files' );
550
551 /**
552 * Filters `wp_content_img_tag` to update images so that they use the preferred MIME type where possible.
553 *
554 * @since 2.5.0
555 *
556 * @param string $filtered_image Full img tag with attributes that will replace the source img tag.
557 * @param string $context Additional context, like the current filter name or the function name from where this was called.
558 * @param int $attachment_id The image attachment ID. May be 0 in case the image is not an attachment.
559 * @return string The updated IMG tag with references to the new MIME type if available.
560 */
561 function webp_uploads_filter_image_tag( string $filtered_image, string $context, int $attachment_id ): string {
562 // Bail early if request is not for the frontend.
563 if ( ! webp_uploads_in_frontend_body() ) {
564 return $filtered_image;
565 }
566
567 $filtered_image = str_replace( $filtered_image, webp_uploads_img_tag_update_mime_type( $filtered_image, 'the_content', $attachment_id ), $filtered_image );
568
569 return $filtered_image;
570 }
571
572 /**
573 * Filters `wp_get_attachment_image` so `<img>` tags produced outside of post
574 * content (archive templates, page builders, custom loops, featured-image
575 * template calls) also use the preferred MIME type where available.
576 *
577 * Only rewrites the HTML `<img>` (or `<picture>` wrapper in picture-element
578 * mode). Sibling URL-returning functions such as `wp_get_attachment_image_url()`
579 * and `wp_get_attachment_image_src()` are intentionally left untouched, since
580 * their return values feed non-HTML contexts (OG tags, RSS, JSON) where
581 * silently substituting a modern format is unsafe.
582 *
583 * @since 2.7.0
584 *
585 * @param string $html HTML img element or empty string on failure.
586 * @param int $attachment_id Image attachment ID.
587 * @param string|array{int, int} $size Requested image size.
588 * @param bool $icon Whether the image should fall back to a mime type icon.
589 * @param array<string, string> $attr Array of attribute values for the image markup, keyed by attribute name.
590 * @phpstan-param int<1, max> $attachment_id
591 * @return string The filtered HTML.
592 */
593 function webp_uploads_filter_wp_get_attachment_image( string $html, int $attachment_id, $size, bool $icon, array $attr ): string {
594 if ( '' === $html || 0 === $attachment_id || true === $icon || ! webp_uploads_in_frontend_body() ) {
595 return $html;
596 }
597
598 /**
599 * Filters whether the Modern Image Formats plugin should rewrite an image returned by `wp_get_attachment_image()`.
600 *
601 * Returning false short-circuits the rewrite and preserves the original HTML. This gives
602 * integrators a surgical per-call opt-out in addition to `remove_filter()`.
603 *
604 * @since 2.7.0
605 *
606 * @param bool $should_filter Whether to apply modern-format rewriting. Default true.
607 * @param int<1, max> $attachment_id Image attachment ID.
608 * @param string|array{int, int} $size Requested image size.
609 * @param array<string, string> $attr Attribute array passed to `wp_get_attachment_image()`.
610 */
611 if ( ! apply_filters( 'webp_uploads_filter_wp_get_attachment_image', true, $attachment_id, $size, $attr ) ) {
612 return $html;
613 }
614
615 if ( webp_uploads_is_picture_element_enabled() ) {
616 return webp_uploads_wrap_image_in_picture( $html, 'wp_get_attachment_image', $attachment_id );
617 }
618
619 return webp_uploads_img_tag_update_mime_type( $html, 'wp_get_attachment_image', $attachment_id );
620 }
621
622 /**
623 * Finds all the urls with *.jpg and *.jpeg extension and updates with *.webp version for the provided image
624 * for the specified image sizes, the *.webp references are stored inside of each size.
625 *
626 * @since 1.0.0
627 *
628 * @param string $original_image An <img> tag where the urls would be updated.
629 * @param string $context The context where this is function is being used.
630 * @param int $attachment_id The ID of the attachment being modified.
631 * @return string The updated img tag.
632 */
633 function webp_uploads_img_tag_update_mime_type( string $original_image, string $context, int $attachment_id ): string {
634 $image = $original_image;
635 $metadata = wp_get_attachment_metadata( $attachment_id );
636
637 if ( ! isset( $metadata['file'] ) || ! is_string( $metadata['file'] ) || '' === $metadata['file'] ) {
638 return $image;
639 }
640
641 $original_mime = get_post_mime_type( $attachment_id );
642 $target_mimes = webp_uploads_get_content_image_mimes( $attachment_id, $context );
643
644 foreach ( $target_mimes as $target_mime ) {
645 if ( $target_mime === $original_mime ) {
646 continue;
647 }
648
649 if ( ! isset( $metadata['sources'][ $target_mime ]['file'] ) ) {
650 continue;
651 }
652
653 /**
654 * Filter to replace additional image source file, by locating the original
655 * mime types of the file and return correct file path in the end.
656 *
657 * Altering the $image tag through this filter effectively short-circuits the default replacement logic using the preferred MIME type.
658 *
659 * @since 1.1.0
660 *
661 * @param string $image An <img> tag where the urls would be updated.
662 * @param int $attachment_id The ID of the attachment being modified.
663 * @param string $size The size name that would be used to create this image, out of the registered subsizes.
664 * @param string $target_mime The target mime in which the image should be created.
665 * @param string $context The context where this is function is being used.
666 */
667 $filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, 'full', $target_mime, $context );
668
669 // If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
670 if ( $filtered_image === $image ) {
671 $basename = wp_basename( $metadata['file'] );
672 $image = str_replace(
673 $basename,
674 $metadata['sources'][ $target_mime ]['file'],
675 $image
676 );
677 } else {
678 $image = $filtered_image;
679 }
680 }
681
682 if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
683 // Replace sub sizes for the image if present.
684 foreach ( $metadata['sizes'] as $size => $size_data ) {
685
686 if ( ! isset( $size_data['file'] ) || ! is_string( $size_data['file'] ) || '' === $size_data['file'] ) {
687 continue;
688 }
689
690 foreach ( $target_mimes as $target_mime ) {
691 if ( $target_mime === $original_mime ) {
692 continue;
693 }
694
695 if ( ! isset( $size_data['sources'][ $target_mime ]['file'] ) ) {
696 continue;
697 }
698
699 if ( $size_data['file'] === $size_data['sources'][ $target_mime ]['file'] ) {
700 continue;
701 }
702
703 /** This filter is documented in plugins/webp-uploads/load.php */
704 $filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, $size, $target_mime, $context );
705
706 // If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
707 if ( $filtered_image === $image ) {
708 $image = str_replace(
709 $size_data['file'],
710 $size_data['sources'][ $target_mime ]['file'],
711 $image
712 );
713 } else {
714 $image = $filtered_image;
715 }
716 }
717 }
718 }
719
720 return $image;
721 }
722
723 /**
724 * Returns an array of image size names that have secondary mime type output enabled. Core sizes and
725 * core theme sizes are enabled by default.
726 *
727 * Developers can control the generation of additional mime images for all sizes using the
728 * webp_uploads_image_sizes_with_additional_mime_type_support filter.
729 *
730 * @since 1.0.0
731 *
732 * @return array<string, bool> An array of image sizes that can have additional mime types.
733 */
734 function webp_uploads_get_image_sizes_additional_mime_type_support(): array {
735 $additional_sizes = wp_get_additional_image_sizes();
736 $allowed_sizes = array(
737 'thumbnail' => true,
738 'medium' => true,
739 'medium_large' => true,
740 'large' => true,
741 'post-thumbnail' => true,
742 );
743
744 foreach ( $additional_sizes as $size => $size_details ) {
745 $allowed_sizes[ $size ] = isset( $size_details['provide_additional_mime_types'] ) && true === (bool) $size_details['provide_additional_mime_types'];
746 }
747
748 /**
749 * Filters whether additional mime types are allowed for image sizes.
750 *
751 * @since 1.0.0
752 *
753 * @param array<string, bool> $allowed_sizes A map of image size names and whether they are allowed to have additional mime types.
754 */
755 $allowed_sizes = (array) apply_filters( 'webp_uploads_image_sizes_with_additional_mime_type_support', $allowed_sizes );
756
757 return $allowed_sizes;
758 }
759
760 /**
761 * Updates the quality of WebP image sizes generated by WordPress to 82.
762 *
763 * @since 1.0.0
764 *
765 * @param int $quality Quality level between 1 (low) and 100 (high).
766 * @param string $mime_type Image mime type.
767 * @return int The updated quality for mime types.
768 */
769 function webp_uploads_modify_webp_quality( int $quality, string $mime_type ): int {
770 // For WebP images, always return 82 (other MIME types were already using 82 by default anyway).
771 if ( 'image/webp' === $mime_type ) {
772 return 82;
773 }
774
775 // Return default quality for non-WebP images in WP.
776 return $quality;
777 }
778 add_filter( 'wp_editor_set_quality', 'webp_uploads_modify_webp_quality', 10, 2 );
779
780 /**
781 * Displays the HTML generator tag for the Modern Image Formats plugin.
782 *
783 * See {@see 'wp_head'}.
784 *
785 * @since 1.0.0
786 */
787 function webp_uploads_render_generator(): void {
788 // Use the plugin slug as it is immutable.
789 echo '<meta name="generator" content="webp-uploads ' . esc_attr( WEBP_UPLOADS_VERSION ) . '">' . "\n";
790 }
791 add_action( 'wp_head', 'webp_uploads_render_generator' );
792
793 /**
794 * Process a block's content to handle background images for specific block types.
795 *
796 * This function targets blocks like Cover and Group that may use background images,
797 * converting them to modern image formats when appropriate.
798 *
799 * @since 2.6.0
800 *
801 * @phpstan-param array{
802 * blockName: string|null,
803 * attrs: array{
804 * id?: positive-int,
805 * url?: string,
806 * style?: array{
807 * background?: array{
808 * backgroundImage?: string
809 * }
810 * }
811 * }
812 * } $block
813 *
814 * @param string|mixed $block_content The block content.
815 * @param array<string, mixed> $block The block.
816 * @return string The filtered block content.
817 */
818 function webp_uploads_filter_block_background_images( $block_content, array $block ): string {
819 // Because plugins can do bad things.
820 if ( ! is_string( $block_content ) ) {
821 $block_content = '';
822 }
823
824 // Only run on frontend.
825 if ( ! webp_uploads_in_frontend_body() || '' === $block_content ) {
826 return $block_content;
827 }
828
829 $attachment_id = null;
830 $image_url = null;
831
832 if ( 'core/cover' === $block['blockName'] ) {
833 if ( isset( $block['attrs']['id'], $block['attrs']['url'] ) ) {
834 $attachment_id = $block['attrs']['id'];
835 $image_url = $block['attrs']['url'];
836 }
837 } elseif ( 'core/group' === $block['blockName'] ) {
838 if ( isset( $block['attrs']['style']['background']['backgroundImage']['id'], $block['attrs']['style']['background']['backgroundImage']['url'] ) ) {
839 $attachment_id = $block['attrs']['style']['background']['backgroundImage']['id'];
840 $image_url = $block['attrs']['style']['background']['backgroundImage']['url'];
841 }
842 }
843
844 // Abort if there is no associated background image.
845 if (
846 ! isset( $attachment_id, $image_url ) ||
847 $attachment_id <= 0 ||
848 '' === $image_url ||
849 ! is_array( wp_get_attachment_metadata( $attachment_id ) )
850 ) {
851 return $block_content;
852 }
853
854 $original_mime = get_post_mime_type( $attachment_id );
855 if ( ! is_string( $original_mime ) ) {
856 return $block_content;
857 }
858
859 $target_mimes = webp_uploads_get_content_image_mimes( $attachment_id, 'background_image' );
860
861 foreach ( $target_mimes as $target_mime ) {
862 if ( $target_mime === $original_mime ) {
863 continue;
864 }
865
866 $new_url = webp_uploads_get_mime_type_image( $attachment_id, $image_url, $target_mime );
867 if ( ! is_string( $new_url ) ) {
868 continue;
869 }
870
871 $processor = new WP_HTML_Tag_Processor( $block_content );
872 while ( $processor->next_tag() ) {
873 $style = $processor->get_attribute( 'style' );
874 if ( is_string( $style ) && str_contains( $style, 'background-image:' ) && str_contains( $style, $image_url ) ) {
875 $updated_style = str_replace( $image_url, $new_url, $style );
876 $processor->set_attribute( 'style', $updated_style );
877 $block_content = $processor->get_updated_html();
878 break 2;
879 }
880 }
881 }
882
883 return $block_content;
884 }
885
886 /**
887 * Initializes custom functionality for handling image uploads and content filters.
888 *
889 * @since 2.1.0
890 */
891 function webp_uploads_init(): void {
892 // Filter regular image tags.
893 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 );
894
895 // Filter `<img>` tags produced by template tags, page builders, and any other code path that calls
896 // `wp_get_attachment_image()` directly. `the_post_thumbnail()` also routes through this, so it covers
897 // featured images previously handled by a dedicated `post_thumbnail_html` filter.
898 add_filter( 'wp_get_attachment_image', 'webp_uploads_filter_wp_get_attachment_image', 10, 5 );
899
900 // Filter blocks that may contain background images.
901 add_filter( 'render_block_core/cover', 'webp_uploads_filter_block_background_images', 10, 2 );
902 add_filter( 'render_block_core/group', 'webp_uploads_filter_block_background_images', 10, 2 );
903 }
904 add_action( 'init', 'webp_uploads_init' );
905
906 /**
907 * Automatically opt into extra image sizes when generating fallback images.
908 *
909 * @since 2.4.0
910 *
911 * @global array $_wp_additional_image_sizes Associative array of additional image sizes.
912 */
913 function webp_uploads_opt_in_extra_image_sizes(): void {
914 if ( ! webp_uploads_is_fallback_enabled() ) {
915 return;
916 }
917
918 global $_wp_additional_image_sizes;
919
920 // Modify global to mimic the "hypothetical" WP core API behavior via an additional `add_image_size()` parameter.
921
922 if ( isset( $_wp_additional_image_sizes['1536x1536'] ) && ! isset( $_wp_additional_image_sizes['1536x1536']['provide_additional_mime_types'] ) ) {
923 $_wp_additional_image_sizes['1536x1536']['provide_additional_mime_types'] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
924 }
925
926 if ( isset( $_wp_additional_image_sizes['2048x2048'] ) && ! isset( $_wp_additional_image_sizes['2048x2048']['provide_additional_mime_types'] ) ) {
927 $_wp_additional_image_sizes['2048x2048']['provide_additional_mime_types'] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
928 }
929 }
930 add_action( 'plugins_loaded', 'webp_uploads_opt_in_extra_image_sizes' );
931
932 /**
933 * Enables additional MIME type support for all image sizes based on the generate all fallback sizes settings.
934 *
935 * @since 2.4.0
936 *
937 * @param array<string, bool> $allowed_sizes A map of image size names and whether they are allowed to have additional MIME types.
938 * @return array<string, bool> Modified map of image sizes with additional MIME type support.
939 */
940 function webp_uploads_enable_additional_mime_type_support_for_all_sizes( array $allowed_sizes ): array {
941 if ( ! webp_uploads_should_generate_all_fallback_sizes() ) {
942 return $allowed_sizes;
943 }
944
945 foreach ( array_keys( $allowed_sizes ) as $size ) {
946 $allowed_sizes[ $size ] = true;
947 }
948
949 return $allowed_sizes;
950 }
951 add_filter( 'webp_uploads_image_sizes_with_additional_mime_type_support', 'webp_uploads_enable_additional_mime_type_support_for_all_sizes' );
952
953 /**
954 * Converts palette PNG images to truecolor PNG images.
955 *
956 * GD cannot convert palette-based PNG to WebP/AVIF formats, causing conversion failures.
957 * This function detects and converts palette PNG to truecolor during upload.
958 *
959 * @since 2.6.0
960 *
961 * @param array<string, mixed>|mixed $file The uploaded file data.
962 * @return array<string, mixed> The modified file data.
963 */
964 function webp_uploads_convert_palette_png_to_truecolor( $file ): array {
965 // Because plugins do bad things.
966 if ( ! is_array( $file ) ) {
967 $file = array();
968 }
969 if ( ! isset( $file['tmp_name'], $file['name'] ) ) {
970 return $file;
971 }
972 if ( isset( $file['type'] ) && is_string( $file['type'] ) ) {
973 if ( 'image/png' !== strtolower( $file['type'] ) ) {
974 return $file;
975 }
976 } elseif ( 'image/png' !== wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] )['type'] ) {
977 return $file;
978 }
979
980 $editor = wp_get_image_editor( $file['tmp_name'] );
981
982 if ( is_wp_error( $editor ) || ! $editor instanceof WP_Image_Editor_GD ) {
983 return $file;
984 }
985
986 $image = imagecreatefrompng( $file['tmp_name'] );
987
988 // Check if the image was created successfully.
989 if ( false === $image ) {
990 return $file;
991 }
992
993 // Check if the image is already truecolor.
994 if ( imageistruecolor( $image ) ) {
995 if ( PHP_VERSION_ID < 80000 ) {
996 imagedestroy( $image ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated -- imagedestroy() has no effect as of PHP 8.0.
997 }
998 return $file;
999 }
1000
1001 // Preserve transparency.
1002 imagealphablending( $image, false );
1003 imagesavealpha( $image, true );
1004
1005 // Convert the palette to truecolor.
1006 if ( imagepalettetotruecolor( $image ) ) {
1007 // Overwrite the upload with the new truecolor PNG.
1008 imagepng( $image, $file['tmp_name'] );
1009 }
1010 if ( PHP_VERSION_ID < 80000 ) {
1011 imagedestroy( $image ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated -- imagedestroy() has no effect as of PHP 8.0.
1012 }
1013
1014 return $file;
1015 }
1016 add_filter( 'wp_handle_upload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );
1017 add_filter( 'wp_handle_sideload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );
1018