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