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 +233 -33 1.0.21.7.7 View file →
@@ -1,54 +1,100 @@
1 1 <?php
2 2
3 -namespace ImageOptimizer\Modules\Stats\Classes;
3 +namespace ImageOptimization\Modules\Stats\Classes;
4 4
5 -use ImageOptimizer\Classes\Image\{
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 ImageOptimizer\Classes\Logger;
13 -use ImageOptimizer\Modules\Settings\Classes\Settings;
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 +};
24 +use ImageOptimization\Classes\Logger;
25 +use ImageOptimization\Modules\Optimization\Classes\Exceptions\Image_Validation_Error;
26 +use ImageOptimization\Modules\Optimization\Classes\Validate_Image;
27 +use ImageOptimization\Modules\Settings\Classes\Settings;
14 28
29 +if ( ! defined( 'ABSPATH' ) ) {
30 + exit; // Exit if accessed directly.
31 +}
32 +
15 33 class Optimization_Stats {
16 - const PAGING_SIZE = 25000;
34 + const PAGING_SIZE = 2000;
35 + const STATS_CALCULATION_DELAY = 60 * 15;
17 36
18 37 /**
19 38 * Returns image stats.
20 39 * If the library is too big, it queries images in chunks.
21 40 *
22 - * @return array{total_image_count: int, optimized_image_count: int, current_image_size: int, initial_image_size: int}
41 + * @return array{total_image_count: int, optimized_image_count: int, current_image_size: int, initial_image_size:
42 + * int}
23 43 */
24 - 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.
25 64 $output = self::get_image_stats_chunk( 1, $image_id );
26 65 $pages_count = $output['pages'];
27 66
28 - if ( $pages_count > 1 ) {
29 - // $i initially is 2 bc we already got the first page, so we don't have to query it again
30 - for ( $i = 2; $i <= $pages_count; $i ++ ) {
31 - $chunk = self::get_image_stats_chunk( $i );
67 + if ( $pages_count <= 1 ) {
68 + unset( $output['pages'] );
32 69
33 - foreach ( array_keys( $chunk ) as $key ) {
34 - if ( isset( $output[ $key ] ) ) {
35 - $output[ $key ] += $chunk[ $key ];
36 - continue;
37 - }
70 + return $output;
71 + }
38 72
39 - $output[ $key ] = $chunk[ $key ];
40 - }
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() );
41 86 }
42 87 }
43 88
44 - unset( $output['pages'] );
89 + unset( $stats['updated_at'] );
45 90
46 - return $output;
91 + return $stats;
47 92 }
48 93
49 94 /**
50 - * @return array{pages: int, total_image_count: int, optimized_image_count: int, current_image_size: int, initial_image_size: int}
95 + * @return array{pages: int, total_image_count: int, optimized_image_count: int, current_image_size: int,
96 + * initial_image_size: int}
51 97 */
52 98 public static function get_image_stats_chunk( int $paged = 1, ?int $image_id = null ): array {
53 99 $output = [
54 100 'pages' => 1,
@@ -67,42 +113,147 @@
67 113 }
68 114
69 115 $query = $query->execute();
70 116
71 - $output['pages'] = $query->max_num_pages;
117 + $output['pages'] = (int) $query->max_num_pages;
72 118
73 119 foreach ( $query->posts as $attachment_id ) {
74 120 try {
121 + Validate_Image::is_valid( $attachment_id );
122 +
123 + $image = new Image( $attachment_id );
75 124 $wp_meta = new WP_Image_Meta( $attachment_id );
76 - } catch ( Invalid_Image_Exception $ii ) {
77 - Logger::log( Logger::LEVEL_ERROR, $ii->getMessage() );
78 -
125 + } catch ( Invalid_Image_Exception | Image_Validation_Error $ie ) {
79 126 continue;
80 127 }
81 128
82 129 $meta = new Image_Meta( $attachment_id );
83 - $image_sizes = $wp_meta->get_size_keys();
130 + $image_sizes = self::filter_only_enabled_sizes( $wp_meta->get_size_keys() );
84 131
85 - $current_sizes = self::filter_only_enabled_sizes( $image_sizes );
132 + foreach ( $image_sizes as $image_size ) {
133 + if ( ! $image->file_exists( $image_size ) ) {
134 + continue;
135 + }
136 +
137 + $output['total_image_count']++;
138 +
139 + $output['current_image_size'] += self::calculate_current_image_file_size( $attachment_id, $wp_meta, $image_size );
140 + $output['initial_image_size'] += self::calculate_initial_image_file_size( $attachment_id, $meta, $wp_meta, $image_size );
141 + }
142 +
86 143 $optimized_sizes = self::filter_only_enabled_sizes( $meta->get_optimized_sizes() );
144 + $output['optimized_image_count'] += count( $optimized_sizes );
145 + }
87 146
88 - $output['total_image_count'] += count( $current_sizes );
89 - $output['optimized_image_count'] += count( $optimized_sizes );
147 + return $output;
148 + }
90 149
91 - foreach ( $image_sizes as $image_size ) {
92 - $output['current_image_size'] += $wp_meta->get_file_size( $image_size );
93 - $output['initial_image_size'] += $meta->get_original_file_size( $image_size ) ?? $wp_meta->get_file_size( $image_size );
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 + ];
94 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 + ];
95 197 }
96 198
97 199 return $output;
98 200 }
99 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 +
223 + private static function calculate_current_image_file_size( int $image_id, WP_Image_Meta $wp_meta, string $image_size ): int {
224 + $size_from_meta = $wp_meta->get_file_size( $image_size );
225 +
226 + if ( $size_from_meta ) {
227 + return $size_from_meta;
228 + }
229 +
230 + try {
231 + return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
232 + } catch ( File_System_Operation_Error $e ) {
233 + return 0;
234 + }
235 + }
236 +
237 + private static function calculate_initial_image_file_size( int $image_id, Image_Meta $meta, WP_Image_Meta $wp_meta, string $image_size ): int {
238 + $size_from_meta = $meta->get_original_file_size( $image_size ) ?? $wp_meta->get_file_size( $image_size );
239 +
240 + if ( $size_from_meta ) {
241 + return $size_from_meta;
242 + }
243 +
244 + try {
245 + return File_System::size( ( new Image( $image_id ) )->get_file_path( $image_size ) );
246 + } catch ( File_System_Operation_Error $e ) {
247 + return 0;
248 + }
249 + }
250 +
100 251 private static function filter_only_enabled_sizes( array $size_keys ): array {
101 252 $enabled_sizes = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
102 253
103 254 if ( 'all' === $enabled_sizes ) {
104 - return $size_keys;
255 + return array_filter( $size_keys, fn( string $size_key ) => ! str_starts_with( $size_key, 'elementor_' ) );
105 256 }
106 257
107 258 return array_filter($size_keys, function( string $size ) use ( $enabled_sizes ) {
108 259 if ( Image::SIZE_FULL === $size ) {
@@ -114,6 +265,55 @@
114 265 }
115 266
116 267 return false;
117 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;
118 318 }
119 319 }