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