PluginProbe
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress / 4.1.2
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress v4.1.2
4.1.2 4.1.1 4.1.0 4.0.1 4.0.0 3.3.1 3.3.0 trunk 1.0.0 2.0.0 2.1.0 3.0.0 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8
quickwebp / admin / class-image-optimizer.php

class-image-optimizer.php in QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress 4.1.2, at admin/class-image-optimizer.php

1,456 lines 48.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The core functionality for image optimizing.
5 *
6 * @link http://webdeclic.com
7 * @since 1.0.0
8 *
9 * @package Quickwebp
10 * @subpackage Quickwebp/admin
11 */
12 class Quickwebp_Image_Optimizer {
13
14 /**
15 * The ID of this plugin.
16 *
17 * @since 1.0.0
18 * @access private
19 * @var string $plugin_name The ID of this plugin.
20 */
21 private $plugin_name;
22
23 /**
24 * The version of this plugin.
25 *
26 * @since 1.0.0
27 * @access private
28 * @var string $version The current version of this plugin.
29 */
30 private $version;
31
32 /**
33 * Allowed mime types for the optimazation
34 */
35 public $allowed_mime_types;
36
37 /**
38 * Initialize the class and set its properties.
39 *
40 * @since 1.0.0
41 * @param string $plugin_name The name of this plugin.
42 * @param string $version The version of this plugin.
43 */
44 public function __construct( $plugin_name, $version ) {
45
46 $this->plugin_name = $plugin_name;
47 $this->version = $version;
48 $this->allowed_mime_types = array( 'image/jpeg', 'image/png', 'image/webp', 'image/avif' );
49 }
50
51 /**
52 * Optimize an image added in wp_media
53 */
54 public function image_optimization( $file ) {
55 global $quickwebp_surecart_client;
56
57 quickwebp_debug_log( 'image_optimization:start', array(
58 'file_name' => $file['name'] ?? '',
59 'file_type' => $file['type'] ?? '',
60 ) );
61
62 $settings = $this->get_settings();
63 $image_file = $this->file_is_image( $file, $settings );
64
65 if ( ! $image_file ) {
66 quickwebp_debug_log( 'image_optimization:not_image' );
67 return $file;
68 }
69
70 $mode_enabled = $settings['quickwebp_settings_conversion'];
71 if ( $mode_enabled === '0' ) {
72 quickwebp_debug_log( 'image_optimization:conversion_disabled' );
73 return $image_file;
74 }
75
76 $save_original = is_array( $settings['quickwebp_settings_conversion_save_original'] ) ? $settings['quickwebp_settings_conversion_save_original'] : array();
77 if ( in_array( 'checked', $save_original ) ) {
78 quickwebp_debug_log( 'image_optimization:save_original_enabled' );
79 return $image_file;
80 }
81
82 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
83 $mime_type = wp_get_image_mime( $image_file['tmp_name'] );
84 if ( in_array( 'checked', $ignore_same_format ) ) {
85 if ( '1' == $mode_enabled && 'image/webp' == $mime_type ) {
86 quickwebp_debug_log( 'image_optimization:skip_same_format', array( 'mime_type' => $mime_type ) );
87 return $image_file;
88 } elseif( '2' == $mode_enabled && 'image/avif' == $mime_type ) {
89 quickwebp_debug_log( 'image_optimization:skip_same_format', array( 'mime_type' => $mime_type ) );
90 return $image_file;
91 }
92 }
93
94 $quality = $this->get_the_quality( $settings );
95 $is_pro = false;
96 if ( $quickwebp_surecart_client ) {
97 $is_pro = $quickwebp_surecart_client->license()->is_valid();
98 }
99
100 $image_file['new_size'] = $image_file['size'];
101 $image_file['new_type'] = $image_file['type'];
102
103 if ( '2' === $mode_enabled && $is_pro ) {
104 quickwebp_debug_log( 'image_optimization:try_avif', array( 'mime_type' => $mime_type, 'quality' => $quality ) );
105 $avif_image = $this->create_avif_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
106 if ( $avif_image ) {
107 $image_file['size'] = filesize( $image_file['tmp_name'] );
108 $image_file['type'] = 'image/avif';
109 $image_file['quickwebp'] = 'optimized';
110 $this->record_optimization_stats( $image_file['new_size'], $image_file['size'] );
111 quickwebp_debug_log( 'image_optimization:avif_success', array( 'new_size' => $image_file['size'] ) );
112 }
113 }
114
115 if ( '1' === $mode_enabled ) {
116 quickwebp_debug_log( 'image_optimization:try_webp', array( 'mime_type' => $mime_type, 'quality' => $quality ) );
117 $webp_image = $this->create_webp_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
118 if ( $webp_image ) {
119 $image_file['size'] = filesize( $image_file['tmp_name'] );
120 $image_file['type'] = 'image/webp';
121 $image_file['quickwebp'] = 'optimized';
122 $this->record_optimization_stats( $image_file['new_size'], $image_file['size'] );
123 quickwebp_debug_log( 'image_optimization:webp_success', array( 'new_size' => $image_file['size'] ) );
124 }
125 }
126
127 return $image_file;
128 }
129
130 /**
131 * Add post meta to the attachment that already optimized
132 */
133 public function add_already_optimized_meta( $metadata, $attachment_id, $context ) {
134 //phpcs:ignore WordPress.Security.NonceVerification.Missing
135 $quickwebp = sanitize_text_field( wp_unslash( $_FILES['async-upload']['quickwebp'] ?? '' ) );
136
137 if ( 'optimized' == $quickwebp ) {
138 //phpcs:ignore WordPress.Security.NonceVerification.Missing
139 $type = sanitize_text_field( wp_unslash( $_FILES['async-upload']['type'] ?? '' ) );
140
141 if ( 'image/webp' == $type ) {
142 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '1' );
143 } elseif ( 'image/avif' == $type ) {
144 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '2' );
145 }
146 }
147
148 return $metadata;
149 }
150
151 /**
152 * Save the original image in the attachment meta
153 */
154 public function save_original_image( $metadata, $attachment_id, $context ) {
155
156 if ( $context != 'create' ) {
157 return $metadata;
158 }
159
160 $settings = $this->get_settings();
161
162 $mode_enabled = $settings['quickwebp_settings_conversion'];
163 if ( $mode_enabled === '0' ) {
164 return $metadata;
165 }
166
167 $save_original = is_array( $settings['quickwebp_settings_conversion_save_original'] ) ? $settings['quickwebp_settings_conversion_save_original'] : array();
168 if ( ! in_array( 'checked', $save_original ) ) {
169 return $metadata;
170 }
171
172 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
173 //phpcs:ignore WordPress.Security.NonceVerification.Missing
174 $mime_type = sanitize_text_field( wp_unslash( $_FILES['async-upload']['type'] ?? '' ) );
175 if ( in_array( 'checked', $ignore_same_format ) ) {
176 if ( '1' == $mode_enabled && 'image/webp' == $mime_type ) {
177 return $metadata;
178 } elseif( '2' == $mode_enabled && 'image/avif' == $mime_type ) {
179 return $metadata;
180 }
181 }
182
183 $sizes = $this->get_media_files( $attachment_id );
184 $new_sizes = array();
185
186 foreach ( $sizes as $key => $size ) {
187 $result = $this->optimize_local_file( $size );
188
189 if ( $result ) {
190 $new_sizes[$key] = $result;
191 }
192 }
193
194 if ( ! empty( $new_sizes ) ) {
195 $this->record_attachment_optimization_stats( $new_sizes );
196
197 if ( '1' == $mode_enabled ) {
198 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '1' );
199 } elseif ( '2' == $mode_enabled ) {
200 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '2' );
201 }
202
203 update_post_meta( $attachment_id, 'quickwebp_data', $new_sizes );
204 delete_post_meta( $attachment_id, 'quickwebp_has_error' );
205 } else {
206 update_post_meta( $attachment_id, 'quickwebp_has_error', '1' );
207 }
208
209 return $metadata;
210 }
211
212 /**
213 * Add data to the attachment
214 */
215 public function add_data_to_attachment( $metadata, $attachment_id, $context ) {
216
217 if ( $context != 'create' ) {
218 return $metadata;
219 }
220
221 $settings = $this->get_settings();
222 if ( $settings['quickwebp_settings_completion'] != '1' ) {
223 return $metadata;
224 }
225
226 //phpcs:ignore WordPress.Security.NonceVerification.Missing
227 $original_name = isset( $_FILES['async-upload']['original_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['async-upload']['original_name'] ) ) : sanitize_text_field( $_FILES['async-upload']['name'] ?? '' );
228 if ( ! empty( $original_name ) ) {
229 $original_name = pathinfo( $original_name, PATHINFO_FILENAME );
230 $post_arr = array();
231
232 $completion_options = is_array( $settings['quickwebp_settings_completion_options'] ) ? $settings['quickwebp_settings_completion_options'] : array();
233
234 if ( in_array( 'title', $completion_options ) ) {
235 $post_arr['post_title'] = $original_name;
236 }
237
238 if ( in_array( 'caption', $completion_options ) ) {
239 $post_arr['post_excerpt'] = $original_name;
240 }
241
242 if ( in_array( 'alt', $completion_options ) ) {
243 $post_arr['meta_input']['_wp_attachment_image_alt'] = $original_name;
244 }
245
246 if ( in_array( 'description', $completion_options ) ) {
247 $post_arr['post_content'] = $original_name;
248 }
249
250 if ( ! empty( $post_arr ) ) {
251 $post_arr['ID'] = $attachment_id;
252 wp_update_post( $post_arr );
253 }
254 }
255
256 return $metadata;
257 }
258
259 /**
260 * Change the default size of wp editor
261 */
262 public function change_wp_max_size( $max_size, $imagesize ) {
263
264 $settings = $this->get_settings();
265 $resize_active = $settings['quickwebp_settings_resize'];
266
267 if ( $resize_active == '1' ) {
268
269 $imagesize_width = isset($imagesize[0]) ? $imagesize[0] : 0;
270 $resize_value = $settings['quickwebp_settings_resize_value'];
271
272 if ( $imagesize_width > $resize_value ) {
273 $max_size = $resize_value;
274 }
275 }
276
277 return $max_size;
278 }
279
280 /**
281 * Change the default quality of wp editor
282 */
283 public function change_wp_quality( $default_quality ) {
284
285 $settings = $this->get_settings();
286 $mode_enabled = $settings['quickwebp_settings_conversion'];
287 if ( $mode_enabled === '0' ) {
288 return $default_quality;
289 }
290
291 return 100;
292 }
293
294 /**
295 * Optimize image through ajax
296 */
297 public function image_optimization_ajax() {
298 global $quickwebp_surecart_client;
299
300 quickwebp_debug_log( 'image_optimization_ajax:start' );
301
302 if ( ! current_user_can( 'upload_files' ) ) {
303 wp_send_json_error( __( 'You are not allowed to upload files.', 'quickwebp' ), 403 );
304 }
305
306 // verify the nonce.
307 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
308 if( !wp_verify_nonce( $nonce, 'image_optimize_nonce' ) ) {
309 wp_send_json_error( __( 'Refresh the page and try again.', 'quickwebp' ) );
310 }
311
312 // Get the file
313 $file = count($_FILES) > 0 ? array_shift($_FILES) : array();
314 if ( empty($file) ) {
315 quickwebp_debug_log( 'image_optimization_ajax:missing_file' );
316 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
317 }
318
319 $settings = $this->get_ajax_settings();
320 $image_file = $this->file_is_image( $file, $settings );
321
322 if ( ! $image_file ) {
323 quickwebp_debug_log( 'image_optimization_ajax:not_image' );
324 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
325 }
326
327 $mode_enabled = $settings['quickwebp_settings_conversion'];
328 if ( $mode_enabled === '0' ) {
329 quickwebp_debug_log( 'image_optimization_ajax:conversion_disabled' );
330 $image_file['new_size'] = $image_file['size'];
331 $image_file['new_type'] = $image_file['type'];
332 $this->return_ajax_data( $image_file );
333 }
334
335 $quality = $this->get_the_quality( $settings );
336 $is_pro = false;
337 if ( $quickwebp_surecart_client ) {
338 $is_pro = $quickwebp_surecart_client->license()->is_valid();
339 }
340
341 $image_file['new_size'] = $image_file['size'];
342 $image_file['new_type'] = $image_file['type'];
343
344 if ( '2' === $mode_enabled && $is_pro ) {
345 quickwebp_debug_log( 'image_optimization_ajax:try_avif', array( 'quality' => $quality ) );
346 $avif_image = $this->create_avif_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
347 if ( $avif_image ) {
348 $image_file['new_size'] = filesize( $image_file['tmp_name'] );
349 $image_file['new_type'] = 'image/avif';
350 }
351 }
352
353 if ( '1' === $mode_enabled ) {
354 quickwebp_debug_log( 'image_optimization_ajax:try_webp', array( 'quality' => $quality ) );
355 $webp_image = $this->create_webp_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
356 if ( $webp_image ) {
357 $image_file['new_size'] = filesize( $image_file['tmp_name'] );
358 $image_file['new_type'] = 'image/webp';
359 }
360 }
361
362 $this->return_ajax_data( $image_file );
363 }
364
365 /**
366 * Optimize a single media
367 */
368 public function single_optimization_ajax() {
369 quickwebp_debug_log( 'single_optimization_ajax:start' );
370
371 // verify the nonce.
372 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
373 if( !wp_verify_nonce( $nonce, 'quickwebp_admin_attachment' ) ) {
374 wp_send_json_error( __( 'Refresh the page and try again.', 'quickwebp' ) );
375 }
376
377 // Sanitize data
378 $attachment_id = isset( $_POST['attachment_id'] ) ? absint( wp_unslash( $_POST['attachment_id'] ) ) : 0;
379
380 if ( ! $attachment_id ) {
381 quickwebp_debug_log( 'single_optimization_ajax:missing_attachment_id' );
382 wp_send_json_error( __( 'No attachment id.', 'quickwebp' ) );
383 }
384
385 $settings = $this->get_settings();
386 $mode_enabled = $settings['quickwebp_settings_conversion'];
387 if ( '0' === $mode_enabled ) {
388 wp_send_json_error( __( 'Choose an image format and save the settings.', 'quickwebp' ) );
389 }
390
391 $already_optimized = get_post_meta( $attachment_id, 'quickwebp_already_optimized', true );
392
393 if ( '1' == $already_optimized && '1' == $mode_enabled ) {
394 wp_send_json_error( __( 'Already optimized.', 'quickwebp' ) );
395 }
396
397 if ( '2' == $already_optimized && '2' == $mode_enabled ) {
398 wp_send_json_error( __( 'Already optimized.', 'quickwebp' ) );
399 }
400
401 $post_mime_type = get_post_mime_type( $attachment_id );
402 if ( ! in_array( $post_mime_type, $this->allowed_mime_types ) ) {
403 wp_send_json_error( __( 'Not a valid image.', 'quickwebp' ) );
404 }
405
406 $sizes = $this->get_media_files( $attachment_id );
407 $new_sizes = array();
408
409 foreach ( $sizes as $key => $size ) {
410 quickwebp_debug_log( 'single_optimization_ajax:optimize_size', array( 'size_key' => $key, 'path' => $size['path'] ?? '' ) );
411 $result = $this->optimize_local_file( $size );
412
413 if ( $result ) {
414 $new_sizes[$key] = $result;
415 }
416 }
417
418 if ( ! empty( $new_sizes ) ) {
419 $this->record_attachment_optimization_stats( $new_sizes );
420
421 $data = get_post_meta( $attachment_id, 'quickwebp_data', true );
422 if ( ! empty( $data ) ) {
423 $this->remove_related_files( $data );
424 }
425
426 if ( '1' == $mode_enabled ) {
427 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '1' );
428 } elseif ( '2' == $mode_enabled ) {
429 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '2' );
430 }
431
432 update_post_meta( $attachment_id, 'quickwebp_data', $new_sizes );
433 delete_post_meta( $attachment_id, 'quickwebp_has_error' );
434 } else {
435 update_post_meta( $attachment_id, 'quickwebp_has_error', '1' );
436 delete_post_meta( $attachment_id, 'quickwebp_already_optimized' );
437 delete_post_meta( $attachment_id, 'quickwebp_data' );
438 }
439
440 $wp_media_extend = new Quickwebp_Wp_Media_Extends( $this->plugin_name, $this->version );
441 $html = $wp_media_extend->attachment_data( $new_sizes, $attachment_id );
442
443 wp_send_json_success( $html );
444 }
445
446 /**
447 * Undo a single media optimization
448 */
449 public function undo_single_optimization_ajax() {
450
451 // verify the nonce.
452 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
453 if( !wp_verify_nonce( $nonce, 'quickwebp_admin_attachment' ) ) {
454 wp_send_json_error( __( 'Refresh the page and try again.', 'quickwebp' ) );
455 }
456
457 // Sanitize data
458 $attachment_id = isset( $_POST['attachment_id'] ) ? absint( wp_unslash( $_POST['attachment_id'] ) ) : 0;
459
460 if ( ! $attachment_id ) {
461 wp_send_json_error( __( 'No attachment id.', 'quickwebp' ) );
462 }
463
464 $data = get_post_meta( $attachment_id, 'quickwebp_data', true );
465 if ( ! empty( $data ) ) {
466 $this->remove_related_files( $data );
467 delete_post_meta( $attachment_id, 'quickwebp_already_optimized' );
468 delete_post_meta( $attachment_id, 'quickwebp_data' );
469
470 $wp_media_extend = new Quickwebp_Wp_Media_Extends( $this->plugin_name, $this->version );
471 $html = $wp_media_extend->optimize_btn( $attachment_id );
472 wp_send_json_success( $html );
473 } else {
474 wp_send_json_error( __( 'Not optimized.', 'quickwebp' ) );
475 }
476 }
477
478 /**
479 * Trigger before delete attachment
480 */
481 public function before_delete_attachment( $post_id ) {
482 $data = get_post_meta( $post_id, 'quickwebp_data', true );
483 if ( ! empty( $data ) ) {
484 $this->remove_related_files( $data );
485 }
486 }
487
488 /**
489 * Get the image data for the preview in the settings page
490 */
491 public function get_image_data_for_preview_ajax() {
492 quickwebp_debug_log( 'get_image_data_for_preview_ajax:start' );
493
494 // verify the nonce.
495 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
496 if( !wp_verify_nonce( $nonce, 'image_optimize_nonce' ) ) {
497 wp_send_json_error( __( 'Refresh the page and try again.', 'quickwebp' ) );
498 }
499
500 // Get the file
501 $file = count($_FILES) > 0 ? array_shift($_FILES) : array();
502 if ( empty($file) ) {
503 quickwebp_debug_log( 'get_image_data_for_preview_ajax:missing_file' );
504 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
505 }
506
507 $image_file = $this->file_is_image( $file );
508 if ( ! $image_file ) {
509 quickwebp_debug_log( 'get_image_data_for_preview_ajax:not_image' );
510 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
511 }
512
513 $preview_image_data = array(
514 'name' => $image_file['name'],
515 'size' => size_format( $image_file['size'], 2 ),
516 'dimensions' => $image_file['width'] . ' x ' . $image_file['height'],
517 );
518
519 $conversions = array( '1', '2' );
520 $qualities = array( 'low', 'medium', 'high', 'extra_high' );
521
522 foreach ( $conversions as $conversion ) {
523 foreach ( $qualities as $quality ) {
524 quickwebp_debug_log( 'get_image_data_for_preview_ajax:preview_variant', array( 'conversion' => $conversion, 'quality' => $quality ) );
525
526 $size_after = 0;
527
528 if ( '2' == $conversion ) {
529 $new_path = $image_file['tmp_name'] . '-' . $quality . '.avif';
530 $quality_value = $this->get_the_quality_values( $quality, $conversion );
531 $avif_image = $this->create_avif_image( $image_file['tmp_name'], $new_path, $quality_value );
532
533 if ( $avif_image ) {
534 $size_after = filesize( $new_path );
535 }
536 } elseif ( '1' == $conversion ) {
537 $new_path = $image_file['tmp_name'] . '-' . $quality . '.webp';
538 $quality_value = $this->get_the_quality_values( $quality, $conversion );
539 $webp_image = $this->create_webp_image( $image_file['tmp_name'], $new_path, $quality_value );
540
541 if ( $webp_image ) {
542 $size_after = filesize( $new_path );
543 }
544 }
545
546 $deference = $image_file['size'] - $size_after;
547 $percent = $deference / $image_file['size'] * 100;
548
549 $preview_image_data[$conversion . '_' . $quality] = array(
550 'size' => size_format( $size_after, 2 ),
551 'save' => size_format( $deference, 2 ),
552 'save_percent' => round( $percent ) . '%',
553 'percent' => round( 100 - $percent ) . '%',
554 );
555 }
556 }
557
558 wp_send_json_success( $preview_image_data );
559 }
560
561 /**
562 * Get the unoptimized media ids
563 *
564 * @param int $limit Maximum number of media IDs to retrieve.
565 * @return int[]
566 */
567 public function get_unoptimized_media_ids( $limit = 5 ) {
568 $query_args = $this->get_unoptimized_media_query_args();
569 $query_args['posts_per_page'] = max( 1, absint( $limit ) );
570 $query_args['fields'] = 'ids';
571 $query_args['no_found_rows'] = true;
572
573 return get_posts( $query_args );
574 }
575
576 /**
577 * Count unoptimized media.
578 *
579 * @return int
580 */
581 public function get_unoptimized_media_count() {
582 $query_args = $this->get_unoptimized_media_query_args();
583 $query_args['posts_per_page'] = 1;
584 $query_args['fields'] = 'ids';
585
586 $query = new WP_Query( $query_args );
587
588 return absint( $query->found_posts );
589 }
590
591 /**
592 * Get the shared query arguments for unoptimized media.
593 *
594 * @return array
595 */
596 private function get_unoptimized_media_query_args() {
597
598 $settings = $this->get_settings();
599 $mode_enabled = $settings['quickwebp_settings_conversion'];
600 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
601
602 $statuses = array(
603 'inherit' => 'inherit',
604 'private' => 'private',
605 );
606 $custom_statuses = get_post_stati( array( 'public' => true ) );
607 unset( $custom_statuses['publish'] );
608 if ( $custom_statuses ) {
609 $statuses = array_merge( $statuses, $custom_statuses );
610 }
611
612 $allowed_mime_types = $this->allowed_mime_types;
613 if ( in_array( 'checked', $ignore_same_format ) ) {
614 if ( '1' == $mode_enabled ) {
615 $webp_index = array_search( 'image/webp', $allowed_mime_types );
616 unset( $allowed_mime_types[$webp_index] );
617 } elseif ( '2' == $mode_enabled ) {
618 $avif_index = array_search( 'image/avif', $allowed_mime_types );
619 unset( $allowed_mime_types[$avif_index] );
620 }
621 }
622
623 $meta_query_already_optimized = array(
624 'relation' => 'OR',
625 array(
626 'key' => 'quickwebp_already_optimized',
627 'compare' => 'NOT EXISTS'
628 ),
629 );
630
631 if ( 'webp' == $mode_enabled ) {
632 $meta_query_already_optimized[] = array(
633 'key' => 'quickwebp_already_optimized',
634 'compare' => '!=',
635 'value' => '1',
636 );
637 } elseif ( 'avif' == $mode_enabled ) {
638 $meta_query_already_optimized[] = array(
639 'key' => 'quickwebp_already_optimized',
640 'compare' => '!=',
641 'value' => '2',
642 );
643 }
644
645 return array(
646 'post_type' => 'attachment',
647 'post_mime_type' => $allowed_mime_types,
648 'post_status' => array_keys( $statuses ),
649 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
650 'meta_query' => array(
651 'relation' => 'AND',
652 array(
653 'key' => 'quickwebp_has_error',
654 'compare' => 'NOT EXISTS'
655 ),
656 $meta_query_already_optimized,
657 ),
658 );
659 }
660
661 /**
662 * Get the list of media files
663 */
664 public function get_media_files( $media_id ) {
665 $fullsize_path = get_attached_file( $media_id );
666
667 if ( ! $fullsize_path ) {
668 return array();
669 }
670
671 $media_data = wp_get_attachment_image_src( $media_id, 'full' );
672 $file_type = wp_check_filetype( $fullsize_path );
673
674 $all_sizes = [
675 'full' => [
676 'size' => 'full',
677 'path' => $fullsize_path,
678 'width' => $media_data[1],
679 'height' => $media_data[2],
680 'mime-type' => $file_type['type'],
681 'disabled' => false,
682 ],
683 ];
684
685 $sizes = wp_get_attachment_metadata( $media_id, true );
686 $sizes = ! empty( $sizes['sizes'] ) && is_array( $sizes['sizes'] ) ? $sizes['sizes'] : [];
687
688 $dir_path = trailingslashit( dirname( $fullsize_path ) );
689
690 foreach ( $sizes as $size => $size_data ) {
691 $all_sizes[ $size ] = [
692 'size' => $size,
693 'path' => $dir_path . $size_data['file'],
694 'width' => $size_data['width'],
695 'height' => $size_data['height'],
696 'mime-type' => $size_data['mime-type'],
697 'disabled' => false
698 ];
699 }
700
701 return $all_sizes;
702 }
703
704 /**
705 * Optimize a local file
706 */
707 public function optimize_local_file( $size ) {
708 global $quickwebp_surecart_client;
709
710 quickwebp_debug_log( 'optimize_local_file:start', array( 'path' => $size['path'] ?? '' ) );
711
712 if ( !is_file($size['path']) ) {
713 quickwebp_debug_log( 'optimize_local_file:missing_file', array( 'path' => $size['path'] ?? '' ) );
714 return false;
715 }
716
717 $real_type = mime_content_type( $size['path']);
718 if ( !in_array( $real_type, $this->allowed_mime_types ) ) {
719 quickwebp_debug_log( 'optimize_local_file:invalid_mime_type', array( 'mime_type' => $real_type ) );
720 return false;
721 }
722
723 $settings = $this->get_settings();
724 $quality = $this->get_the_quality( $settings );
725 $is_pro = false;
726 if ( $quickwebp_surecart_client ) {
727 $is_pro = $quickwebp_surecart_client->license()->is_valid();
728 }
729 $mode_enabled = $settings['quickwebp_settings_conversion'];
730 $size_before = filesize( $size['path'] );
731 $new_path = $size['path'] . '.' . ( '2' === $mode_enabled && $is_pro ? 'avif' : 'webp' );
732 $size_after = 0;
733
734 if ( '2' === $mode_enabled && $is_pro ) {
735 quickwebp_debug_log( 'optimize_local_file:try_avif', array( 'path' => $size['path'], 'quality' => $quality ) );
736 $avif_image = $this->create_avif_image( $size['path'], $new_path, $quality );
737 if ( $avif_image ) {
738 $size_after = filesize( $new_path );
739 }
740 } elseif ( '1' === $mode_enabled ) {
741 quickwebp_debug_log( 'optimize_local_file:try_webp', array( 'path' => $size['path'], 'quality' => $quality ) );
742 $webp_image = $this->create_webp_image( $size['path'], $new_path, $quality );
743 if ( $webp_image ) {
744 $size_after = filesize( $new_path );
745 }
746 }
747
748 if ( ! $size_after ) {
749 quickwebp_debug_log( 'optimize_local_file:conversion_failed', array( 'path' => $size['path'], 'target_path' => $new_path ) );
750 return false;
751 }
752
753 $deference = $size_before - $size_after;
754 $percent = $deference / $size_before * 100;
755
756 return array(
757 'success' => 1,
758 'original_size' => $size_before,
759 'optimized_size' => $size_after,
760 'percent' => round( $percent, 2 ),
761 'path' => $new_path,
762 'format' => $mode_enabled,
763 );
764 }
765
766 /**
767 * Record aggregated optimization statistics for one image attachment.
768 *
769 * @param array $optimized_sizes Optimization results for the attachment sizes.
770 */
771 public function record_attachment_optimization_stats( $optimized_sizes ) {
772 $stats = $this->get_attachment_optimization_stats( $optimized_sizes );
773 $this->record_optimization_stats( $stats['bytes_before'], $stats['bytes_after'], $stats['images_optimized'] );
774 }
775
776 /**
777 * Calculate optimization statistics for attachment sizes without persisting them.
778 *
779 * @param array $optimized_sizes Optimization results for the attachment sizes.
780 * @return array
781 */
782 public function get_attachment_optimization_stats( $optimized_sizes ) {
783 $stats = array(
784 'images_optimized' => 0,
785 'bytes_before' => 0,
786 'bytes_after' => 0,
787 );
788
789 foreach ( $optimized_sizes as $optimized_size ) {
790 $stats['bytes_before'] += absint( $optimized_size['original_size'] ?? 0 );
791 $stats['bytes_after'] += absint( $optimized_size['optimized_size'] ?? 0 );
792 }
793
794 if ( $stats['bytes_before'] && $stats['bytes_after'] ) {
795 $stats['images_optimized'] = 1;
796 }
797
798 return $stats;
799 }
800
801 /**
802 * Record local cumulative optimization statistics.
803 *
804 * @param int $size_before File size before optimization in bytes.
805 * @param int $size_after File size after optimization in bytes.
806 * @param int $images_optimized Number of optimized attachments.
807 */
808 public function record_optimization_stats( $size_before, $size_after, $images_optimized = 1 ) {
809 $size_before = absint( $size_before );
810 $size_after = absint( $size_after );
811 $images_optimized = absint( $images_optimized );
812
813 if ( ! $size_before || ! $size_after || ! $images_optimized ) {
814 return;
815 }
816
817 $lock_token = $this->acquire_optimization_stats_lock();
818 if ( ! $lock_token ) {
819 $this->queue_optimization_stats_delta( $size_before, $size_after, $images_optimized );
820 return;
821 }
822
823 $stats = get_option( 'quickwebp_optimization_stats', array() );
824 $stats = wp_parse_args(
825 is_array( $stats ) ? $stats : array(),
826 array(
827 'images_optimized' => 0,
828 'bytes_before' => 0,
829 'bytes_after' => 0,
830 'bytes_saved' => 0,
831 )
832 );
833 $pending_stats = $this->consume_optimization_stats_deltas();
834
835 $stats['images_optimized'] = absint( $stats['images_optimized'] ) + $images_optimized + $pending_stats['images_optimized'];
836 $stats['bytes_before'] = absint( $stats['bytes_before'] ) + $size_before + $pending_stats['bytes_before'];
837 $stats['bytes_after'] = absint( $stats['bytes_after'] ) + $size_after + $pending_stats['bytes_after'];
838 $stats['bytes_saved'] = max( 0, $stats['bytes_before'] - $stats['bytes_after'] );
839
840 update_option( 'quickwebp_optimization_stats', $stats, false );
841 $this->release_optimization_stats_lock( $lock_token );
842 }
843
844 /**
845 * Queue a statistics delta when another request owns the update lock.
846 *
847 * @param int $size_before File size before optimization in bytes.
848 * @param int $size_after File size after optimization in bytes.
849 * @param int $images_optimized Number of optimized attachments.
850 */
851 private function queue_optimization_stats_delta( $size_before, $size_after, $images_optimized ) {
852 $option_name = 'quickwebp_optimization_stats_delta_' . str_replace( '-', '', wp_generate_uuid4() );
853 add_option(
854 $option_name,
855 array(
856 'images_optimized' => $images_optimized,
857 'bytes_before' => $size_before,
858 'bytes_after' => $size_after,
859 ),
860 '',
861 false
862 );
863 }
864
865 /**
866 * Consume queued statistics deltas while holding the update lock.
867 *
868 * @return array
869 */
870 private function consume_optimization_stats_deltas() {
871 global $wpdb;
872
873 $pending_stats = array(
874 'images_optimized' => 0,
875 'bytes_before' => 0,
876 'bytes_after' => 0,
877 );
878 $option_prefix = 'quickwebp_optimization_stats_delta_';
879 $option_names = $wpdb->get_col(
880 $wpdb->prepare(
881 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
882 $wpdb->esc_like( $option_prefix ) . '%'
883 )
884 );
885
886 foreach ( $option_names as $option_name ) {
887 $delta = get_option( $option_name, array() );
888 if ( is_array( $delta ) ) {
889 $pending_stats['images_optimized'] += absint( $delta['images_optimized'] ?? 0 );
890 $pending_stats['bytes_before'] += absint( $delta['bytes_before'] ?? 0 );
891 $pending_stats['bytes_after'] += absint( $delta['bytes_after'] ?? 0 );
892 }
893 delete_option( $option_name );
894 }
895
896 return $pending_stats;
897 }
898
899 /**
900 * Acquire an expiring lock for cumulative statistics updates.
901 *
902 * @return string|false
903 */
904 private function acquire_optimization_stats_lock() {
905 $lock_token = wp_generate_uuid4();
906 $lock = array(
907 'token' => $lock_token,
908 'expires' => time() + MINUTE_IN_SECONDS,
909 );
910
911 if ( add_option( 'quickwebp_optimization_stats_lock', $lock, '', false ) ) {
912 return $lock_token;
913 }
914
915 $existing_lock = get_option( 'quickwebp_optimization_stats_lock', array() );
916 if ( is_array( $existing_lock ) && absint( $existing_lock['expires'] ?? 0 ) < time() ) {
917 delete_option( 'quickwebp_optimization_stats_lock' );
918 if ( add_option( 'quickwebp_optimization_stats_lock', $lock, '', false ) ) {
919 return $lock_token;
920 }
921 }
922
923 return false;
924 }
925
926 /**
927 * Release the statistics lock owned by this process.
928 *
929 * @param string $lock_token Lock ownership token.
930 */
931 private function release_optimization_stats_lock( $lock_token ) {
932 $lock = get_option( 'quickwebp_optimization_stats_lock', array() );
933 if ( is_array( $lock ) && hash_equals( (string) ( $lock['token'] ?? '' ), $lock_token ) ) {
934 delete_option( 'quickwebp_optimization_stats_lock' );
935 }
936 }
937
938 /**
939 * Remove the related files of an optimized attachment
940 */
941 public function remove_related_files( $data ) {
942 foreach ( $data as $value ) {
943 $path = $value['path'] ?? '';
944
945 if ( ! empty( $path ) && file_exists( $path ) ) {
946 wp_delete_file( $path );
947 }
948 }
949 }
950
951 /**
952 * Get the optimization settings
953 */
954 public function get_settings() {
955 return array(
956 'quickwebp_settings_conversion' => get_option('quickwebp_settings_conversion', quickwebp_settings_default('quickwebp_settings_conversion') ),
957 'quickwebp_settings_conversion_quality' => get_option('quickwebp_settings_conversion_quality', quickwebp_settings_default('quickwebp_settings_conversion_quality') ),
958 'quickwebp_settings_conversion_ignore_webp' => get_option('quickwebp_settings_conversion_ignore_webp', quickwebp_settings_default('quickwebp_settings_conversion_ignore_webp') ),
959 'quickwebp_settings_conversion_save_original' => get_option('quickwebp_settings_conversion_save_original', quickwebp_settings_default('quickwebp_settings_conversion_save_original') ),
960 'quickwebp_settings_resize' => get_option('quickwebp_settings_resize', quickwebp_settings_default('quickwebp_settings_resize') ),
961 'quickwebp_settings_resize_value' => get_option('quickwebp_settings_resize_value', quickwebp_settings_default('quickwebp_settings_resize_value') ),
962 'quickwebp_settings_completion' => get_option('quickwebp_settings_completion', quickwebp_settings_default('quickwebp_settings_completion') ),
963 'quickwebp_settings_completion_options' => get_option('quickwebp_settings_completion_options', quickwebp_settings_default('quickwebp_settings_completion_options') ),
964 'quickwebp_settings_cleanup' => get_option('quickwebp_settings_cleanup', quickwebp_settings_default('quickwebp_settings_cleanup') ),
965 );
966 }
967
968 /**
969 * Get the optimization settings
970 */
971 private function get_ajax_settings() {
972 //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
973 $raw_settings = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
974 $settings = is_string( $raw_settings ) ? json_decode( $raw_settings, true ) : array();
975
976 if ( ! is_array( $settings ) ) {
977 $settings = array();
978 }
979
980 return array(
981 'quickwebp_settings_conversion' => isset( $settings['quickwebp_settings_conversion'] ) ? sanitize_text_field( (string) $settings['quickwebp_settings_conversion'] ) : '0',
982 'quickwebp_settings_conversion_quality' => isset( $settings['quickwebp_settings_conversion_quality'] ) ? sanitize_text_field( $settings['quickwebp_settings_conversion_quality'] ) : 'high',
983 'quickwebp_settings_conversion_ignore_webp' => isset( $settings['quickwebp_settings_conversion_ignore_webp'] ) && is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? array_map( 'sanitize_text_field', $settings['quickwebp_settings_conversion_ignore_webp'] ) : array(),
984 'quickwebp_settings_resize' => isset( $settings['quickwebp_settings_resize'] ) ? sanitize_text_field( (string) $settings['quickwebp_settings_resize'] ) : '',
985 'quickwebp_settings_resize_value' => isset( $settings['quickwebp_settings_resize_value'] ) ? absint( $settings['quickwebp_settings_resize_value'] ) : 0,
986 'quickwebp_settings_completion' => isset( $settings['quickwebp_settings_completion'] ) ? sanitize_text_field( (string) $settings['quickwebp_settings_completion'] ) : '',
987 'quickwebp_settings_completion_options' => isset( $settings['quickwebp_settings_completion_options'] ) && is_array( $settings['quickwebp_settings_completion_options'] ) ? array_map( 'sanitize_text_field', $settings['quickwebp_settings_completion_options'] ) : array(),
988 'quickwebp_settings_cleanup' => isset( $settings['quickwebp_settings_cleanup'] ) ? sanitize_text_field( (string) $settings['quickwebp_settings_cleanup'] ) : '',
989 );
990 }
991
992 /**
993 * Check if the file is an image
994 */
995 private function file_is_image( $file, $settings = array() ) {
996
997 $file_name = isset($file['name']) ? $file['name'] : '';
998 $file_type = isset($file['type']) ? $file['type'] : '';
999 $file_tmp_name = isset($file['tmp_name']) ? $file['tmp_name'] : '';
1000 $file_error = isset($file['error']) ? $file['error'] : '';
1001 $file_size = isset($file['size']) ? $file['size'] : '';
1002
1003 if ( empty($file_tmp_name) ) {
1004 quickwebp_debug_log( 'file_is_image:missing_tmp_name', array( 'file_name' => $file_name ) );
1005 return false;
1006 }
1007
1008 $allowed_mime_types = apply_filters( 'quickwebp_mime_types_allowed', $this->allowed_mime_types );
1009 $is_image = wp_getimagesize($file_tmp_name);
1010 $mime_type = wp_get_image_mime($file_tmp_name);
1011 if ( ! $is_image || ! in_array( $mime_type, $allowed_mime_types ) ) {
1012 quickwebp_debug_log( 'file_is_image:rejected', array( 'file_name' => $file_name, 'mime_type' => $mime_type ) );
1013 return false;
1014 }
1015
1016 $name = $file_name;
1017
1018 if ( isset($settings['quickwebp_settings_cleanup']) && $settings['quickwebp_settings_cleanup'] == '1' ) {
1019 $name = quickwebp_sanitize_name($file_name);
1020 }
1021
1022 return array(
1023 'original_name' => $file_name,
1024 'name' => $name,
1025 'type' => $file_type,
1026 'tmp_name' => $file_tmp_name,
1027 'error' => $file_error,
1028 'size' => $file_size,
1029 'width' => $is_image[0],
1030 'height' => $is_image[1],
1031 );
1032 }
1033
1034 /**
1035 * Return ajax data
1036 */
1037 private function return_ajax_data( $data ) {
1038 $return = array(
1039 'original_name' => $data['original_name'],
1040 'size' => $data['size'],
1041 'type' => $this->type_from_mime_type( $data['type'] ),
1042 'mime_type' => $data['type'],
1043 'image' => 'data:'.$data['new_type'].';base64,' . base64_encode( file_get_contents( $data['tmp_name'] ) ),
1044 'name' => $data['name'],
1045 'new_size' => $data['new_size'],
1046 'new_type' => $this->type_from_mime_type( $data['new_type'] ),
1047 'new_mime_type' => $data['new_type']
1048 );
1049
1050 wp_send_json_success( $return );
1051 }
1052
1053 /**
1054 * Get the type from the mime type
1055 */
1056 private function type_from_mime_type( $mime_type ) {
1057 $array = array(
1058 'image/jpeg' => 'JPEG',
1059 'image/png' => 'PNG',
1060 'image/webp' => 'WebP',
1061 'image/avif' => 'AVIF',
1062 );
1063
1064 return $array[$mime_type] ?? '';
1065 }
1066
1067 /**
1068 * Get the quality
1069 */
1070 private function get_the_quality( $settings ) {
1071 $mode_enabled = $settings['quickwebp_settings_conversion'];
1072 $quality = $settings['quickwebp_settings_conversion_quality'];
1073
1074 return $this->get_the_quality_values( $quality, $mode_enabled );
1075 }
1076
1077 /**
1078 * Get the quality values for avif and webp
1079 */
1080 private function get_the_quality_values( $quality, $mode_enabled ) {
1081 $result = 50;
1082
1083 switch ( $quality ) {
1084 case 'low':
1085 $result = 50;
1086 if ( '2' === $mode_enabled ) {
1087 $result = 30;
1088 }
1089 break;
1090 case 'medium':
1091 $result = 60;
1092 if ( '2' === $mode_enabled ) {
1093 $result = 40;
1094 }
1095 break;
1096 case 'high':
1097 $result = 75;
1098 if ( '2' === $mode_enabled ) {
1099 $result = 50;
1100 }
1101 break;
1102 case 'extra_high':
1103 $result = 90;
1104 if ( '2' === $mode_enabled ) {
1105 $result = 70;
1106 }
1107 break;
1108 }
1109
1110 return $result;
1111 }
1112
1113 /**
1114 * Get the extension matching an image mime type.
1115 *
1116 * @param string $mime_type Image mime type.
1117 *
1118 * @return string
1119 */
1120 private function get_extension_from_mime_type( $mime_type ) {
1121 $extensions = array(
1122 'image/avif' => 'avif',
1123 'image/webp' => 'webp',
1124 'image/jpeg' => 'jpg',
1125 'image/png' => 'png',
1126 );
1127
1128 return $extensions[ $mime_type ] ?? '';
1129 }
1130
1131 /**
1132 * Create an image using the WordPress image editor abstraction.
1133 *
1134 * @param string $file_path Path to the source image.
1135 * @param string $output_path Path where the converted image should be written.
1136 * @param int $quality Compression quality.
1137 * @param string $target_mime_type Output mime type.
1138 *
1139 * @return bool
1140 */
1141 private function create_image_with_wp_editor( $file_path, $output_path, $quality, $target_mime_type ) {
1142 quickwebp_debug_log( 'create_image_with_wp_editor:start', array(
1143 'file_path' => $file_path,
1144 'output_path' => $output_path,
1145 'quality' => $quality,
1146 'target_mime_type' => $target_mime_type,
1147 ) );
1148
1149 $editor = wp_get_image_editor( $file_path );
1150
1151 if ( is_wp_error( $editor ) ) {
1152 quickwebp_debug_log( 'create_image_with_wp_editor:editor_error', array( 'message' => $editor->get_error_message() ) );
1153 return false;
1154 }
1155
1156 $extension = $this->get_extension_from_mime_type( $target_mime_type );
1157 if ( empty( $extension ) ) {
1158 quickwebp_debug_log( 'create_image_with_wp_editor:missing_extension', array( 'target_mime_type' => $target_mime_type ) );
1159 return false;
1160 }
1161
1162 $save_path = $output_path;
1163 if ( pathinfo( $output_path, PATHINFO_EXTENSION ) !== $extension ) {
1164 $save_path = $output_path . '.' . $extension;
1165 }
1166
1167 $editor->set_quality( $quality );
1168 $result = $editor->save( $save_path, $target_mime_type );
1169
1170 if ( is_wp_error( $result ) || empty( $result['path'] ) || ! file_exists( $result['path'] ) ) {
1171 quickwebp_debug_log( 'create_image_with_wp_editor:save_failed', array(
1172 'output_path' => $save_path,
1173 'error' => is_wp_error( $result ) ? $result->get_error_message() : 'missing_generated_file',
1174 ) );
1175 return false;
1176 }
1177
1178 $source_size = file_exists( $file_path ) ? filesize( $file_path ) : 0;
1179 $generated_size = filesize( $result['path'] );
1180
1181 if ( $source_size > 0 && $generated_size > $source_size ) {
1182 quickwebp_debug_log( 'create_image_with_wp_editor:larger_than_source', array(
1183 'source_size' => $source_size,
1184 'generated_size' => $generated_size,
1185 'output_path' => $result['path'],
1186 ) );
1187 wp_delete_file( $result['path'] );
1188 return false;
1189 }
1190
1191 if ( $result['path'] !== $output_path ) {
1192 $file_contents = file_get_contents( $result['path'] );
1193
1194 if ( false === $file_contents || false === file_put_contents( $output_path, $file_contents ) ) {
1195 quickwebp_debug_log( 'create_image_with_wp_editor:copy_failed', array( 'output_path' => $output_path ) );
1196 return false;
1197 }
1198
1199 wp_delete_file( $result['path'] );
1200 }
1201
1202 quickwebp_debug_log( 'create_image_with_wp_editor:success', array(
1203 'output_path' => $output_path,
1204 'generated_size' => file_exists( $output_path ) ? filesize( $output_path ) : $generated_size,
1205 'target_mime_type'=> $target_mime_type,
1206 ) );
1207
1208 return true;
1209 }
1210
1211 /**
1212 * Create a GD image resource when the required loader is available.
1213 *
1214 * @param string $file_path Path to the source image.
1215 * @param string $mime_type Source mime type.
1216 *
1217 * @return GdImage|resource|false
1218 */
1219 private function create_gd_image_resource( $file_path, $mime_type ) {
1220 $loaders = array(
1221 'image/avif' => 'imagecreatefromavif',
1222 'image/webp' => 'imagecreatefromwebp',
1223 'image/jpeg' => 'imagecreatefromjpeg',
1224 'image/png' => 'imagecreatefrompng',
1225 );
1226
1227 if ( empty( $loaders[ $mime_type ] ) ) {
1228 return false;
1229 }
1230
1231 $loader = $loaders[ $mime_type ];
1232
1233 if ( ! function_exists( $loader ) ) {
1234 return false;
1235 }
1236
1237 return $loader( $file_path );
1238 }
1239
1240 /**
1241 * Check whether the source mime type can be loaded through GD.
1242 *
1243 * @param string $mime_type Source mime type.
1244 *
1245 * @return bool
1246 */
1247 private function can_load_image_with_gd( $mime_type ) {
1248 $loaders = array(
1249 'image/avif' => 'imagecreatefromavif',
1250 'image/webp' => 'imagecreatefromwebp',
1251 'image/jpeg' => 'imagecreatefromjpeg',
1252 'image/png' => 'imagecreatefrompng',
1253 );
1254
1255 if ( empty( $loaders[ $mime_type ] ) ) {
1256 return false;
1257 }
1258
1259 return function_exists( $loaders[ $mime_type ] );
1260 }
1261
1262 /**
1263 * Create the avif image
1264 */
1265 private function create_avif_image( $file_path, $output_path, $quality ) {
1266 $image = false;
1267 $mime_type = wp_get_image_mime( $file_path );
1268
1269 quickwebp_debug_log( 'create_avif_image:start', array(
1270 'file_path' => $file_path,
1271 'output_path' => $output_path,
1272 'mime_type' => $mime_type,
1273 'quality' => $quality,
1274 ) );
1275
1276 if ( ! $this->can_load_image_with_gd( $mime_type ) && $this->create_image_with_wp_editor( $file_path, $output_path, $quality, 'image/avif' ) ) {
1277 quickwebp_debug_log( 'create_avif_image:used_wp_editor', array( 'mime_type' => $mime_type ) );
1278 return true;
1279 }
1280
1281 $image = $this->create_gd_image_resource( $file_path, $mime_type );
1282
1283 if ( ! $image ) {
1284 quickwebp_debug_log( 'create_avif_image:gd_load_failed', array( 'mime_type' => $mime_type ) );
1285 return false;
1286 }
1287
1288 // Handle EXIF orientation for proper image rotation
1289 $exif_data = @exif_read_data( $file_path );
1290 if ( ! empty( $exif_data['Orientation'] ) ) {
1291 $orientation = (int) $exif_data['Orientation'];
1292 //phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
1293 $orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $file_path );
1294 if ( $orientation && 1 !== $orientation ) {
1295 switch ( $orientation ) {
1296 case 2:
1297 // Flip horizontally.
1298 imageflip( $image, IMG_FLIP_HORIZONTAL );
1299 break;
1300 case 3:
1301 // Rotate 180 degrees.
1302 $image = imagerotate( $image, 180, 0 );
1303 break;
1304 case 4:
1305 // Flip vertically.
1306 imageflip( $image, IMG_FLIP_VERTICAL );
1307 break;
1308 case 5:
1309 // Rotate 90 degrees counter-clockwise and flip vertically.
1310 $image = imagerotate( $image, -90, 0 );
1311 imageflip( $image, IMG_FLIP_VERTICAL );
1312 break;
1313 case 6:
1314 // Rotate 90 degrees clockwise (270 counter-clockwise).
1315 $image = imagerotate( $image, -90, 0 );
1316 break;
1317 case 7:
1318 // Rotate 90 degrees counter-clockwise and flip horizontally.
1319 $image = imagerotate( $image, 90, 0 );
1320 imageflip( $image, IMG_FLIP_HORIZONTAL );
1321 break;
1322 case 8:
1323 // Rotate 90 degrees counter-clockwise.
1324 $image = imagerotate( $image, 90, 0 );
1325 break;
1326 }
1327 }
1328 }
1329
1330 if ( ! imageistruecolor( $image ) ) {
1331 $truecolor = imagecreatetruecolor( imagesx( $image ), imagesy( $image ) );
1332
1333 if ( $mime_type === 'image/png' ) {
1334 imagealphablending( $truecolor, false );
1335 imagesavealpha( $truecolor, true );
1336 $transparent = imagecolorallocatealpha( $truecolor, 0, 0, 0, 127 );
1337 imagefilledrectangle( $truecolor, 0, 0, imagesx( $image ), imagesy( $image ), $transparent );
1338 }
1339
1340 imagecopy( $truecolor, $image, 0, 0, 0, 0, imagesx( $image ), imagesy( $image ) );
1341 $image = $truecolor;
1342 }
1343
1344 $avif_image = imageavif( $image, $output_path, $quality );
1345 imagedestroy( $image );
1346 if ( ! $avif_image ) {
1347 quickwebp_debug_log( 'create_avif_image:gd_save_failed', array( 'output_path' => $output_path ) );
1348 return false;
1349 }
1350
1351 quickwebp_debug_log( 'create_avif_image:gd_success', array(
1352 'output_path' => $output_path,
1353 'file_size' => file_exists( $output_path ) ? filesize( $output_path ) : 0,
1354 ) );
1355
1356 return $avif_image;
1357 }
1358
1359 /**
1360 * Create the webp image
1361 */
1362 private function create_webp_image( $file_path, $output_path, $quality ) {
1363 $image = false;
1364 $mime_type = wp_get_image_mime( $file_path );
1365
1366 quickwebp_debug_log( 'create_webp_image:start', array(
1367 'file_path' => $file_path,
1368 'output_path' => $output_path,
1369 'mime_type' => $mime_type,
1370 'quality' => $quality,
1371 ) );
1372
1373 if ( ! $this->can_load_image_with_gd( $mime_type ) && $this->create_image_with_wp_editor( $file_path, $output_path, $quality, 'image/webp' ) ) {
1374 quickwebp_debug_log( 'create_webp_image:used_wp_editor', array( 'mime_type' => $mime_type ) );
1375 return true;
1376 }
1377
1378 $image = $this->create_gd_image_resource( $file_path, $mime_type );
1379
1380 if ( ! $image ) {
1381 quickwebp_debug_log( 'create_webp_image:gd_load_failed', array( 'mime_type' => $mime_type ) );
1382 return false;
1383 }
1384
1385 // Handle EXIF orientation for proper image rotation
1386 $exif_data = @exif_read_data( $file_path );
1387 if ( ! empty( $exif_data['Orientation'] ) ) {
1388 $orientation = (int) $exif_data['Orientation'];
1389 //phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
1390 $orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $file_path );
1391 if ( $orientation && 1 !== $orientation ) {
1392 switch ( $orientation ) {
1393 case 2:
1394 // Flip horizontally.
1395 imageflip( $image, IMG_FLIP_HORIZONTAL );
1396 break;
1397 case 3:
1398 // Rotate 180 degrees.
1399 $image = imagerotate( $image, 180, 0 );
1400 break;
1401 case 4:
1402 // Flip vertically.
1403 imageflip( $image, IMG_FLIP_VERTICAL );
1404 break;
1405 case 5:
1406 // Rotate 90 degrees counter-clockwise and flip vertically.
1407 $image = imagerotate( $image, -90, 0 );
1408 imageflip( $image, IMG_FLIP_VERTICAL );
1409 break;
1410 case 6:
1411 // Rotate 90 degrees clockwise (270 counter-clockwise).
1412 $image = imagerotate( $image, -90, 0 );
1413 break;
1414 case 7:
1415 // Rotate 90 degrees counter-clockwise and flip horizontally.
1416 $image = imagerotate( $image, 90, 0 );
1417 imageflip( $image, IMG_FLIP_HORIZONTAL );
1418 break;
1419 case 8:
1420 // Rotate 90 degrees counter-clockwise.
1421 $image = imagerotate( $image, 90, 0 );
1422 break;
1423 }
1424 }
1425 }
1426
1427 if ( ! imageistruecolor( $image ) ) {
1428 $truecolor = imagecreatetruecolor( imagesx( $image ), imagesy( $image ) );
1429
1430 if ( $mime_type === 'image/png' ) {
1431 imagealphablending( $truecolor, false );
1432 imagesavealpha( $truecolor, true );
1433 $transparent = imagecolorallocatealpha( $truecolor, 0, 0, 0, 127 );
1434 imagefilledrectangle( $truecolor, 0, 0, imagesx( $image ), imagesy( $image ), $transparent );
1435 }
1436
1437 imagecopy( $truecolor, $image, 0, 0, 0, 0, imagesx( $image ), imagesy( $image ) );
1438 $image = $truecolor;
1439 }
1440
1441 $webp_image = imagewebp( $image, $output_path, $quality );
1442 imagedestroy( $image );
1443 if ( ! $webp_image ) {
1444 quickwebp_debug_log( 'create_webp_image:gd_save_failed', array( 'output_path' => $output_path ) );
1445 return false;
1446 }
1447
1448 quickwebp_debug_log( 'create_webp_image:gd_success', array(
1449 'output_path' => $output_path,
1450 'file_size' => file_exists( $output_path ) ? filesize( $output_path ) : 0,
1451 ) );
1452
1453 return $webp_image;
1454 }
1455 }
1456