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
← All changes | admin/class-image-optimizer.php +447 -23 4.0.04.1.2 View file →
@@ -53,22 +53,30 @@
53 53 */
54 54 public function image_optimization( $file ) {
55 55 global $quickwebp_surecart_client;
56 56
57 + quickwebp_debug_log( 'image_optimization:start', array(
58 + 'file_name' => $file['name'] ?? '',
59 + 'file_type' => $file['type'] ?? '',
60 + ) );
61 +
57 62 $settings = $this->get_settings();
58 63 $image_file = $this->file_is_image( $file, $settings );
59 64
60 65 if ( ! $image_file ) {
66 + quickwebp_debug_log( 'image_optimization:not_image' );
61 67 return $file;
62 68 }
63 69
64 70 $mode_enabled = $settings['quickwebp_settings_conversion'];
65 71 if ( $mode_enabled === '0' ) {
72 + quickwebp_debug_log( 'image_optimization:conversion_disabled' );
66 73 return $image_file;
67 74 }
68 75
69 76 $save_original = is_array( $settings['quickwebp_settings_conversion_save_original'] ) ? $settings['quickwebp_settings_conversion_save_original'] : array();
70 77 if ( in_array( 'checked', $save_original ) ) {
78 + quickwebp_debug_log( 'image_optimization:save_original_enabled' );
71 79 return $image_file;
72 80 }
73 81
74 82 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
@@ -74,10 +82,12 @@
74 82 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
75 83 $mime_type = wp_get_image_mime( $image_file['tmp_name'] );
76 84 if ( in_array( 'checked', $ignore_same_format ) ) {
77 85 if ( '1' == $mode_enabled && 'image/webp' == $mime_type ) {
86 + quickwebp_debug_log( 'image_optimization:skip_same_format', array( 'mime_type' => $mime_type ) );
78 87 return $image_file;
79 88 } elseif( '2' == $mode_enabled && 'image/avif' == $mime_type ) {
89 + quickwebp_debug_log( 'image_optimization:skip_same_format', array( 'mime_type' => $mime_type ) );
80 90 return $image_file;
81 91 }
82 92 }
83 93
@@ -86,23 +96,32 @@
86 96 if ( $quickwebp_surecart_client ) {
87 97 $is_pro = $quickwebp_surecart_client->license()->is_valid();
88 98 }
89 99
100 + $image_file['new_size'] = $image_file['size'];
101 + $image_file['new_type'] = $image_file['type'];
102 +
90 103 if ( '2' === $mode_enabled && $is_pro ) {
104 + quickwebp_debug_log( 'image_optimization:try_avif', array( 'mime_type' => $mime_type, 'quality' => $quality ) );
91 105 $avif_image = $this->create_avif_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
92 106 if ( $avif_image ) {
93 107 $image_file['size'] = filesize( $image_file['tmp_name'] );
94 108 $image_file['type'] = 'image/avif';
95 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'] ) );
96 112 }
97 113 }
98 114
99 115 if ( '1' === $mode_enabled ) {
116 + quickwebp_debug_log( 'image_optimization:try_webp', array( 'mime_type' => $mime_type, 'quality' => $quality ) );
100 117 $webp_image = $this->create_webp_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
101 118 if ( $webp_image ) {
102 119 $image_file['size'] = filesize( $image_file['tmp_name'] );
103 120 $image_file['type'] = 'image/webp';
104 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'] ) );
105 124 }
106 125 }
107 126
108 127 return $image_file;
@@ -172,8 +191,10 @@
172 191 }
173 192 }
174 193
175 194 if ( ! empty( $new_sizes ) ) {
195 + $this->record_attachment_optimization_stats( $new_sizes );
196 +
176 197 if ( '1' == $mode_enabled ) {
177 198 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '1' );
178 199 } elseif ( '2' == $mode_enabled ) {
179 200 update_post_meta( $attachment_id, 'quickwebp_already_optimized', '2' );
@@ -275,8 +296,10 @@
275 296 */
276 297 public function image_optimization_ajax() {
277 298 global $quickwebp_surecart_client;
278 299
300 + quickwebp_debug_log( 'image_optimization_ajax:start' );
301 +
279 302 if ( ! current_user_can( 'upload_files' ) ) {
280 303 wp_send_json_error( __( 'You are not allowed to upload files.', 'quickwebp' ), 403 );
281 304 }
282 305
@@ -288,8 +311,9 @@
288 311
289 312 // Get the file
290 313 $file = count($_FILES) > 0 ? array_shift($_FILES) : array();
291 314 if ( empty($file) ) {
315 + quickwebp_debug_log( 'image_optimization_ajax:missing_file' );
292 316 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
293 317 }
294 318
295 319 $settings = $this->get_ajax_settings();
@@ -295,13 +319,15 @@
295 319 $settings = $this->get_ajax_settings();
296 320 $image_file = $this->file_is_image( $file, $settings );
297 321
298 322 if ( ! $image_file ) {
323 + quickwebp_debug_log( 'image_optimization_ajax:not_image' );
299 324 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
300 325 }
301 326
302 327 $mode_enabled = $settings['quickwebp_settings_conversion'];
303 328 if ( $mode_enabled === '0' ) {
329 + quickwebp_debug_log( 'image_optimization_ajax:conversion_disabled' );
304 330 $image_file['new_size'] = $image_file['size'];
305 331 $image_file['new_type'] = $image_file['type'];
306 332 $this->return_ajax_data( $image_file );
307 333 }
@@ -311,9 +337,13 @@
311 337 if ( $quickwebp_surecart_client ) {
312 338 $is_pro = $quickwebp_surecart_client->license()->is_valid();
313 339 }
314 340
341 + $image_file['new_size'] = $image_file['size'];
342 + $image_file['new_type'] = $image_file['type'];
343 +
315 344 if ( '2' === $mode_enabled && $is_pro ) {
345 + quickwebp_debug_log( 'image_optimization_ajax:try_avif', array( 'quality' => $quality ) );
316 346 $avif_image = $this->create_avif_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
317 347 if ( $avif_image ) {
318 348 $image_file['new_size'] = filesize( $image_file['tmp_name'] );
319 349 $image_file['new_type'] = 'image/avif';
@@ -320,8 +350,9 @@
320 350 }
321 351 }
322 352
323 353 if ( '1' === $mode_enabled ) {
354 + quickwebp_debug_log( 'image_optimization_ajax:try_webp', array( 'quality' => $quality ) );
324 355 $webp_image = $this->create_webp_image( $image_file['tmp_name'], $image_file['tmp_name'], $quality );
325 356 if ( $webp_image ) {
326 357 $image_file['new_size'] = filesize( $image_file['tmp_name'] );
327 358 $image_file['new_type'] = 'image/webp';
@@ -334,8 +365,9 @@
334 365 /**
335 366 * Optimize a single media
336 367 */
337 368 public function single_optimization_ajax() {
369 + quickwebp_debug_log( 'single_optimization_ajax:start' );
338 370
339 371 // verify the nonce.
340 372 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
341 373 if( !wp_verify_nonce( $nonce, 'quickwebp_admin_attachment' ) ) {
@@ -345,8 +377,9 @@
345 377 // Sanitize data
346 378 $attachment_id = isset( $_POST['attachment_id'] ) ? absint( wp_unslash( $_POST['attachment_id'] ) ) : 0;
347 379
348 380 if ( ! $attachment_id ) {
381 + quickwebp_debug_log( 'single_optimization_ajax:missing_attachment_id' );
349 382 wp_send_json_error( __( 'No attachment id.', 'quickwebp' ) );
350 383 }
351 384
352 385 $settings = $this->get_settings();
@@ -373,8 +406,9 @@
373 406 $sizes = $this->get_media_files( $attachment_id );
374 407 $new_sizes = array();
375 408
376 409 foreach ( $sizes as $key => $size ) {
410 + quickwebp_debug_log( 'single_optimization_ajax:optimize_size', array( 'size_key' => $key, 'path' => $size['path'] ?? '' ) );
377 411 $result = $this->optimize_local_file( $size );
378 412
379 413 if ( $result ) {
380 414 $new_sizes[$key] = $result;
@@ -381,8 +415,9 @@
381 415 }
382 416 }
383 417
384 418 if ( ! empty( $new_sizes ) ) {
419 + $this->record_attachment_optimization_stats( $new_sizes );
385 420
386 421 $data = get_post_meta( $attachment_id, 'quickwebp_data', true );
387 422 if ( ! empty( $data ) ) {
388 423 $this->remove_related_files( $data );
@@ -453,8 +488,9 @@
453 488 /**
454 489 * Get the image data for the preview in the settings page
455 490 */
456 491 public function get_image_data_for_preview_ajax() {
492 + quickwebp_debug_log( 'get_image_data_for_preview_ajax:start' );
457 493
458 494 // verify the nonce.
459 495 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
460 496 if( !wp_verify_nonce( $nonce, 'image_optimize_nonce' ) ) {
@@ -463,13 +499,15 @@
463 499
464 500 // Get the file
465 501 $file = count($_FILES) > 0 ? array_shift($_FILES) : array();
466 502 if ( empty($file) ) {
503 + quickwebp_debug_log( 'get_image_data_for_preview_ajax:missing_file' );
467 504 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
468 505 }
469 506
470 507 $image_file = $this->file_is_image( $file );
471 508 if ( ! $image_file ) {
509 + quickwebp_debug_log( 'get_image_data_for_preview_ajax:not_image' );
472 510 wp_send_json_error( __( 'No image uploaded, try again.', 'quickwebp' ) );
473 511 }
474 512
475 513 $preview_image_data = array(
@@ -482,8 +520,9 @@
482 520 $qualities = array( 'low', 'medium', 'high', 'extra_high' );
483 521
484 522 foreach ( $conversions as $conversion ) {
485 523 foreach ( $qualities as $quality ) {
524 + quickwebp_debug_log( 'get_image_data_for_preview_ajax:preview_variant', array( 'conversion' => $conversion, 'quality' => $quality ) );
486 525
487 526 $size_after = 0;
488 527
489 528 if ( '2' == $conversion ) {
@@ -520,11 +559,43 @@
520 559 }
521 560
522 561 /**
523 562 * Get the unoptimized media ids
563 + *
564 + * @param int $limit Maximum number of media IDs to retrieve.
565 + * @return int[]
524 566 */
525 - public function get_unoptimized_media_ids() {
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;
526 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 +
527 598 $settings = $this->get_settings();
528 599 $mode_enabled = $settings['quickwebp_settings_conversion'];
529 600 $ignore_same_format = is_array( $settings['quickwebp_settings_conversion_ignore_webp'] ) ? $settings['quickwebp_settings_conversion_ignore_webp'] : array();
530 601
@@ -570,14 +641,12 @@
570 641 'value' => '2',
571 642 );
572 643 }
573 644
574 - $media_ids = get_posts( array(
645 + return array(
575 646 'post_type' => 'attachment',
576 647 'post_mime_type' => $allowed_mime_types,
577 648 'post_status' => array_keys( $statuses ),
578 - 'posts_per_page' => -1,
579 - 'fields' => 'ids',
580 649 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
581 650 'meta_query' => array(
582 651 'relation' => 'AND',
583 652 array(
@@ -585,11 +654,9 @@
585 654 'compare' => 'NOT EXISTS'
586 655 ),
587 656 $meta_query_already_optimized,
588 657 ),
589 - ) );
590 -
591 - return $media_ids;
658 + );
592 659 }
593 660
594 661 /**
595 662 * Get the list of media files
@@ -639,14 +706,18 @@
639 706 */
640 707 public function optimize_local_file( $size ) {
641 708 global $quickwebp_surecart_client;
642 709
710 + quickwebp_debug_log( 'optimize_local_file:start', array( 'path' => $size['path'] ?? '' ) );
711 +
643 712 if ( !is_file($size['path']) ) {
713 + quickwebp_debug_log( 'optimize_local_file:missing_file', array( 'path' => $size['path'] ?? '' ) );
644 714 return false;
645 715 }
646 716
647 717 $real_type = mime_content_type( $size['path']);
648 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 ) );
649 720 return false;
650 721 }
651 722
652 723 $settings = $this->get_settings();
@@ -657,15 +728,18 @@
657 728 }
658 729 $mode_enabled = $settings['quickwebp_settings_conversion'];
659 730 $size_before = filesize( $size['path'] );
660 731 $new_path = $size['path'] . '.' . ( '2' === $mode_enabled && $is_pro ? 'avif' : 'webp' );
732 + $size_after = 0;
661 733
662 734 if ( '2' === $mode_enabled && $is_pro ) {
735 + quickwebp_debug_log( 'optimize_local_file:try_avif', array( 'path' => $size['path'], 'quality' => $quality ) );
663 736 $avif_image = $this->create_avif_image( $size['path'], $new_path, $quality );
664 737 if ( $avif_image ) {
665 738 $size_after = filesize( $new_path );
666 739 }
667 740 } elseif ( '1' === $mode_enabled ) {
741 + quickwebp_debug_log( 'optimize_local_file:try_webp', array( 'path' => $size['path'], 'quality' => $quality ) );
668 742 $webp_image = $this->create_webp_image( $size['path'], $new_path, $quality );
669 743 if ( $webp_image ) {
670 744 $size_after = filesize( $new_path );
671 745 }
@@ -670,8 +744,13 @@
670 744 $size_after = filesize( $new_path );
671 745 }
672 746 }
673 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 +
674 753 $deference = $size_before - $size_after;
675 754 $percent = $deference / $size_before * 100;
676 755
677 756 return array(
@@ -684,8 +763,180 @@
684 763 );
685 764 }
686 765
687 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 + /**
688 939 * Remove the related files of an optimized attachment
689 940 */
690 941 public function remove_related_files( $data ) {
691 942 foreach ( $data as $value ) {
@@ -749,8 +1000,9 @@
749 1000 $file_error = isset($file['error']) ? $file['error'] : '';
750 1001 $file_size = isset($file['size']) ? $file['size'] : '';
751 1002
752 1003 if ( empty($file_tmp_name) ) {
1004 + quickwebp_debug_log( 'file_is_image:missing_tmp_name', array( 'file_name' => $file_name ) );
753 1005 return false;
754 1006 }
755 1007
756 1008 $allowed_mime_types = apply_filters( 'quickwebp_mime_types_allowed', $this->allowed_mime_types );
@@ -756,8 +1008,9 @@
756 1008 $allowed_mime_types = apply_filters( 'quickwebp_mime_types_allowed', $this->allowed_mime_types );
757 1009 $is_image = wp_getimagesize($file_tmp_name);
758 1010 $mime_type = wp_get_image_mime($file_tmp_name);
759 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 ) );
760 1013 return false;
761 1014 }
762 1015
763 1016 $name = $file_name;
@@ -857,8 +1110,157 @@
857 1110 return $result;
858 1111 }
859 1112
860 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 + /**
861 1263 * Create the avif image
862 1264 */
863 1265 private function create_avif_image( $file_path, $output_path, $quality ) {
864 1266 $image = false;
@@ -863,19 +1265,24 @@
863 1265 private function create_avif_image( $file_path, $output_path, $quality ) {
864 1266 $image = false;
865 1267 $mime_type = wp_get_image_mime( $file_path );
866 1268
867 - if ( 'image/avif' == $mime_type ) {
868 - $image = imagecreatefromavif( $file_path );
869 - } elseif ( 'image/webp' == $mime_type ) {
870 - $image = imagecreatefromwebp( $file_path );
871 - } elseif ( 'image/jpeg' == $mime_type ) {
872 - $image = imagecreatefromjpeg( $file_path );
873 - } elseif ( 'image/png' == $mime_type ) {
874 - $image = imagecreatefrompng( $file_path );
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;
875 1279 }
876 1280
1281 + $image = $this->create_gd_image_resource( $file_path, $mime_type );
1282 +
877 1283 if ( ! $image ) {
1284 + quickwebp_debug_log( 'create_avif_image:gd_load_failed', array( 'mime_type' => $mime_type ) );
878 1285 return false;
879 1286 }
880 1287
881 1288 // Handle EXIF orientation for proper image rotation
@@ -936,11 +1343,17 @@
936 1343
937 1344 $avif_image = imageavif( $image, $output_path, $quality );
938 1345 imagedestroy( $image );
939 1346 if ( ! $avif_image ) {
1347 + quickwebp_debug_log( 'create_avif_image:gd_save_failed', array( 'output_path' => $output_path ) );
940 1348 return false;
941 1349 }
942 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 +
943 1356 return $avif_image;
944 1357 }
945 1358
946 1359 /**
@@ -949,19 +1362,24 @@
949 1362 private function create_webp_image( $file_path, $output_path, $quality ) {
950 1363 $image = false;
951 1364 $mime_type = wp_get_image_mime( $file_path );
952 1365
953 - if ( 'image/avif' == $mime_type ) {
954 - $image = imagecreatefromavif( $file_path );
955 - } elseif ( 'image/webp' == $mime_type ) {
956 - $image = imagecreatefromwebp( $file_path );
957 - } elseif ( 'image/jpeg' == $mime_type ) {
958 - $image = imagecreatefromjpeg( $file_path );
959 - } elseif ( 'image/png' == $mime_type ) {
960 - $image = imagecreatefrompng( $file_path );
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;
961 1376 }
962 1377
1378 + $image = $this->create_gd_image_resource( $file_path, $mime_type );
1379 +
963 1380 if ( ! $image ) {
1381 + quickwebp_debug_log( 'create_webp_image:gd_load_failed', array( 'mime_type' => $mime_type ) );
964 1382 return false;
965 1383 }
966 1384
967 1385 // Handle EXIF orientation for proper image rotation
@@ -1022,10 +1440,16 @@
1022 1440
1023 1441 $webp_image = imagewebp( $image, $output_path, $quality );
1024 1442 imagedestroy( $image );
1025 1443 if ( ! $webp_image ) {
1444 + quickwebp_debug_log( 'create_webp_image:gd_save_failed', array( 'output_path' => $output_path ) );
1026 1445 return false;
1027 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 + ) );
1028 1452
1029 1453 return $webp_image;
1030 1454 }
1031 1455 }