PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.7
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.7
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
← All changes | modules/stats/classes/optimization-stats.php +187 -26 1.4.11.7.7 View file →
@@ -5,13 +5,23 @@
5 5 use ImageOptimization\Classes\Image\{
6 6 Image,
7 7 Image_Meta,
8 8 Image_Query_Builder,
9 + Image_Status,
9 10 WP_Image_Meta,
10 11 Exceptions\Invalid_Image_Exception
11 12 };
12 -use ImageOptimization\Classes\File_System\Exceptions\File_System_Operation_Error;
13 -use ImageOptimization\Classes\File_System\File_System;
13 +use ImageOptimization\Classes\Async_Operation\{
14 + Async_Operation,
15 + Async_Operation_Hook,
16 + Async_Operation_Queue,
17 + Exceptions\Async_Operation_Exception,
18 + Queries\Operation_Query,
19 +};
20 +use ImageOptimization\Classes\File_System\{
21 + Exceptions\File_System_Operation_Error,
22 + File_System,
23 +};
14 24 use ImageOptimization\Classes\Logger;
15 25 use ImageOptimization\Modules\Optimization\Classes\Exceptions\Image_Validation_Error;
16 26 use ImageOptimization\Modules\Optimization\Classes\Validate_Image;
17 27 use ImageOptimization\Modules\Settings\Classes\Settings;
@@ -20,9 +30,10 @@
20 30 exit; // Exit if accessed directly.
21 31 }
22 32
23 33 class Optimization_Stats {
24 - const PAGING_SIZE = 25000;
34 + const PAGING_SIZE = 2000;
35 + const STATS_CALCULATION_DELAY = 60 * 15;
25 36
26 37 /**
27 38 * Returns image stats.
28 39 * If the library is too big, it queries images in chunks.
@@ -29,31 +40,56 @@
29 40 *
30 41 * @return array{total_image_count: int, optimized_image_count: int, current_image_size: int, initial_image_size:
31 42 * int}
32 43 */
33 - public static function get_image_stats( ?int $image_id = null ): array {
44 + public static function get_image_stats( ?int $image_id = null, ?bool $no_cache = false ): array {
45 + // No caching needed if we check a specific image.
46 + if ( $image_id ) {
47 + $output = self::get_image_stats_chunk( 1, $image_id );
48 +
49 + unset( $output['pages'] );
50 +
51 + return $output;
52 + }
53 +
54 + // Look at the stored data first. If it's still valid -- use it.
55 + $stats = self::get_stored_stats();
56 +
57 + if ( ! $no_cache && ( time() - $stats['updated_at'] ) <= self::STATS_CALCULATION_DELAY ) {
58 + unset( $stats['updated_at'] );
59 +
60 + return $stats;
61 + }
62 +
63 + // Otherwise, recalculate the stats.
34 64 $output = self::get_image_stats_chunk( 1, $image_id );
35 65 $pages_count = $output['pages'];
36 66
37 - if ( $pages_count > 1 ) {
38 - // $i initially is 2 bc we already got the first page, so we don't have to query it again
39 - for ( $i = 2; $i <= $pages_count; $i ++ ) {
40 - $chunk = self::get_image_stats_chunk( $i );
67 + if ( $pages_count <= 1 ) {
68 + unset( $output['pages'] );
41 69
42 - foreach ( array_keys( $chunk ) as $key ) {
43 - if ( isset( $output[ $key ] ) ) {
44 - $output[ $key ] += $chunk[ $key ];
45 - continue;
46 - }
70 + return $output;
71 + }
47 72
48 - $output[ $key ] = $chunk[ $key ];
49 - }
73 + if ( $pages_count > 1 && Async_Operation::OPERATION_STATUS_NOT_STARTED === self::get_stats_calculation_status() ) {
74 + try {
75 + Async_Operation::create(
76 + Async_Operation_Hook::CALCULATE_OPTIMIZATION_STATS,
77 + [
78 + 'page' => 2,
79 + 'pages_count' => $pages_count,
80 + 'output' => $output,
81 + ],
82 + Async_Operation_Queue::STATS
83 + );
84 + } catch ( Async_Operation_Exception $aoe ) {
85 + Logger::log( Logger::LEVEL_ERROR, 'Error while creating a stats calculation task: ' . $aoe->getMessage() );
50 86 }
51 87 }
52 88
53 - unset( $output['pages'] );
89 + unset( $stats['updated_at'] );
54 90
55 - return $output;
91 + return $stats;
56 92 }
57 93
58 94 /**
59 95 * @return array{pages: int, total_image_count: int, optimized_image_count: int, current_image_size: int,
@@ -77,38 +113,114 @@
77 113 }
78 114
79 115 $query = $query->execute();
80 116
81 - $output['pages'] = $query->max_num_pages;
117 + $output['pages'] = (int) $query->max_num_pages;
82 118
83 119 foreach ( $query->posts as $attachment_id ) {
84 120 try {
85 121 Validate_Image::is_valid( $attachment_id );
122 +
123 + $image = new Image( $attachment_id );
86 124 $wp_meta = new WP_Image_Meta( $attachment_id );
87 125 } catch ( Invalid_Image_Exception | Image_Validation_Error $ie ) {
88 - Logger::log( Logger::LEVEL_ERROR, $ie->getMessage() );
89 -
90 126 continue;
91 127 }
92 128
93 129 $meta = new Image_Meta( $attachment_id );
94 - $image_sizes = $wp_meta->get_size_keys();
130 + $image_sizes = self::filter_only_enabled_sizes( $wp_meta->get_size_keys() );
95 131
96 - $current_sizes = self::filter_only_enabled_sizes( $image_sizes );
97 - $optimized_sizes = self::filter_only_enabled_sizes( $meta->get_optimized_sizes() );
132 + foreach ( $image_sizes as $image_size ) {
133 + if ( ! $image->file_exists( $image_size ) ) {
134 + continue;
135 + }
98 136
99 - $output['total_image_count'] += count( $current_sizes );
100 - $output['optimized_image_count'] += count( $optimized_sizes );
137 + $output['total_image_count']++;
101 138
102 - foreach ( $image_sizes as $image_size ) {
103 139 $output['current_image_size'] += self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
104 140 $output['initial_image_size'] += self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
105 141 }
142 +
143 + $optimized_sizes = self::filter_only_enabled_sizes( $meta->get_optimized_sizes() );
144 + $output['optimized_image_count'] += count( $optimized_sizes );
106 145 }
107 146
108 147 return $output;
109 148 }
110 149
150 + public static function get_optimization_details( int $attachment_id ): array {
151 + try {
152 + $wp_meta = new WP_Image_Meta( $attachment_id );
153 + } catch ( Invalid_Image_Exception $iie ) {
154 + Logger::log( Logger::LEVEL_ERROR, "No metadata found for image ID: {$attachment_id}" );
155 +
156 + throw $iie;
157 + }
158 +
159 + $output = [
160 + 'total' => 0,
161 + 'sizes' => [],
162 + ];
163 +
164 + $meta = new Image_Meta( $attachment_id );
165 + $image_sizes = self::filter_only_enabled_sizes( $wp_meta->get_size_keys() );
166 +
167 + foreach ( $image_sizes as $image_size ) {
168 + $current_image_size = self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
169 + $output['total'] += $current_image_size;
170 +
171 + $status = self::get_image_size_optimization_status( $attachment_id, $image_size );
172 +
173 + if ( Image_Status::OPTIMIZED === $status ) {
174 + $dimensions_updated = $wp_meta->get_width( $image_size ) !== (int) $meta->get_original_width( $image_size ) ||
175 + $wp_meta->get_height( $image_size ) !== (int) $meta->get_original_height( $image_size );
176 +
177 + $new_dimensions = $dimensions_updated ? [
178 + 'width' => $wp_meta->get_width( $image_size ),
179 + 'height' => $wp_meta->get_height( $image_size ),
180 + ] : null;
181 +
182 + $initial_image_size = self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
183 +
184 + $saved = [
185 + 'relative' => max( 100 - round( $current_image_size / $initial_image_size * 100 ), 0 ),
186 + 'absolute' => $initial_image_size - $current_image_size,
187 + ];
188 + }
189 +
190 + $output['sizes'][] = [
191 + 'size_name' => $image_size,
192 + 'file_size' => $current_image_size,
193 + 'status' => self::get_image_size_optimization_status( $attachment_id, $image_size ),
194 + 'saved' => $saved ?? null,
195 + 'new_dimensions' => $new_dimensions ?? null,
196 + ];
197 + }
198 +
199 + return $output;
200 + }
201 +
202 + private static function get_image_size_optimization_status( int $attachment_id, string $size_name ): string {
203 + $image = new Image( $attachment_id );
204 + $meta = new Image_Meta( $attachment_id );
205 +
206 + if ( in_array( $size_name, $meta->get_optimized_sizes(), true ) ) {
207 + return Image_Status::OPTIMIZED;
208 + }
209 +
210 + if ( ! $image->file_exists( $size_name ) ) {
211 + return 'file-not-found';
212 + }
213 +
214 + try {
215 + Validate_Image::validate_file_size( $attachment_id, $size_name );
216 + } catch ( Image_Validation_Error $ive ) {
217 + return 'file-too-large';
218 + }
219 +
220 + return Image_Status::NOT_OPTIMIZED;
221 + }
222 +
111 223 private static function calculate_current_image_file_size( int $image_id, WP_Image_Meta $wp_meta, string $image_size ): int {
112 224 $size_from_meta = $wp_meta->get_file_size( $image_size );
113 225
114 226 if ( $size_from_meta ) {
@@ -153,6 +265,55 @@
153 265 }
154 266
155 267 return false;
156 268 });
269 + }
270 +
271 + /**
272 + * Retrieves stats data.
273 + *
274 + * @return array{total_image_count: ?int, optimized_image_count: ?int, current_image_size: ?int, initial_image_size:
275 + * ?int, updated_at: ?int}
276 + */
277 + public static function get_stored_stats(): array {
278 + $default = [
279 + 'total_image_count' => null,
280 + 'optimized_image_count' => null,
281 + 'current_image_size' => null,
282 + 'initial_image_size' => null,
283 + 'updated_at' => null,
284 + ];
285 +
286 + $output = json_decode( get_option( 'image_optimizer_optimization_stats' ), ARRAY_A );
287 +
288 + return is_array( $output ) ? $output : $default;
289 + }
290 +
291 + /**
292 + * Updates the optimization stats with fresh values.
293 + *
294 + * @param array $stats
295 + *
296 + * @return bool
297 + */
298 + public static function set_stored_stats( array $stats ): bool {
299 + $stats['updated_at'] = time();
300 +
301 + return update_option( 'image_optimizer_optimization_stats', json_encode( $stats ) );
302 + }
303 +
304 + private static function get_stats_calculation_status(): string {
305 + $active_query = ( new Operation_Query() )
306 + ->set_hook( Async_Operation_Hook::CALCULATE_OPTIMIZATION_STATS )
307 + ->set_status( [
308 + Async_Operation::OPERATION_STATUS_PENDING,
309 + Async_Operation::OPERATION_STATUS_RUNNING,
310 + ] )
311 + ->set_limit( 1 );
312 +
313 + $active_operations = Async_Operation::get( $active_query );
314 +
315 + return ! empty( $active_operations )
316 + ? Async_Operation::OPERATION_STATUS_RUNNING
317 + : Async_Operation::OPERATION_STATUS_NOT_STARTED;
157 318 }
158 319 }