PluginProbe ʕ •ᴥ•ʔ
Modern Image Formats / 2.6.0
Modern Image Formats v2.6.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 / helper.php
webp-uploads Last commit date
deprecated.php 1 year ago helper.php 10 months ago hooks.php 10 months ago image-edit.php 1 year ago load.php 10 months ago picture-element.php 1 year ago readme.txt 6 months ago rest-api.php 1 year ago settings.php 1 year ago uninstall.php 1 year ago
helper.php
515 lines
1 <?php
2 /**
3 * Helper functions 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 * Returns an array with the list of valid mime types that a specific mime type can be converted into it,
18 * for example an image/jpeg can be converted into an image/webp.
19 *
20 * @since 1.0.0
21 * @since 2.0.0 Added support for AVIF.
22 * @since 2.2.0 Added support for PNG.
23 *
24 * @return array<string, array<string>> An array of valid mime types, where the key is the mime type and the value is the extension type.
25 */
26 function webp_uploads_get_upload_image_mime_transforms(): array {
27
28 // Check the selected output format.
29 $output_format = webp_uploads_mime_type_supported( 'image/avif' ) ? webp_uploads_get_image_output_format() : 'webp';
30
31 $default_transforms = array(
32 'image/jpeg' => array( 'image/' . $output_format ),
33 'image/webp' => array( 'image/' . $output_format ),
34 'image/avif' => array( 'image/avif' ),
35 'image/png' => array( 'image/' . $output_format ),
36 );
37
38 // Check setting for whether to generate both JPEG and the modern output format.
39 if ( webp_uploads_is_fallback_enabled() ) {
40 $default_transforms = array(
41 'image/jpeg' => array( 'image/jpeg', 'image/' . $output_format ),
42 'image/png' => array( 'image/png', 'image/' . $output_format ),
43 'image/' . $output_format => array( 'image/' . $output_format, 'image/jpeg' ),
44 );
45 }
46
47 /**
48 * Filter to allow the definition of a custom mime types, in which a defined mime type
49 * can be transformed and provide a wide range of mime types.
50 *
51 * The order of supported mime types matters. If the original mime type of the uploaded image
52 * is not needed, then the first mime type in the list supported by the image editor will be
53 * selected for the default subsizes.
54 *
55 * @since 1.0.0
56 *
57 * @param array $default_transforms A map with the valid mime transforms.
58 */
59 $transforms = apply_filters( 'webp_uploads_upload_image_mime_transforms', $default_transforms );
60
61 // Return the default mime transforms if a non-array result is returned from the filter.
62 if ( ! is_array( $transforms ) ) {
63 return $default_transforms;
64 }
65
66 // Ensure that all mime types have correct transforms. If a mime type has invalid transforms array,
67 // then fallback to the original mime type to make sure that the correct subsizes are created.
68 foreach ( $transforms as $mime_type => $transform_types ) {
69 if ( ! is_array( $transform_types ) || 0 === count( $transform_types ) ) {
70 $transforms[ $mime_type ] = array( $mime_type );
71 }
72 }
73
74 return $transforms;
75 }
76
77 /**
78 * Creates a resized image with the provided dimensions out of an original attachment, the created image
79 * would be saved in the specified mime and stored in the destination file. If the image can't be saved correctly
80 * a WP_Error would be returned otherwise an array with the file and filesize properties.
81 *
82 * @since 1.0.0
83 * @access private
84 *
85 * @param int $attachment_id The ID of the attachment from where this image would be created.
86 * @param string $image_size The size name that would be used to create the image source, out of the registered subsizes.
87 * @param array{ width: int, height: int, crop: bool|array{string, string}} $size_data An array with the dimensions of the image: height, width and crop.
88 * @param string $mime The target mime in which the image should be created.
89 * @param string|null $destination_file_name The path where the file would be stored, including the extension. If null, `generate_filename` is used to create the destination file name.
90 *
91 * @return array{ file: string, filesize: int }|WP_Error An array with the file and filesize if the image was created correctly, otherwise a WP_Error.
92 */
93 function webp_uploads_generate_additional_image_source( int $attachment_id, string $image_size, array $size_data, string $mime, ?string $destination_file_name = null ) {
94 /**
95 * Filter to allow the generation of additional image sources, in which a defined mime type
96 * can be transformed and create additional mime types for the file.
97 *
98 * Returning an image data array or WP_Error here effectively short-circuits the default logic to generate the image source.
99 *
100 * @since 1.1.0
101 *
102 * @param array{
103 * file: string,
104 * path?: string,
105 * filesize?: int
106 * }|null|WP_Error $image Image data, null, or WP_Error.
107 * @param int $attachment_id The ID of the attachment from where this image would be created.
108 * @param string $image_size The size name that would be used to create this image, out of the registered subsizes.
109 * @param array{
110 * width: int,
111 * height: int,
112 * crop: bool|array{string, string}
113 * } $size_data An array with the dimensions of the image.
114 * @param string $mime The target mime in which the image should be created.
115 */
116 $image = apply_filters( 'webp_uploads_pre_generate_additional_image_source', null, $attachment_id, $image_size, $size_data, $mime );
117 if ( is_wp_error( $image ) ) {
118 return $image;
119 }
120 if ( is_array( $image ) && array_key_exists( 'file', $image ) && is_string( $image['file'] ) ) {
121 // The filtered image provided all we need to short-circuit here.
122 if ( array_key_exists( 'filesize', $image ) && is_int( $image['filesize'] ) && $image['filesize'] > 0 ) {
123 return $image;
124 }
125
126 // Supply the filesize based on the filter-provided path.
127 if ( array_key_exists( 'path', $image ) && is_int( $image['path'] ) ) {
128 $filesize = wp_filesize( $image['path'] );
129 if ( $filesize > 0 ) {
130 return array(
131 'file' => $image['file'],
132 'filesize' => $filesize,
133 );
134 }
135 }
136 }
137
138 $allowed_mimes = array_flip( wp_get_mime_types() );
139 if ( ! isset( $allowed_mimes[ $mime ] ) || ! is_string( $allowed_mimes[ $mime ] ) ) {
140 return new WP_Error( 'image_mime_type_invalid', __( 'The provided mime type is not allowed.', 'webp-uploads' ) );
141 }
142
143 if ( ! wp_image_editor_supports( array( 'mime_type' => $mime ) ) ) {
144 return new WP_Error( 'image_mime_type_not_supported', __( 'The provided mime type is not supported.', 'webp-uploads' ) );
145 }
146
147 $image_path = wp_get_original_image_path( $attachment_id );
148 if ( false === $image_path || ! file_exists( $image_path ) ) {
149 return new WP_Error( 'original_image_file_not_found', __( 'The original image file does not exists, subsizes are created out of the original image.', 'webp-uploads' ) );
150 }
151
152 $editor = wp_get_image_editor( $image_path, array( 'mime_type' => $mime ) );
153 if ( is_wp_error( $editor ) ) {
154 return $editor;
155 }
156
157 $height = isset( $size_data['height'] ) ? (int) $size_data['height'] : 0;
158 $width = isset( $size_data['width'] ) ? (int) $size_data['width'] : 0;
159 $crop = isset( $size_data['crop'] ) ? $size_data['crop'] : false;
160 if ( $width <= 0 && $height <= 0 ) {
161 return new WP_Error( 'image_wrong_dimensions', __( 'At least one of the dimensions must be a positive number.', 'webp-uploads' ) );
162 }
163
164 $image_meta = wp_get_attachment_metadata( $attachment_id );
165 // If stored EXIF data exists, rotate the source image before creating sub-sizes.
166 if ( isset( $image_meta['image_meta'] ) && is_array( $image_meta['image_meta'] ) && count( $image_meta['image_meta'] ) > 0 ) {
167 $editor->maybe_exif_rotate();
168 }
169
170 $editor->resize( $width, $height, $crop );
171
172 if ( null === $destination_file_name ) {
173 $ext = pathinfo( $image_path, PATHINFO_EXTENSION );
174 $suffix = $editor->get_suffix();
175 $suffix .= "-{$ext}";
176 $extension = explode( '|', $allowed_mimes[ $mime ] );
177 $destination_file_name = $editor->generate_filename( $suffix, null, $extension[0] );
178 }
179
180 remove_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10 );
181 $image = $editor->save( $destination_file_name, $mime );
182 add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
183
184 if ( is_wp_error( $image ) ) {
185 return $image;
186 }
187
188 if ( ! isset( $image['file'] ) || ! is_string( $image['file'] ) || '' === $image['file'] ) {
189 return new WP_Error( 'image_file_not_present', __( 'The file key is not present on the image data', 'webp-uploads' ) );
190 }
191
192 return array(
193 'file' => $image['file'],
194 'filesize' => isset( $image['path'] ) ? wp_filesize( $image['path'] ) : 0,
195 );
196 }
197
198 /**
199 * Creates a new image based of the specified attachment with a defined mime type
200 * this image would be stored in the same place as the provided size name inside the
201 * metadata of the attachment.
202 *
203 * @since 1.0.0
204 *
205 * @see wp_create_image_subsizes()
206 *
207 * @param int $attachment_id The ID of the attachment we are going to use as a reference to create the image.
208 * @param string $size The size name that would be used to create this image, out of the registered subsizes.
209 * @param string $mime A mime type we are looking to use to create this image.
210 *
211 * @return array{ file: string, filesize: int }|WP_Error
212 */
213 function webp_uploads_generate_image_size( int $attachment_id, string $size, string $mime ) {
214 $sizes = wp_get_registered_image_subsizes();
215 $metadata = wp_get_attachment_metadata( $attachment_id );
216
217 if (
218 ! isset( $metadata['sizes'][ $size ], $sizes[ $size ] )
219 || ! is_array( $metadata['sizes'][ $size ] )
220 || ! is_array( $sizes[ $size ] )
221 ) {
222 return new WP_Error( 'image_mime_type_invalid_metadata', __( 'The image does not have a valid metadata.', 'webp-uploads' ) );
223 }
224
225 $size_data = array(
226 'width' => 0,
227 'height' => 0,
228 'crop' => false,
229 );
230
231 if ( isset( $sizes[ $size ]['width'] ) ) {
232 $size_data['width'] = $sizes[ $size ]['width'];
233 } elseif ( isset( $metadata['sizes'][ $size ]['width'] ) ) {
234 $size_data['width'] = $metadata['sizes'][ $size ]['width'];
235 }
236
237 if ( isset( $sizes[ $size ]['height'] ) ) {
238 $size_data['height'] = $sizes[ $size ]['height'];
239 } elseif ( isset( $metadata['sizes'][ $size ]['height'] ) ) {
240 $size_data['height'] = $metadata['sizes'][ $size ]['height'];
241 }
242
243 if ( isset( $sizes[ $size ]['crop'] ) ) {
244 $size_data['crop'] = $sizes[ $size ]['crop'];
245 }
246
247 return webp_uploads_generate_additional_image_source( $attachment_id, $size, $size_data, $mime );
248 }
249
250 /**
251 * Returns mime types that should be used for an image in the specific context.
252 *
253 * @since 1.0.0
254 *
255 * @param int $attachment_id The attachment ID.
256 * @param string $context The current context.
257 * @return string[] Mime types to use for the image.
258 */
259 function webp_uploads_get_content_image_mimes( int $attachment_id, string $context ): array {
260 $target_mimes = array( 'image/' . webp_uploads_get_image_output_format(), 'image/jpeg' );
261
262 /**
263 * Filters mime types that should be used to update all images in the content. The order of
264 * mime types matters. The first mime type in the list will be used if it is supported by an image.
265 *
266 * @since 1.0.0
267 *
268 * @param array $target_mimes The list of mime types that can be used to update images in the content.
269 * @param int $attachment_id The attachment ID.
270 * @param string $context The current context.
271 */
272 $target_mimes = apply_filters( 'webp_uploads_content_image_mimes', $target_mimes, $attachment_id, $context );
273 if ( ! is_array( $target_mimes ) ) {
274 $target_mimes = array();
275 }
276
277 return $target_mimes;
278 }
279
280 /**
281 * Verifies if the request is for a frontend context within the <body> tag.
282 *
283 * @since 1.0.0
284 *
285 * @global WP_Query $wp_query WordPress Query object.
286 *
287 * @return bool True if in the <body> within a frontend request, false otherwise.
288 */
289 function webp_uploads_in_frontend_body(): bool {
290 global $wp_query;
291
292 // Check if this request is generally outside (or before) any frontend context.
293 if ( ! isset( $wp_query ) || defined( 'REST_REQUEST' ) || defined( 'XMLRPC_REQUEST' ) || is_feed() ) {
294 return false;
295 }
296
297 // Check if we're anywhere before 'template_redirect' or within the 'wp_head' action.
298 if ( 0 === did_action( 'template_redirect' ) || doing_action( 'wp_head' ) ) {
299 return false;
300 }
301
302 return true;
303 }
304
305 /**
306 * Check whether the additional image is larger than the original image.
307 *
308 * @since 1.0.0
309 *
310 * @param array{ filesize?: int } $original An array with the metadata of the attachment.
311 * @param array{ filesize?: int } $additional An array containing the filename and file size for additional mime.
312 * @return bool True if the additional image is larger than the original image, otherwise false.
313 */
314 function webp_uploads_should_discard_additional_image_file( array $original, array $additional ): bool {
315 $original_image_filesize = isset( $original['filesize'] ) ? (int) $original['filesize'] : 0;
316 $additional_image_filesize = isset( $additional['filesize'] ) ? (int) $additional['filesize'] : 0;
317 if ( $original_image_filesize > 0 && $additional_image_filesize > 0 ) {
318 /**
319 * Filter whether WebP images that are larger than the matching JPEG should be discarded.
320 *
321 * By default the performance lab plugin will use the mime type with the smaller filesize
322 * rather than defaulting to `webp`.
323 *
324 * @since 1.0.0
325 *
326 * @param bool $preferred_filesize Prioritize file size over mime type. Default true.
327 */
328 $webp_discard_larger_images = apply_filters( 'webp_uploads_discard_larger_generated_images', true );
329
330 if ( $webp_discard_larger_images && $additional_image_filesize >= $original_image_filesize ) {
331 return true;
332 }
333 }
334 return false;
335 }
336
337 /**
338 * Checks if a mime type is supported by the server.
339 *
340 * Includes special handling for false positives on AVIF support.
341 *
342 * @since 2.0.0
343 *
344 * @param string $mime_type The mime type to check.
345 * @return bool Whether the server supports a given mime type.
346 */
347 function webp_uploads_mime_type_supported( string $mime_type ): bool {
348 if ( ! wp_image_editor_supports( array( 'mime_type' => $mime_type ) ) ) {
349 return false;
350 }
351
352 // In certain server environments Image editors can report a false positive for AVIF support.
353 if ( 'image/avif' === $mime_type ) {
354 $editor = _wp_image_editor_choose( array( 'mime_type' => 'image/avif' ) );
355 if ( false === $editor ) {
356 return false;
357 }
358 if ( is_a( $editor, WP_Image_Editor_GD::class, true ) ) {
359 return function_exists( 'imageavif' );
360 }
361 if ( is_a( $editor, WP_Image_Editor_Imagick::class, true ) && class_exists( 'Imagick' ) ) {
362 return 0 !== count( Imagick::queryFormats( 'AVIF' ) );
363 }
364 }
365
366 return true;
367 }
368
369 /**
370 * Get the image output format setting from the option. Default is avif.
371 *
372 * @since 2.0.0
373 *
374 * @return string The image output format. One of 'webp' or 'avif'.
375 */
376 function webp_uploads_get_image_output_format(): string {
377 $image_format = get_option( 'perflab_modern_image_format' );
378 return webp_uploads_sanitize_image_format( $image_format );
379 }
380
381 /**
382 * Sanitizes the image format.
383 *
384 * @since 2.0.0
385 *
386 * @param string|mixed $image_format The image format to check.
387 * @return string Supported image format.
388 */
389 function webp_uploads_sanitize_image_format( $image_format ): string {
390 return in_array( $image_format, array( 'webp', 'avif' ), true ) ? $image_format : 'webp';
391 }
392
393 /**
394 * Checks if the `webp_uploads_use_picture_element` option is enabled.
395 *
396 * @since 2.0.0
397 *
398 * @return bool True if the option is enabled, false otherwise.
399 */
400 function webp_uploads_is_picture_element_enabled(): bool {
401 return webp_uploads_is_fallback_enabled() && (bool) get_option( 'webp_uploads_use_picture_element', false );
402 }
403
404 /**
405 * Checks if the `perflab_generate_webp_and_jpeg` option is enabled.
406 *
407 * @since 2.0.0
408 * @since 2.2.0 Renamed to webp_uploads_is_fallback_enabled().
409 *
410 * @return bool True if the option is enabled, false otherwise.
411 */
412 function webp_uploads_is_fallback_enabled(): bool {
413 return (bool) get_option( 'perflab_generate_webp_and_jpeg' );
414 }
415
416 /**
417 * Checks if the `perflab_generate_all_fallback_sizes` option is enabled.
418 *
419 * @since 2.4.0
420 *
421 * @return bool Whether the option is enabled. Default is false.
422 */
423 function webp_uploads_should_generate_all_fallback_sizes(): bool {
424 return (bool) get_option( 'perflab_generate_all_fallback_sizes', 0 );
425 }
426
427 /**
428 * Retrieves the image URL for a specified MIME type from the attachment metadata.
429 *
430 * This function attempts to locate an alternate image source URL in the
431 * attachment's metadata that matches the provided MIME type.
432 *
433 * @since 2.2.0
434 *
435 * @param int $attachment_id The ID of the attachment.
436 * @param string $src The original image src url.
437 * @param string $mime A mime type we are looking to get image url.
438 * @return non-empty-string|null Returns mime type image if available.
439 */
440 function webp_uploads_get_mime_type_image( int $attachment_id, string $src, string $mime ): ?string {
441 $metadata = wp_get_attachment_metadata( $attachment_id );
442 $src_basename = wp_basename( $src );
443 if ( isset( $metadata['sources'][ $mime ]['file'] ) ) {
444 $basename = wp_basename( $metadata['file'] );
445
446 if ( $src_basename === $basename ) {
447 $result = str_replace(
448 $basename,
449 $metadata['sources'][ $mime ]['file'],
450 $src
451 );
452 return '' === $result ? null : $result;
453 }
454 }
455
456 if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
457 foreach ( $metadata['sizes'] as $size => $size_data ) {
458
459 if ( ! isset( $size_data['file'] ) ) {
460 continue;
461 }
462
463 if ( ! isset( $size_data['sources'][ $mime ]['file'] ) ) {
464 continue;
465 }
466
467 if ( $size_data['file'] === $size_data['sources'][ $mime ]['file'] ) {
468 continue;
469 }
470
471 if ( $src_basename !== $size_data['file'] ) {
472 continue;
473 }
474
475 $result = str_replace(
476 $size_data['file'],
477 $size_data['sources'][ $mime ]['file'],
478 $src
479 );
480 return '' === $result ? null : $result;
481 }
482 }
483
484 return null;
485 }
486
487 /**
488 * Retrieves the MIME type of an attachment file, checking the file directly if possible.
489 *
490 * If checking the file directly fails, the function falls back to the attachment's MIME type.
491 *
492 * @since 2.3.0
493 *
494 * @param int $attachment_id The attachment ID.
495 * @param string $file Optional. The path to the file.
496 * @return string The MIME type of the file, or an empty string if not found.
497 */
498 function webp_uploads_get_attachment_file_mime_type( int $attachment_id, string $file = '' ): string {
499 if ( '' === $file ) {
500 $file = get_attached_file( $attachment_id, true );
501 // File does not exist.
502 if ( false === $file ) {
503 return '';
504 }
505 }
506
507 /*
508 * We need to get the MIME type ideally from the file, as WordPress Core may have already altered it.
509 * The post MIME type is typically not updated during that process.
510 */
511 $filetype = wp_check_filetype( $file );
512 $mime_type = $filetype['type'] ?? get_post_mime_type( $attachment_id );
513 return is_string( $mime_type ) ? $mime_type : '';
514 }
515